A constant-cycle music engine

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
A constant-cycle music engine
by on (#198437)
Over the weekend I wrote a music engine that runs in a constant number of cycles per frame. The intention was to have an engine that could be hidden in timing-critical code such as parallax effects, and also to avoid the dreaded lag spikes. Currently, the cycle count is 766, which makes it far faster than other open-source engines. For comparison, Famitone peaks at 3000+ cycles and GGSound peaks at 5000+. I think Pently peaks at 2500+, but I'm not sure.

The converter converts from Famitracker and supports instruments with Pitch/Volume/Arpeggio/Duty. Nothing else is supported; no effects nor volume columns. DMC is not supported, nor are sound effects or PAL pitches.

It uses 12 bytes of zeropage and $100-$156 in stack space. The converted songs are not as space efficient as other engines, but they're small enough for my purposes.

Anyway, you can find the code here: https://github.com/pubby/penguin Keep in mind this is not a library; I wrote the code for myself.

I'll attach a ROM that plays Ralph 4's music.
Re: A constant-cycle music engine
by on (#198451)
Cool, the idea of running the sound code at times you'd otherwise not do anything (e.g. while waiting for raster effects) is very interesting. And 766 cycles is pretty fast, about 7 scanlines only. Will take a better look at this when I get to my PC.
Re: A constant-cycle music engine
by on (#198461)
Quote:
nor are sound effects or PAL pitches.

PAL pitches are usually as simple as adapting the pitch table to PAL values.

As for sound effects, the lack of them would really make your engine unsuitable for a game, although the concept of a constance-cycle music engine still apply.

Personally I was wondering how feasible was a fully constant-cycle game engine feasible. This could ease raster split a lot, perhaps impressive parallax scrolling could be done in-game without even needed neither sprite-zero hits nor IRQs.
Re: A constant-cycle music engine
by on (#198537)
Very cool, and very impressive too! Less than 7 scanlines of time, that fits well within a row of background tiles.
Re: A constant-cycle music engine
by on (#198926)
Thanks.

The newest update adds sound effects and PAL pitches.

Sound effects were implemented as one-off instruments that play single notes. This makes them zero-cost; memory and cycles were not increased. PAL pitches on the other hand added 24 cycles and 2 bytes zeropage. The current cycle count is now 790 per frame.
Re: A constant-cycle music engine
by on (#198967)
pubby wrote:
PAL pitches on the other hand added 24 cycles and 2 bytes zeropage. The current cycle count is now 790 per frame.

They should come at zero costs, really. All you need is to change the pitch table's values. Sound engines needs not supporting PAL or NTSC pitches at runtimes - they should just be configured at compile time to run either version.

Yes, technically you *could* have a runtime check and this comes with some advantages (only one version of the ROM), however that's not how it's typically done - most of the time you'd want to have one NTSC version and one PAL version of the ROM, and all the difference is made compile-time only.
Re: A constant-cycle music engine
by on (#198970)
This is an interesting idea. Though it's not something I think I would ever want to do, it's always interesting to see a different approach to the problem. Thanks for sharing the source code.
Re: A constant-cycle music engine
by on (#212495)
I finally got around to using this, and found a few bugs along the way. The repo has been updated.

I forgot to mention, but it does support the full range of pitches and duty cycles, which Famitone does not. The ROM usage is pretty bad though; about 2x that of Famitone. Maybe one day I'll optimize it better.
Re: A constant-cycle music engine
by on (#212500)
this piece of BGM was written with the intention to comply with your driver, just thought i'd let you know. :) . Sadly, the mini game got shelved indefinitely as elseyf needed to tend to other things. It is meant to use the cycle constancy to reliably time a few scroll splits without irq support.
Re: A constant-cycle music engine
by on (#212502)
:shock: Well, this is humbling! By the time ggsound burns 760 cycles, it has just had its coffee and is still waking up in the morning. Ah, time to actually make sounds now! :lol: I can really learn something from this code. Thanks for sharing.
Re: A constant-cycle music engine
by on (#212504)
Feel free to mention anything pertinent in the engine's entry in the wiki's list of music engines.
Re: A constant-cycle music engine
by on (#212557)
Quote:
this piece of BGM was written with the intention to comply with your driver, just thought i'd let you know.

Cool! That sounds really, really good.

GradualGames wrote:
:shock: Well, this is humbling! By the time ggsound burns 760 cycles, it has just had its coffee and is still waking up in the morning. Ah, time to actually make sounds now! :lol: I can really learn something from this code. Thanks for sharing.

The biggest optimization comes from dividing the work out across multiple frames. I divided it into 4, and well, that's why it's 4x faster than famitone.
Re: A constant-cycle music engine
by on (#212558)
pubby wrote:
The biggest optimization comes from dividing the work out across multiple frames. I divided it into 4, and well, that's why it's 4x faster than famitone.

I've considered doing some of that in my own engine, checking for loop commands during downtime on frames that are not the start of a row. But to compensate for the lack of Sxx and Gxx, someone might need to use speed 3 to get (say) thirty-second-note resolution at 150 BPM. How would an engine that splits work over four frames cope with speed 3?

Also throw std::runtime_error("pattern size not multiple of 8"); would blow up on the 36-row patterns of the 2 AM theme in the FamiTracker version of the Thwaite soundtrack, which is in 9/8 time.
Re: A constant-cycle music engine
by on (#212559)
Division of workload across frames - that ought to be a good technique for any future driver aspiring to drive INL:s expansion audio project. More channels = more burden (and potentially more data, depending on how the extra channels are facilitated).
Re: A constant-cycle music engine
by on (#212561)
If I ever rewrote it I would probably use 3 frame resolution instead of 4. I didn't know much about FT music back then and it turns out that 3 speed is common and useful.

You can sometimes fake faster speed with arpeggio sequences, but that's a pain.
Re: A constant-cycle music engine
by on (#212569)
pubby wrote:

GradualGames wrote:
:shock: Well, this is humbling! By the time ggsound burns 760 cycles, it has just had its coffee and is still waking up in the morning. Ah, time to actually make sounds now! :lol: I can really learn something from this code. Thanks for sharing.

The biggest optimization comes from dividing the work out across multiple frames. I divided it into 4, and well, that's why it's 4x faster than famitone.

I'm having trouble imagining how I would do that. If I have an envelope with a loop point for example, I'm going to update the envelope and the associated channel registers on every frame. What work can you get away with spreading out? Maybe tempo and length counters as one example?
Re: A constant-cycle music engine
by on (#212588)
Reading the notes and preparing/assigning the instruments and doing other bookkeeping can be broken up across frames. In my case, it saved about ~450 cycles.

Reading the sequences and writing the APU registers has to be done every frame. I didn't implement any instrument compression, so that's a pretty big speed advantage over libraries that do.
Re: A constant-cycle music engine
by on (#212601)
pubby wrote:
Reading the notes and preparing/assigning the instruments and doing other bookkeeping can be broken up across frames. In my case, it saved about ~450 cycles.

Reading the sequences and writing the APU registers has to be done every frame. I didn't implement any instrument compression, so that's a pretty big speed advantage over libraries that do.

That's a great idea, this is making me want to refactor ggsound to work a similar way. Highly doubt it'll come close to the same speed, but I like the idea of making it the best it can be. Thanks for the inspiration. Sounds like I may need a little bit more ram, so that I can read the next envelope being prepared and then swap it into place for actual playback, where as at the moment I only store the current envelope.

I have another question. Do you precompile instruments such that their volume, pitch, duty sequences are all predetermined as register values? In my engine, I modify the current volume, pitch and duty values per channel per envelope at runtime. Is that what you're referring to? That might be a nice improvement as well...
Re: A constant-cycle music engine
by on (#212641)
Volume+duty is combined into a single sequence that maps directly to the APU register. Pitch and Arpeggio are separate sequences and a bit of arithmetic is done each frame to calculate them.

I only implemented one of FamiTracker's relative/absolute/fixed behavior (I used the default setting) for each sequence type, so that may also explain the difference.
Re: A constant-cycle music engine
by on (#212672)
pubby wrote:
Volume+duty is combined into a single sequence that maps directly to the APU register. Pitch and Arpeggio are separate sequences and a bit of arithmetic is done each frame to calculate them.

I only implemented one of FamiTracker's relative/absolute/fixed behavior (I used the default setting) for each sequence type, so that may also explain the difference.

Ah! Yeah I was thinking about this last night and realized it wouldn't be possible to precompile pitch or arpeggio since it has to work for any note that is playing. But I do have a lot of rather fiddly arithmetic in place to juggle the duty cycle values and volume values. If I leave in the capability to have a duty cycle envelope complete with loop points, I might not be able to pre-bake these if the loop points cause glitches in the duty cycle (like if the lengths of each envelope are not multiples of each other, and when the volume envelope goes back the duty cycle would have been in a different location). It looks like famitone and your engine perhaps only allow a single duty value to be used? *edit* I might want to change ggsound to work this way because I don't think I ever do anything more than one different duty value at the beginning for a crisp attack sound. having several values in a row sounds too rough/odd.

I actually have the ability to totally compile out arpeggios and dpcm from my engine because I never use them myself, haha. Arpeggios sound like telephones to me and dpcm causes that controller bug; don't want to deal with it :lol:

I also realized if famitone uses precompiled register values for sfx, that ggsound's sfx may be more flexible since they are basically tiny songs on their own that terminate by default (but could loop if desired)

Everything is a trade off!
Re: A constant-cycle music engine
by on (#212679)
GradualGames wrote:
I do have a lot of rather fiddly arithmetic in place to juggle the duty cycle values and volume values. If I leave in the capability to have a duty cycle envelope complete with loop points, I might not be able to pre-bake these if the loop points cause glitches in the duty cycle (like if the lengths of each envelope are not multiples of each other, and when the volume envelope goes back the duty cycle would have been in a different location).

In my engine, I just decided that volume envelopes aren't allowed to loop, and duty and arpeggio envelopes will loop only to the length of the volume envelope and stop. The envelope format is 1 byte for duty (2 bits), volume (4 bits), and whether arpeggio on this frame is nonzero (1 bit), followed by an arpeggio envelope value if needed. If you want to preserve ability to loop envelopes while baking duty into them, you could take the least common multiple of the volume and duty envelope loop lengths, use that as the overall loop length, and possibly emit a diagnostic in the converter if the resulting length exceeds the greater of the volume or duty envelope loop length.

GradualGames wrote:
having several values in a row sounds too rough/odd.

In some styles, you want rough.

GradualGames wrote:
I actually have the ability to totally compile out arpeggios and dpcm from my engine because I never use them myself, haha.

Likewise with PENTLY_USE_* flags.

GradualGames wrote:
Arpeggios sound like telephones to me and dpcm causes that controller bug; don't want to deal with it :lol:

Sometimes you want them to sound like telephones, as in my cover of "Disconnected" by Inspector K.

GradualGames wrote:
I also realized if famitone uses precompiled register values for sfx, that ggsound's sfx may be more flexible since they are basically tiny songs on their own that terminate by default (but could loop if desired)

Sound effects in Pently behave more like instruments, but they do have their own "speed" value such that you could encode a jingle in one.
Re: A constant-cycle music engine
by on (#212740)
GradualGames wrote:
It looks like famitone and your engine perhaps only allow a single duty value to be used?


Famitone is like this but my engine handles duty sequences. The converter does a little work to comebine the duty sequence with the volume sequence in a way that preserves both.

It breaks down if you loop both sequences and the loop lengths aren't multiple of each other, but that's extremely rare to run into.