Ripping a sidtune is the process of detaching the code and data of a music player
from an enclosing program, and getting the isolated music to play.
This section is not meant to be a full guide on ripping C64 musics. It is
not even meant to be an introductory guide.
Individual
pieces of music may be very difficult and time consuming to rip for a
beginner. And there must clearly be distinguished between just a rip
and a so-called clean rip.
In this and subsequent paragraphs the term ripper
refers to a human person, unless specified in any other way.
Clean rips
Ripping sometimes can be as easy as finding the start of two
machine code sub-routines and saving a small memory area.
Clean rips often are much harder to achieve. Their aim is to strip
down the source program to nothing else than the music player and
its data, thereby making the ripped fragment as short as possible.
Additionally, rippers often copy the remaining fragments of data
into a more compact form. Sometimes they even relocate the player.
This may be required if one wants to merge all subsongs (perhaps
each with a separate player) while considering the C64's memory
size of 64 KiB only. Concerning SIDPLAY, this is easier, as the
whole data are reloaded before a subsong is started, permitting
the ripper to use always the full memory regardless of whether
he likes to overwrite unused parts of the music data or not. The
entire sidtune data will be recovered upon restarting a song or
starting another song (note, that this makes it impossible to
leave previously used parts of the memory untouched).
It is a crucial case if the player and its complete data (all
subsongs) are spread throughout the whole memory. It is then
difficult to determine whether certain regions of memory actually
belong to the music player. Proving the integrity of a fragmented
song can only be done by listening to it till the end and comparing
it to the output of its original program. This might be the most
boring part upon performing a rip if you don't want to fully read
and analyze the music player (which, by the way, would be the only
true way to track down the location of all belonging data; except
using a self-written utility). Consider a long song. You would have
to listen to it entirely to find some parts or its end to be broken.
The effect of only a bit of data missing might not be as obvious as
you would guess. A single instrument might be altered only slightly.
You probably would not even notice a difference, a weaker vibrato
effect for instance. Better save a wider region of the memory rather
than cutting off any parts by accident.
Consider some of the required music data (such as vibrato tables,
instrument definitions, or track and pattern data) to be put into
a memory region, which you would not even think of. For instance,
sensitive data might have been put into the stack address space
or into the zero-page. Zero-page pointers sometimes are only
initialized once by some code at the early start of the program.
It can be required to write an own music player initialization,
most often based on available code fragments throughout the entire
memory.
Usually only musics in demonstration programs are likely to be
protected. Game musics that are hard to rip generally are
protected by accident (one exception is Arkanoid by Martin Galway),
which either means the game programmer mixed parts of the music
player code with his own code, or he did the music and player
himself, or he had a very confusing coding style (socalled
spaghetti-code), making the code unnecessarily difficult
to read and understand, and making it almost impossible to detach
music player code from program code without much effort.
More on this further below.
As long as a (partially) ripped music can be determined to be
obviously damaged, you can still decide to leave it in its most
basic form. For instance, if the code of the application and the
code of the music player are nested, saving the entire memory
might be a hint. Making the rip a clean rip is just an
optimization and could be done later. Also, consider asking
another ripper whether he can help. Often four eyes see more than
two.
Requirements
The availability of special software or external hardware tools
is of much help. Try to get one of the following (sorted by
recommended priority):
- Any hardware modul with monitoring functions: Memory dump, search, fill,
disassembler, assembler, load and save of any memory region, ability to restore
the screen buffer and the stack.
- A hardware freezer cardridge for the C64 expansion port. For instance,
this will enable you to freeze the running program at any point you like. This
is especially helpful, since it gives the possibility to repeatedly access a
song at the end of a game or demo without having to run the program from the
very beginning.
- A self-made EPROM module, which provides at least a subset of the
monitoring functions listed above.
- A software monitor, which provides any of the above functions;
usually sufficient enough to take a close look at those parts of memory, which
remain unmodified after a hardware reset.
- A ripping utility. This chapter does not cover any of these.
- A utility, that can read/write C64 disks with the C64 floppy drive
connected to your PC. Or a utility that can transfer data via a serial
cable from the C64 to a different platform.
If you don't have access to any of the tools listed above, you are
definitely out of luck. At least I can't think of any other
equivalent tools. However, nowadays you do not need a real C64 for
ripping music (a real SID for listening to music is appreciated,
though). Some of the existing C64 emulators provide a built-in
emulation of good monitoring and freezing functions, e.g. the
freely available C64 emulator VICE [1].
As a side note, it is also possible to rip directly from linearly
written *.D64 emulator disk images provided that you know how to
follow tracks and sectors.
Of course, you need to know of the capabilities of your tools and how to use
them efficiently. Finally, you certainly need to know the C64's hardware and
the 6502/6510 machine language. A bit of knowledge of hexadecimal machine codes
would be useful but is rarely necessary as you will almost never
have to read plain hex machine code. You don't need to be a programmer either,
though you should be able to read and understand (and thus be able to follow)
disassemblings.
A quick guide on ripping
The method presented here is just one particular way to rip a sidtune.
It may not be directly applicable to every sidtune and its enclosing program.
The overall ability to rip a specific sidtune depends mainly on
experience and instinct as well as a bit of machine code programming skills.
You will certainly build your own individual ripping algorithm based upon the
experiences you make during trial-and-error ripping.
Most of the examples and recommendations in this
section are based upon questions me and others on Usenet have been asked. For the
weird case that you want to test this guide on some program, please take into
account that intro/demo tunes generally are much more easy to rip than game
tunes. If you want to rip a very easy game tune, check out the game Commando.
It is probably the most famous tune and very easy to rip due to the simplicity
of Rob Hubbard's modularized music player. Note, that a
lot of other music player are modularized as well.
Have you ever tried to rip a sidtune from a game or demo? Maybe you have always
wanted to be able to listen to your favourite music without having to load and
run a larger program? If you've ever tried to rip, did you succeed? If not, what
was the main problem? Maybe you didn't know where to start? Maybe you erratically
browsed through hundreds of lines of machine code and randomly displayed kilobytes
of C64 memory without a clue? Or maybe you did not even come that far?
Depending on the tools which aid you on your ripping-task, make a hardware
reset to stop the running program and start the monitor (here the term
monitor always refers to the monitoring utility and not the screen :).
Making a reset does not mean turning off the computer and then on
again. By doing that you erase its memory! Inside a C64 emulator you often
have the choice between soft and hard reset. Soft reset should be the
recommended one unless the application is protected against resetting.
Without an expansion module the easiest way to reset the C64 is to connect
pins 1+3 (GND and RESET) of the user port (starting to count the pins from
the left while looking at the back of the C64) for a short time. For the case
that you don't have an external hardware module installed and need to load
and start a software monitor, you will overwrite a part of the memory by
loading the monitor. Some monitors are relocatable. Make and save a couple
of versions for different memory regions, e.g. $1000, $6000
and $C000. Unless you find out a memory region that is definitely not
used by the music player and data, you will likely have to repeat this
procedure:
- optionally clear the whole C64 memory by filling every RAM address with a zero
(if the program moves memory blocks, the zeroes can serve as a visible hint on
unused memory areas)
- load and start the program which contains music
- make a hardware reset
- (load and) start your monitor
You already had come so far, but didn't know any further? Did you think about the
simple fact that SID music can only be produced by machine code that accesses the
SID chip? That is the most trivial hint. The following steps in trying
to rip the tune will be:
- search for machine code that writes values into the SID chip registers
- determine the part of the program where most of the code is
stored which accesses the SID (most likely the music player or sound fx driver)
- search the whole memory for code that branches to the music player area
- determine the main subroutines of the music player (most common as the
Init and Play addresses)
Unless the ROM and the I/O address space are turned off, the SID chip registers
are found at a constant place in memory, i.e. address $D400 to (and including) $D41D.
On the contrary, all subsequent chunks of 32-bytes from $D420 to $D800 are mapped to
the SID chip, too. But only a very few tunes access the SID chip via these mirrored registers.
So, first we need to find the music player code. We assume the music player uses the absolute
addressing mode to access at least one SID register. This is most common, but must not be true for
a specific player. Basically, you could search for the appearance of any valid SID address. But
starting with the Master Volume and Filter Type Register $D418 next to the
Oscillator 1 Control Register $D404 is a good hint. In your monitor enter something
like the following at the prompt to search the C64 memory for the appearance of each value:
> H 0800 D000 18,D4
This searches the memory from address $0800 to $d000 (where the VIC chip starts) for
appearance of the consecutive bytes $18 and $D4. Notice the little-endian order of the
low and high byte of the 16-bit address word. Please also take into account, that if you want to
search other parts of the C64 memory, like the RAM under the ROM $d000 - $FFFF, you
first have to enable/disable the desired memory bank. Further, superior monitoring tools are able
to search for full valid machine code instructions instead of just hex values. Consider the
following output:
> H 0800 D000 18,D4
087C 127A 15F4 CFEA
>
> H 0800 D000 04,D4
117A 1208 13F8 1410 15FE
Searching for alternate $D4?? values would probably also hit the area at $1???.
Avoid searching for the SID registers of voice two and three, as most players access these via indices
and the SID base address $D400. Since repeatedly searching the C64 memory takes some time,
you can narrow down the region once you had a couple of hits. In this example there is most likely
some SID sound code between $1000 and $1800. Examining the code in that area
might be enough. Keep in mind, that unless you are able to directly search for full valid
machine instructions, these hits might only be binary data. Manually searching for possible
instructions could be a hint. Consider the instruction STA $D418:
> H 1000 2000 8D,18,D4
1279 15F3
Further, keep in mind, that there might be more than one player in memory as well as additional
SID code outside a compact music player. Hence you might find your searches to hit two completely
distinct memory regions.
Next the often more difficult task of finding the initialization and play addresses of the music
player, although the overall procedure is similar:
- search for the appearance of interrupt vector $0314/$0315 or vector
$FFFE
/$FFFF and their set-up routines; then follow the interrupt handler to find a
call into the music player area; likely to be the official player entry
- track down a decompressor on the stack for the starting address of the program; then
follow the code to find a call of a subroutine in the music player area; likely to be the official
initialization entry; search the whole program for the number and range of parameters used to
initialize the different subsongs or sound effects
- search for the use of Kernal-ROM subroutines (like CLR-screen) and hardware initialization
code; likely at the start of the program; follow it to see what the original code does
Special problems need special treatment:
- no initialization subroutine available: reset as early as possible or try to write
an own one; determine dynamic pointers and indices, which are increased during playback
- music code and data start inside screen memory $0400 and no hardware module available:
insert code into the program to save the data to disk after it was moved into the screen memory;
insert code into the program to not let the program move the data, but terminate (reset)
- multiple init/play addresses
- separate music players
- separate init functions for each subsong
A universal timer interrupt handler can be
used to verify the found play and init addresses of a ripped
sidtune.
Finally, you just need to save the tune to disk. Worth knowing is, that the music data may
be either stored in front of or behind the player. Or it is stored at a more distant location. The
safest way to search for the true location of the data is to follow the initialization subroutine
and try to determine where pointers, arrays, note tables and such, are kept. But generally it is
enough to do a quick memory dump of the full player environment, searching for boundaries, which
often consist out of a larger number of zero padded bytes. With a bit of experience you will be
able to hit the right region just by looking at a hex dump.
Bank switching
Without bank-switching it is nearly impossible for an emulator to determine which memory region
should be accessed. There are some sidtunes that do not only use the memory under the address
space of the Basic-ROM and Kernal-ROM, but also the memory under the I/O address space. To have all
sidtunes in a unique format, it would be preferred to use valid machine code, which would also
run on a real C64, except for the PlaySID-compatible sample-player modifications of course.
Please don't neglect bank-switching! It doesn't make the code of a ripped sidtune much
longer. Where necessary, it generally takes only 2*12 bytes of additional code in front of the
initialization and main player routines.
SIDPLAY's built-in machine code interpreter provides default bank settings for the C64 music
player starting addresses:
- $0000 to $9FFF -> $01 = #$07
- $A000 to $CFFF -> $01 = #$06
- $D000 to $DFFF -> $01 = #$00
- $E000 to $FFFF -> $01 = #$05
The main aim of these default values is to provide a sane initial state prior to executing the
music player machine code. Further, these defaults make it possible to jump directly from SIDPLAY
to any RAM area without turning off too many components. The single value $05 is not
used for all the memory banks because, for instance, it would be necessary to turn back on the
Kernal area to use the end-of-irq functions.
The quickest way to add C64 bank-switching code to a sidtune is to append the following code and
adjust the initialization and player address accordingly. This example switches off
Basic-ROM and Kernal-ROM, but leaves on the I/O address space:
New <init>/<play> subroutine:
LDX #$35
STX $01
JSR <init>/<play>
LDX #$37
STX $01
RTS
Or something like:
TAX
LDA $01
PHA
LDA #$35
STA $01
TXA
JSR <init>/<play>
PLA
STA $01
RTS
Note: Keep in mind, that some PlaySID-specific rips assume all registers to contain a zero
prior to executing the player subroutine (and the X and Y-Register to contain a zero prior to
executing the init subroutine). Thus, when modifying those rips to use bank-switching, the
additional code does not satisfy the PlaySID-specific assumption. To satisfy such a rip,
add LDX #$00, LDY #$00 in front of the music player call.
Variable playing speed
Variable replaying-speed for a song can be set up via CIA 1 Timer A:
- Upon song initialization or during playback set up the CIA timer speed
via the I/O registers $DC04/$DC05.
- Configure the sidtune info file (or header) to enable timer speed
support. On how to do that refer to the documentation on file formats.
Consult your C64 programmers reference book for additional information.
The clock speed of a PAL C64 is 985248.4 Hz. The CIA 1 Timer A value is
calculated with this formula:
985248.4 Hz
---------------- = timer_value
song_speed in Hz
Normal double-speed:
985248.4 Hz / 100 Hz ~= $267C
Some music composers demand subsequent player calls to be done after
a specific number of scan-lines. This way effect updates are affected
and, according to the composers, result in better sound.
Usually it is wrong to simply call the music player via several
JSR instructions one after each other in order to increase the speed
of playback. That technique lacks any delays between
subsequent player calls.
Without the availability of emulation of scan-line interrupts you can
nevertheless achieve an accurate timing by dynamically updating the timer
speed for each player call. You can divide the normal speed timer value
into small chunks that would equal the system time for one scan-line:
On a PAL system:
$4CF8 (50 Hz) / 312 scan-lines ~= 63.15 cycles per scan-line
63.15 * X scan-lines = timer value for one particular
player call
Normal double speed example
New <init> subroutine:
LDX #$26 ; plain 100 Hz
LDY #$7C
STX $DC05
STY $DC04
LDA #$00
STA <counter>
JMP <init>
Timed double speed example
New <play> subroutine for double-speed and $48 scan-lines delay:
LDA <counter> ; decide which timer value to set
INC <counter>
AND #$01 ; 0=1st or 1=2nd player call
ASL ; two bytes per timer value
TAX
LDA <timer_table>,X ; $11D4 (duration of $48 scan-lines)
STA $DC05 ; $3B24 (the rest of the screen)
LDA <timer_table+1>,X ; ($11D4 + $3B24 = $4CF8 full screen)
STA $DC04
LDA $DC0E ; read CIA 1 CRA
ORA #$10 ; set STROBE bit
STA $DC0E ; write CIA 1 CRA
JMP <play>/<play2> ; maybe call different player entries
Note that the <counter> can be the immediate operand of the
LDA instruction (self-modifying code).
Interrupts
A lot of C64 music players contain a built-in instance of an interrupt handler and a corresponding
interrupt set-up routine. It was probably used to provide a small test player, making it able
to quickly start the music for demonstration purposes. And it often serves as an example of
what is required to correctly initialize a song, its speed and where the music player has to be
called to produce continous music. A certain simplicity of such a built-in handler provided, it is
very easy to convert such a sidtune to SIDPLAY compatible form. Specifying the play address
$0000 makes SIDPLAY search and use an installed interrupt vector in either
$0314/$0315 or $FFFE/$FFFF.
A universal C64 Timer Interrupt Handler at address $6000 is set up like this and can
be used to verify the found play and init addresses of a rip:
;6000 78 SEI ; disable interrupts
;6001 ad 0e dc LDA $dc0e ; load CIA 1 CR A
;6004 29 fe AND #$fe ; stop timer A
;6006 8d 0e dc STA $dc0e ; write back
;6009 a9 4d LDA #$4d ; PAL clock speed / $4D00
;600b a2 00 LDX #$00 ; ~= 50 Hz
;600d 8d 05 dc STA $dc05 ; set CIA 1 timer A
;6010 8e 04 dc STX $dc04
;6013 a9 60 LDA #$60 ; point IRQ vector to
;6015 a2 40 LDX #$40 ; interrupt handler at $6040
;6017 8d 15 03 STA $0315
;601a 8e 14 03 STX $0314
;601d a9 00 LDA #$00 ; disable VIC interrupts
;601f 8d 1a d0 STA $d01a ; via IMR
;6022 a5 01 LDA $01 ; load and save bank-select
;6024 48 PHA ; register on stack
;6025 a9 36 LDA #$35 ; disable Basic-ROM
;6027 85 01 STA $01 ; and Kernal-ROM
;6029 a9 0f LDA #$0f ; set SID Master Volume
;602b 8d 18 d4 STA $d418 ; to maximum
;602e a9 00 LDA #$00 ; select sub-song #1
;6030 20 00 50 JSR <INIT> ; call music player
;6033 68 PLA ; restore bank-select
;6034 85 01 STA $01 ; register via stack
;6036 ad 0e dc LDA $dc0e ; load CIA 1 CR A
;6039 09 01 ORA #$01 ; start timer A
;603b 8d 0e dc STA $dc0e ; write back
;603e 58 CLI ; enable interrupts
;603f 60 RTS
;6040 a5 01 LDA $01
;6042 48 PHA
;6043 a9 36 LDA #$35
;6045 85 01 STA $01
;6047 20 12 50 JSR <PLAY> ; call music player
;604a 68 PLA
;604b 85 01 STA $01
;604d 4c 31 ea JMP $ea31 ; jump to Kernal, EOI()
Or this one which also kills the Kernal run-time system and
is located in the default screen address space:
;0400 78 SEI ; disable interrupts
;0401 a9 05 LDA #$35 ; disable Basic-ROM
;0403 85 01 STA $01 ; and Kernal-ROM
;0405 a9 04 LDA #$04 ; point IRQ vector
;0407 a2 51 LDX #$44 ; to interrupt handler at $0444
;0409 8d ff ff STA $ffff
;040c 8e fe ff STX $fffe
;040f a9 04 LDA #$04 ; point NMI vector
;0411 a2 7d LDX #$65 ; to RTI instruction at $0465
;0413 8d fb ff STA $fffb
;0416 8e fa ff STX $fffa
;0419 a9 00 LDA #$00 ; disable VIC interrupts
;041b 8d 1a d0 STA $d01a ; via IMR
;041e ad 0e dc LDA $dc0e ; load CIA 1 CR A
;0421 29 fe AND #$fe ; stop timer A
;0423 8d 0e dc STA $dc0e ; write back
;0426 a9 81 LDA #$81 ; CIA 1 ICR, enable
;0428 8d 0d dc STA $dc0d ; underflow timer A
;042b ad 0f dc LDA $dc0f ; load CIA 1 CR B
;042e 29 fe AND #$fe ; stop timer B
;0430 8d 0f dc STA $dc0f ; write back
;0433 a9 00 LDA #$00 ; select sub-song #1
;0435 20 00 4c JSR <INIT> ; call music player
;0438 58 CLI ; enable interrupts
;0439 ad 0e dc LDA $dc0e ; load CIA 1 CR A
;043c 09 01 ORA #$01 ; start timer A
;043e 8d 0e dc STA $dc0e ; write back
;0441 4c 41 04 JMP $0441 ; endless jump
;0444 48 PHA ; save MPU's registers
;0445 8a TXA
;0446 48 PHA
;0447 98 TYA
;0448 48 PHA
;0449 a5 01 LDA $01
;044b 48 PHA
;044c a9 05 LDA #$05
;044e 85 01 STA $01
;0450 ad 0d dc LDA $dc0d ; acknowledge interrupt
;0453 ee 20 d0 INC $d020 ; (or 3x NOP)
;0456 20 89 ec JSR <PLAY> ; call music player
;0459 ce 20 d0 DEC $d020 ; (or 3x NOP)
;045c 68 PLA
;045d 85 01 STA $01
;045f 68 PLA ; restore MPU's registers
;0460 a8 TAY
;0461 68 PLA
;0462 aa TAX
;0463 68 PLA
;0464 58 CLI
;0465 40 RTI
ROM routines
Avoid any usage of Basic-ROM or Kernal-ROM routines. Not necessarily because SIDPLAY does not
include the original ROM images and does not otherwise emulate any ROM routines, but because ROM
functions generally imply any sort of I/O usage. A jump into the ROM memory is considered equal to
an ``RTS'' instruction. Some specific versions may include faked emulation
of a few often used and interrupt related ROM routines. If you don't want to break the
compatibility to other SID emulators, don't use these routines either.
Converting sample players
Some sidtunes play back 4-bit samples (or 3-bit, or less precision)
by changing the SID's master volume register $D418 in quick
succession (other techniques involve pulse width modulation, but
are not covered here).
This method of playing low-quality samples in sidtunes has been
referred to as volume samples, fake samples, or just
playback of digis (short form of digitized sounds).
Usually, such a music player uses a Non-Maskable-Interrupt
(NMI, vectors $0318/19 or $FFFA/FB) next to the
standard player subroutine to copy sample values into the master
volume register continuously. Sample frequencies of 4 kHz to 8 KHz
have been very common.
PlaySID introduced a set of new SID
registers, called the Extended Sound Interface
Device, which requires a modification of the original C64
sample players in order to activate the samples in SID emulators
which don't support emulation of unmodified C64 sample players,
but do support PlaySID's extensions. The possibility to modify
sample players comes at the cost of the necessity to disable the
original machine code routines that would play samples using the
SID chip normally.
|
The modified sidtunes no longer play on a real C64 and
neither in any emulator that doesn't support PlaySID's
extensions. Once modified, they are emulator-specific.
But often it is possible to leave most of the original
player untouched and add the modifications in a way that one
can re-activate (or re-rip) the old player without much
effort. You are advised to keep the unmodified sidtune
anyway.
|
Basically, the main aim of these sample registers has been in
aiding PlaySID in directly replaying the C64 samples via one of
the AMIGA's audio channels. Using these extended registers is
not as flexible and powerful as full-featured interrupt
emulation would be. But it allows sample playback at maximum
quality (well, sort of :).
SIDPLAY provides compatibility with these registers, but also
adds a second set of registers for a second sample channel (at
$D500).
This section only deals with setting up an examplary sample
player for SIDPLAY or PlaySID. If you want more examples,
consult the already converted sidtunes, or even better, contact
people who are familiar with these PlaySID extensions.
Modify the sample player to do the following once for
each sampled waveform you want to start. Usually you must search
for that particular part of the original music player which
communicates with its sample player interrupt handler (or
separate processor loop), and starts a new sample, or modifies
the interrupt speed (i.e. sample period). $D41D must
be the last register written to.
- Set the average master volume in $D418. This is
usually around $08 for 4-bit sample players. Find the
original value, and don't try to compensate for lack of loudness
in the emulation by setting a higher value.
- Set the sampling frequency in $D45D/$D45E in number
of cycles. This is usually the NMI refresh speed of the sample
player interrupt handler, e.g. the CIA 1 Timer A value in
$DC04/$DC05.
- Set the sample data start address in $D41E/$D41F.
- Set the sample data end address in $D43D/$D43E,
which is (!) the address of the last byte to be played
+1 (end-start = number of samples to be played).
- Set the sample data repeat address in $D47E/$D47F.
- Set the number of repeats in $D43F: $00 = none, $FF = endless,
$01 to $FE = else.
- Set the octave in $D45F: $00 = 1st,
$01 = 2nd, $02 = 3rd, $04 = 4th, and so on.
- Set the sample order in $D47D: $00 = low-nibble/high-nibble (the most common),
$01 = high-nibble/low-nibble.
- Enable sample replay in $D41D: $FF = 4-bit samples, $FE = 3-bit samples,
$FC = 2-bit samples. This depends on what range of values originally was written to the
master volume register.
- Disable sample replay with $D41D = $FD on demand.
Set all registers newly each time you start a sample. There is
no guarantee that any register preserves its value once the
sample playback has been started via $D41D. The emulator
sets $D41D to zero after is has started a sample. This
way it can detect when the next sample is to be started.
Ripping in disassemblings
A powerful way of ripping is to make a disassembling of the
complete memory using modern tools, and then search the code
listing within a normal text editor. This allows you to search
for pieces of text rather than hexadecimal numbers. For instance,
you can search for LDA $C70 if you are in search
of all LDA-instructions that access memory at $C70?. Or
you can search for $D400,X easily. This way way you can even
use regular expressions if the editor has them.
|