Skip to content

Instantly share code, notes, and snippets.

@jeremyfromearth
Created December 9, 2010 12:06
Show Gist options
  • Save jeremyfromearth/734650 to your computer and use it in GitHub Desktop.
Save jeremyfromearth/734650 to your computer and use it in GitHub Desktop.
A wavetable synthesis experiment implementing wavelet tremolo effects
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='225', frameRate='60', backgroundColor="0x222222" ) ]
public class TremoloWaveTableDemo 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 SAMPLE_RATE:int = 44100;
public static const BUFFER_SIZE :int = 2048;
public static const PI_2:Number = Math.PI * 2;
public static const PI_2_OVR_SR:Number = PI_2 / SAMPLE_RATE;
public static const MIN_NOTE_INDEX:int = -108;
public static const MAX_NOTE_INDEX:int = 56;
public static const BASE_NOTE:int = 440;
protected var _sine:Vector.<Number>;
protected var _square:Vector.<Number>;
protected var _triangle:Vector.<Number>;
protected var _sawtooth:Vector.<Number>;
protected var _playing:Boolean;
protected var _output:Sound;
protected var _channel:SoundChannel;
protected var _waveforms:Array;
protected var _noteTable:Dictionary;
protected var _waveTable:Dictionary;
protected var _noteNameTable:Dictionary;
protected var _filterOutput:Number;
protected var _filterAmp:Number;
protected var _cutOff:Number;
protected var _resonance:Number;
protected var _phase:Number;
protected var _readIndex1:int;
protected var _readIndex2:int;
protected var _tremReadIndex:int;
protected var _phaseAccumulator:Number;
protected var _wavelet1:Vector.<Number>;
protected var _wavelet2:Vector.<Number>;
protected var _noteNames:Array;
public function TremoloWaveTableDemo()
{
addEventListener( Event.ENTER_FRAME, validateStage );
}
/**
* wait for the stage to have width before adding initializing
*/
protected function validateStage( event:Event ):void
{
if( !stage ) return;
if( stage.stageWidth <= 0 ) return;
_output = new Sound();
_filterOutput =
_filterAmp =
_cutOff =
_resonance = 0;
createNoteTable();
createWaveTable();
createDisplay();
removeEventListener( Event.ENTER_FRAME, validateStage );
}
// -----------------------------------------
//
// sound
//
// -----------------------------------------
/**
* generates a list of notes, maps the note index ( the distance from the BASE_NOTE ) to the numeric frequency
* also, map the note name to the note index
*/
protected function createNoteTable():void
{
_noteTable = new Dictionary();
_noteNameTable = new Dictionary();
_noteNames = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#' ];
var i:int = MIN_NOTE_INDEX;
while( i <= MAX_NOTE_INDEX )
{
var freq:Number = BASE_NOTE * Math.pow( 1.059463094359, i );
_noteTable[ i ] = freq
if( i < 0 )
{
_noteNameTable[ i ] = _noteNames[ ( i + Math.abs( MIN_NOTE_INDEX ) ) % _noteNames.length ];
} else {
_noteNameTable[ i ] = _noteNames[ i % _noteNames.length ];
}
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>>();
createWavelets( _waveTable[ SINE ], generateSine );
createWavelets( _waveTable[ SQUARE ], generateSquare );
createWavelets( _waveTable[ SAW ], generateSawtooth );
createWavelets( _waveTable[ TRIANGLE ], generateTriangle );
_waveforms = [ TRIANGLE, SAW, SINE, SQUARE ];
}
/**
* 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];
_phase = 0;
_phaseAccumulator = note / SAMPLE_RATE;
var length:int = Math.ceil( SAMPLE_RATE / note );
for( j = 0; j < length; j++ )
{
wavelet[j] = func();
_phase += _phaseAccumulator;
}
i++;
}
}
/**
* fills the audio buffer
* each waveform is computed
* readIndex is used to loop through the samples in the current wavelet
* waveforms are merged using addition and multiplied by mainAmp which attenutates the overall amplitude
*/
protected function onSampleData( event:SampleDataEvent ):void
{
var i:int;
var outSample:Number = 0;
// -- waveform 1
var sample1:Number = 0;
var amp1:Number = _ampSlider1.value;
var waveform1:String = _waveforms[ _dropDown1.selectedIndex ];
var noteIndex1:Number = Math.round( _freqKnob1.value ) + Math.abs( MIN_NOTE_INDEX );
_wavelet1 = _waveTable[ waveform1 ][ noteIndex1 ];
// -- waveform 2
var sample2:Number = 0;
var amp2:Number = _ampSlider2.value;
var waveform2:String = _waveforms[ _dropDown2.selectedIndex ];
var noteIndex2:Number = Math.round( _freqKnob2.value ) + Math.abs( MIN_NOTE_INDEX );
_wavelet2 = _waveTable[ waveform2 ][ noteIndex2 ];
// -- using a wavelet as a source for tremolo amplitude
var tremAmp:Number;
var tremSpeed:int = ( MAX_NOTE_INDEX - MIN_NOTE_INDEX ) * _tremSpeedKnob.value + MIN_NOTE_INDEX;
var tremWavelet:Vector.<Number> = _waveTable[ _waveforms[_tremDropDown.selectedIndex] ][tremSpeed + Math.abs( MIN_NOTE_INDEX ) ];
var tremDepth1:Number = _tremDepthKnob1.value;
var tremDepth2:Number = _tremDepthKnob2.value;
var mainAmp:Number = _mainVolumeKnob.value;
var filter:Number = _filterKnob.value;
_cutOff = _filterKnob.value;
_resonance = .999 - _filterKnob.value;
for( i = 0; i < BUFFER_SIZE; i++ )
{
// -- readIndex for the tremolo
_tremReadIndex = _tremReadIndex + 1 >= tremWavelet.length ? 0 : _tremReadIndex + 1;
// -- wavelet 1
_readIndex1 = _readIndex1 + 1 >= _wavelet1.length ? 0 : _readIndex1 + 1;
tremAmp = tremWavelet[ _tremReadIndex ] * tremDepth1;
sample1 = _wavelet1[ _readIndex1 ]
sample1 -= sample1 * tremAmp;
sample1 *= amp1;
// -- wavelet 2
_readIndex2 = _readIndex2 + 1 >= _wavelet2.length ? 0 : _readIndex2 + 1;
tremAmp = tremWavelet[ _tremReadIndex ] * tremDepth2;
sample2 = _wavelet2[ _readIndex2 ];
sample2 -= sample2 * tremAmp;
sample2 *= amp2;
// -- merge sample 1 & 2 and attenuate
outSample = ( sample1 + sample2 ) * .25;
// -- apply the filter
_filterAmp *= _resonance;
_filterAmp += ( outSample - _filterOutput ) * _cutOff;
_filterOutput += _filterAmp;
outSample = _filterOutput * mainAmp;
// -- constrain the amplitude
outSample = Math.min( Math.max( outSample, -1 ), 1 );
event.data.writeFloat( outSample );
event.data.writeFloat( outSample );
}
}
// -----------------------------------------
// -- wave form functions
// -- from Tonfall: http://code.google.com/p/tonfall/source/browse/trunk/src/tonfall/util/WaveFunction.as
// -----------------------------------------
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;
}
// -----------------------------------------
//
// event handlers
//
// -----------------------------------------
/**
* starts/stops audio
*/
protected function toggle( event:Event ):void
{
if( _playing )
{
stop();
} else {
start();
}
}
/**
* stops audio
* removes listeners
* updates ui
*/
protected function stop():void
{
if( _channel && _playing )
{
_playing = false;
_playButton.label = 'Play';
_playButton.selected = false;
_channel.stop();
_output.removeEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData );
removeEventListener( Event.ENTER_FRAME, updateVisualizer );
_channel = null;
}
}
/**
* starts audio
* resets readIndex vars
* updates ui
* adds listeners
*/
protected function start():void
{
_readIndex1 = 0;
_readIndex2 = 0;
_tremReadIndex = 0;
_playing = true;
_playButton.label = 'Stop';
_playButton.selected = true;
_output.addEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData );
addEventListener( Event.ENTER_FRAME, updateVisualizer );
_channel = _output.play();
}
/**
* updates the label on the "Note" knob
*/
protected function onFreqChange( event:Event ):void
{
if( event.target is Knob )
{
var knob:Knob = event.target as Knob;
knob.label = 'Note: ' + _noteNameTable[ Math.round( knob.value ) ];
}
}
/**
* 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';
}
}
/**
* draws the visualizer
*/
protected function updateVisualizer( event:Event ):void
{
if( !SoundMixer.areSoundsInaccessible() )
{
var h:int = 100;
var w:int = 160;
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 );
}
}
}
}
// -----------------------------------------
//
// display
//
// -----------------------------------------
protected var _playButton:PushButton;
protected var _dropDown1:ComboBox;
protected var _dropDown2:ComboBox;
protected var _ampSlider1:VUISlider;
protected var _ampSlider2:VUISlider;
protected var _freqKnob1:Knob;
protected var _freqKnob2:Knob;
protected var _tremDepthKnob1:Knob;
protected var _tremDepthKnob2:Knob;
protected var _mainVolumeKnob:Knob;
protected var _tremSpeedKnob:Knob;
protected var _tremDropDown:ComboBox;
protected var _visualizer:Shape;
protected var _filterKnob:Knob;
protected var _fsButton:PushButton;
/**
* builds the u.i.
*/
protected function createDisplay():void
{
// -- oscillator 1
drawRect( graphics, 5, 5, 160, 215 );
_dropDown1 = new ComboBox( this, 10, 10, 'Waveform 1', _waveforms );
_dropDown1.selectedIndex = 3;
_dropDown1.numVisibleItems = 4;
_dropDown1.width = 150;
_ampSlider1 = new VUISlider( this, 0, 0, 'Amplitude' );
_ampSlider1.maximum = 1;
_ampSlider1.value = 1;
_ampSlider1.y = 50;
_ampSlider1.x = 20;
_ampSlider1.tick = .01;
_ampSlider1.height = 165;
_freqKnob1 = new Knob( this, 100, 50, 'Note', onFreqChange );
_freqKnob1.minimum = -50;
_freqKnob1.maximum = 20;
_freqKnob1.value = -40;
_freqKnob1.labelPrecision = 0;
_tremDepthKnob1 = new Knob( this, 100, 138, 'Tremelo Depth' );
_tremDepthKnob1.minimum = 0;
_tremDepthKnob1.maximum = 1;
// -- oscillator 2
drawRect( graphics, 170, 5, 160, 215 )
_dropDown2 = new ComboBox( this, 175, 10, 'Waveform 2', _waveforms );
_dropDown2.selectedIndex = 1;
_dropDown2.numVisibleItems = 4;
_dropDown2.width = 150;
_ampSlider2 = new VUISlider( this, 0, 0, 'Amplitude' );
_ampSlider2.maximum = 1;
_ampSlider2.value = .5;
_ampSlider2.x = 185;
_ampSlider2.y = 50;
_ampSlider2.tick = .01;
_ampSlider2.height = 165;
_freqKnob2 = new Knob( this, 265, 50, 'Note', onFreqChange );
_freqKnob2.minimum = -50;
_freqKnob2.maximum = 20;
_freqKnob2.value = -16;
_freqKnob2.labelPrecision = 0;
_tremDepthKnob2 = new Knob( this, 265, 138, 'Tremelo Depth' );
_tremDepthKnob2.minimum = 0;
_tremDepthKnob2.maximum = 1;
addChild( _dropDown1 );
addChild( _dropDown2 );
// -- tremolo
drawRect( graphics, 335, 5, 110, 150 );
new Label( this, 340, 10, 'Tremolo' );
_tremDropDown = new ComboBox( this, 340, 30, 'Waveform', _waveforms );
_tremDropDown.numVisibleItems = 4;
_tremDropDown.selectedIndex = 0;
_tremSpeedKnob = new Knob( this, 370, 70, 'Tremelo Speed' );
_tremSpeedKnob.minimum = 0;
_tremSpeedKnob.maximum = 1;
_tremSpeedKnob.labelPrecision = 2;
// -- fs btn
drawRect( graphics, 335, 160, 110, 60 );
_fsButton = new PushButton( this, 340, 180, 'Fullscreen', toggleFullscreen );
// -- main
drawRect( graphics, 450, 110, 165, 110 );
_playButton = new PushButton( this, 455, 195, 'Start', toggle );
_playButton.toggle = true;
_playButton.height = 20;
_playButton.width = 155;
_filterKnob = new Knob( this, 470, 110, 'Filter' );
_filterKnob.minimum = 0.02;
_filterKnob.maximum = 1;
_filterKnob.labelPrecision = 2;
_mainVolumeKnob = new Knob( this, 560, 110, 'Main Volume', null );
_mainVolumeKnob.minimum = 0;
_mainVolumeKnob.maximum = 1;
_mainVolumeKnob.value = .5;
_visualizer = new Shape();
_visualizer.x = 450;
_visualizer.y = 5;
drawRect( _visualizer.graphics, 0, 0, 165, 100 );
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();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment