Opinon needed: Data structure for my sound engine

Discuss NSF files, FamiTracker, MML tools, or anything else related to NES music.

Moderator: Moderators

User avatar
DRW
Posts: 1914
Joined: Sat Sep 07, 2013 2:59 pm

Opinon needed: Data structure for my sound engine

Post by DRW » Sat Oct 24, 2015 11:24 am

I did most of the Nerdy Nights sound tutorials. I'm at chapter 7 now. But now the tutorial includes all the really advanced features, like volume envelopes, opcodes etc.
But my music engine is supposed to be simple since it is intended for a simple game.

So, I created my own sound data structure that I want to base my code on. And I'd like you to judge it. Is this a reasonable attempt?


O.k., the first data structure is a tune.
A tune consists of the following values:

1. Volume and duty cycle (1 byte)
Just 1:1 the value that you would write into the corresponding APU port.

2. Note or length (1 byte, can occur multiple times)
If bit 7 of this value is 0, then it is a note. Or rather: It is the index to a lookup table that includes the 96 frequency values from this list, each of them consisting of two bytes: www.freewebs.com/the_bott/NotesTableNTSC.txt
If bit 7 of the value is a 1, then this value (or rather: value AND %01111111) is the length (in frames) of all the following notes until the next length value is read.

3. $FF as an indicator for the end of the tune.

That was the "inner" data structure.


The second data structure, the "outer" data structure, is a song.
A song consists of the following values:

1. Address to a tune on square wave channel 1 (2 bytes, can occur multiple times)
2. Value $0000 (2 bytes) as an indicator that the song needs to be looped now, i.e. it starts at the first tune again.
3-8: Like 1 and 2, only for square wavel channel 2, triangle channel and noise channel.
If any of the tune address values is 0, this channel is deactivated for the song.

So, if we have the song "For he's a jolly good fellow", we would have the following tunes:

Tune 1: "For he's a jolly good"
Tune 2: "fellow."
Tune 3: "fehellooow."
Tune 4: "Which nobody can deny."

And the song array (for one channel) would look like this:
Tune 1
Tune 2
Tune 1
Tune 2
Tune 1
Tune 3
Tune 4
0


Sound effects are not included yet. But they're basically the same, only that they don't loop and will probably just consist of one tune anyway. When they happen, they "steal" one of the sound channels from the corresponding music stream.


What do you say?
Available now: My game "City Trouble".
Sales website: https://megacatstudios.com/products/city-trouble
Gameplay: https://youtu.be/Eee0yurkIW4
Download website: http://www.denny-r-walter.de/city.htm

User avatar
tokumaru
Posts: 11471
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Opinon needed: Data structure for my sound engine

Post by tokumaru » Sat Oct 24, 2015 11:39 am

DRW wrote:2. Note or length (1 byte, can occur multiple times)
If bit 7 of this value is 0, then it is a note. Or rather: It is the index to a lookup table that includes the 96 frequency values from this list, each of them consisting of two bytes: http://www.freewebs.com/the_bott/NotesTableNTSC.txt
If bit 7 of the value is a 1, then this value (or rather: value AND %01111111) is the length (in frames) of all the following notes until the next length value is read.
You could increase the range of the length (from 128 to 256 frames) if only the note byte used bit 7 as a flag: if the bit is 0, play the note using the previous length, if the bit is 1, play the note using the length specified in the next byte (all 8 bits of it), which becomes the new default length.

Joe
Posts: 437
Joined: Mon Apr 01, 2013 11:17 pm

Re: Opinon needed: Data structure for my sound engine

Post by Joe » Sat Oct 24, 2015 12:00 pm

DRW wrote:Or rather: It is the index to a lookup table that includes the 96 frequency values from this list, each of them consisting of two bytes:
You can represent the same information in 36 bytes. To do that, you'd use a table of 12 frequency values and 12 "rounding" values. The frequency value is right-shifted to the correct octave, and the "rounding" either keeps the result or subtracts 1 to arrive at the correct value.

I also worked out tables that round to the nearest pitch* instead of the nearest frequency. I can give you a copy if you're interested. (Really, it probably belongs on the wiki.)

