Skip to content

Instantly share code, notes, and snippets.

@xavriley
Created February 11, 2016 09:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xavriley/b0fc5c7989b2f0b4c353 to your computer and use it in GitHub Desktop.
Save xavriley/b0fc5c7989b2f0b4c353 to your computer and use it in GitHub Desktop.
Emulating the 2A03 NES sound chip in SuperCollider

These are just proof of concept at the moment. All taken from the following sources:

Credits for Nescaline to:

* Copyright (c) 2014 Vesa Kivimäki
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>

You can try out/compare with Nescaline by downloading LMMS from https://lmms.io

Pulse

Easy - Pulse.ar with widths of 0.125, 0.25 or 0.5

Triangle

This is a 5 bit (32 step) Triangle waveform. The best I could do was this:

{ OscN.ar([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0].linlin(0,15,-1,1).as(LocalBuf), 330) }.plot

Which gives this kind of thing

Noise

This was awkward. The algorithm for noise in Nescaline sounds pulsey, slightly pitched and filtered so as not to be too harsh. The thing is it all comes from a deterministic algorithm where you take 1 as a 16 bit int

00000001

get bit 8 (or 13 depending on mode) with it shifted left by one, xor with the original bit 14, shift the contents of that left by one and then cycle bit 14 to 0. I think.

You then take that number and use it as the source for the next noise cycle. It's similar to some techniques used in "Byte Beat".

Anyway the resulting sound is this characteristic pulsey noise. The reason this is hard to implement exactly in SuperCollider is the single sample feedback required to feed the results of the last step into the current one. I'm a beginner with SuperCollider effectively so I don't fancy writing a plugin at this stage (although it might be nice to try in future). For those reasons I've faked the noise with:

(
	{
	    var noise_freqs =[1.0,2.0,4.0,5.34,8.0,10.68,16.0,19.03,25.4,32.0,42.71,64.0,128.0,256.0,512.0,1024.0];
	    Decimator.ar(Latch.ar(LPF.ar(WhiteNoise.ar,3000), Impulse.ar(220*b[0])),44100,4) 
	 }.play
)

I'm Latching onto noise which is giving it that pulsey square quality and using the frequencies defined in the technical documentation (or close to) to get that slightly pitched quality. Finally the filter and the Decimator help to shape the sound to something fairly close to the original, although admittedly not as good.

@xavriley
Copy link
Author

Leaving notes for myself for future reference.

Turns out Fredrik (redFrik) has already implemented a 2A03 emulator - Doh! Still it's more of an emulator than a synth, and the wrapper classes don't work in SC 3.6.6 (complaining about control rate) They are available to download here http://www.fredrikolofsson.com/pages/code-sc.html#plugins

My rough build process

# Get SC source to match installed version on OSX
$ git clone git@github.com:supercollider/supercollider.git
$ git checkout Version-3.6.6 
$ git submodule init && git sub module update
### Download the plugins to a folder, follow the README but don't bother with the build folder
$ cmake -DSC_PATH=/Users/xriley/Projects/supercollider -DINSTALL_DESTINATION="/Users/xriley/Library/Application Support/SuperCollider/Extensions"
$ make install

It also turns out that the noise generation on the NES (or at least the Nescaline) has a name - it's a Linear Feedback Shift Register and Fredrik (again) has implemented that directly in SC code here:

