Action 52 music engine data format ---------------------------------- 120508 K.Horton --- I must say I was somewhat surprised at the complexity of the music engine in this cart, especially after the poor quality of the games. One of two things is to blame I think. 1) this is a ripped music engine, and was hacked/used 2) this was written by the original author but barely used to its potential. Either one of these cases is probably as likely. -- Anyways, the music engine is a track based affair, with 8 identical tracks: Four tracks for the background music, and four for SFX. All 8 tracks are identical, and each channel (squares, tri, noise) get two tracks. one for music, one for SFX. If SFX are playing on a channel, it will take precidence over the music. The music tracks will continue to play, they just will not produce any audible output. There's a complete ADSR system for each track, except triangle which of course has no volume control. Noise is treated slightly special too when it comes to loading a pitch since it has only 16 settings. (plus the looped/non looped bit) -- To use the music engine, you must first load in two sets of pointers- a) You must load the SFX pointer b) Then you load the music pointer, which inits the rest of the engine. The games all seem to load this via a giant pointer reference table at the beginning when the game code starts. I disassembled the code from 'Cry Baby', but the code appears identical in all banks... however it moves around wildly in memory. I can only guess that it is dynamically linked at compile time or something. It references the exact same RAM area (0390-048F) which is exactly 256 bytes. There's a few unused "test" variables that are set, then a JSR to 80bbh occurs... This address results in an immediate RTS. It must've been some kind of test/debug subroutine. For Cry Baby, the following occurs: (note: simplified. it actually pulls the pointers from a list) LDX #06fh LDY #0c2h JSR loadsfxptr LDX #06fh LDY #0c2h LDA #07h ;this value is never used and stored in a debug reg. JSR loadmusptr So Cry Baby sets up both pointers to c26fh. We'll come back to that. -- To actually get music/SFX, you then initalize one or the other by writing the desired value into sfxinit or musicinit, depending on the desired function. This location usually holds 0ffh, meaning there is no update to perform. Writing a number here causes the engine to fetch the pointer to the desired data entry and start it playing. After you do this, then the main play routine is called every VBI.. This is the routine called "vbi". It expects vbiwait to be nonzero, otherwise it will sit here waiting for it. Once it's non-zero, it is cleared, and exceution continues. It appears the game uses this as its 'wait once a frame' loop. After the music code runs, it ends with an RTS and then the game does some other updates and then jumps back into here to wait again. -- So, that's how to work it, now for how the music is actually stored. We saw that the music and SFX can have two separate pointer tables... but this is never done on any of the games. Both pointers are always set to the same value. This doesn't really matter, but it's kinda funny they went through all the trouble and never used it. In the Cry Baby case, it loads the pointer as 0c26fh. Looking here, we see: C26F: .dw 0d1c9h, 0ffffh, 0d416h, 0d1b4h, 0d1a3h, 0ffffh C27B: .dw 0ffffh, 0ffffh, 0b8d8h, 0b8e9h, 0d416h There are 7 valid pointers, and 4 ffffh pointers which belong to SFX/music that were either removed or never existed. Why the games don't just use the first few entries, I never understood. It's almost like there were only 10 or so sfx and they just NOP'd out the unused ones. Also, one of the sfx is listed twice. So, the order: 00 -> d1c9: main tune 01 -> ffff: - 02 -> d416: player dies 03 -> d1b4: hit enemy 04 -> d1a3: bottle spray 05 -> ffff: - 06 -> ffff: - 07 -> ffff: - 08 -> b8d8: silence all 4 channels 09 -> b8e9: unpause doodoodooooo 0a -> d416: player dies (again) You can dink with this yourself in an NES emulator: load up the Action 52, select Cry Baby, and then open up the debugger. FCEU is good for this. Then in the "poke me!" box, put in 392 (SFX start byte) and poke 2, 3, or 4 in there and the SFX will play... You can poke other numbers in but the result will be silence or it will hose the SFX up as it tries to render other code/data as sounds. Poking 391 will start the title tune again (if you poke 0). It's not very interesting since all games only have 1 song. You can poke SFX in here but they won't start since it's outside the channel range. So, knowing that, let's look at the title tune's data: This is where we point to, with handy separation by me D1C9: .db 000h ;this is the channel # D1CA: .dw 0d1d3h ;and where this channel's data resides D1CC: .db 001h ;same as above D1CD: .dw 0d29ah D1CF: .db 003h ;same as above D1D0: .dw 0d338h D1D2: .db 0ffh ;special "end of data" marker. We can see then that this tune uses channels 0, 1, and 3... which are of course square 1, square 2, and the noise channel... skipping channel 2 which is the triangle. The channel designation of 0ffh (actually anything with bit 7 set) will tell the loader that this is the last channel to play. Jumping to the first pointer we see, here is channel 0's data: D1D3: .db 080h ;load the envelope/other channel attributes D1D4: .db 003h ;select waveform + ADR scale D1D5: .db 00fh ;get volume D1D6: .db 000h ;get pitch bend info D1D7: .db 01fh,01fh,000h,0f1h ;attack/decay/sustain/release D1DB: .db 08fh ;attack/sustain level D1DC: .db 018h,00fh ; note time, note D1DE: .db 024h,01bh, 00ch,00fh, 018h,01bh, 018h,00ah D1E6: .db 024h,016h, 00ch,00ah, 018h,016h, 018h,008h D1EE: .db 024h,014h, 00ch,008h, 018h,014h, 018h,00ah D1F6: .db 024h,016h, 00ch,00ah, 018h,016h D1FC: .db 080h ;load env+other attributes D1FD: .db 002h ;waveform + ADR scale D1FE: .db 00fh ;volume D1FF: .db 000h ;pitch bend info D200: .db 0f1h,01fh,000h,011h ;ADSR D204: .db 08fh ;att/sust level D205: .db 00ch,00fh, 00ch,00fh, 00ch,01bh, 00ch,01bh D20D: .db 00ch,016h, 00ch,016h, 00ch,022h, 00ch,022h D215: .db 00ch,014h, 00ch,014h, 00ch,020h, 00ch,020h D21D: .db 00ch,016h, 00ch,016h, 00ch,022h, 00ch,022h D225: .db 00ch,014h, 00ch,014h, 00ch,020h, 00ch,016h D22D: .db 00ch,022h, 00ch,014h, 00ch,020h, 00ch,016h D235: .db 00ch,022h, 00ch,011h, 00ch,01dh, 00ch,00fh D23D: .db 00ch,01fh, 00ch,01bh, 018h,01bh D243: .db 080h D244: .db 003h D245: .db 00fh D246: .db 000h D247: .db 01fh,01fh,000h,0f1h D24B: .db 08fh D24C: .db 018h,00fh, 024h,01bh, 00ch,00fh, 018h,01bh D254: .db 018h,00ah, 024h,016h, 00ch,00ah, 018h,016h D25C: .db 018h,008h, 024h,014h, 00ch,008h, 018h,014h D264: .db 018h,00ah, 024h,016h, 00ch,00ah, 018h,016h D26C: .db 080h D26D: .db 003h D26E: .db 00fh D26F: .db 000h D270: .db 01fh,01fh,000h,0f1h D274: .db 08fh D275: .db 018h,00fh, 024h,01bh, 00ch,00fh, 018h,01bh D27D: .db 018h,00ah, 024h,016h, 00ch,00ah, 018h,016h D285: .db 018h,008h, 024h,014h, 00ch,008h, 018h,014h D28D: .db 018h,00ah, 024h,016h, 00ch,00ah, 018h,016h D295: .db 0c0h ;command 4: loop from pointer to here 255 times D296: .dw 0d1d3h ;loop start point D298: .db 0ffh ;255 times through the loop! D299: .db 0a0h ;command 2: silence this channel --- So, there's a real live data example. I found it highly amusing that they actually use the loop command to restart the tune, since there's no provision for just jumping to the start again... this makes me think more and more that it's a "borrowed" music routine of some form. I confirmed that if you play the song and then poke 2 or 3 into the loop count registers (looppoint in the disasm) it does indeed silence the channel. You can also watch those RAM addresses count down each time the music loops. Interestingly, the "percussion" track is much shorter and loops much quicker. It will probably silence after only 6 or 8 minutes vs. the main part of the tune which will silence after god knows how long. Knowing that, let's look at the other 2 channels. --- channel 1: (note: I will abbriviate the channel init from now on) D29A: .db 080h,002h,00fh,000h ;channel init stuff D29E: .db 0f1h,01fh,000h,011h ;ADSR D2A2: .db 08fh D2A3: .db 018h,027h, 018h,027h, 018h,02eh, 018h,02eh D2AB: .db 018h,030h, 018h,030h, 018h,02eh, 018h,029h D2B3: .db 024h,02ch, 00ch,02ch, 018h,02ch, 018h,02bh D2BB: .db 018h,02bh, 00ch,029h, 00ch,027h, 018h,029h D2C3: .db 018h,02bh, 018h,027h, 018h,027h, 018h,02eh D2CB: .db 018h,02eh, 018h,030h, 00ch,032h, 00ch,030h D2D3: .db 018h,02eh, 018h,02eh, 00ch,02bh, 00ch,02bh D2DB: .db 00ch,02eh, 00ch,02bh, 00ch,029h, 00ch,029h D2E3: .db 00ch,02ch, 00ch,029h, 00ch,027h, 00ch,027h D2EB: .db 00ch,02bh, 00ch,02ch, 018h,02bh, 018h,029h D2F3: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h ;command 3: silence D2FB: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D303: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D30B: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D313: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D31B: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D323: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D32B: .db 0b0h,030h, 0b0h,030h, 0b0h,030h, 0b0h,030h D333: .db 0c0h ;command 4: loop D334: .dw 0d29ah ;address to loop from D336: .db 0ffh ;255 times D337: .db 0a0h ;silence --- channel 3 (noise) D338: .db 080h,001h,00fh,000h ;channel init stuff D33C: .db 0f1h,01fh,000h,011h ;ADSR D340: .db 08fh D341: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D349: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D351: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D359: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D361: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D369: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D371: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D379: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D381: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D389: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D391: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D399: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D3a1: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D3a9: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D3b1: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D3b9: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D3c1: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D3c9: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D3d1: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D3d9: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D3e1: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D3e9: .db 001h,000h, 0b0h,00bh, 001h,001h, 0b0h,005h D3f1: .db 001h,001h, 0b0h,005h, 001h,000h, 0b0h,00bh D3f9: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D401: .db 001h,000h, 0b0h,005h, 001h,000h, 0b0h,005h D409: .db 001h,001h, 0b0h,005h, 001h,001h, 0b0h,005h D411: .db 0c0h ;command 4: loop D412: .dw 0d338h ;loop back to here D414: .db 0ffh ;255 times D415: .db 0a0h ;then finally silence --------------------- So there's the entire main tune. The noise section is pretty repetitive with the same data loaded lots of times. The last 2 lines of data (D401, D409) are different than the others. I broke it up into 3 line groups since the data is the same in each. Now, for some SFX: One sits directly after the channel above, so let's look: (player dies) D416: .db 000h ;we'll use channel 0 D417: .dw 0d41ah ;here is where it starts D419: .db 0ffh ;and that's it. D41A: .db 080h,003h,00fh,000h ;channel init D41E: .db 01fh,01fh,000h,0f1h D422: .db 08fh D423: .db 018h,01bh, 018h,00fh, 00ch,00ch, 00ch,00ah D42B: .db 030h,003h D42D: .db 0c0h ;loop D42E: .dw 0d41ah ;to here D430: .db 00fh ;15 times D431: .db 0a0h ;then silence This sound effect is the "you died" one. not much else. let's look at the others... they sit in front of the main tune, for some reason: --- (bottle spray) D1A3: .db 003h ;channel 3 (noise) D1A4: .dw 0d1a8h ;which sits here D1A6: .db 0ffh ;last channel to load D1A7: .db 0a0h ;command 2: stop (not used anywhere) D1A8: .db 080h,001h,00fh,000h ;channel command D1AC: .db 0f1h,0f1h,015h,031h D1B0: .db 013h D1B1: .db 016h,001h ;note D1B3: .db 0a0h ;stop command --- the other SFX (hit enemy) D1B4: .db 000h D1B5: .dw 0d1b8h D1B7: .db 0ffh D1B8: .db 080h,002h,00fh,00ah D1BC: .db 01fh,01fh,000h,0f1h D1C0: .db 08fh D1C1: .db 091h ;command 1: detune/note tie. 91 = detune D1C2: .db 010h ;detune by 10 D1C3: .db 009h,003h, 0b0h,001h ;1 note, 1 rest D1C7: .db 0a0h,0a0h ;2 stop commands --- After the main whack of stuff up there, I found these floating around in the middle of what looks like unused code. This just silences all 4 channels, playing the silence command on each B8D8: .db 000h B8D9: .dw 0b8e5h B8DB: .db 001h B8DC: .dw 0b8e6h B8DE: .db 002h B8DF: .dw 0b8e5h B8E1: .db 003h B8E2: .dw 0b8e6h B8E4: .db 0ffh B8E5: .db 0a0h B8E6: .db 0a0h B8E7: .db 0a0h B8E8: .db 0a0h --- And the last sound effect: (unpause sound) B8E9: .db 000h B8EA: .dw 0b8f0h B8EC: .db 001h B8ED: .dw 0b900h B8EF: .db 0ffh B8F0: .db 080h,002h,005h,009h ;init channel B8F4: .db 01fh,01fh,000h,01fh B8F8: .db 08fh B8F9: .db 006h,01dh, 006h,01dh, 006h,01bh ;3 notes B8FF: .db 0a0h ;stop - B900: .db 080h,002h,00fh,009h B904: .db 0f1h,01fh,000h,011h B908: .db 08fh B909: .db 006h,01dh, 006h,01dh, 006h,01bh ;3 more notes B911: .db 0a0h ;stop ----------------------------- So, after all that here is the entire list of commands: (pattern list) musicpointers: .dw pattern0,pattern1,pattern2 .... pattern0: .db .dw .db .dw ... .db 0ffh ;marker for last channel chandata: commands: 00-7f: (2 bytes) ------ play note+duration 1 - number of VBIs for this note 2 - the note to play -- 00ch,012h Play note 12h for 0ch VBIs 8x: (9 bytes) ------ initialize a channel, loading ADSR regs + waveform + attributes 1 <80h> - command 0 2 - bitfield: bit 0,1 = waveform bits 2,3,4 = release multiplier (rel shifts left this many times) bits 5,6 = decay multiplier (decay shifts left this many times) bit 7 = attack multiplier (attack shifts left none or one time) 3 - lower 4 bits = initial envelope volume 4 - see below 5 - lower 4 bits = stepsize, upper 4 = VBI counts per step 6 - lower 4 bits = stepsize, upper 4 = VBI counts per step 7 - all 8 bits = how many VBIs to sustain 8 - lower 4 bits = stepsize, upper 4 = VBI counts per step 9 - lower 4 bits = attack lev., upper 4 = sust. lev. How the ADSR system works: When a note starts... 1) The volume is set to the initial volume (entry 3 above) (attack phase) 2) as time progresses, the volume is increased each time the attack counter counts to 0 and expires. The attack stepsize is added each time. When the volume reaches or exceeds the attack level, it is clamped and then... (decay phase) 3) The volume will decrease when the decay counter expires and is reloaded. The decay stepsize is subtracted. When we reach (or exceed) the sustain level, we clamp to the sustain level and... (sustain phase) 4) We simply wait the number of sustain counts, then... (release phase) 5) The volume will decrease when the release counter expires and is reloaded. The release stepsize is subtracted. When we reach (or exceed) 0, the volume is clamped to 0, and the channel is done playing. Notes: * All the counters except release are 8 bits, but the maximum count is limited depending on the number of times you can left shift it. * Attack is limited to 5 bits since it starts out as 4 and you can shift it once. i.e. 00fh -> 01eh is the limit. * Decay is limited to 8 bits since it starts out as 4 and you can left shift it up to 4x, so 00fh -> 0f0h. * Sustain has no shift, and is just 8 bits. * Release, unlike the previous 3 is 16 bits. It can be shifted up to 7 times, giving you 15 bits. This is a very long time. 0fh -> 0e800h. Pitch bend stuff: This is odd. Here is how it works: If 0 is loaded, nothing happens. If it is non-zero, 1 is subtracted, the result is ANDed with 0fh, then bit 7 is set, and finally the result is shoved into 4001/4005. I don't think it is used by any of the games. They manually pitch bend using the detune thing. -- 90: (1 byte) ------- note tie 1 <90> - command, note tie Commands 90,92,93... 9Fh all perform a note tie. A note tie is where you load the next note without restarting the ADSR system, so the new note starts but doesn't retrigger it. 91: (2 bytes) ------- Detune 1 <91> - command, detune 2 - amount to detune by: positive numbers will add to the detune register, negative numbers will subtract from it. The only way to clear the detune register is to reload a channel by restarting the sfx/music. Each VBI, this number will be subtracted from the frequency registers. Ax: (1 byte) ------- Silence 1 - command, silence This will silence a channel, and stop it playing. After this, the channel is dead and will no longer execute commands. Bx: (2 bytes) ------- Play a rest 1 - command, rest 2 - Loop command 2,3 - 16 bit pointer to the start of our looping section 4 - number of times to loop This is basically a "do... until" loop. When this command is run, it will load the loop count into the loop counter, then go back to the loop start via the pointer. Next time this executes, it will decrement the loop count until it's 0. When count hits 0, the next command will run after the loop command. Dx: (3 bytes) ------- Load music subroutine pointer address 1 - command, load pointer address 2,3 - 16 bit pointer to the subroutine address table I am not sure this was used. This will load the address of a pointer table into the music or SFX music subroutine pointer, depending on if it is called from a music or subroutine music snippet. This allows you to perform basic tracker pattern playing... These pointer tables are simply a list of patterns to play. The following command is then used to play these patterns from the list. E0-FE: (1 byte) ------ Call a music subroutine, i.e. play a pattern from the list. The instruction is ANDed with 1fh, which then is used to pull the address of one of the patterns. This way, you get a maximum of 31 patterns you can play. The old music data pointer is saved, then the new one is loaded from the list. FF: (1 byte) ------ music subroutine RTS This restores the data pointer to the previous value, exiting the pattern.