*If you have an offset the same number of Hz above and below the target frequency, below will sound further away because pitch does not map linearly to frequency.

User avatar
DRW
Posts: 1914
Joined: Sat Sep 07, 2013 2:59 pm

Re: Opinon needed: Data structure for my sound engine

Post by DRW » Sat Oct 24, 2015 12:36 pm

tokumaru wrote:You could increase the range of the length (from 128 to 256 frames) if only the note byte used bit 7 as a flag
I dont understand this. Bit 7 is already there to check whether it's a length at all or whether it's a note.
Can you write down what you mean in pseudo code with a few ifs and elses please?
Joe wrote:You can represent the same information in 36 bytes. To do that, you'd use a table of 12 frequency values and 12 "rounding" values. The frequency value is right-shifted to the correct octave, and the "rounding" either keeps the result or subtracts 1 to arrive at the correct value.
I don't think I will do this. The lookup table is merely 96 bytes for the whole program. So, with your method I would save 30 bytes. But it would add logic to the function. And time per frame is a much more serious issue than a few constants in the ROM.
Joe wrote:I also worked out tables that round to the nearest pitch* instead of the nearest frequency. I can give you a copy if you're interested. (Really, it probably belongs on the wiki.)
Sure, post it. I'd need some of the music experts on this forum to compare which one is better, though. Since I have no idea.

Which rounding method does the Famitracker use? (Since my composer works with that program, so if he enters a certain note and I paste it into my game, then it should of course still sound the same in my own engine.)


Another question: How important is it to be able to change the volume and duty cycle of the square waves in the middle of the song? I'm thinking about putting these values at the beginning of the whole song instead of the start of every tune. Saves me a few bytes, but most important: It saves me time since those values only need to be set once in the beginning instead writing (and even checking for them) every tune.
So, do songs with a constant duty cycle per square wave stream still sound good?
Available now: My game "City Trouble".
Sales website: https://megacatstudios.com/products/city-trouble
Gameplay: https://youtu.be/Eee0yurkIW4
Download website: http://www.denny-r-walter.de/city.htm

Joe
Posts: 437
Joined: Mon Apr 01, 2013 11:17 pm

Re: Opinon needed: Data structure for my sound engine

Post by Joe » Sat Oct 24, 2015 3:05 pm

DRW wrote:So, with your method I would save 30 bytes.
96 - 36 = 60
DRW wrote:But it would add logic to the function. And time per frame is a much more serious issue than a few constants in the ROM.
Fair enough. It's an option you can consider if ROM space becomes more important than CPU time.
DRW wrote:Sure, post it. I'd need some of the music experts on this forum to compare which one is better, though. Since I have no idea.
Here's the NTSC table. It looks like I stopped when I started getting duplicate values for adjacent notes, so the (basically useless) highest values are missing. There might be some typos since I did all of the math with a graphing calculator.

Code: Select all

A  | 7F0 | 3F8 | 1FB | FD | 7E | 3F | 1F | F
Bb | 77E | 3BF | 1DF | EF | 77 | 3B | 1D | E
B  | 713 | 389 | 1C4 | E1 | 70 | 38 | 1B | D
C  | 6AD | 356 | 1AA | D5 | 6A | 34 | 1A | C
C# | 64D | 326 | 192 | C9 | 64 | 31 | 18 |
D  | 5F2 | 2F9 | 17C | BD | 5E | 2F | 17 |
Eb | 59D | 2CE | 166 | B3 | 59 | 2C | 15 |
E  | 54C | 2A6 | 152 | A9 | 54 | 29 | 14 |
F  | 500 | 27F | 13F | 9F | 4F | 27 | 13 |
F# | 4B8 | 25C | 12D | 96 | 4B | 25 | 12 |
G  | 474 | 23A | 11C | 8E | 46 | 23 | 11 |
Ab | 434 | 21A | 10C | 86 | 42 | 21 | 10 |
DRW wrote:Another question: How important is it to be able to change the volume and duty cycle of the square waves in the middle of the song?
That's probably up to your composer. For something like this, it's very important.

