Hey, Mr. DJ
Big thanks to this project: http://blog.glowinteractive.com/2011/01/vinyl-scratch-emulation-on-iphone/
/**
* Copyright devon_o ( http://wonderfl.net/user/devon_o )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/iYlE
*/
package
{
import com.bit101.components.Knob;
import com.bit101.components.Label;
import com.bit101.components.ProgressBar;
import com.bit101.components.PushButton;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.ProgressEvent;
import flash.media.Sound;
import flash.media.SoundLoaderContext;
import flash.net.URLRequest;
import flash.system.Security;
[SWF(width='465', height='465', backgroundColor='#000000', frameRate='60')]
public class Main extends Sprite
{
private const POLICY:String = "http://www.onebyonedesign.com/crossdomain.xml";
private const SOUND:String = "http://www.onebyonedesign.com/flash/djscratch/scratch.mp3";
private var mLoadedSong:Sound;
private var mScratchDisplay:ScratchDisplay;
private var mRightSoundDiplay:ScratchDisplay;
// ui
private var mToggle:PushButton;
private var mProgressBar:ProgressBar;
private var mVolume:Knob;
public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(event:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
Security.loadPolicyFile(POLICY);
initProgressBar();
loadSound();
}
private function initProgressBar():void
{
mProgressBar = new ProgressBar(this);
mProgressBar.x = (stage.stageWidth - mProgressBar.width) >> 1;
mProgressBar.y = (stage.stageHeight - mProgressBar.height) >> 1;
}
private function loadSound():void
{
var s:Sound = new Sound();
s.addEventListener(Event.COMPLETE, onSoundLoad);
s.addEventListener(ProgressEvent.PROGRESS, onSoundProgress);
s.load(new URLRequest(SOUND), new SoundLoaderContext(1000, true));
}
private function onSoundProgress(event:ProgressEvent):void
{
mProgressBar.value = event.bytesLoaded / event.bytesTotal;
}
private function onSoundLoad(event:Event):void
{
event.currentTarget.removeEventListener(Event.COMPLETE, onSoundLoad);
removeChild(mProgressBar);
mScratchDisplay = new ScratchDisplay(event.currentTarget as Sound);
mScratchDisplay.x = (stage.stageWidth - 300) >> 1;
mScratchDisplay.y = (stage.stageHeight - 150) >> 1;
addChild(mScratchDisplay);
var l:Label = new Label(this, mScratchDisplay.x, mScratchDisplay.y - 20, "Press 'PLAY' - click and drag waveform to scratch");
mToggle = new PushButton(this, mScratchDisplay.x, mScratchDisplay.y + 150 + 50, "PLAY", onToggle);
mToggle.toggle = true;
mVolume = new Knob(this, mScratchDisplay.x + 300 - 40, mScratchDisplay.y + 150 + 10, "Vol", onVolume);
mVolume.minimum = 0.0;
mVolume.maximum = 1.0;
mVolume.value = 1.0;
}
private function onToggle(event:MouseEvent):void
{
if (mToggle.selected)
{
mScratchDisplay.play();
mToggle.label = "STOP";
} else {
mScratchDisplay.stop();
mToggle.label = "PLAY";
}
}
private function onVolume(event:Event):void
{
mScratchDisplay.setVolume(mVolume.value);
}
}
}
// ***** SCRATCH DISPLAY **** //
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.SampleDataEvent;
import flash.geom.Rectangle;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.media.SoundTransform;
import flash.utils.ByteArray;
import flash.utils.getTimer;
class ScratchDisplay extends Sprite
{
private const WRITE_SIZE:int = 2048;
private const PLAYBACK_FREQUENCY:Number = 44100.0;
private const DRAG:Number = .25;
private const SPEED_EASE:Number = 1.0;
private const SPEED_MULT:Number = 24;
private const MOVE_EASE:Number = 1.0;
private const BYTE_POSITION_TO_PIXELS:Number = 0.00004; // original = .0004
public var mSoundData:ByteArray;
private var mSamples:Vector.<Number>;
private var mNumSamples:Number;
private var mSound:Sound;
private var mWaveHolder:Sprite;
private var mPlayingSound:Sound;
private var mDisplay:Shape;
private var mIsScratching:Boolean;
private var mPosition:Number = 0.0;
private var mTargetSpeed:Number;
private var mSpeed:Number = 2.0;
private var mChannel:SoundChannel;
private var mVolume:SoundTransform;
private var mRatio:Number;
private var mCenterX:Number;
// throwing
private var mDiffX:Number;
private var mVelX:Number = 0.0;
private var mCurrX:Number;
private var mPrevX:Number;
private var mFarLeft:Number;
private var mFarRight:Number;
public function ScratchDisplay(sound:Sound)
{
mSound = sound;
mPlayingSound = new Sound();
mVolume = new SoundTransform();
mWaveHolder = new Sprite();
drawWaveForm();
extractSamples();
initSmoothBuffer();
initDisplay();
}
public function play():void
{
mPlayingSound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSample);
mChannel = mPlayingSound.play();
mWaveHolder.buttonMode = true;
mWaveHolder.addEventListener(MouseEvent.MOUSE_DOWN, startScratch);
}
public function stop():void
{
mPlayingSound.removeEventListener(SampleDataEvent.SAMPLE_DATA, onSample);
mChannel.stop();
mWaveHolder.buttonMode = false;
mWaveHolder.removeEventListener(MouseEvent.MOUSE_DOWN, startScratch);
}
public function setVolume(value:Number):void
{
mVolume.volume = value;
mChannel.soundTransform = mVolume;
}
private function initSmoothBuffer():void
{
var i:int = mSmoothBufferSize;
while (i--)
{
mSmoothBuffer[i] = 0.0;
}
}
public function wave_interpolator(x:Number, y:Vector.<Number>):Number
{
const offset:int = 2;
var z:Number = x - 1 / 2.0;
var even1:Number = y[int(offset + 1)] + y[int(offset + 0)], odd1:Number = y[int(offset + 1)] - y[int(offset + 0)];
var even2:Number = y[int(offset + 2)] + y[int(offset + -1)], odd2:Number = y[int(offset + 2)] - y[int(offset + -1)];
var even3:Number = y[int(offset + 3)] + y[int(offset + -2)], odd3:Number = y[int(offset + 3)] - y[int(offset + -2)];
var c0:Number = even1 * 0.42685983409379380 + even2 * 0.07238123511170030
+ even3 * 0.00075893079450573;
var c1:Number = odd1 * 0.35831772348893259 + odd2 * 0.20451644554758297
+ odd3 * 0.00562658797241955;
var c2:Number = even1 * -0.217009177221292431 + even2 * 0.20051376594086157
+ even3 * 0.01649541128040211;
var c3:Number = odd1 * -0.25112715343740988 + odd2 * 0.04223025992200458
+ odd3 * 0.02488727472995134;
var c4:Number = even1 * 0.04166946673533273 + even2 * -0.06250420114356986
+ even3 * 0.02083473440841799;
var c5:Number = odd1 * 0.08349799235675044 + odd2 * -0.04174912841630993
+ odd3 * 0.00834987866042734;
return ((((c5 * z + c4) * z + c3) * z + c2) * z + c1) * z + c0;
}
private function onSample(event:SampleDataEvent):void
{
var data:ByteArray = event.data;
if (mIsScratching)
{
writeScratchStream(data);
} else {
writeNormalStream(data);
mRatio = mPosition / mNumSamples;
mWaveHolder.x = mCenterX - (mWaveHolder.width * mRatio);
}
}
private function writeNormalStream(stream:ByteArray):void
{
for (var i:int = 0; i < WRITE_SIZE; i++)
{
var sample:Number = getNextSample();
stream.writeFloat(sample);
stream.writeFloat(sample);
mPosition += mSpeed;
if (mPosition >= mNumSamples) mPosition = 0;
if (mPosition < 0) mPosition = mNumSamples - 1;
}
}
// http://blog.glowinteractive.com/2011/01/vinyl-scratch-emulation-on-iphone/
private var mAudioSampleSize:uint = 8; // original = sizeof(float) * 2
private var mScratchingBufferSize:uint = 500000;
private var mSmoothBufferSize:uint = 3000;
private var mScratchCircularBufferSamplePosition:int = 0;
private var mScratchingPositionOffset:Number = 0.0;
private var mScratchingPositionSmoothedVelocity:Number = 0.0;
private var mScratchingPositionVelocity:Number = 0.0;
// for smoothing of input
private var mSmoothBuffer:Vector.<Number> = new Vector.<Number>(mSmoothBufferSize, true);
private var mSmoothBufferPositionsUsed:int = 0;
private var mSmoothBufferPosition:int = 0;
private var mLeftInterpData:Vector.<Number> = new Vector.<Number>(6, true);
private var mRightInterpData:Vector.<Number> = new Vector.<Number>(6, true);
// comments from original source. http://blog.glowinteractive.com/2011/01/vinyl-scratch-emulation-on-iphone/
private function writeScratchStream(stream:ByteArray):void
{
var bufferBasePosition:Number = Number(mScratchCircularBufferSamplePosition);
for (var i:int = 0; i < WRITE_SIZE; i++)
{
if (++mSmoothBufferPositionsUsed > int(mSmoothBufferSize))
{
mSmoothBufferPositionsUsed = mSmoothBufferSize;
}
// STEP 1
// find moving average
mScratchingPositionSmoothedVelocity -= mSmoothBuffer[mSmoothBufferPosition];
mSmoothBuffer[mSmoothBufferPosition] = mScratchingPositionVelocity / mSmoothBufferPositionsUsed;
mScratchingPositionSmoothedVelocity += mSmoothBuffer[mSmoothBufferPosition];
mSmoothBufferPosition = (++mSmoothBufferPosition) % mSmoothBufferSize;
var velocity:Number = mScratchingPositionSmoothedVelocity;
// STEP 2
// modify velocity to point to the correct position
var targetOffset:Number = mPosition + mSpeed;
var offsetDiff:Number = targetOffset - mPosition;
velocity += offsetDiff * 10.0;
// STEP 3
// update scratch buffer position
mScratchingPositionOffset += velocity / PLAYBACK_FREQUENCY;
// find absolute scratch buffer position
var fBufferPosition:Number = bufferBasePosition + mScratchingPositionOffset;
// wrap fBufferPosition
//TODO use fmodf but what's faster?
while (fBufferPosition >= mScratchingBufferSize) fBufferPosition -= mScratchingBufferSize;
while (fBufferPosition < 0) fBufferPosition += mScratchingBufferSize;
// STEP 4
// use interpolation to find a sample value
var iBufferPosition:int = int(fBufferPosition);
var rem:Number = fBufferPosition - iBufferPosition;
var pos:int = 2 * (iBufferPosition - 2)
var sample:Number = readSample(pos);
mLeftInterpData[0] = sample;
++pos;
sample = readSample(pos);
mRightInterpData[0] = sample;
++pos;
sample = readSample(pos);
mLeftInterpData[1] = sample;
++pos;
sample = readSample(pos);
mRightInterpData[1] = sample;
++pos;
sample = readSample(pos);
mLeftInterpData[2] = sample;
++pos;
sample = readSample(pos);
mRightInterpData[2] = sample;
++pos;
sample = readSample(pos);
mLeftInterpData[3] = sample;
++pos;
sample = readSample(pos);
mRightInterpData[3] = sample;
++pos;
sample = readSample(pos);
mLeftInterpData[4] = sample;
++pos;
sample = readSample(pos);
mRightInterpData[4] = sample;
++pos;
sample = readSample(pos);
mLeftInterpData[5] = sample;
++pos;
sample = readSample(pos);
mRightInterpData[5] = sample;
stream.writeFloat(wave_interpolator(rem, mLeftInterpData));
stream.writeFloat(wave_interpolator(rem, mRightInterpData));
}
}
public function getNextSample():Number
{
var intPos:int = int(mPosition);
return readSample(intPos);
}
private function readSample(pos:int):Number
{
while (pos < 0) pos += mNumSamples;
while (pos >= mNumSamples) pos -= mNumSamples;
return mSamples[pos];
}
private function clamp(target:Number, min:Number, max:Number):Number
{
if (target < min) target = min;
if (target > max) target = max;
return target;
}
private function startScratch(event:MouseEvent):void
{
mIsScratching = true;
mPrevX = mWaveHolder.x
mDiffX = mouseX - mWaveHolder.x;
stage.addEventListener(MouseEvent.MOUSE_UP, endScratch);
addEventListener(Event.ENTER_FRAME, mouseMove);
}
private function endScratch(event:MouseEvent):void
{
stage.removeEventListener(MouseEvent.MOUSE_UP, endScratch);
removeEventListener(Event.ENTER_FRAME, mouseMove);
addEventListener(Event.ENTER_FRAME, slide);
}
// ENTER FRAME FUNCTION
private function mouseMove(event:Event):void
{
var tx:Number = mouseX - mDiffX;
mWaveHolder.x += (tx - mWaveHolder.x) / MOVE_EASE;
if (mWaveHolder.x < mFarLeft) mWaveHolder.x = mFarLeft;
if (mWaveHolder.x > mFarRight) mWaveHolder.x = mFarRight;
mVelX = mWaveHolder.x - mPrevX;
mPrevX = mWaveHolder.x;
setSpeed();
setByteOffset(mSpeed / BYTE_POSITION_TO_PIXELS);
update();
}
private var mPreviousTime:Number = 0.0;
private var mPositionOffset:Number = 0.0;
private var mPreviousPositionOffset:Number = 0.0;
private function update():void
{
var time:int = getTimer();
var dt:Number = time - mPreviousTime;
if (dt == 0.0)
mScratchingPositionVelocity = 0.0;
else
mScratchingPositionVelocity = (mPosition - mScratchingPositionOffset) / dt;
mPreviousTime = time;
}
private function setByteOffset(offset:Number):void
{
mPosition = offset / mAudioSampleSize;
}
private function initDisplay():void
{
mDisplay = new Shape();
mDisplay.graphics.beginFill(0x0, 0);
mDisplay.graphics.lineStyle(0, 0x00FF00);
mDisplay.graphics.drawRect(0, 0, 300, 150);
mDisplay.graphics.lineStyle(0, 0xFF0000);
mDisplay.graphics.moveTo(150, 0);
mDisplay.graphics.lineTo(150, 150);
mDisplay.graphics.endFill();
var waveMask:Shape = new Shape();
waveMask.graphics.beginFill(0xFF00FF);
waveMask.graphics.drawRect(0, 0, 300, 150);
waveMask.graphics.endFill();
mCenterX = mDisplay.width >> 1;
mWaveHolder.y = mDisplay.height >> 1;
mWaveHolder.x = mCenterX;
addChild(mWaveHolder);
var r:Rectangle = new Rectangle( -mWaveHolder.width + mCenterX +2, mWaveHolder.y, mWaveHolder.width -4, 0);
mFarLeft = r.left;
mFarRight = r.right;
mWaveHolder.mask = waveMask;
addChild(waveMask);
addChild(mDisplay);
}
private function drawWaveForm():void
{
mSoundData = new ByteArray();
var max:Number = PLAYBACK_FREQUENCY * (mSound.length / 1000) - WRITE_SIZE;
mSound.extract(mSoundData, max);
mSoundData.position = 0;
var yr:int = 35;
var step:int = 1;
var xp:int = 0;
var waveform:Shape = new Shape();
waveform.graphics.lineStyle(0, 0x00FF00);
while(mSoundData.bytesAvailable > PLAYBACK_FREQUENCY * 2)
{
var minLeft:Number = Number.MAX_VALUE;
var minRight:Number = Number.MAX_VALUE;
var maxRight:Number = Number.MIN_VALUE;
var maxLeft:Number = Number.MIN_VALUE;
for (var i:uint = 0; i < 1024; i++)
{
var left:Number = mSoundData.readFloat();
if (left > maxLeft) maxLeft = left;
if (left < minLeft) minLeft = left;
var right:Number = mSoundData.readFloat();
if (right > maxRight) maxRight = right;
if (right < minRight) minRight = right;
}
minLeft *= yr;
minRight *= yr;
maxLeft *= yr;
maxRight *= yr;
waveform.graphics.moveTo(xp, minLeft);
waveform.graphics.lineTo(xp, maxLeft);
xp++;
waveform.graphics.moveTo(xp, minRight);
waveform.graphics.lineTo(xp, maxRight);
xp++;
}
waveform.cacheAsBitmap = true;
mWaveHolder.graphics.beginFill(0x0);
mWaveHolder.graphics.drawRect(0, -waveform.height * .5, waveform.width, waveform.height);
mWaveHolder.graphics.endFill();
mWaveHolder.cacheAsBitmap = true;
mWaveHolder.addChild(waveform);
}
private function extractSamples():void
{
mNumSamples = Math.floor(mSoundData.length / 4);
mSamples = new Vector.<Number>(mNumSamples, true);
mSoundData.position = 0;
for (var i:int = 0; i < mNumSamples; i++)
{
mSamples[i] = Number(mSoundData.readFloat());
}
}
private function slide(event:Event):void
{
mWaveHolder.x += mVelX;
if (mWaveHolder.x < mFarLeft) mVelX *= -1;
if (mWaveHolder.x > mFarRight) mVelX *= -1;
setSpeed();
mVelX *= DRAG;
// scratch is complete
if (Math.abs(mVelX) <= .025)
{
removeEventListener(Event.ENTER_FRAME, slide);
completeScratch();
}
}
private function completeScratch():void
{
mRatio = (mWaveHolder.x - mCenterX) / mWaveHolder.width;
mPosition = -mRatio * mNumSamples;
mPosition = clamp(mPosition, 0, mNumSamples - 1);
mSpeed = 2.0;
mIsScratching = false;
}
private function setSpeed():void
{
mTargetSpeed = mVelX * SPEED_MULT;
mSpeed += (mTargetSpeed - mSpeed) / SPEED_EASE;
mSpeed *= -1;
}
}