I give up, I can't find a way to emulate chiptune sound!
Moderator: Moderators
I give up, I can't find a way to emulate chiptune sound!
Ok, I've been able to understand and mostly emulate just about everything in the NES....except the sound. Sound is my absolute weakest link in my Computer Science knowledge(along with networking) and I'm having the hardest time trying to find out how to play this stuff on a modern machine!
Now I could use that one sound emulator that I think blargg created, but I really feel that I should do this on my own as I think I'll have to learn how to deal with sound sooner or later.
I've been searching for days in how to emulate this stuff using Allegro, with no luck at all. Right now, I'm looking into OpenAl, but I don't think I should get my hopes up.
Tutorials on Allegro5's audio seems to be non existent except for the documentation, which only lists the functions and structs and very briefly what they do. Even if there was one, it would most likely be about loading sample files and not about manipulating the sound programmaticaly. So maybe I should look to another another library like OpenAl? Or perhaps there's something fundamental that I'm not getting here?
Now, from my incredibly limited understanding I believe I can produce the sound by producing these simple waves? Like the square wave. It looks pretty simple, but how do I produce this with a sound library? Can I produce a square wave using samples?
What do you guys use for sound emulation?
Thanks for your time!
Now I could use that one sound emulator that I think blargg created, but I really feel that I should do this on my own as I think I'll have to learn how to deal with sound sooner or later.
I've been searching for days in how to emulate this stuff using Allegro, with no luck at all. Right now, I'm looking into OpenAl, but I don't think I should get my hopes up.
Tutorials on Allegro5's audio seems to be non existent except for the documentation, which only lists the functions and structs and very briefly what they do. Even if there was one, it would most likely be about loading sample files and not about manipulating the sound programmaticaly. So maybe I should look to another another library like OpenAl? Or perhaps there's something fundamental that I'm not getting here?
Now, from my incredibly limited understanding I believe I can produce the sound by producing these simple waves? Like the square wave. It looks pretty simple, but how do I produce this with a sound library? Can I produce a square wave using samples?
What do you guys use for sound emulation?
Thanks for your time!
Ok, I used a stream object and kind of copied the stream method of ex_synth and I'm glad that I actually got a sound to come out that kind of sounds like a square wave, but testing it against other programs that produce square waves(including ex_synth) it sounds waaaay off. Most likely frequency related.
Wish I had the code with me, but I don't
But basically I have a class called Square Wave that just takes a wavelength(in the same range as the nes's square wave 1). Now, from my understanding you can get the frequency from wavelength from an equation that I can't remember right now... ( wavelength = (C / (16f) )-1; solve for f... I solved it yesterday but I can't do it again today for some reason >_<; oh and C == NES's CPU cycles per second)
Anyway, I barely have any idea what I need to do with the frequency. I'm fairly sure that I need to play wavelength
samples frequency times in a span of a second, but I don't know how to do the algorithm for that in conjunction with Allegro's stream object and functions.
EDIT: Before I just had makeshift code from memory. Now it's the real code.
In the main code I make an ALLEGRO_AUDIO_STREAM object that is made with al_create_audio_stream(), with one fragment, samples == the wavelength, frequency is 44100, depth is float32 and channel is CONF_2. Also, before that, allegro init, the audio add-on init and al_reserve_samples() are called...
then I call al_get_audio_stream_fragment() which is assigned to a pointer to a void called buffer and then it's looped until it doesn't equal zero. Then I call al_set_audio_stream_fragment() and that's basically what I did.
A sound DOES play and it does sound like a square wave, but at, say, a wavelength of 9, where it's supposed to sound very high....doesn't sound very high at all. and when the wavelength is 0x7FF(the max value accepted for nes's wavelength) it sounds...very little like what other programs produce(including emulators) at that same wavelength.
So, I'm stuck until I can find a solution to this...
I'm pretty sure it has to do with the frequency of SquareWave and the frequency that I put into the al_create_audio_stream function and I've experimented, but it just ends with worst results >_<.
Wish I had the code with me, but I don't
But basically I have a class called Square Wave that just takes a wavelength(in the same range as the nes's square wave 1). Now, from my understanding you can get the frequency from wavelength from an equation that I can't remember right now... ( wavelength = (C / (16f) )-1; solve for f... I solved it yesterday but I can't do it again today for some reason >_<; oh and C == NES's CPU cycles per second)
Anyway, I barely have any idea what I need to do with the frequency. I'm fairly sure that I need to play wavelength
samples frequency times in a span of a second, but I don't know how to do the algorithm for that in conjunction with Allegro's stream object and functions.
EDIT: Before I just had makeshift code from memory. Now it's the real code.
Code: Select all
#include <iostream>
#include <cstdlib>
#include <allegro5/allegro.h>
#include <allegro5/allegro_audio.h>
using namespace std;
const int NES_CPU_CYCLES = 1790000;
class SquareWave
{
public:
SquareWave(int wl)
{
WaveLength = wl;
Frequency = NES_CPU_CYCLES / (16 * (wl + 1));
dt = WaveLength * Frequency;
}
~SquareWave(){}
void ProduceSamples(float *buffer)
{
// Am I supposed to cut these periods in half?
float half = WaveLength / 2.0;
float amplitude = 1.0;
for (int index = 0; index < WaveLength; index++)
{
if (index <= half)
buffer[index] = amplitude;
else
buffer[index] = -amplitude;
}
}
int GetWaveLength() {return WaveLength;}
int GetFrequency() {return Frequency;}
int GetDT() {return dt;}
private:
int WaveLength;
int Frequency; // Can't find a place to use this with without
// making it not work at all
int dt; // or this
};
int main()
{
ALLEGRO_AUDIO_STREAM *stream = NULL;
SquareWave square(0x7FF);
if (!al_init()) {
fprintf(stderr, "Could not init Allegro.\n");
return 1;
}
if (!al_install_audio()) {
cout << "Could not init sound!" << endl;
return 1;
}
if (!al_reserve_samples(0)) {
cout << "Could not set up voice and mixer." << endl;
return 1;
}
ALLEGRO_AUDIO_DEPTH depth = ALLEGRO_AUDIO_DEPTH_FLOAT32;
ALLEGRO_CHANNEL_CONF ch = ALLEGRO_CHANNEL_CONF_1;
// Tried to use freq and it doesn't work; using 44100 for now
int freq = square.GetFrequency();
stream = al_create_audio_stream(1, square.GetWaveLength(), 44100,
depth, ch);
if (!stream) {
cout << "Could not create stream." << endl;
return 1;
}
ALLEGRO_MIXER *mixer = al_get_default_mixer();
if (!al_attach_audio_stream_to_mixer(stream, mixer)) {
cout << "Could not attach stream to mixer." << endl;
return 1;
}
void *buf = al_get_audio_stream_fragment(stream);
while (!buf)
{
// have to wait for some reason....
buf = al_get_audio_stream_fragment(stream);
}
//where the buffer gets loaded
square.ProduceSamples((float *)buf);
if (!al_set_audio_stream_fragment(stream, buf)) {
fprintf(stderr, "Error setting stream fragment.\n");
}
system("PAUSE");
al_destroy_audio_stream(stream);
al_uninstall_audio();
return 0;
}
In the main code I make an ALLEGRO_AUDIO_STREAM object that is made with al_create_audio_stream(), with one fragment, samples == the wavelength, frequency is 44100, depth is float32 and channel is CONF_2. Also, before that, allegro init, the audio add-on init and al_reserve_samples() are called...
then I call al_get_audio_stream_fragment() which is assigned to a pointer to a void called buffer and then it's looped until it doesn't equal zero. Then I call al_set_audio_stream_fragment() and that's basically what I did.
A sound DOES play and it does sound like a square wave, but at, say, a wavelength of 9, where it's supposed to sound very high....doesn't sound very high at all. and when the wavelength is 0x7FF(the max value accepted for nes's wavelength) it sounds...very little like what other programs produce(including emulators) at that same wavelength.
So, I'm stuck until I can find a solution to this...
I'm pretty sure it has to do with the frequency of SquareWave and the frequency that I put into the al_create_audio_stream function and I've experimented, but it just ends with worst results >_<.
if it helps, this is the APU code from my emulator (FreeBASIC) ... the code is kind of sloppy, but it does work and it shouldn't be too (i hope) difficult to decipher.
and then this is at the end of my 6502 opcode execution loop after each instruction:
my sound emulation is NOT perfect, but i've got it improved to the point where it's pretty damn close (imo anyway)... i don't emulate the DPCM channel yet though.
here's an MP3 of what it generates from metal man's stage music in megaman 2 - http://rubbermallet.org/moarnes-metalman.mp3
Code: Select all
Dim Shared As LongInt cpuclock = 1789773, samplerate = 44100, sampleticks = 1789773/44100
Dim Shared As Integer buflen, freqmod = 7402, bufpos = 0
Dim Shared As Byte cursample = 0, square1sample = 0, square2sample = 0, trianglesample = 0,_
noisesample = 0, dmcsample = 0, gain = 1, tester = 0, square1=0, square2=0, triangle=0, noise=0,_
tricounter = 0, counterstep = 4, curstep = 1, nosound
Dim Shared As Integer square1_duty = 0, square1_dutypos = 0, square1_loopenv = 0, square1_env = 15,_
square1_disableenv = 0, square1_envperiod = 0, square1_enablesweep = 0, square1_period = 0,_
square1_negative = 0, square1_shift = 0, square1counter = 0, square1_sweep = 0,_
square1_length = 0, square1_enable = 0, square1_write = 0
Dim Shared As LongInt square1_timer = 1, square1_freq = 100000, s1t = 1
Dim Shared As Integer square2_duty = 0, square2_dutypos = 0, square2_loopenv = 0, square2_env = 15,_
square2_disableenv = 0, square2_envperiod = 0, square2_enablesweep = 0, square2_period = 0,_
square2_negative = 0, square2_shift = 0, square2counter = 0, square2_sweep = 0,_
square2_length = 0, square2_enable = 0, square2_write = 0
Dim Shared As LongInt square2_timer = 1, square2_freq = 100000, s2t = 1
Dim Shared As Integer triangle_control = 0, triangle_counter = 0, triangle_length = 0, triangle_period = 0,_
triangle_enable = 0, triangle_write = 0, triangle_halt = 0
Dim Shared As LongInt triangle_timer = 1, triangle_freq = 10000000
Dim Shared As Integer noise_loopenv = 0, noise_disableenv = 0, noise_envperiod = 0, noise_shortmode = 0,_
noise_shift = 1, noise_period = 0, noise_length = 0, noise_write = 0, noise_halt = 0, noise_env = 0,_
noise_enable = 0, noise_loop = 0, noise_origenv = 0
Dim Shared As LongInt noise_timer = 1
Dim Shared As Integer dmc_irqenable = 0, dmc_loop = 1, dmc_freqindex = 0, dmc_dac = 0, dmc_addr = 0,_
dmc_length = 0, dmc_enable = 0
Dim Shared As Integer lengthcounterenable = 1, dmclen = 1, noiselen = 1, trianglelen = 1, square1len = 1, square2len = 1
Dim Shared As Integer frameinterrupt = 0
Dim Shared As Integer squarelookup(32), otherlookup(204)
Dim Shared As UByte lengthtable(&h20) => {_
10,254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14,_
12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30 }
Dim Shared As Integer noisefreq(&h10) => {_
4, 8, 16, 32, 64, 96, 128, 160, 202,_
254, 380, 508, 762, 1016, 2034, 4068 }
Dim Shared As UByte tritable(32) => {_
8, 9, 10, 11, 12, 13, 14, 15, 15, 14, 13, 12, 11, 10, 9 ,8,_
7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7 }
Dim Shared As UByte duty0(16) => { 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
Dim Shared As UByte duty1(16) => { 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
Dim Shared As UByte duty2(16) => { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0 }
Dim Shared As UByte duty3(16) => { 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
Dim Shared buf(11025) As Byte
Function APUreadregs(ByVal addr As UShort) As Integer
Dim tmpstatus As Integer
tmpstatus = dmc_irqenable*128 + frameinterrupt*64
if (dmc_length) Then tmpstatus = tmpstatus + 16
if (triangle_length) Then tmpstatus = tmpstatus + 4
If (square2_length) Then tmpstatus = tmpstatus + 2
If (square1_length) then tmpstatus = tmpstatus + 1
APUreadregs = tmpstatus
End Function
Sub APUwriteregs(ByVal addr As UShort, ByVal value As UByte)
Select Case As Const (addr)
'START SQUARE 1 HERE
case &h4000:
square1_duty = value Shr 6
if (value And 32) Then square1_disableenv = 1 else square1_disableenv = 0
square1_env = value And &hF
square1_env = square1_env + ((value Shr 1) And &h10)
case &h4001:
if (value And 128) Then square1_enablesweep = 1 else square1_enablesweep = 0
square1_sweep = ((value Shr 4) And 7)+1
if (value And &h10) Then square1_negative = 1 else square1_negative = 0
square1_shift = value And 7
case &h4002:
square1_timer = (square1_timer And &hF00) + value
case &h4003:
square1_length = lengthtable(value Shr 3)
square1_timer = (square1_timer And &hFF) + ((value And &h7) Shl 8)
square1_dutypos = 0
'START SQUARE 2 HERE
case &h4004:
square2_duty = value Shr 6
if (value And 32) Then square2_disableenv = 1 else square2_disableenv = 0
square2_env = value And &hF
square2_env = square2_env + ((value Shr 1) And &h10)
case &h4005:
if (value And 128) Then square2_enablesweep = 1 else square2_enablesweep = 0
square2_sweep = ((value Shr 4) And 7)+1
if (value And &h10) Then square2_negative = 1 else square2_negative = 0
square2_shift = value And 7
case &h4006:
square2_timer = (square2_timer And &hF00) + value
case &h4007:
square2_length = lengthtable(value Shr 3)
square2_timer = (square2_timer And &hFF) + ((value And &h7) Shl 8)
square2_dutypos = 0
'START TRIANGLE HERE
case &h4008:
triangle_halt = value And &h80 Shr 7
if (triangle_control=0) Then triangle_halt = 1
triangle_counter = value And &h7F
case &h400A:
triangle_timer = (triangle_timer And &hF00) + value
case &h400B:
triangle_timer = (triangle_timer And &hFF) + ((value And &h7) Shl 8)
triangle_length = lengthtable(value Shr 3)
triangle_freq = cpuclock / (32*(triangle_timer)+1)
triangle_halt = 1
'START NOISE HERE
case &h400C:
if (value And 32) Then noise_halt = 1 else noise_halt = 0
noise_env = value And &h1F
noise_origenv = noise_env
case &h400E:
if (value And 128) Then noise_loop = 1 else noise_loop = 0
noise_timer = noisefreq(value And &hF)
case &h400F:
noise_length = lengthtable(value Shr 3)
noise_env = noise_origenv
case &h4015:
if (value And 1) Then square1_enable = 1 else square1_enable = 0
if (value And 2) Then square2_enable = 1 else square2_enable = 0
if (value And 4) Then triangle_enable = 1 else triangle_enable = 0
if (value And 8) Then noise_enable = 1 else noise_enable = 0
if (value And 16) Then dmc_enable = 1 else dmc_enable = 0
case &h4017:
if (value And &h80) Then counterstep = 5 else counterstep = 4
curstep = 1
End Select
End Sub
Sub putinbuf()
buf(bufpos) = cursample
bufpos = bufpos + 1
End Sub
Sub square1_clock()
if ((square1_enable=1) And (square1_length>0)) Then
if (square1_disableenv=1) Then square1_env = &hF
Select case (square1_duty)
case 0: if (duty0(square1_dutypos)) Then square1sample = square1_env else square1sample = 0
case 1: if (duty1(square1_dutypos)) Then square1sample = square1_env else square1sample = 0
case 2: if (duty2(square1_dutypos)) Then square1sample = square1_env else square1sample = 0
case 3: if (duty3(square1_dutypos)) Then square1sample = -1*square1_env else square1sample = 0
End Select
square1_dutypos = (square1_dutypos + 1) Mod 16
Else
square1sample = 0
End If
End Sub
Sub square2_clock()
if ((square2_enable=1) and (square2_length>0)) Then
if (square2_disableenv=1) Then square2_env = &hF
Select Case (square2_duty)
case 0: if (duty0(square2_dutypos)) Then square2sample = square2_env else square2sample = 0
case 1: if (duty1(square2_dutypos)) Then square2sample = square2_env else square2sample = 0
case 2: if (duty2(square2_dutypos)) Then square2sample = square2_env else square2sample = 0
case 3: if (duty3(square2_dutypos)) Then square2sample = -1*square2_env else square2sample = 0
End Select
square2_dutypos = (square2_dutypos + 1) Mod 16
Else
square2sample = 0
End If
End Sub
Sub triangle_clock()
if ((triangle_enable=0) or (triangle_length<=0)) Then Exit Sub
trianglesample = tritable(tricounter)
if (trianglesample>15) Then trianglesample = 31 - trianglesample
tricounter = (tricounter + 1) mod 32
End Sub
sub noise_clock()
dim feedback, tmpbit As Integer
if (noise_loop) Then
if (noise_shift And 64) Then tmpbit = 1 else tmpbit = 0
feedback = (noise_shift And 1) Xor tmpbit
Else
if (noise_shift And 2) Then tmpbit = 1 else tmpbit = 0
feedback = (noise_shift And 1) Xor tmpbit
End If
noise_shift = (noise_shift Shr 1) + (feedback Shl 13)
if ((noise_enable) and (noise_length>0) and ((noise_shift And 1)=0) and (noise_timer>=8)) Then
noisesample = noise_env
Else
noisesample = 0
End If
End Sub
Sub APU_frame_seq_tick()
if (counterstep=4) Then
if (triangle_length>0) Then triangle_length = triangle_length - 1
if (square1_length>0) Then square1_length = square1_length - 1
if (square2_length>0) Then square2_length = square2_length - 1
if (noise_length>0) Then noise_length = noise_length - 1
if ((curstep=4) And ((flag_reg And 4)=0)) Then flag_reg = flag_reg or &h14
if (square1_sweep>0) Then square1_sweep = square1_sweep - 1
if (square2_sweep>0) Then square2_sweep = square2_sweep - 1
if ((square1_negative=1) and (square1_enablesweep=1) and (square1_sweep>0) and (curstep<5)) Then
square1_timer = square1_timer - (square1_timer Shr square1_shift)
ElseIf ((curstep<5) And (square1_enablesweep=1) and (square1_sweep>0)) Then
square1_timer = square1_timer + (square1_timer Shr square1_shift)
End If
if ((square2_negative=1) and (square2_enablesweep=1) and (square2_sweep>0) and (curstep<5)) Then
square2_timer = square2_timer - (square2_timer Shr square2_shift)
ElseIf ((curstep<5) and (square2_enablesweep=1) and (square2_sweep>0)) Then
square2_timer = square2_timer + (square2_timer Shr square2_shift)
End If
End If
curstep = (curstep+1) mod counterstep
End Sub
Sub fillaudio Cdecl (ByVal userdata As Any Ptr, ByVal audbuffer As UByte Ptr, ByVal audlen As Integer)
Dim As UByte Ptr audbyte
For n = 0 To buflen-1
audbyte = audbuffer + n
*audbyte = buf(n)
Next n
bufpos = 0
End Sub
and then this is at the end of my 6502 opcode execution loop after each instruction:
Code: Select all
If ((triangle_enable) And (triangle_timer<=(totalticks-lasttriangletick))) Then triangle_clock(): lasttriangletick = totalticks
If ((square1_enable) And (square1_length>0) And (square1_timer>=8) and (square1_timer<=(totalticks-lastsquare1tick))) Then square1_clock(): lastsquare1tick = totalticks
If ((square2_enable) And (square2_length>0) And (square2_timer>=8) And (square2_timer<=(totalticks-lastsquare2tick))) Then square2_clock(): lastsquare2tick = totalticks
If ((noise_enable) and (noise_length>0) and (noise_timer>=8) and (noise_timer<=(totalticks-lastnoisetick))) Then noise_clock(): lastnoisetick = totalticks
If sampleticks<=(totalticks-lastsampletick) Then
cursample = square1sample + square2sample + trianglesample + 0.2 * noisesample
If (bufpos<buflen) Then putinbuf()
lastsampletick = totalticks
End If
If ((cpuclock/240)<=(totalticks-lastframeseqtick)) Then
APU_frame_seq_tick()
lastframeseqtick = totalticks
End If
here's an MP3 of what it generates from metal man's stage music in megaman 2 - http://rubbermallet.org/moarnes-metalman.mp3
seriously... i'm trying to help show the guy how i did my sound emulation.thefox wrote:What is the point of posting all these images? I can understand doing it every once in a while to spice things up, but you're doing it quite a lot, and in sometimes inappropriate places (why put down a guy who shares his code?).Zepper wrote:HMM...
It's not quite exactly what he wants. Basically, it's the Allegro sound interface, not the sound emulation side.
Zepper
RockNES author
RockNES author
well, he was asking about allegro yeah but he also said he's not sure how to generate a square wave, etc using samples. he was wondering if allegro or another lib had a way to generate it. i was showing how you have to generate sound when emulating the NES APU, and all the variables involved in the waveform output.
Well, basically, I never could interface the Allegro' sound in my emulator "error-free". It's not an easy task and I even couldn't take the "risk" of trying Allegro 5.
Regarding the samples, well... as far as I know of it, you would need to generate 1 sample at every CPU clock cycle. That means a resample to the PC frequency. I add all the samples and divide by the number of updates. Using Allegro, the amount of samples is always the same; so, you would feed the buffer with, let's say, 2048 samples per sound sync.
Regarding the samples, well... as far as I know of it, you would need to generate 1 sample at every CPU clock cycle. That means a resample to the PC frequency. I add all the samples and divide by the number of updates. Using Allegro, the amount of samples is always the same; so, you would feed the buffer with, let's say, 2048 samples per sound sync.
Zepper
RockNES author
RockNES author
the following may not be 100% accurate, but as i understand it, every APU channel has a countdown timer n and a new sample for that channel is generated every n CPU ticks.
at the same time the new channel samples are calculated, the variables for the channel are updated/stepped (like the position of the duty cycle step on the squares, and for the triangle it steps where it is in the 32-byte lookup table)
then every 1789773 divided by 240(on NTSC) clock ticks, it steps another counter. every 4 steps of that, the square sweep counters are decremented and all of the length counters are decremented. every 4 or 5 steps (depending on a value written to register $4017) that counter wraps back to zero.
every 1789773 divided by samplerate CPU ticks you'll want to mix together all the channels' sample values and there's your output sample to put in your final stream.
that's oversimplying the process, but that's basically how things would be done in an emulator.
i can't speak for getting allegro going, as i've always used SDL, and it's great. never had an issue with it.
at the same time the new channel samples are calculated, the variables for the channel are updated/stepped (like the position of the duty cycle step on the squares, and for the triangle it steps where it is in the 32-byte lookup table)
then every 1789773 divided by 240(on NTSC) clock ticks, it steps another counter. every 4 steps of that, the square sweep counters are decremented and all of the length counters are decremented. every 4 or 5 steps (depending on a value written to register $4017) that counter wraps back to zero.
every 1789773 divided by samplerate CPU ticks you'll want to mix together all the channels' sample values and there's your output sample to put in your final stream.
that's oversimplying the process, but that's basically how things would be done in an emulator.
i can't speak for getting allegro going, as i've always used SDL, and it's great. never had an issue with it.
- Probably the same reason that many of you have criticized a simple dash before my posts!thefox wrote:What is the point of posting all these images?
Zepper
RockNES author
RockNES author