Skip to content

Instantly share code, notes, and snippets.

@jeremyfromearth
Created December 20, 2010 12:05
Show Gist options
  • Save jeremyfromearth/748307 to your computer and use it in GitHub Desktop.
Save jeremyfromearth/748307 to your computer and use it in GitHub Desktop.
Wavetable oscillator with portamento and frequency modulation
package makemachine.demos.audio.wavetables
{
import com.bit101.components.*;
import flash.display.*;
import flash.events.*;
import flash.media.*;
import flash.utils.*;
[SWF( width="620", height="260", backgroundColor="0x222222", frameRate="60" )]
public class WavetablePitchControl extends Sprite
{
public static const SINE:String = 'Sine';
public static const SQUARE:String = 'Square';
public static const SAW:String = 'Saw';
public static const TRIANGLE:String = 'Triangle';
public static const BATMAN:String = 'Batman';
public static const SAMPLE_RATE:int = 44100;
public static const BUFFER_SIZE :int = 2048;
public static const MIN_NOTE_INDEX:int = -108;
public static const MAX_NOTE_INDEX:int = 20;
public static const BASE_NOTE:int = 440;
protected var _playing:Boolean;
protected var _output:Sound;
protected var _channel:SoundChannel;
protected var _phase:Number;
protected var _phaseAccumulator:Number;
protected var _wavelets:Vector.<Vector.<Number>>;
protected var _waveTable:Dictionary;
protected var _noteTable:Dictionary;
protected var _waveforms:Array;
public function WavetablePitchControl()
{
addEventListener( Event.ENTER_FRAME, validate );
}
protected function validate( event:Event ):void
{
if( !stage ) return;
if( stage.stageWidth == 0 ) return ;
_output = new Sound();
createNoteTable();
createWaveTable();
createDisplay();
removeEventListener( Event.ENTER_FRAME, validate );
}
// -----------------------------------------
//
// set up
//
// -----------------------------------------
// -- generates a list of notes ranging
// -- notes are listed as half step with index 0 being -108 half steps lower than A440
protected function createNoteTable():void
{
var i:int = MIN_NOTE_INDEX;
_noteTable = new Dictionary();
while( i <= MAX_NOTE_INDEX )
{
var freq:Number = BASE_NOTE * Math.pow( 1.059463094359, i );
_noteTable[ i ] = freq
i++;
}
}
// -- creates the wavetable dictionary using waveform name as key
// --allows for simple switching of waveforms when item is selected from dropdown
protected function createWaveTable():void
{
_waveTable = new Dictionary();
_waveTable[ SINE ] = new Vector.<Vector.<Number>>();
_waveTable[ SQUARE ] = new Vector.<Vector.<Number>>();
_waveTable[ SAW ] = new Vector.<Vector.<Number>>();
_waveTable[ TRIANGLE ] = new Vector.<Vector.<Number>>();
_waveTable[ BATMAN ] = new Vector.<Vector.<Number>>();
createWavelets( _waveTable[ SINE ], generateSine );
createWavelets( _waveTable[ SQUARE ], generateSquare );
createWavelets( _waveTable[ SAW ], generateSawtooth );
createWavelets( _waveTable[ TRIANGLE ], generateTriangle );
createWavelets( _waveTable[ BATMAN ], generateBatman );
_waveforms = [ SINE, SQUARE, SAW, TRIANGLE, BATMAN ];
}
// -- a wavelet is essentially a list of numbers ( samples )
// -- this method generates such a list, using the supplied func parameter to generate the numbers
// -- the phaseAccumulator is quite important here, it is added to the phase variable which
// -- is used by each of the wave generating functions
protected function createWavelets( vec:Vector.<Vector.<Number>>, func:Function ):void
{
var note:Number;
var j:int;
var i:int = MIN_NOTE_INDEX;
var wavelet:Vector.<Number>
while( i <= MAX_NOTE_INDEX )
{
wavelet = new Vector.<Number>();
vec.push( wavelet );
note = _noteTable[i];
var length:int = Math.floor( SAMPLE_RATE / note );
_phase = 0;
_phaseAccumulator = ( note / SAMPLE_RATE );
for( j = 0; j < length; j++ )
{
wavelet[j] = func();
_phase += _phaseAccumulator;
}
i++;
}
}
// -----------------------------------------
//
// generators
//
// -----------------------------------------
protected function generateSine():Number
{
return Math.sin( _phase * 2.0 * Math.PI );
}
protected function generateSquare():Number
{
return _phase < .5 ? 1.0 : -1.0;
}
protected function generateSawtooth():Number
{
return _phase * 2.0 - 1.0;;
}
protected function generateTriangle():Number
{
if( 0.5 > _phase ) return _phase * 4.0 - 1.0;
return 3.0 - _phase * 4.0;
}
protected function generateBatman():Number
{
var red:Number = 1;
if( _phase < .16 )
red = 1;
if( _phase > .16 && _phase < .33 )
red = .5;
if( _phase > .33 && _phase < .5 )
red = 1;
if( _phase > .5 && _phase < .66 )
red = 1;
if( _phase > .66 && _phase < .82 )
red = .5;
return generateSine() * red;
}
// -----------------------------------------
//
// sound processing - onSampleData
//
// -----------------------------------------
protected var mod:int = 0;
protected var _readIndex:Number = 0;
protected var _pa:Number = 0;
protected var _prevFreq:Number;
protected var _freq:Number;
protected var _targetFreq:Number;
protected function onSampleData( event:SampleDataEvent ):void
{
// -- using the longest wavelet only
var wavelet:Vector.<Number> = _waveTable[ _oscWaveMenu.selectedItem ][ 0 ];
var length:Number = wavelet.length;
// -- only using the wavetable for modulation input
var modSpeed:int = Math.round( _modSpeedKnob.value );
var modTable:Vector.<Number> = _waveTable[ _modWaveMenu.selectedItem ][modSpeed];
var portTime:Number = Math.max( _portamentoKnob.maximum - _portamentoKnob.value, _portamentoKnob.minimum );
_targetFreq = _noteTable[ Math.round( _freqKnob.value ) ];
for( var i:int = 0; i < BUFFER_SIZE; i++ )
{
var n:Number = wavelet[ Math.round( _readIndex ) ] * _volumeSlider.value;
event.data.writeFloat( n );
event.data.writeFloat( n );
// -- easing equation for the frequency
_freq += ( _targetFreq - _freq ) * portTime;
_pa = ( _freq / SAMPLE_RATE ) * length;
// -- using another wavelets for looking up modulation speed
mod = mod + 1 >= modTable.length ? 0 : mod + 1;
_pa += _pa * modTable[mod] * _modDepthKnob.value;
// -- increment the readIndex
if( Math.round( _readIndex + _pa ) > length -1 )
{
_readIndex = 0;
} else {
_readIndex += _pa;
}
}
}
// -----------------------------------------
//
// event handlers
//
// -----------------------------------------
// -- starts/stops audio
protected function toggle( event:Event ):void
{
if( _playing )
{
stop();
} else {
start();
}
}
// -- stops audio, removes listeners, update ui
protected function stop():void
{
if( _channel && _playing )
{
_playing = false;
_playButton.label = 'Play »';
_playButton.selected = false;
_channel.stop();
removeEventListener( Event.ENTER_FRAME, updateVisualizer );
_output.removeEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData );
_channel = null;
}
}
// -- starts audio, resets readIndex vars, updates ui, adds listeners
protected function start():void
{
_playing = true;
_playButton.label = 'Stop';
_playButton.selected = true;
_freq = _targetFreq = _noteTable[ Math.round( _freqKnob.value ) ];
_output.addEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData );
addEventListener( Event.ENTER_FRAME, updateVisualizer );
_channel = _output.play();
}
/**
* draws the visualizer
*/
protected function updateVisualizer( event:Event ):void
{
if( !SoundMixer.areSoundsInaccessible() )
{
var h:int = 155;
var w:int = 610;
var g:Graphics = _visualizer.graphics;
g.clear();
g.lineStyle( 1, 0xf05151 );
drawRect( g, 0, 0, w, h );
g.moveTo( 0, h * .5 );
g.lineTo( w, h * .5 );
var bytes:ByteArray = new ByteArray();
SoundMixer.computeSpectrum( bytes );
bytes.position = 0;
var n:Number = 0;
var xpos:Number;
var ypos:Number;
if( bytes.bytesAvailable )
{
g.moveTo( 0, h * .5 );
g.lineStyle( 1, 0x00c6ff );
for( var i:int = 0; i < 255; i++ )
{
n = bytes.readFloat();
xpos = ( w / 255 ) * i;
ypos = ( h * .5 ) + ( h * .5 * n )
if( i == 0 )
{
g.moveTo( xpos, ypos );
}
g.lineTo( xpos, ypos );
}
}
}
}
/**
* toggles fullscreen mode when button is clicked
*/
protected function toggleFullscreen( event:Event ):void
{
if( stage.displayState == StageDisplayState.FULL_SCREEN )
{
stage.displayState = StageDisplayState.NORMAL;
} else {
stage.displayState = StageDisplayState.FULL_SCREEN;
}
}
/**
* change the text of the button based on this event because fs mode can change
* when someone presses escape
*/
protected function onFullscreen( event:FullScreenEvent ):void
{
if( stage.displayState == StageDisplayState.FULL_SCREEN )
{
_fsButton.label = 'Exit Fullscreen';
} else {
_fsButton.label = 'Fullscreen';
}
}
// -----------------------------------------
//
// event handlers
//
// -----------------------------------------
protected var _visualizer:Sprite;
protected var _freqKnob:Knob;
protected var _portamentoKnob:Knob;
protected var _modDepthKnob:Knob;
protected var _modSpeedKnob:Knob;
protected var _oscWaveMenu:ComboBox;
protected var _modWaveMenu:ComboBox;
protected var _volumeSlider:HSlider;
protected var _playButton:PushButton;
protected var _fsButton:PushButton;
protected function createDisplay():void
{
var h:int = 155;
var w:int = 610;
_visualizer = new Sprite();
_visualizer.x = _visualizer.y = 5;
var g:Graphics = _visualizer.graphics;
g.clear();
g.lineStyle( 1, 0xf05151 );
drawRect( g, 0, 0, w, h );
g.moveTo( 0, h * .5 );
g.lineTo( w, h * .5 );
var container:Sprite = new Sprite();
container.y = 160;
var hbox:HBox = new HBox( container, 10, 10 );
hbox.spacing = 20;
_freqKnob = getKnob( hbox, 'Frequency', -40, MAX_NOTE_INDEX, 0 );
_portamentoKnob = getKnob( hbox, 'Portamento', .0001, .01, 4 );
_portamentoKnob.value = _portamentoKnob.maximum;
_modDepthKnob = getKnob( hbox, 'Mod Depth', 0, 1, 3 );
_modSpeedKnob = getKnob( hbox, 'Mod Speed', 0, Math.abs( MIN_NOTE_INDEX ) + MAX_NOTE_INDEX, 0 );
hbox.draw();
new Label( container, 250, 10, 'Oscillator Waveform' );
_oscWaveMenu = new ComboBox( container, 250, 25, '', _waveforms );
_oscWaveMenu.numVisibleItems = 2;
_oscWaveMenu.selectedIndex = 0;
new Label( container, 355, 10, 'Modulation Waveform' );
_modWaveMenu = new ComboBox( container, 355, 25, '', _waveforms );
_modWaveMenu.numVisibleItems = 2;
_modWaveMenu.selectedIndex = 0;
drawRect( container.graphics, 5, 5, 235, 90 );
drawRect( container.graphics, 245, 5, 215, 90 );
drawRect( container.graphics, 465, 5, 150, 90 );
new Label( container, 470, 10, 'Volume' );
_volumeSlider = new HSlider( container, 470, 30 );
_volumeSlider.minimum = 0;
_volumeSlider.maximum = 1;
_volumeSlider.tick = .01
_volumeSlider.value = .5;
_volumeSlider.width = 140;
_playButton = new PushButton( container, 470, 45, 'Play »', toggle );
_playButton.toggle = true;
_playButton.width = 140;
_fsButton = new PushButton( container, 470, 70, 'Fullscreen', toggleFullscreen );
_fsButton.toggle = true;
_fsButton.width = 140;
addChild( container );
addChild( _visualizer );
stage.addEventListener( FullScreenEvent.FULL_SCREEN, onFullscreen );
}
/**
* convenience method for drawing rectanlges
*/
protected function drawRect( g:Graphics, x:int, y:int, w:int, h:int ):void
{
g.beginFill( 0xFFFFFF, .1 );
g.drawRect( x, y, w, h );
g.endFill();
}
protected function getKnob( obj:DisplayObjectContainer, label:String, min:Number, max:Number, prec:Number, callback:Function = null ):Knob
{
var knob:Knob = new Knob( obj, 0, 0, label, callback );
knob.minimum = min;
knob.maximum = max;
knob.labelPrecision = prec;
knob.draw();
return knob;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment