It is currently Tue Nov 21, 2017 10:42 am

All times are UTC - 7 hours





Post new topic Reply to topic  [ 32 posts ]  Go to page 1, 2, 3  Next
Author Message
PostPosted: Sat Oct 24, 2015 11:24 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1461
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".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Sat Oct 24, 2015 11:39 am 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10117
Location: Rio de Janeiro - Brazil
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.


Top
 Profile  
 
PostPosted: Sat Oct 24, 2015 12:00 pm 
Offline

Joined: Mon Apr 01, 2013 11:17 pm
Posts: 437
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.


Top
 Profile  
 
PostPosted: Sat Oct 24, 2015 12:36 pm 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1461
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".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Sat Oct 24, 2015 3:05 pm 
Offline

Joined: Mon Apr 01, 2013 11:17 pm
Posts: 437
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:
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.


Top
 Profile  
 
PostPosted: Sat Oct 24, 2015 5:20 pm 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1461
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".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Sat Oct 24, 2015 5:28 pm 
Offline

Joined: Sun Apr 13, 2008 11:12 am
Posts: 6447
Location: UK (temporarily)
Joe wrote:
Code:
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:
   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;


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 2:19 am 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10117
Location: Rio de Janeiro - Brazil
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.


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 3:46 am 
Offline

Joined: Mon Apr 01, 2013 11:17 pm
Posts: 437
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:
frequency = 39375000 / 22 / 16 / divisor;

Isn't that off by one? The wiki says this:
Code:
f = CPU / (16 * (t + 1))


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 3:56 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1461
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:
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".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 4:13 am 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10117
Location: Rio de Janeiro - Brazil
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:
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.


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 4:33 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1461
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".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 4:41 am 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10117
Location: Rio de Janeiro - Brazil
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...


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 5:18 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1461
Oh, right. Now I understand it. Yeah, that's better. Thanks.

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Sun Oct 25, 2015 9:37 am 
Offline
User avatar

Joined: Fri May 08, 2015 7:17 pm
Posts: 1825
Location: DIGDUG
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


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 32 posts ]  Go to page 1, 2, 3  Next

All times are UTC - 7 hours


Who is online

Users browsing this forum: No registered users and 7 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group