Created
December 20, 2010 12:05
-
-
Save jeremyfromearth/748307 to your computer and use it in GitHub Desktop.
Wavetable oscillator with portamento and frequency modulation
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="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