I give up, I can't find a way to emulate chiptune sound!

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Undubbed
Posts: 24
Joined: Sun Aug 09, 2009 7:46 am

I give up, I can't find a way to emulate chiptune sound!

Post by Undubbed »

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!
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples »

I've never used Allegro 5, but in Allegro 4, what you want is AUDIOSTREAM.
Undubbed
Posts: 24
Joined: Sun Aug 09, 2009 7:46 am

Post by Undubbed »

Alright, thanks!

I searched the examples in my allegro folder and found ex_synth.cpp which seems to contain the algorithms to produce a square and triangle wave and I'll check that out for a while.
Undubbed
Posts: 24
Joined: Sun Aug 09, 2009 7:46 am

Post by Undubbed »

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.

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 >_<.
User avatar
miker00lz
Posts: 235
Joined: Thu Sep 23, 2010 7:28 pm

Post by miker00lz »

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.

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
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
User avatar
Zepper
Formerly Fx3
Posts: 3262
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper »

Image
User avatar
thefox
Posts: 3134
Joined: Mon Jan 03, 2005 10:36 am
Location: 🇫🇮
Contact:

Post by thefox »

Zepper wrote:HMM...
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?).
User avatar
miker00lz
Posts: 235
Joined: Thu Sep 23, 2010 7:28 pm

Post by miker00lz »

thefox wrote:
Zepper wrote:HMM...
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?).
seriously... i'm trying to help show the guy how i did my sound emulation.
User avatar
Zepper
Formerly Fx3
Posts: 3262
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper »

It's not quite exactly what he wants. Basically, it's the Allegro sound interface, not the sound emulation side.
User avatar
miker00lz
Posts: 235
Joined: Thu Sep 23, 2010 7:28 pm

Post by miker00lz »

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.
User avatar
Zepper
Formerly Fx3
Posts: 3262
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper »

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.
User avatar
miker00lz
Posts: 235
Joined: Thu Sep 23, 2010 7:28 pm

Post by miker00lz »

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.
User avatar
Zepper
Formerly Fx3
Posts: 3262
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper »

Yup. That's the basic idea.
User avatar
Zepper
Formerly Fx3
Posts: 3262
Joined: Fri Nov 12, 2004 4:59 pm
Location: Brazil
Contact:

Post by Zepper »

thefox wrote:What is the point of posting all these images?
- Probably the same reason that many of you have criticized a simple dash before my posts!
User avatar
cpow
NESICIDE developer
Posts: 1097
Joined: Mon Oct 13, 2008 7:55 pm
Location: Minneapolis, MN
Contact:

Post by cpow »

Zepper wrote:Yup. That's the basic idea.
What is??
Post Reply