Skip to content

Instantly share code, notes, and snippets.

@Bolloxim
Created August 26, 2014 09:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Bolloxim/c91ae9e76a6368b44b54 to your computer and use it in GitHub Desktop.
Save Bolloxim/c91ae9e76a6368b44b54 to your computer and use it in GitHub Desktop.
A Pen by Andi Smithers.

Atari Pokey Chip Emulator

Simple Web Audio Implementation for emulating the atari pokey chip

version 0.1 Its early yet but got the pure tone frequency done. Noise next along with some game effects

version 0.2 after trying to emulate a 1.79mhz clock used as a RNG then turned into noise .. I ended up with a randomized buffer.

*note tonal was working as a port for pokey but the noise RNG is tough to emulator without using scriptnode in webaudio and thats a no-no as it hurts perf. so going for a hybrid system

A Pen by Andi Smithers on CodePen.

License.

<body>
<canvas id='audio'></canvas>
<!-- link to google fonts -->
<link href='http://fonts.googleapis.com/css?family=Orbitron' rel='stylesheet' type='text/css'>
<!-- https://www.youtube.com/watch?v=hFsCG7v9Y4c&feature=youtu.be&t=18m22s
Awesome presentation on web-audio. 47mins for fm synth
Pokey Player Reference for frequencies
http://krap.pl/mirrorz/atari/homepage.ntlworld.com/kryten_droid/Atari/800XL/atari_hw/pokey.htm#Audio
-->
<script src="http://bolloxim.github.io/Star-Raiders/javascripts/uicontrols.js"></script>
</body>
/*****************************************************************************
The MIT License (MIT)
Copyright (c) 2014 Andi Smithers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*****************************************************************************/
// conceptualized and written by andi smithers
// constant options
const focalDepth = 80;
const focalPoint = 256;
// variables
var centreX;
var centreY;
var mouseX;
var mouseY;
var spawnX;
var spawnY;
var frameCount=0;
var audioContext;
var freqSelect = 1; // triple octave settings .. use 1 for regular
var waveform = 0;
var noiseOn = false;
var whiteNoiseBuffer;
var pinkNoiseBuffer;
var brownNoiseBuffer;
var noiseSelect = 0;
var finTable = [1789790.0, 63921.0, 15699.9];
// starts high C
var pokeyFrequencyDivsors = [
29, 31, 33, 35, 37, 40, 42, 45, 47, 50, 53, 57, // C,A,B descends 12 notes per range
60, 64, 68, 72, 76, 81, 85, 91, 96,102,108,114, // C,A,B etc
121,128,136,144,153,162,173,182,193,204,217,230, // middle C
243]; // lowest c
var waveTable = [ "sine", "square", "sawtooth","triangle", "noise"];
function Envelope(at,dt,dv,st,rt)
{
this.init(dv, at,dt,st,rt);
}
Envelope.prototype.init = function(dv,at,dt,st,rt)
{
this.decay2 = dv; // decay %
this.attack = at;
this.decay = dt;
this.sustain = st;
this.release = rt;
this.gain = audioContext.createGain();
}
Envelope.prototype.node = function(time)
{
this.time = time || audioContext.currentTime;
this.gain.gain.linearRampToValueAtTime(0.0, time);
this.gain.gain.linearRampToValueAtTime(1.0, time+this.attack);
this.gain.gain.linearRampToValueAtTime(this.decay2, time+this.decay);
this.gain.gain.setValueAtTime(this.decay2, time+this.sustain);
this.gain.gain.linearRampToValueAtTime(0.0, time+this.release);
return this.gain;
}
function FMSynth(noise)
{
if (noise == null) noise =false;
this.init(noise);
}
FMSynth.prototype.init = function(noise)
{
if (waveform<4)
{
this.oscilator = audioContext.createOscillator();
this.oscilator.type = waveTable[waveform];
this.oscilator.frequency.value = 0; // Default frequency in hertz
this.node = this.oscilator;
}
else
{
this.node = audioContext.createBufferSource();
switch (noiseSelect)
{
case 0: this.node.buffer = whiteNoiseBuffer;
break;
case 1: this.node.buffer = pinkNoiseBuffer;
break;
case 2: this.node.buffer = brownNoiseBuffer;
break;
}
this.node.loop = true;
}
this.gainNode = audioContext.createGain();
this.gainNode.gain.value = 1;
if (waveform<4)
{
this.node.connect(this.gainNode);
}
else
{
this.filter = audioContext.createBiquadFilter();
this.filter.type = "lowpass";
this.filter.frequency.value = 300;
this.filter.gain.value = 0;
this.filter.Q.value = 10;
this.node.connect(this.filter);
this.filter.connect( this.gainNode);
}
this.gainNode.connect(audioContext.destination);
}
FMSynth.prototype.play = function(note, delay, duration)
{
var N = pokeyFrequencyDivsors[note]+1;
var Fin = finTable[freqSelect];
var M = 6;
var Fout = Fin;
if (freqSelect!=0) Fout/= (2*N);
else Fout /= (2 * (N+M));
if (waveform<4)
this.node.frequency.value = Fout; // Default frequency in hertz
else
this.filter.frequency.value = Fout;
var playNode = this.node;
playNode.start(delay);
playNode.stop(delay+duration);
}
// initialization
function init()
{
// setup canvas and context
canvas = document.getElementById('audio');
context = canvas.getContext('2d');
// set canvas to be window dimensions
resize();
// create event listeners
canvas.addEventListener('mousemove', mouseMove);
canvas.addEventListener('click', mouseClick);
window.addEventListener('resize', resize);
// initialze variables
window.AudioContext = window.AudioContext||window.webkitAudioContext;
audioContext = new AudioContext();
createWhiteNoiseBuffer();
createPinkNoiseBuffer();
createBrownNoiseBuffer();
SetupButtons();
}
// input functions
function mouseMove(event)
{
var rect = canvas.getBoundingClientRect();
mouseX = event.clientX - rect.left,
mouseY = event.clientY - rect.top
}
function mouseClick()
{
buttonpressed = CheckButtons(mouseX, mouseY, false);
}
function resize()
{
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// compute centre of screen
centreX = canvas.width/2;
centreY = canvas.height/2;
}
// rendering functions
function render()
{
context.fillStyle = 'black';
context.clearRect(0, 0, canvas.width, canvas.height);
RenderInfo();
RenderButtons();
context.globalAlpha = 1.0;
context.font = '20pt Calibri';
context.fillStyle = 'rgb(255,255,255)';
context.textAlign = "center";
context.fillText('Atari Pokey Chip Audio Emulation', canvas.width/2, 100);
context.fillText('using web audio api', canvas.width/2, 130);
}
// movement functions
function update()
{
CheckButtons();
}
// per frame tick functions
function animate()
{
frameCount++;
// movement update
update();
// render update
render();
// trigger next frame
requestAnimationFrame(animate);
}
function SetupButtons()
{
// erase old buttons
buttons = [];
var bx = canvas.width/2 -400;
var by = 100;
var mx = canvas.width/2 +260;
new Button(bx, by+100, 140, 40, "Waveform", SwitchWaveform, '0');
new Button(bx, by+50, 140, 40, "Frequency", SwitchFrequency, '0');
new Button(bx, by, 140, 40, "Play Scale Down", PlayScaleDown, '1');
new Button(bx, by-50, 140, 40, "Play Scale Up", PlayScaleUp, '2');
new Button(bx, by+150, 140, 40, "Noise Type", PlayNoise, '2');
// new Button(bx, by+200, 140, 40, "Noise Off", PlayNoiseCancel, '2');
new Button(mx, by-50, 140, 40, "Play Confirm", PlayConfirm, '2');
new Button(mx, by, 140, 40, "Play Red Alert", PlayRedAlert, '2');
new Button(mx, by+50, 140, 40, "Play Photon", PlayPhoton, '3');
new Button(mx, by+100, 140, 40, "Play Disrubtor", PlayDisruptor, '3');
new Button(mx, by+150, 140, 40, "Play Explosion", PlayExplosion, '3');
new Button(mx-150, by+150, 140, 40, "Play Explosion Thud", PlayExplosionThud, '3');
new Button(mx+150, by+150, 140, 40, "Play Shield", PlayShield, '3');
new Button(mx, by+200, 140, 40, "Hyperspace", PlayHyperspace, '3');
new Button(mx+150, by+200, 140, 40, "Hyperspace Exit", PlayExit, '3');
}
function RenderInfo()
{
context.globalAlpha = 1.0;
context.font = '20pt Calibri';
context.fillStyle = 'rgb(255,255,255)';
context.textAlign = "center";
if (freqSelect)
context.fillText(finTable[freqSelect]/1000+' Khz', canvas.width/2, 180);
else
context.fillText(finTable[freqSelect]/1000000+' Mhz', canvas.width/2, 180);
context.fillText(waveTable[waveform] +' waveform', canvas.width/2, 230);
noiseNames = ['white', 'pink', 'brown'];
context.fillText(noiseNames[noiseSelect]+'-noise', canvas.width/2, 280);
}
function SwitchWaveform(button)
{
waveform = (waveform+1)%5;
}
function SwitchFrequency(button)
{
freqSelect = (freqSelect+1) %3;
}
function PlayScaleDown(button)
{
for (var note=0; note<37; note++)
{
time = note * 0.2;
(new FMSynth()).play(note, audioContext.currentTime+time, 0.2);
}
}
function PlayScaleUp(button)
{
for (var note=0; note<37; note++)
{
time = note * 0.2;
(new FMSynth()).play(37-note, audioContext.currentTime+time, 0.2);
}
}
function PlayConfirm()
{
time = 0
inc = 1/8;
dur = inc/2;
freqSelect = 0;
waveform = 3;
note = 36;
for (var i=0; i<3; i++)
{
(new FMSynth()).play(note, audioContext.currentTime+time, dur);
time += inc;
}
}
function PlayRedAlert()
{
time = 0
inc = 1/3;
dur = inc;
freqSelect = 1;
waveform = 1;
note1 = 14;
note2 = 26;
for (var i=0; i<4; i++)
{
(new FMSynth()).play(note1, audioContext.currentTime+time, dur);
time += inc;
(new FMSynth()).play(note2, audioContext.currentTime+time, dur);
time += inc;
}
}
function PlayDisruptor()
{
waveform = 0;
freqSelect = 1;
photonTone= new FMSynth();
photonTone.play(0, audioContext.currentTime, 0.4);
photonTone.oscilator.frequency.linearRampToValueAtTime(30,audioContext.currentTime);
photonTone.oscilator.frequency.linearRampToValueAtTime(300,audioContext.currentTime+1);
waveform = 4;
freqSelect = 1;
photonSound = new FMSynth();
photonSound.play(0, audioContext.currentTime, 2);
photonSound.filter.frequency.linearRampToValueAtTime(600, audioContext.currentTime);
photonSound.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime+2);
photonSound.filter.Q.linearRampToValueAtTime(25, audioContext.currentTime);
photonSound.filter.Q.linearRampToValueAtTime(8, audioContext.currentTime+1);
photonSound.filter.Q.linearRampToValueAtTime(3, audioContext.currentTime+1.1);
}
function PlayExplosion()
{
freqSelect = 2;
waveform = 0;
noiseSelect = 0;
(new FMSynth()).play(24, audioContext.currentTime, 0.5);
waveform = 4;
noiseSelect = 0;
explode1 = new FMSynth();
explode1.play(0, audioContext.currentTime, 2);
explode1.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime);
explode1.filter.frequency.linearRampToValueAtTime(1000, audioContext.currentTime+1);
explode1.filter.frequency.linearRampToValueAtTime(8200, audioContext.currentTime+2);
explode1.filter.Q.linearRampToValueAtTime(0.1, audioContext.currentTime);
explode1.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime+1);
explode1.filter.Q.linearRampToValueAtTime(2.5, audioContext.currentTime+1.7);
envelope = new Envelope(0, 0.9, 0.5, 1.2, 1.4);
explode1.filter.disconnect(explode1.gainNode);
explode1.gainNode = envelope.node(audioContext.currentTime);
explode1.filter.connect(explode1.gainNode);
explode1.gainNode.connect(audioContext.destination);
noiseSelect = 1;
explode2 = new FMSynth();
explode2.play(0, audioContext.currentTime, 2);
explode2.filter.frequency.linearRampToValueAtTime(1600, audioContext.currentTime);
explode2.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime+1);
explode2.filter.frequency.linearRampToValueAtTime(18000, audioContext.currentTime+2);
explode2.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime);
explode2.filter.Q.linearRampToValueAtTime(10.3, audioContext.currentTime+1);
explode2.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime+2);
envelope = new Envelope(0.0, 0.9, 0.4, 1.0, 2);
explode2.filter.disconnect(explode2.gainNode);
explode2.gainNode = envelope.node(audioContext.currentTime);
explode2.filter.connect(explode2.gainNode);
explode2.gainNode.connect(audioContext.destination);
}
function PlayExplosionThud()
{
freqSelect = 2;
waveform = 0;
noiseSelect = 0;
(new FMSynth()).play(24, audioContext.currentTime, 0.5);
waveform = 4;
noiseSelect = 0;
explode1 = new FMSynth();
explode1.play(0, audioContext.currentTime, 1);
explode1.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime);
explode1.filter.frequency.linearRampToValueAtTime(1000, audioContext.currentTime+1);
explode1.filter.Q.linearRampToValueAtTime(0.1, audioContext.currentTime);
explode1.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime+1);
explode1.filter.Q.linearRampToValueAtTime(2.5, audioContext.currentTime+1.7);
envelope = new Envelope(0, 0.9, 0.5, 1.2, 1.4);
explode1.filter.disconnect(explode1.gainNode);
explode1.gainNode = envelope.node(audioContext.currentTime);
explode1.filter.connect(explode1.gainNode);
explode1.gainNode.connect(audioContext.destination);
noiseSelect = 1;
explode2 = new FMSynth();
explode2.play(0, audioContext.currentTime, 1);
explode2.filter.frequency.linearRampToValueAtTime(1600, audioContext.currentTime);
explode2.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime+1);
explode2.filter.frequency.linearRampToValueAtTime(18000, audioContext.currentTime+2);
explode2.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime);
explode2.filter.Q.linearRampToValueAtTime(10.3, audioContext.currentTime+1);
explode2.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime+2);
envelope = new Envelope(0.0, 0.9, 0.4, 1.0, 2);
explode2.filter.disconnect(explode2.gainNode);
explode2.gainNode = envelope.node(audioContext.currentTime);
explode2.filter.connect(explode2.gainNode);
explode2.gainNode.connect(audioContext.destination);
}
function PlayHyperspace()
{
waveform = 4;
noiseSelect = 2;
photonSound = new FMSynth();
photonSound.play(0, audioContext.currentTime, 8);
photonSound.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime);
photonSound.filter.frequency.linearRampToValueAtTime(1000, audioContext.currentTime+2); photonSound.filter.frequency.linearRampToValueAtTime(2500, audioContext.currentTime+8);
photonSound.filter.Q.linearRampToValueAtTime(25, audioContext.currentTime);
photonSound.filter.Q.linearRampToValueAtTime(8, audioContext.currentTime+1);
photonSound.filter.Q.linearRampToValueAtTime(5, audioContext.currentTime+1.1);
photonSound.filter.Q.linearRampToValueAtTime(19, audioContext.currentTime+5.1);
}
function PlayExit()
{
waveform = 4;
noiseSelect = 2;
photonSound = new FMSynth();
photonSound.play(0, audioContext.currentTime, 2);
photonSound.filter.frequency.linearRampToValueAtTime(2500, audioContext.currentTime);
photonSound.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime+2);
photonSound.filter.Q.linearRampToValueAtTime(21, audioContext.currentTime);
photonSound.filter.Q.linearRampToValueAtTime(8, audioContext.currentTime+2);
}
function PlayPhoton(button)
{
waveform = 0;
freqSelect = 1;
photonTone= new FMSynth();
photonTone.play(0, audioContext.currentTime, 0.4);
photonTone.oscilator.frequency.linearRampToValueAtTime(220,audioContext.currentTime);
photonTone.oscilator.frequency.linearRampToValueAtTime(50,audioContext.currentTime+0.1);
photonTone.oscilator.frequency.linearRampToValueAtTime(100,audioContext.currentTime+1.4);
waveform = 4;
freqSelect = 1;
photonSound = new FMSynth();
photonSound.play(0, audioContext.currentTime, 1.2);
photonSound.filter.frequency.linearRampToValueAtTime(2000, audioContext.currentTime);
photonSound.filter.frequency.linearRampToValueAtTime(100, audioContext.currentTime+2);
photonSound.filter.Q.linearRampToValueAtTime(40, audioContext.currentTime);
photonSound.filter.Q.linearRampToValueAtTime(10, audioContext.currentTime+0.5);
photonSound.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime+1.2);
}
function PlayShield()
{
freqSelect = 0;
waveform = 4;
noiseSelect = 1;
var shield = new FMSynth();
shield.play(24, audioContext.currentTime, 0.25);
shield.filter.type = 'highpass';
shield.filter.frequency.linearRampToValueAtTime(13000, audioContext.currentTime);
shield.filter.frequency.linearRampToValueAtTime(5000, audioContext.currentTime+0.25);
shield.filter.Q.linearRampToValueAtTime(1, audioContext.currentTime);
shield.filter.Q.linearRampToValueAtTime(12, audioContext.currentTime+0.25);
PlayExplosionThud();
}
function PlayNoise(button)
{
noiseSelect = (noiseSelect+1)%3;
}
createWhiteNoiseBuffer = function(bufferSize)
{
bufferSize = bufferSize || 131072;
whiteNoiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
var dataChannel = whiteNoiseBuffer.getChannelData(0);
console.log(dataChannel.length);
for (var i=0; i<dataChannel.length; i++)
{
dataChannel[i] = Math.random()*2-1;
}
}
createPinkNoiseBuffer = function(bufferSize)
{
bufferSize = bufferSize || 131072;
pinkNoiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
var output = pinkNoiseBuffer.getChannelData(0);
var b0, b1, b2, b3, b4, b5, b6;
b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0;
for (var i = 0; i < bufferSize; i++)
{
var white = Math.random() * 2 - 1;
b0 = 0.99886 * b0 + white * 0.0555179;
b1 = 0.99332 * b1 + white * 0.0750759;
b2 = 0.96900 * b2 + white * 0.1538520;
b3 = 0.86650 * b3 + white * 0.3104856;
b4 = 0.55000 * b4 + white * 0.5329522;
b5 = -0.7616 * b5 - white * 0.0168980;
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
output[i] *= 0.11; // (roughly) compensate for gain
b6 = white * 0.115926;
}
};
createBrownNoiseBuffer = function(bufferSize)
{
bufferSize = bufferSize || 131072;
var lastOut = 0.0;
brownNoiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
var output = brownNoiseBuffer.getChannelData(0);
for (var i = 0; i < bufferSize; i++)
{
var white = Math.random() * 2 - 1;
output[i] = (lastOut + (0.02 * white)) / 1.02;
lastOut = output[i];
output[i] *= 3.5; // (roughly) compensate for gain
}
};
// entry point
init();
animate();
body {
background: #000000;
margin: 0px;
padding: 0px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment