Skip to content

Instantly share code, notes, and snippets.

@grapefrukt
Created February 4, 2014 23:04
Show Gist options
  • Save grapefrukt/8814245 to your computer and use it in GitHub Desktop.
Save grapefrukt/8814245 to your computer and use it in GitHub Desktop.
Here's a few bits and pieces I once used to pitch/filter a sound in OpenFL
/*
The MIT License
Copyright (c) <year> <copyright holders>
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.
*/
package com.anttikupila.media.filters;
interface IFilter {
public var channelCopy(get, never):IFilter;
function process(input:Float ) : Float;
function duplicate( ) : IFilter;
}
private var sound:SoundPitcher;
private var lowpass:MoogFilter;
sound = new SoundPitcher();
#if flash
sound.playFromMp3("music/885027322.mp3");
#else
sound.playFromData("music/audio.dat");
#end
sound.setPhase(Std.random(sound.numSamples));
lowpass = new MoogFilter(44100, .1);
sound.filters = [ lowpass ];
/*
The MIT License
Copyright (c) <year> <copyright holders>
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.
*/
package com.anttikupila.media.filters;
import com.anttikupila.media.SoundFX;
import flash.errors.RangeError;
class MoogFilter implements IFilter {
//---------------------------------------------------------------------
//
// Variables
//
//---------------------------------------------------------------------
private var cutoff:Float;
private var res:Float;
private var fs:Float = SoundFX.SAMPLE_RATE;
private var x:Float;
private var y1:Float;
private var y2:Float;
private var y3:Float;
private var y4:Float;
private var oldx:Float;
private var oldy1:Float;
private var oldy2:Float;
private var oldy3:Float;
private var f:Float;
private var p:Float;
private var k:Float;
private var t:Float;
private var t2:Float;
private var r:Float;
private var _channelCopy:MoogFilter;
public var channelCopy(get, never):IFilter;
public var cutoffFrequency(get, set):Float;
public var resonance(get, set):Float;
//---------------------------------------------------------------------
//
// Constructor
//
//---------------------------------------------------------------------
public function new(cutoffFrequency:Float = 8000, resonance:Float = SoundFX.SQRT2) {
cutoff = cutoffFrequency;
res = resonance;
init();
}
//---------------------------------------------------------------------
//
// Private methods
//
//---------------------------------------------------------------------
private function init():Void {
y1 = y2 = y3 = y4 = oldx = oldy1 = oldy2 = oldy3 = 0;
calc();
}
private function calc():Void {
f = cutoff * 2 / fs;
p = f * (1.8 - 0.8 * f);
k = p + p - 1;
t = (1 - p) * 1.386249;
t2 = 12 + t * t;
r = res * (t2 + 6 * t) / (t2 - 6 * t);
}
//---------------------------------------------------------------------
//
// Public methods
//
//---------------------------------------------------------------------
public function process(input:Float):Float {
// process input
x = input - r * y4;
//Four cascaded onepole filters (bilinear transform)
y1 = x * p + oldx * p - k * y1;
y2 = y1 * p + oldy1 * p - k * y2;
y3 = y2 * p + oldy2 * p - k * y3;
y4 = y3 * p + oldy3 * p - k * y4;
//Clipper band limited sigmoid
y4 -= (y4 * y4 * y4) / 6;
oldx = x;
oldy1 = y1;
oldy2 = y2;
oldy3 = y3;
return y4;
}
public function duplicate():IFilter {
_channelCopy = new MoogFilter(cutoffFrequency, resonance);
return _channelCopy;
}
private function set_cutoffFrequency(frequency:Float) {
if (frequency < 0 || frequency > SoundFX.SAMPLE_RATE * 0.5) throw new RangeError("Cutoff frequency " + frequency + " is out of range, valid range is 0 - " + (SoundFX.SAMPLE_RATE * 0.5));
cutoff = frequency;
if (_channelCopy != null) _channelCopy.cutoffFrequency = cutoff;
calc();
return cutoff;
}
private function get_cutoffFrequency() {
return cutoff;
}
private function set_resonance(resonance:Float) {
if (resonance < 0.1 || resonance > SoundFX.SQRT2) throw new RangeError("Resonance is out of range, valid range is 0.1 - sqrt(2)");
res = resonance;
if (_channelCopy != null) _channelCopy.resonance = res;
calc();
return res;
}
private function get_resonance() {
return res;
}
private function get_channelCopy() { return _channelCopy; }
}
/*
The MIT License
Copyright (c) <year> <copyright holders>
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.
http://www.anttikupila.com/flash/soundfx-out-of-the-box-audio-filters-with-actionscript-3/
*/
package com.anttikupila.media;
import com.anttikupila.events.StreamingEvent;
import com.anttikupila.media.filters.IFilter;
import flash.errors.IllegalOperationError;
import flash.events.Event;
import flash.events.SampleDataEvent;
import flash.events.TimerEvent;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.media.SoundLoaderContext;
import flash.media.SoundTransform;
import flash.net.URLRequest;
import flash.utils.ByteArray;
import flash.utils.Timer;
/**
* <p>Provides an easy way to transform audio output in real time with a syntax familiar from display object filters</p>
*
* <listing version="3.0">
* var sound:SoundFX = new SoundFX(new URLRequest("music.mp3"));
* sound.filters = [ new CutoffFilter(12000) ];
* sound.play();
* </listing>
*
* <p>SoundFX also provides greated control over buffering and precise seeking</p>
*
* <listing version="3.0">
* var sound:SoundFX = new SoundFX(null, null, 3); // 3 seconds need to be loaded before playback starts
* sound.load(new URLRequest("music.mp3"));
* sound.play();
* // a bit later..
* sound.position = 3.12312;
* </listing>
*/
class SoundFX extends Sound {
//---------------------------------------------------------------------
//
// Constants
//
//---------------------------------------------------------------------
public static inline var SAMPLE_RATE:Int = 44100;
public static inline var DEFAULT_OUTPUT_BUFFER:Int = 2048;
public static inline var DEFAULT_NETWORK_BUFFER:Float = 1; // seconds
public static inline var SQRT2:Float = 1.41421356237;
//---------------------------------------------------------------------
//
// Variables
//
//---------------------------------------------------------------------
private var output:Sound;
private var bufferTimer:Timer;
private var soundChannel:SoundChannel;
private var stream:URLRequest;
private var samples:ByteArray;
private var loops:Int;
private var soundTransform:SoundTransform;
private var buffering:Bool;
private var sampleIndex:Int = 0;
private var _networkBuffer:Float;
private var _outputBuffer:Int;
private var _filtersLeft:Array<IFilter>;
private var _filtersRight:Array<IFilter>;
private var _paused:Bool = false;
public var outputBuffer(get, set):Int;
public var networkBuffer(get, set):Float;
public var position(get, set):Float;
public var filters(get, set):Array<IFilter>;
//---------------------------------------------------------------------
//
// Constructor
//
//---------------------------------------------------------------------
/**
* @param stream File to load
* @param context Context
* @param networkBuffer Network buffer in seconds. Similar to the buffer in NetStream
* @param outputBufferLength Output buffer in samples. If the output is choppy even if the network is smooth try increasing the output buffer
*/
public function new(stream:URLRequest = null, context:SoundLoaderContext = null, networkBufferSeconds:Float = DEFAULT_NETWORK_BUFFER, outputBufferLength:Int = DEFAULT_OUTPUT_BUFFER) {
super(null, null);
samples = new ByteArray();
output = new Sound();
output.addEventListener(SampleDataEvent.SAMPLE_DATA, sampleDataHandler);
outputBuffer = outputBufferLength;
networkBuffer = networkBufferSeconds;
bufferTimer = new Timer(30);
bufferTimer.addEventListener(TimerEvent.TIMER, bufferTimerHandler);
if (stream != null) load(stream, context);
}
//---------------------------------------------------------------------
//
// private methods
//
//---------------------------------------------------------------------
/**
* @internal Starts a timer that checks when enough of the track has loaded for playback
*/
private function startBuffering(force:Bool = false):Void {
if (super.isBuffering || bytesLoaded < bytesTotal || force) {
if (!buffering) {
buffering = true;
bufferTimer.start();
dispatchEvent(new StreamingEvent(StreamingEvent.BUFFER_EMPTY));
}
bufferTimerHandler(null);
}
}
//---------------------------------------------------------------------
//
// Events
//
//---------------------------------------------------------------------
/**
* @internal Main audio processor
*/
private function sampleDataHandler(event:SampleDataEvent):Void {
#if cpp
return;
#else
samples.position = 0;
var availableSampleCount:Int = Std.int(extract(samples, _outputBuffer, sampleIndex));
samples.position = 0;
if (availableSampleCount < _outputBuffer) {
if (!buffering) startBuffering();
}
if (buffering || availableSampleCount > 0) {
var left:Float,
right:Float,
filter:IFilter;
for (i in 0 ... Std.int(Math.min(_outputBuffer, availableSampleCount))) {
if (buffering || _paused) {
// Input silence into filters while paused or buffering
left = right = 0;
} else {
left = samples.readFloat();
right = samples.readFloat();
}
for (filter in _filtersLeft) {
left = filter.process(left);
}
for (filter in _filtersRight) {
right = filter.process(right);
}
event.data.writeFloat(left);
event.data.writeFloat(right);
}
if (!buffering && !_paused) sampleIndex += _outputBuffer;
}
#end
}
/**
* @internal Checks if the network buffer has been filled and starts playback if enough data is available
*/
private function bufferTimerHandler(event:TimerEvent):Void {
if (length * 0.001 - sampleIndex / SAMPLE_RATE >= _networkBuffer) { // convert length to milliseconds
bufferTimer.stop();
buffering = false;
if (soundChannel != null) {
soundChannel.removeEventListener(Event.SOUND_COMPLETE, soundCompleteHandler);
soundChannel.stop();
}
soundChannel = output.play(0, loops, soundTransform);
soundChannel.addEventListener(Event.SOUND_COMPLETE, soundCompleteHandler);
dispatchEvent(new StreamingEvent(StreamingEvent.BUFFER_FULL));
}
}
/**
* @internal Easy bridge to check for sound completion
*/
private function soundCompleteHandler(event:Event):Void {
if (bytesLoaded >= bytesTotal) dispatchEvent(event);
}
//---------------------------------------------------------------------
//
// Public methods
//
//---------------------------------------------------------------------
/**
* <p>Starts playing the sound</p>
* <p>Note: unlike <code>flash.media.Sound</code> play() does <strong>not</strong> return a SoundChannel</p>
*
* @param startTime Start time in seconds
* @param loops Float of loops
* @param sndTransform Sound transform
*
* @return null
*/
override public function play(startTime:Float = 0, loops:Int = 0, sndTransform:SoundTransform = null):SoundChannel {
if (stream == null) throw new IllegalOperationError("Sound cannot be played without a valid stream");
this.loops = loops;
this.soundTransform = sndTransform;
position = startTime;
startBuffering(true);
return null;
}
/**
* @see flash.media.Sound#load()
*/
override public function load(stream:URLRequest, context:SoundLoaderContext = null #if(cpp) , forcePlayAsMusic:Bool = true #end):Void {
this.stream = stream;
super.load(stream, context);
}
/**
* Indexed array of filters to process the audio through before output
*/
private function get_filters() {
return _filtersLeft;
}
/**
* @private
*/
private function set_filters(filters:Array<IFilter>) {
_filtersLeft = filters;
_filtersRight = [ ];
for (filter in _filtersLeft) {
_filtersRight.push(filter.duplicate());
}
return _filtersLeft;
}
#if cpp override #end private function get_isBuffering():Bool {
return buffering;
}
/**
* Amount of seconds that need to be buffered before sound will start playing
*/
private function get_networkBuffer():Float {
return _networkBuffer;
}
/**
* @private
*/
private function set_networkBuffer(networkBuffer:Float){
return _networkBuffer = Math.max(networkBuffer, _outputBuffer / SAMPLE_RATE);
}
/**
* Output buffer size
*/
private function get_outputBuffer():Int {
return _outputBuffer;
}
/**
* @private
*/
private function set_outputBuffer(outputBuffer:Float) {
_outputBuffer = Std.int(Math.max(outputBuffer, 0));
return _outputBuffer;
}
/**
* Current playhead position
*/
private function get_position():Float {
return sampleIndex / SAMPLE_RATE;
}
/**
* @private
*/
private function set_position(position:Float){
return sampleIndex = Std.int(Math.min(length, position) * SAMPLE_RATE);
}
/**
* <p>Return an estimated length based on currently loaded bytes</p>
* <p>While the result isn't exactly correct it is useful for showing progress of the playing track, dividing SoundFX.position with SoundFX.getLenght()</p>
*
* @see #position
*
* @return An estimated length
*/
private function getLength():Float {
if (bytesLoaded <= 0) return 0;
return length / (bytesLoaded / bytesTotal) * 0.001;
}
/**
* Specifies if the track is paused. Unlike stopping a track pausing it will continue to process filters without advancing the playhead.
*
* @param paused True if track should be paused
*/
private function get_paused():Bool {
return _paused;
}
/**
* @private
*/
private function set_paused(paused:Bool):Void {
_paused = paused;
}
}
package com.grapefrukt.utils;
/**
* ...
* @author Kelvin Luck
* @author Martin Jonasson, m@grapefrukt.com
*/
import com.anttikupila.media.filters.IFilter;
import flash.events.Event;
import flash.events.SampleDataEvent;
import flash.media.Sound;
import flash.net.URLRequest;
import flash.utils.ByteArray;
import openfl.Assets;
class SoundPitcher {
public var playbackSpeed(default, default):Float = 1;
private var headerSize:Int = 48;
private var _sound:Sound;
private var _loadedMP3Samples:ByteArray;
private var _dynamicSound:Sound;
private var _phase:Float;
public var numSamples(default, null):Int;
public var filters:Array<IFilter>;
public function new() {
filters = [];
}
public function playFromData(id:String) {
var bytes:ByteArray = Assets.getBytes(id);
bytes.position = headerSize;
playBytes(bytes);
}
public function playFromMp3(id:String) {
#if flash
var bytes:ByteArray = new ByteArray();
var s = Assets.getSound(id);
s.extract(bytes, Std.int(s.length * 44.1));
//var f = new flash.net.FileReference();
//f.save(bytes, "audio.dat");
headerSize = 0;
playBytes(bytes);
#end
}
public function stop() {
if (_dynamicSound != null) {
_dynamicSound.removeEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
_dynamicSound = null;
}
}
private function playBytes(bytes:ByteArray) {
stop();
_dynamicSound = new Sound();
_dynamicSound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
_loadedMP3Samples = bytes;
numSamples = Std.int((bytes.length - headerSize) / 8);
_phase = 0;
trace("Loaded " + _loadedMP3Samples.length + " samples");
_dynamicSound.play();
}
private function onSampleData(event:SampleDataEvent) {
var l:Float;
var r:Float;
var outputLength:Int = 0;
while (outputLength < 4096) {
// until we have filled up enough output buffer
// move to the correct location in our loaded samples ByteArray
_loadedMP3Samples.position = headerSize + Std.int(_phase) * 8; // 4 bytes per float and two channels so the actual position in the ByteArray is a factor of 8 bigger than the phase
// read out the left and right channels at this position
l = _loadedMP3Samples.readFloat();
r = _loadedMP3Samples.readFloat();
// apply any filters
for (filter in filters) {
if (filter.channelCopy == null) filter.duplicate();
r = filter.process(r);
l = filter.channelCopy.process(l);
}
// write the samples to our output buffer
event.data.writeFloat(l);
event.data.writeFloat(r);
outputLength++;
// advance the phase by the speed...
setPhase(_phase + playbackSpeed);
}
}
public function setPhase(value:Float)
{
_phase = value;
// and deal with looping (including looping back past the beginning when playing in reverse)
if (_phase < 0) {
_phase += numSamples;
} else if (_phase >= numSamples) {
_phase -= numSamples;
}
}
}
package com.anttikupila.events;
import flash.events.Event;
/**
* @author Antti Kupila
*/
class StreamingEvent extends Event {
public static inline var BUFFER_EMPTY : String = "bufferEmpty";
public static inline var BUFFER_FULL : String = "bufferFull";
public function new(type : String, bubbles : Bool = false, cancelable : Bool = false) {
super( type, bubbles, cancelable );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment