This question is in response to a YT viewer who asked about the above and to which I have no info.
OK so back in the eighties/nineties IIRC games often had this file format:
BASIC loader, loading screen, m/c file and a music file. So, with a discrete music file it was relatively(!) simple - copy the file to another disc and write a short program to load and play it.
But.....what about games and demos were there was no single music file?
Were/are there software (hardware?) tools for extracting music in this format?
The format you described (ie music file separated from the code) wasn't that common - it could be used with multilevel games that have different soundtracks for each level. In other cases, the code and music player usually were put together in one binary. Often the music data were also incorporated together with player and game code.
As someone who (albeit long time ago) ripped dozens of cpc game music I can tell you how it was done then :)
First, You needed to find out where the binary is loaded and what is the start address. Usually it could be determined by examining the loader. The loader could be in basic or in machine code - the latter would require disassembling. Sometimes the loader was protected against such analysis, so first you needed to crack/unprotect the loader. Sometimes the main binary was protected/encrypted so you needed to decrypt it first. Then you need to analyze the main game code to detect how the music is played. Most tunes were played from interrupt handler, but some were called from the main game loop. Finding out were was interrupt handler located was the key. Usually the interrupt handler was set at the beginning around the game start address - there were two methods of setting it up: one using system call or setting interrupt vector directly (most common) located at #38.
Fortunately most tunes were provided by composers as an independent module with 3/4 callable routines at the beginning of the module (eg. InitMusic, PlayMusic, StopMusic, PlaySoundEffect) so it was pretty easy to locate such module by just checking what address was called from an interrupt routine and in the main code before setting up/enabling interrupts.
The other option would be eg. searching for certain parameters for 'out' assembly opcode, that addressed audio chip.
When you had this done, you needed to save the play routine and music data and add some sort of init that would relocate the music data and the replay code to their original addresses, call the music init routine, setup and enable the interrupt and loop indefinitely.
Voila, the music has been ripped :)
Thank you for your interesting informative and detailed reply.
So, I had a fight with a friend over whether to add an example to the thread to complement the information by Docent. In the end, I guess he won and here's a lengthy post about extracting the three tunes from Ping Pong.
Older games (about 84, 85) mostly use the firmware calls for graphics and sound.
The most used SOUND calls are:
&BCA7 - SOUND RESET
&BCAA - SOUND QUEUE
&BCAD - SOUND CHECK
&BCBC - SOUND AMPL ENVELOPE
&BCBF - SOUND TONE ENVELOPE
The last two are needed for initialisation, SOUND QUEUE is essential to play the music, SOUND CHECK checks if the queue has an empty slot, and SOUND RESET makes sure that any prior configuration is undone.
Those calls are usually used with straightforward CALL commands, so searching them will be easy. This can be done nicely in WinAPE.
For a more sophisticated approach to reverse engineering I use Ghidra nowadays, there's also IDA (Pro) which in the free version supports only x86, and radare2, which is open source but too complicated for my tastes.
A game with one binary file is simple to import. Start WinAPE, load the disk, edit the disk, look at the properties of the file and note the loading address as well as the start address - if the binary does not contain the latter, look for it in the BASIC or machine code loader.
Export the file next, but make sure you exclude the AMSDOS header (there's an option in WinAPE for that).
I'm not going into details about Ghidra, it's a very powerful tool with a lot of options. Create a project load the binary and choose the right processor, Ghidra disassembles the code automatically, but this has its limits so you will have to press "D" manually sometimes in places to disassemble parts it has not recognised yet for various reasons.
At first there's a bit of work to do to make sense of the code. Many memory references have to be set manually, like ld hl, 0xABCD so that it points to the location. It pays off as soon as you have to identify the code parts and make sense of many of the variables because the memory location lists all the code addresses that reference this address. And you can even peek into the code by moving the mouse over those references, thus getting an idea where it is used and to which values it will be set in the code.
Code-PingPong.bin.png
Searching for these firmware calls is done rather quickly and the memory references can be relabeled which renames all the references throughout the code as well.
Since I dabbled with PingPong recently, I thought I'd give the three tunes in it a try.
The game initialises the envelopes right at the start (10 ampl envelopes, 11 tone envelopes), but luckily only one of each are needed for the game tunes.
; SOUND_AMPL_ENVELOPE - address 5A9E
.SOUND_AMPL_ENVELOPE_BLOCK
db #02
db #01, #0F, #01
db #0C, #FF, #06
; SOUND_TONE_ENVELOPE - address 5AE2
.SOUND_TONE_ENVELOPE_BLOCK
db #84
db #05, #00, #01
db #01, #F9, #01
db #05, #00, #01
db #01, #07, #01
The game has two routines that use SOUND QUEUE, the first one is reserved exclusively for sound effects. I extracted the routine and added one of the effects for demonstration purposes.
The code fetches the first byte which contains the number of SOUND QUEUE blocks it should play then loops through those blocks of 9 bytes each. I called those constructs "enhanced" sound queue blocks (viz. label: SOUND_QUEUE_BLOCK_ENH_01)
To make the code snippet runnable I included a simple header that initialises the sound based on the game data, and then loops this sound effect endlessly via the original routine. You can copy and paste it into WinAPE.
.SOUND_QUEUE equ #BCAA
.SOUND_AMPL_ENVELOPE equ #BCBC
.SOUND_TONE_ENVELOPE equ #BCBF
org #4000
run #4000
.START
call SE_SOUND_INIT
call SE_SOUND_EFFECTS
jr START
.SE_SOUND_INIT
ld a, 5
ld hl, AMPL_ENV_BLOCK_5
call SOUND_AMPL_ENVELOPE
ld a, 5
ld hl, TONE_ENV_BLOCK_5
call SOUND_TONE_ENVELOPE
ret
.SE_SOUND_EFFECTS ; address 5E84
push af
push bc
push de
ld b, (hl)
inc hl
.SE_SOUND_QUEUES_LOOP
push bc
push hl
.SE_QUEUE_READY_YET
call SOUND_QUEUE
jr nc, SE_QUEUE_READY_YET
pop hl
ld de, 9 ; length of one SOUND QUEUE data block
add hl, de
pop bc
djnz SE_SOUND_QUEUES_LOOP
pop de
pop bc
pop af
ret
.AMPL_ENV_BLOCK_5 ; address 5A7B
db #03
db #05, #FD, #07
db #02, #00, #01
db #03, #04, #01
.TONE_ENV_BLOCK_5 ; address 5ABB
db #82
db #01, #0C, #03
db #04, #FD, #03
.SOUND_QUEUE_BLOCK_ENH_01 ; address 5807
db #04 ; no. of sound queue blocks
db #01, #00, #00, #3C, #00, #00, #0F, #07, #00 ; S Q block no. 1
db #01, #00, #00, #28, #00, #00, #0F, #07, #00 ; S Q block no. 2
db #01, #00, #00, #1E, #00, #00, #0F, #07, #00 ; S Q block no. 3
db #01, #05, #05, #0F, #00, #00, #0F, #3C, #00 ; S Q block no. 4
The second routine that uses SOUND_QUEUE is invoked through a kernel event routine. I made the mistake to include those event initialisation routines in the music program, which didn't work. To avoid diving deeper into the code and finding the reason, I simply strapped these parts and made a simple header routine that allows you to listen to every one of the three tunes in Pingpong. I also made sure to leave all the original code and data untouched. Only one "ret z" was substitued with a "jr z, ..." to make the code work without the kernel event routine.
Again, this is playable in WinAPE as is.
; firmware labels
.KM_RESET equ #BB03
.KM_WAIT_KEY equ #BB18
.TXT_OUTPUT equ #BB5A
.SCR_SET_MODE equ #BC0E
.SCR_SET_INK equ #BC32
.SCR_SET_BORDER equ #BC38
.SOUND_QUEUE equ #BCAA
.SOUND_CHECK equ #BCAD
.SOUND_AMPL_ENVELOPE equ #BCBC
.SOUND_TONE_ENVELOPE equ #BCBF
; program labels
.START equ #4000
run START
org START
call PRINT_TEXT
call SOUND_INIT
.KEYPRESS_LOOP
call KM_RESET
call KM_WAIT_KEY
cp '1'
jr z, KEYPRESS_MUSIC_START_GAME
cp '2'
jr z, KEYPRESS_MUSIC_WIN
cp '3'
jr nz, KEYPRESS_LOOP
.KEYPRESS_MUSIC_GAME_OVER
ld hl, MUSIC_GAME_OVER
.SOUND_LOOP
ld (SMR_SOUND_MUSIC_ROUTINE+1), hl
call SMR_SOUND_MUSIC_ROUTINE
jr KEYPRESS_LOOP
.KEYPRESS_MUSIC_START_GAME
ld hl, MUSIC_START_GAME
jr SOUND_LOOP
.KEYPRESS_MUSIC_WIN
ld hl, MUSIC_WIN
jr SOUND_LOOP
; sets colours and prints a few explanations
.PRINT_TEXT
ld a, 1
call SCR_SET_MODE
ld bc, 0
call SCR_SET_BORDER
xor a
ld bc, 0
call SCR_SET_INK
ld a, 1
ld bc, #1A1A
call SCR_SET_INK
ld hl, TEXT_EXPLANATION
.PRINT_TEXT_LOOP
ld a, (hl)
inc hl
and a
ret z
call TXT_OUTPUT
jr PRINT_TEXT_LOOP
.TEXT_EXPLANATION
db ' PINGPONG - IN GAME MUSIC', #0A, #0D
db ' ------------------------', #0A, #0D, #0A, #0D
db ' Press keys 1, 2, or 3 to', #0A, #0D
db ' listen to the music.', #00
.SOUND_INIT
ld hl, SOUND_AMPL_ENVELOPE_BLOCK
ld a, 12
call SOUND_AMPL_ENVELOPE
ld hl, SOUND_TONE_ENVELOPE_BLOCK
ld a, 12
call SOUND_TONE_ENVELOPE
ret
.SMR_SOUND_MUSIC_ROUTINE ; address 5E9C
ld hl, 0 ; must be filled in advance with the pointer to the music data
ld a, (hl)
or a
ret z
and %00000001
inc a
call SOUND_CHECK
and %00000111
jr z, SMR_SOUND_MUSIC_ROUTINE ; if bits 0-2 are 0 then there is no free space in the sound queue. This originally was "ret z"
ld hl, (SMR_SOUND_MUSIC_ROUTINE+1)
ld a, (hl)
and %00000001
inc a
bit 1, (hl)
jr z, SMR_ONE_CHANNEL_ONLY
or %00011000 ; rendez-vous channel A and B
.SMR_ONE_CHANNEL_ONLY
ld (SOUND_QUEUE_BLOCK_1), a
ld a, (hl)
inc hl
and %11111100
jr nz, SMR_QUEUE_BLOCK_1
ld de, SOUND_QUEUE_BLOCK_2
jr SMR_OUTPUT_SOUND_QUEUE
.SMR_QUEUE_BLOCK_1
ld b, a
add a, a
add a, b
cp 244
jr nz, SMR_COUNTER_NO_RESET
ld a, 6
.SMR_COUNTER_NO_RESET
ld (SOUND_QUEUE_BLOCK_1+7), a ; sound duration
ld e, (hl)
inc hl
ld d, (hl)
inc hl
ld (SOUND_QUEUE_BLOCK_1+3), de ; tone period
ld de, SOUND_QUEUE_BLOCK_1
.SMR_OUTPUT_SOUND_QUEUE
ld (SMR_SOUND_MUSIC_ROUTINE+1), hl
ex de, hl
call SOUND_QUEUE
jp SMR_SOUND_MUSIC_ROUTINE
; SOUND_QUEUE_BLOCKS
.SOUND_QUEUE_BLOCK_1 ; address 5EEB
db #00, #0C, #00, #00, #00, #00, #00, #00, #00
.SOUND_QUEUE_BLOCK_2 ; address 5908
db #0A, #00, #0C, #3C, #00, #00, #0A, #D8, #00
; SOUND_AMPL_ENVELOPE
.SOUND_AMPL_ENVELOPE_BLOCK ; address 5A9E
db #02
db #01, #0F, #01
db #0C, #FF, #06
; SOUND_TONE_ENVELOPE
.SOUND_TONE_ENVELOPE_BLOCK ; address 5AE2
db #84
db #05, #00, #01
db #01, #F9, #01
db #05, #00, #01
db #01, #07, #01
; three pieces of music
.MUSIC_START_GAME ; address 58A0
db #04, #1C, #01
db #04, #FD, #00
db #0A, #EF, #00
db #04, #EF, #00
db #0F, #DE, #01
db #06, #EF, #00
db #04, #FD, #00
db #04, #EF, #00
db #0F, #7E, #02
db #0E, #BE, #00
db #0F, #7B, #01
db #0E, #EF, #00
db #0F, #DE, #01
db #0A, #D5, #00
db #04, #D5, #00
db #0F, #AA, #01
db #06, #D5, #00
db #04, #EF, #00
db #04, #D5, #00
db #0F, #38, #02
db #0E, #B3, #00
db #0F, #66, #01
db #0A, #D5, #00
db #04, #BE, #00
db #0F, #AA, #01
db #01, #0A, #B3
db #00, #04, #9F
db #00, #08, #B3
db #00, #04, #9F
db #00, #08, #8E
db #00, #04, #7F
db #00, #08, #8E
db #00, #04, #7F
db #00, #18, #77
db #00, #00
.MUSIC_WIN ; address 592E
db #08, #BE, #00
db #04, #B3, #00
db #06, #9F, #00
db #04, #9F, #00
db #04, #9F, #00
db #0F, #3F, #01
db #0A, #A9, #00
db #04, #9F, #00
db #0F, #52, #01
db #0E, #A9, #00
db #0F, #7B, #01
db #06, #8E, #00
db #04, #9F, #00
db #04, #A9, #00
db #0F, #AA, #01
db #1A, #77, #00
db #0F, #EF, #00
db #09, #3F, #01
db #05, #3F, #01
db #0D, #EF, #00
db #00
.MUSIC_GAME_OVER ; address 596B
db #04, #EF, #00
db #04, #EF, #00
db #04, #D5, #00
db #04, #BE, #00
db #16, #B3, #00
db #FC, #9F, #00
db #FC, #96, #00
db #0F, #DE, #01
db #0D, #FA, #01
db #0A, #8E, #00
db #04, #77, #00
db #0F, #38, #02
db #0A, #B3, #00
db #04, #77, #00
db #0F, #7E, #02
db #06, #9F, #00
db #04, #9F, #00
db #04, #9F, #00
db #07, #7E, #02
db #05, #7E, #02
db #05, #7E, #02
db #0A, #8E, #00
db #04, #7F, #00
db #0B, #38, #02
db #05, #FA, #01
db #0E, #77, #00
db #0F, #DE, #01
db #00
Here's another - and the final - rip of game music: Roland on the Ropes. Everything said above applies here as well.
Plus:
Games usually have at least one big block of CALLs after the necessary initialisation code that is looped through. I usually name this main_game_loop.
Within it the title music must be hidden. Looking at how the game starts up, it's obvious that it must start right after printing the title screen with the menu. This one was easier than Ping Pong because Roland on the Ropes uses three CALLs in the same loop where the keys "1" to "5" are tested. Those CALLs correspond to the three sound channels. And there's not much more to it. Extract the code, find the data and write a little header routine to initialise the envelopes. VoilĂ !
The code as before is usable in WinAPE as is, and the addresses in the comments are the original addresses in the game.
.SOUND_RESET equ #BCA7
.SOUND_CHECK equ #BCAD
.SOUND_QUEUE equ #BCAA
.SOUND_AMPL_ENVELOPE equ #BCBC
org #8000
run START
.START
call INIT_TITLE_SONG
.REPEAT
call TITLE_SONG_CHANNEL_A
call TITLE_SONG_CHANNEL_B
call TITLE_SONG_CHANNEL_C
jr REPEAT
.INIT_TITLE_SONG
call SOUND_RESET
ld a, 1
ld hl, SOUND_AMPL_ENVELOPE_CHANNEL_AB
call SOUND_AMPL_ENVELOPE
ld a, 2
ld hl, SOUND_AMPL_ENVELOPE_CHANNEL_C
call SOUND_AMPL_ENVELOPE
ld a, 252
ld (SONG_COUNTER_CHANNEL_A), a
ld a, 255
ld (SONG_COUNTER_CHANNEL_B), a
dec a
ld (SONG_COUNTER_CHANNEL_C), a
ret
.TITLE_SONG_CHANNEL_A ; address A510
ld a, 1
call SOUND_CHECK
and %00000111
ret z ; check if queue of channel A is empty, return if not
ld a, (SONG_COUNTER_CHANNEL_A)
cp 200
jr c, TSA_JUMPFORWARD_1
ld a, 255
ld (SONG_COUNTER_CHANNEL_B), a
ld a, 252
.TSA_JUMPFORWARD_1
add a, 4
ld (SONG_COUNTER_CHANNEL_A), a
ld hl, SONG_DATA_CHANNEL_A
ld d, 0
ld e, a
add hl, de
cp 36
jr nz, TSA_JUMPFORWARD_2
ld a, %00100001 ; status byte - output to channel A + rendez-vous with channel C
ld (SOUND_QUEUE_BLOCK_CHANNEL_A), a
.TSA_JUMPFORWARD_2
ld ix, SOUND_QUEUE_BLOCK_CHANNEL_A
ld a,(hl)
ld (ix+3), a ; tone period low
inc hl
ld a,(hl)
ld (ix+4), a ; tone period high
inc hl
ld a,(hl)
ld (ix+7), a ; duration low
inc hl
ld a,(hl)
ld (ix+8), a ; duration high
ld hl, SOUND_QUEUE_BLOCK_CHANNEL_A
call SOUND_QUEUE
ld a, %00000001 ; status byte - output to channel A
ld (SOUND_QUEUE_BLOCK_CHANNEL_A), a
ret
.TITLE_SONG_CHANNEL_B ; address A5B5
ld a, 2
call SOUND_CHECK
and %00000111
ret z ; check if queue of channel B is empty, return if not
ld a, (SONG_COUNTER_CHANNEL_B)
cp 80
jr c, TSB_JUMP_FORWARD
ld a, 255
.TSB_JUMP_FORWARD
inc a
ld (SONG_COUNTER_CHANNEL_B), a
ld hl, SONG_DATA_CHANNEL_B
ld d, 0
ld e, a
add hl, de
add hl, de
add hl, de
add hl, de
ld ix, SOUND_QUEUE_BLOCK_CHANNEL_B
ld a,(hl)
ld (ix+3), a ; tone period low
inc hl
ld a,(hl)
ld (ix+4), a ; tone period high
inc hl
ld a,(hl)
ld (ix+7), a ; duration low
inc hl
ld a,(hl)
ld (ix+8), a ; duration high
ld hl, SOUND_QUEUE_BLOCK_CHANNEL_B
call SOUND_QUEUE
ld a, %00000010 ; status byte - output to channel B
ld (SOUND_QUEUE_BLOCK_CHANNEL_B), a
ret
.TITLE_SONG_CHANNEL_C ; address A662
ld a, 4 ; check if queue of channel C is empty, return if not
call SOUND_CHECK
and %00000111
ret z
ld a, (SONG_COUNTER_CHANNEL_C)
cp 24
jr c, TSC_CONTINUE
ld a, (SONG_COUNTER_CHANNEL_A)
cp 48
jr nc, TSC_NO_SOUND_QUEUE
ld a, 12 ; status byte - output to channel C, rendez-vous with channel A
ld (SOUND_QUEUE_BLOCK_CHANNEL_C), a
.TSC_NO_SOUND_QUEUE
ld a, 254
.TSC_CONTINUE
add a, 2
ld (SONG_COUNTER_CHANNEL_C), a
ld hl, SONG_DATA_CHANNEL_C
ld d, 0
ld e, a
add hl, de
ld ix, SOUND_QUEUE_BLOCK_CHANNEL_C
ld a, (hl)
ld (ix+7), a ; duration low
inc hl
ld a,(hl)
ld (ix+8), a ; duration high
ld hl, SOUND_QUEUE_BLOCK_CHANNEL_C
call SOUND_QUEUE
ld a, %00000100 ; status byte - output to channel C
ld (SOUND_QUEUE_BLOCK_CHANNEL_C), a
ret
.SOUND_AMPL_ENVELOPE_CHANNEL_AB ; address A4FF
db 2, 1, 0, 7, 9, 255, 9
.SOUND_AMPL_ENVELOPE_CHANNEL_C ; address A506
db 4, 3, 4, 2, 2, 253, 2, 6, 255, 8
.SONG_COUNTER_CHANNEL_A ; address A4FE
db 0
.SONG_COUNTER_CHANNEL_B ; address A600
db 0
.SONG_COUNTER_CHANNEL_C ; address A6C7
db 0
.SOUND_QUEUE_BLOCK_CHANNEL_A ; address A55E
db 1, 1, 0, 0, 0, 0, 15, 0, 0
.SOUND_QUEUE_BLOCK_CHANNEL_B ; address A5F7
db 2, 1, 0, 0, 0, 0, 14, 0, 0
.SOUND_QUEUE_BLOCK_CHANNEL_C ; address A6A4
db 4, 2, 0, 0, 0, 2, 0, 0, 0
; song data contains the tone period and the duration, one per line
; db low-byte tone period, high-byte tone period, low-byte duration, high-byte duration
.SONG_DATA_CHANNEL_A ; address 9F84
db 0, 0, 40, 0
db 119, 0, 40, 0
db 89, 0, 40, 0
db 80, 0, 40, 0
db 71, 0, 40, 0
db 89, 0, 40, 0
db 67, 0, 40, 0
db 71, 0, 40, 0
db 80, 0, 40, 0
db 89, 0, 240, 0
db 0, 0, 40, 0
db 119, 0, 40, 0
db 89, 0, 40, 0
db 80, 0, 40, 0
db 71, 0, 40, 0
db 89, 0, 40, 0
db 60, 0, 40, 0
db 89, 0, 40, 0
db 53, 0, 240, 0
db 0, 0, 40, 0
db 53, 0, 40, 0
db 53, 0, 40, 0
db 47, 0, 40, 0
db 45, 0, 40, 0
db 47, 0, 40, 0
db 47, 0, 40, 0
db 53, 0, 40, 0
db 60, 0, 40, 0
db 89, 0, 240, 0
db 0, 0, 40, 0
db 89, 0, 40, 0
db 89, 0, 40, 0
db 80, 0, 40, 0
db 71, 0, 40, 0
db 60, 0, 120, 0
db 0, 0, 40, 0
db 89, 0, 40, 0
db 89, 0, 40, 0
db 80, 0, 40, 0
db 71, 0, 40, 0
db 60, 0, 120, 0
db 0, 0, 40, 0
db 89, 0, 40, 0
db 89, 0, 40, 0
db 71, 0, 40, 0
db 60, 0, 80, 0
db 71, 0, 80, 0
db 89, 0, 80, 0
db 80, 0, 80, 0
db 89, 0, 240, 0
db 0, 0, 40, 0
db 240, 240, 240, 240
db 240, 240, 240, 240
db 240, 240, 240, 240
db 240, 240, 240, 240
db 240, 240, 240, 240
db 240, 240, 240, 240
db 240, 240, 240, 240
db 224, 240, 240, 240
db 224, 240, 240, 240
db 225, 240, 240, 240
db 225, 240, 240, 240
db 224, 240, 240, 240
db 224, 240, 240, 240
db 225, 240, 240, 240
db 225, 240, 240, 240
; song data contains the tone period and the duration, one per line
; db low-byte tone period, high-byte tone period, low-byte duration, high-byte duration
.SONG_DATA_CHANNEL_B ; address 6D60
db 179, 0, 80, 0
db 119, 0, 80, 0
db 179, 0, 80, 0
db 119, 0, 80, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 80, 0
db 119, 0, 80, 0
db 179, 0, 80, 0
db 89, 0, 80, 0
db 134, 0, 40, 0
db 89, 0, 20, 0
db 89, 0, 20, 0
db 134, 0, 40, 0
db 89, 0, 40, 0
db 142, 0, 40, 0
db 106, 0, 20, 0
db 106, 0, 20, 0
db 213, 0, 40, 0
db 106, 0, 40, 0
db 159, 0, 40, 0
db 106, 0, 20, 0
db 106, 0, 20, 0
db 159, 0, 40, 0
db 106, 0, 40, 0
db 159, 0, 80, 0
db 150, 0, 80, 0
db 142, 0, 40, 0
db 89, 0, 20, 0
db 89, 0, 20, 0
db 142, 0, 40, 0
db 89, 0, 40, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 89, 0, 40, 0
db 134, 0, 80, 0
db 89, 0, 80, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 119, 0, 40, 0
db 134, 0, 80, 0
db 89, 0, 80, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 119, 0, 40, 0
db 134, 0, 80, 0
db 89, 0, 80, 0
db 119, 0, 40, 0
db 60, 0, 80, 0
db 60, 0, 40, 0
db 239, 0, 40, 0
db 119, 0, 80, 0
db 119, 0, 40, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 119, 0, 20, 0
db 119, 0, 20, 0
db 179, 0, 40, 0
db 119, 0, 40, 0
db 179, 0, 40, 0
db 0, 0, 40, 0
db 0, 0
; song data for channel c contains the duration, one per line
; it's the noise channel for the percussion
; db low-byte duration, high-byte duration
.SONG_DATA_CHANNEL_C ; address A6AD
db 40, 0
db 20, 0
db 20, 0
db 40, 0
db 20, 0
db 20, 0
db 20, 0
db 20, 0
db 20, 0
db 20, 0
db 40, 0
db 20, 0
db 20, 0