User avatar
DRW
Posts: 1914
Joined: Sat Sep 07, 2013 2:59 pm

Re: Opinon needed: Data structure for my sound engine

Post by DRW » Sat Oct 24, 2015 5:20 pm

Joe wrote:
DRW wrote:So, with your method I would save 30 bytes.
96 - 36 = 60
Yeah, that's all a matter of opinion. That's what I like so much about mathematics: There isn't the one correct answer. :wink:

So, what do the others say? Which list is better? Joe's or this one?

Also, which list does the Famitracker use? How do they convert the notes into 11 bit hex values?
Available now: My game "City Trouble".
Sales website: https://megacatstudios.com/products/city-trouble
Gameplay: https://youtu.be/Eee0yurkIW4
Download website: http://www.denny-r-walter.de/city.htm

lidnariq
Posts: 8796
Joined: Sun Apr 13, 2008 11:12 am
Location: Seattle

Re: Opinon needed: Data structure for my sound engine

Post by lidnariq » Sat Oct 24, 2015 5:28 pm

Joe wrote:

Code: Select all

A  | 7F0 | 3F8 | 1FB | FD | 7E | 3F | 1F | F
Bb | 77E | 3BF | 1DF | EF | 77 | 3B | 1D | E
That table is wonky. Are you using A440? Or are you doing something clever with tuning harmonics?
my program wrote: 7F0 → A-1 +01.5
7F1 → A-1 +00.7
7F2 → A-1 -00.1
7F3 → A-1 -00.9
7F4 → A-1 -01.8
All five periods here are within two cents of 12-TET A55, but 7F0 is not what I'd've picked if I was trying to be as close as possible.

On the NES, with its NTSC-based timing, there may be something to be said for detuning to 13 or 15 cents flat; you get more in-tune notes in the highest octaves that way. There may also be something to be said for using D-6=1202.8Hz (40 cents sharp, period of exactly 93): then the noise channel's tonal noise mode is in tune.


Anyway, here's some simple math to convert from a divisor (as used by the NES) to a note name and a detuning in cents.

Code: Select all

	frequency = 39375000 / 22 / 16 / divisor;
	pitch = 1200*log(frequency / 440)/log(2);
	semitones = floor( (pitch + 50) / 100);
	cents = pitch - semitones * 100;
	octave = (semitones - 3) / 12 + 5;
	notenameindex = (semitones + 120 - 3) % 12;

User avatar
tokumaru
Posts: 11471
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Opinon needed: Data structure for my sound engine

Post by tokumaru » Sun Oct 25, 2015 2:19 am

DRW wrote:I dont understand this. Bit 7 is already there to check whether it's a length at all or whether it's a note.
My point is you stop using it to tell notes and lengths apart, and assume all bytes are notes, but when the bit is set, you load a new length, which will be specified in the next byte.

I find it redundant to have a bit identifying notes and lengths, because there's no need (as far as I can tell) to have consecutive length bytes, since you're using the concept of persistent lengths that are changed sporadically. Notes on the other hand are absolutely necessary all the time, and since you have the free bit, you can simply have a note "request" that a new length be loaded when the current one is not what it needs.

Joe
Posts: 437
Joined: Mon Apr 01, 2013 11:17 pm

Re: Opinon needed: Data structure for my sound engine

Post by Joe » Sun Oct 25, 2015 3:46 am

lidnariq wrote:That table is wonky. Are you using A440? Or are you doing something clever with tuning harmonics?
I wasn't trying to do anything clever beyond rounding to the nearest pitch, so you probably found a mistake.
lidnariq wrote:

Code: Select all

frequency = 39375000 / 22 / 16 / divisor;
Isn't that off by one? The wiki says this:

Code: Select all

f = CPU / (16 * (t + 1))

User avatar
DRW
Posts: 1914
Joined: Sat Sep 07, 2013 2:59 pm

Re: Opinon needed: Data structure for my sound engine

Post by DRW » Sun Oct 25, 2015 3:56 am

