News:

Printed Amstrad Addict magazine announced, check it out here!

Main Menu
avatar_ComSoft6128

Ripping music query

Started by ComSoft6128, 10:51, 02 April 22

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

ComSoft6128

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?


Docent

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 :)

 

ComSoft6128

#2
Thank you for your interesting informative and detailed reply.

MaV

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.

You cannot view this attachment.

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
Black Mesa Transit Announcement System:
"Work safe, work smart. Your future depends on it."

MaV

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
Black Mesa Transit Announcement System:
"Work safe, work smart. Your future depends on it."

Powered by SMFPacks Menu Editor Mod