Hey guys, just going to pull up to the plate and start working on my own NES sound engine! Now, first thing is first:the file format. What do you guys think of this little bit so far?
http://ideone.com/r6YxN
Code:
0x00-0x5F=Note value pulled from chart.
0x60-0x7F=Opcodes without tramping into $80 range?
Format:
#%1100.00BB=Whole byte bit value to follow. Single value. May be scrapped and the opcode #%1101.BBBB will be used only.
#%1101.BBBB=Array of bytes to follow opcode that each go into a register, for changing 2-3 or so registers at a time with minimal opcodes.
Way more opcodes to come for looping and other stuff. Will get there when I get there.
0x80-0xFF=Bit set for half bytes of each register.
Format:
#%1HBB.VVVV
H=High value byte to change.
BB=Byte to change inside the buffer, choose between 4 bytes.
VVVV=Value of the bits to set in the byte.
I plan on making opcode #%1101.BBBB just take over #%1100.00BB and make it #%1100.BBBB, think that's just a god idea to keep it simple? Will this work? I'm looking at it more of a "data writing scripting engine" moreso than a sound engine, is that okay to do?
Can you define your goals first? Do you aim for replayer speed, data size, advanced features, etc?
So far it looks like you going to pack a register dump a bit. I.e. very fast player, but huge data for complex songs.
My goal is to play music using the sound channels, I plan on adding more stuff to it like looping (indefinitely or a number of times), not much (any) software mixing for anything, and just standard features for now. I mean, I don't think this format would require a huge amount of space, I'll add opcodes as I need them to do more dynamic stuff. I also plan on each channel having 8 or so bytes of "stack" to dump off the location for loops, and other variables, so more can be added to it easily if need be. Any more questions? I want it to be as expandable but small as possible. Is this data format looking okay so far? The 0x00-0x5F region is also ALL notes, 100% taken up. So 0x60+ is all I have to work with for any other data. This is what I'm trying to come up with, I think I'll be good with only 10 or so opcodes, but it looks like with this I'll have up to 32+. Is that good or average or terrible for a sound engines features?
If you want an example of how the music data format might look, Famitracker is a good one. The macro/instrument, and pattern/order features are very effective at keeping data size down, and are good units to work with for the user. Its actual generated data isn't the most compact it could be, but it's not bad. With a little tweaking, or removal of some unneeded features it could be quite compact indeed. Its player, however, is not written to minimize code size or RAM usage.
Direct register writes like you're proposing, 3gen, are generally very verbose and the music data will take up a lot of space. The player code itself can be tiny, however.
Unless you only have one small song in your ROM, if you're trying to minimize data size it's better to focus on the music data being small at the expense of more complexity in the player. (i.e. if you write a volume macro feature, the player code takes up space once, but it saves space in the music data as many times as you use it.)
I mean, what more is needed though? Help me improve on it by telling me what to do differently. I myself don't see how this would be bad as it'd be like 5 bytes for the start of the song, 1 per note, I will probably have to add an opcode in to set the "standard" spacing between updates, and maybe more...but I dunno what else to look for. Anyone have documents of other more efficient formats?
NES sound hardware does not know anything about notes, does not support vibrato or arbitrary (read: actually useful) volume envelopes - all this has to be done in software, which leads to lots of register writes. You either do it on the content creation side, having huge data (although with LZ-like packing you can reduce it very well, but that's a difficult and tricky way), or in the player itself, which is more CPU-intensive, but allows to have much more compact data.
I don't see where your 1 byte per note comes from, as if you going to write registers directly, you will have to put 2-4 bytes for every frequency change, 1-2 bytes for every volume change, etc. And these could happen almost every frame for pulse channels, less often for triangle and noise.
You missed the part where I'm trying to think of how to do things, nothing is set in stone, I'll change whatever needs to be changed. Plus, I do have what, 50ish opcodes to add that kinda of stuff in to it? I have plenty of room, just give me ideas on what needs to happen.
Just compare your apporach with FamiTone's format (which is still not the most effective). You'll see a major difference in abstraction level.
Next step to reduce size of music data is to handle situation 'note then empty row' in a special way, because this happens very often in actual music.
Code:
Envelope format:
<127 is a number of repeats of previous output value
127 is end of an envelope, next byte is new offset in envelope
128..255 is output value + 192 (it is in -64..63 range)
Envelopes can't be longer than 255 bytes
Stream format (one stream per hardware sound channel):
%00nnnnnn is a note (0..59 are octaves 1-5, 63 note stop)
%01iiiii0 is an instrument number (0 is default, silence)
%10rrrrrr is a number of empty rows (up to 63)
%11eeeeee is a special tag or effect
eeeeee $01..19 speed
%11111110 is end of the stream, two next bytes are new pointer
%11111111 is a reference (next two bytes of absolute address, and number of rows)
No octaves on Noise channel, it is always 0..15; no instruments and octaves on DPCM channel, it is always one octave
interesting, what are the instruments? I never fully understood those, is it automatic pitch changes and such to make it sound like another sound? And yeah, I'll probably re-work the whole thing to work better, the byte by byte changes is just something I know will work for most needs, just really need to work with what more I need. I guess more stuff in hardware would be even easier than hardware, like envelopes and such. I'll have to plan it more out and look more into that format and understand it more before doing anything else. Anyone else have any formats or ideas, please share!
Instruments here are few envelopes. For pulse and triangle channels it is three envelopes per channel - volume, arpeggio (semitone offsets from current note), and pitch (period offsets from current note, useful for vibrato). For noise channel there is just two envelopes, since it only has 15 pitches, so having both arpeggio and pitch envelopes is redundant.
There is no duty cycle envelope for pulse channels in FamiTone, it is fixed for an instrument, but you may want to have one. I opted to not use it, since processing envelopes is the most time consuming part, and it is still possible to make good music without duty envelope.
So basically every frame there are 3+3+3+2 envelopes updated, and if it was a new row frame, 5 streams updated. Outputs of the envelopes are then converted to certain 2A03 registers state. I.e. output of a volume envelope is written into a volume register, output of the arpeggio envelope of a channel is added to the current note of the channel, then a period is taken from the note table for this resulting note, then pitch envelope is added to the period, and resulting period is written into the corresponding registers.
DragNSF merged duty cycle selection into the volume envelope. Since duty and volume use the same register anyway, this makes perfect sense. Just OR with a couple of bits (HW envelope disable, and length counter disable), and you're good to go.
Okay, so each instrument is a set event that happens over frames to a note that is processed in software, like the first square channels pitch bend on Battletoads title screen basically? That makes sense. And makes sense why the noise doesn't have it, it only has 15 "notes" so not much to be done outside of drum effects. I'll maul over their format more and consider more to add to mine now for sure.
Dwedit wrote:
DragNSF merged duty cycle selection into the volume envelope. Since duty and volume use the same register anyway, this makes perfect sense. Just OR with a couple of bits (HW envelope disable, and length counter disable), and you're good to go.
This is a nice idea, however it adds a limitation - volume and duty envelopes should have the same length and loop point.