tokumaru wrote:My point is you stop using it to tell notes and lengths apart, and assume all bytes are notes, but when the bit is set, you load a new length, which will be specified in the next byte.
No, the length won't be specified in the next byte, but in the current byte:

Code: Select all

ReadNextNote()
{
start:

    value = ReadNextValue();

    if (value AND %10000000 == 0)
        CurrentNote = Notes[value];
    else
    {
        CurrentLength = value AND %01111111;
        goto start;
    }
}
Is that alright?
Available now: My game "City Trouble".
Sales website: https://megacatstudios.com/products/city-trouble
Gameplay: https://youtu.be/Eee0yurkIW4
Download website: http://www.denny-r-walter.de/city.htm

User avatar
tokumaru
Posts: 11471
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Opinon needed: Data structure for my sound engine

Post by tokumaru » Sun Oct 25, 2015 4:13 am

DRW wrote:No, the length won't be specified in the next byte, but in the current byte:
I know how it is, I just think that using a bit to identify the length byte is a waste of a perfectly good bit. Here's what I mean:

Code: Select all

ReadNextNote() {
	Value = ReadNextValue();
	if (Value AND %10000000 != 0) {
		CurrentLength = ReadNextValue();
		Value = Value AND %01111111;
	}
	CurrentNote = Notes[Value];
}
There's no need to identify the byte as a length byte if it implicitly comes after the note when needed. The meaning of the high bit of the note is:

0: play this note using the previous length;
1: play this note use a new length, provided in the next byte;

And then you don't need to "sign" each byte as being either a note or a length... They're all notes, except for the bytes that follow notes that request new lengths.

User avatar
DRW
Posts: 1914
Joined: Sat Sep 07, 2013 2:59 pm

Re: Opinon needed: Data structure for my sound engine

Post by DRW » Sun Oct 25, 2015 4:33 am

I don't know why this is better than the other method. You still have a bit to indicate that a length is coming and you still need an additional byte for the length. So, why does this make a difference?

The only thing that I noticed here is that you have a strange order in your song data:

My data: LengthA, NoteA1, NoteA2, NoteA3, LengthB, NoteB1, NoteB2, LengthC, NoteC1, NoteC2 ...
Your data: NoteA1, LengthA, NoteA2, NoteA3, NoteB1, LengthB, NoteB2, NoteC1, LengthC, NoteC2 ...

I guess I'm missing something here, but I just don't know what exactly. What do I gain from switching the two values around?
Available now: My game "City Trouble".
Sales website: https://megacatstudios.com/products/city-trouble
Gameplay: https://youtu.be/Eee0yurkIW4
Download website: http://www.denny-r-walter.de/city.htm

User avatar
tokumaru
Posts: 11471
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Opinon needed: Data structure for my sound engine

Post by tokumaru » Sun Oct 25, 2015 4:41 am

DRW wrote:So, why does this make a difference?
The length can be 1..256 instead of 1..128, that's all. You shouldn't need such long lengths very often, but if you do...

User avatar
DRW
Posts: 1914
Joined: Sat Sep 07, 2013 2:59 pm

Re: Opinon needed: Data structure for my sound engine

Post by DRW » Sun Oct 25, 2015 5:18 am

Oh, right. Now I understand it. Yeah, that's better. Thanks.
Available now: My game "City Trouble".
Sales website: https://megacatstudios.com/products/city-trouble
Gameplay: https://youtu.be/Eee0yurkIW4
Download website: http://www.denny-r-walter.de/city.htm

User avatar
dougeff
Posts: 2617
Joined: Fri May 08, 2015 7:17 pm
Location: DIGDUG
Contact:

Re: Opinon needed: Data structure for my sound engine

Post by dougeff » Sun Oct 25, 2015 9:37 am

I think this whole conversation points out that Famitracker could use a better tool (like Famitone) to specifically use Famitracker songs in a game. The easiest approach (IMHO) would be to modify Famitone to accept more/all Famitracker features.

If I were smarter, I would write a tool to convert MIDI files directly to usable game song data. (And write an engine designed to play it). But that may be above my skill level.
nesdoug.com -- blog/tutorial on programming for the NES

Post Reply