Skip to content

Instantly share code, notes, and snippets.

@xavriley
Created February 11, 2016 09:44
Show Gist options
  • 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

@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