(copied from http://www.fredrikolofsson.com/f0blog/?q=node/607)

//4-bit Fibonacci LFSR
//https://en.wikipedia.org/wiki/Linear_feedback_shift_register
(
b= 2r1000; //seed
a= [];
15.do{
        a= a++b.asBinaryDigits(4);
        b= (b>>1)|((b&1).bitXor(b&2>>1)<<3);
        b.postln;
};
"";
)
s.boot
c= Buffer.loadCollection(s, a)
c.plot
{PlayBuf.ar(1, c, MouseX.kr(0, 1), loop:1)!2}.play
//4-bit Fibonacci LFSR with local in/out
(
a= {|rate= 4, iseed= 2r1000|
        var b, trig;
        var in= LocalIn.kr(1, iseed);
        trig= Impulse.kr(rate);
        b= Latch.kr(in, trig);//read
        b= (b>>1)|((b&1).bitXor(b&2>>1)<<3);//modify
        LocalOut.kr(b);
        b.poll(trig);
        DC.ar(0);
}.play;
)
a.set(\rate, 30)
a.free
//4-bit Fibonacci LFSR as single sample feedback with dbufrd/dbufwr
(
a= {|rate= 4, iseed= 2r1000|
        var b, trig;
        var buf= LocalBuf(1);
        buf.set(iseed);
        trig= Impulse.ar(rate);
        b= Demand.ar(trig, 0, Dbufrd(buf));//read
        b= (b>>1)|((b&1).bitXor(b&2>>1)<<3);//modify
        Demand.ar(trig, 0, Dbufwr(b, buf));//write
        b.poll(trig);
        DC.ar(0);
}.play;
)
a.set(\rate, 30)
a.free
//lfsr sound example (not sure this is correct but sounds ok)
(
a= {|rate= 400, iseed= 2r1000, tap1= 1, tap2= 3, tap3= 5, length= 16|
        var l, b, trig, o;
        var buf= LocalBuf(1);
        buf.set(iseed);
        trig= Impulse.ar(rate);
        l= Demand.ar(trig, 0, Dbufrd(buf));//read
        b= l.bitXor(l>>tap1).bitXor(l>>tap2).bitXor(l>>tap3)&1;//modify
        l= (l>>1)|(b<<15);//lfsr
        Demand.ar(trig, 0, Dbufwr(l, buf));//write
        o= PulseCount.ar(Impulse.ar(rate*length), trig);//bits
        l>>o&1!2;//output
}.play;
)
a.set(\rate, 300)
a.set(\rate, 100)
a.set(\length, 3)
a.set(\length, 14)
a.set(\tap1, 14.rand)
a.set(\tap2, 14.rand)
a.set(\tap3, 14.rand)
a.set(\length, 32)
a.set(\rate, 1000)
a.free
//lfsr sound with gui  (not sure this is correct but sounds ok)
(
var w, a;
w= Window("lfsr", Rect(100, 100, 520, 200)).front;
Slider(w, Rect(10, 10, 500, 25)).action_({|view| a.set(\rate, view.value*2000)}).value= 400/2000;
Slider(w, Rect(10, 40, 500, 25)).action_({|view| a.set(\length, view.value*32)}).value= 16/32;
a= {|rate= 400, iseed= 2r1000, tap1= 1, tap2= 3, tap3= 5, length= 16|
        var l, b, trig, o;
        var buf= LocalBuf(1);
        buf.set(iseed);
        trig= Impulse.ar(rate);
        l= Demand.ar(trig, 0, Dbufrd(buf));//read
        b= l.bitXor(l>>tap1).bitXor(l>>tap2).bitXor(l>>tap3)&1;//modify
        l= (l>>1)|(b<<15);//lfsr
        Demand.ar(trig, 0, Dbufwr(l, buf));//write
        o= PulseCount.ar(Impulse.ar(rate*length), trig);//bits
        l>>o&1!2;//output
}.play;
CmdPeriod.doOnce({w.close});
)

@xavriley
Copy link
Author

More good notes here: http://wiki.nesdev.com/w/index.php/APU_Noise

Above source says the resolution is 1-bit?!? Not how I understood it...

@xavriley
Copy link
Author

Eventually ended up implementing this using LFDNoise in SuperCollider which sounds pretty close, but doesn't use the LFSR stuff

More research notes:

http://people.ee.duke.edu/~dwyer/courses/ece52/taps.html
https://docs.google.com/spreadsheets/d/1hnFigZpPEBg9hdFjImPzOgRsgZWAYupFBaTePN0BuAw/edit?hl=en#gid=0

Good explanation of the noise LFSR in the 2A03 chip
https://diplograph.net/posts/the_nes_tetris_prng

@xavriley
Copy link
Author

@mjsyts
Copy link

mjsyts commented Jun 2, 2021

//I know this is an old project, but I was working on a similar implementation and I have some suggestions. FWIW I will post them here.

//Triangle:

//your original code for the triangle was very smart. There are a few alternates I tried, all of which are basically just as good.
//1) make an env with the same values you have listed (note that your triangle wave has a different phase than the original which starts at 15 and goes down to 0) but store it as a buf and use Osc.
//2) Use LFTri, round the values to 1/15 (reproduced here). I also added Select.kr so I could use \asKick as a binary for a kick drum like frequency envelope.

(
SynthDef.new(\tri, {
arg freq=261.63, amp=1/8, buf=0, atk=0.01, dec=0.075, sus=1, rel=1, hold=0, asKick=0;
var sig, env, fenv;
fenv = Select.kr(asKick,[freq, EnvGen.kr(Env.new([400, freq], [0.05], \exp))]);
sig = LFTri.ar(fenv, mul: 0.5, add:1).round(1/15)!2;
env = EnvGen.kr(Env.new([0,1,1,0], [atk, dec, hold, rel]), doneAction:2);
sig = sig * amp * env;
Out.ar(0, sig);
}).add;
)

//Noise is still in progress, but this should be close. Using LFNoise since you can control the frequency, which I believe would just correspond to sample rate. Then round amplitude to 2^8bits = 256. Could be wrong but this makes sense to me. So this is the $8F period setting:

x = {arg freq=440; (LFNoise0.ar(freq).round(1/256))!2}.play

@xavriley
Copy link
Author

xavriley commented Jun 7, 2021

Thanks for sharing! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment