Created
December 9, 2010 12:06
-
-
Save jeremyfromearth/734650 to your computer and use it in GitHub Desktop.
A wavetable synthesis experiment implementing wavelet tremolo effects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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