How I made the music engine for Tetramino 0.37

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
How I made the music engine for Tetramino 0.37
by on (#44280)
On 43folders, I learned that a to-do list is more effective when broken up into tasks that can be done in one sitting, each of the form "use X to do Y, resulting in Z". It's also easier to make a music engine if you can factor it out into a separate module that exposes specific functions to the game. I wrote a separate test harness that calls the same API that I use in the game.

External API used by the game:
init_sound() - Inits the audio subsystem. Called only at system startup.
init_music(A=song_id) - Starts a song.
stop_music() - Stops a song.
start_sound(A=fx_id) - Picks a channel for a PSG sound effect and starts it.
start_sample(A=dpcm_id) - Starts a DPCM sound effect.
update_sound() - Called once per vertical blank, after the PPU update code, to update the PSG registers.

Compare to the NSF API:
nsf_init_music(A=song_id) - Calls init_sound() and init_music(A=song_id)
update_sound()

Internal API used by update_sound():
update_music() - Interprets sequence data.
update_instrument(X=channel_id)
update_sound_effect(X=channel_id)

Then it's easy to have sound effects interrupt music properly: First call update_instrument(), then have update_sound_effect() overwrite the duty+volume and pitch if a sound effect is running on that channel, then write the duty+volume and pitch to the channel's register. In fact, I'm planning on doing drums using the sound effect system, which would make triangle drums easy as pie.

Tetramino 0.33 implements all of this except music. Because I had broken up the tasks, I was able to sketch out init_music(), update_music() and update_instrument() in about four fifteen-minute sessions on the bus today, and you'll see the result in Tetramino 0.37 once I get some non-trivial sequences and instruments developed.
Re: How I made the music engine for Tetramino 0.37
by on (#44282)
tepples wrote:
Internal API used by update_sound():
update_music() - Interprets sequence data.
update_instrument(X=channel_id)
update_sound_effect(X=channel_id)

Then it's easy to have sound effects interrupt music properly: First call update_instrument(), then have update_sound_effect() overwrite the duty+volume and pitch if a sound effect is running on that channel, then write the duty+volume and pitch to the channel's register.


What if you did it backwards? You might be able skip a lot of unnecessary code calculating and writing duty/vol/pitch for instruments that you aren't going to use:

1) Update the SFX first, and set bitflags for which channels are running SFX.
2) update the instruments (I'm assuming this is music instruments?), checking the bitflags and skipping calculations and writes for channels that are already used by the SFX.

by on (#44285)
I still need to step the instruments to the next frame, even if a sound effect is running. At that point, it's only about 6 instructions to stuff them into the $0002 (duty/vol) and $0003 (pitch) output addresses.