Pixel Bender can be a great tool for number crunching, but has a couple of limitations when trying to create an audio mixer.
- The number of inputs is limited when using PixelBender toolkit to compile .pbj files.
- Even if one overcomes the limitation by writing Pixel Bender assembly, the track count is fixed to a predetermined number determined by the number of inputs you have in the .pbj.
In some cases you might want your application to act as a normal audio app where you can add as many tracks as your CPU can handle, but not use more CPU than needed for the current track count.
What we need to accomlish that is a way to dynamically create a shader with the desired number of inputs. James Ward has created pbjAS, a library that enables you to create Pixel Bender shaders at runtime, based on Nicolas Cannasees haXe library.
Also, Tinic Uro has posted Pixel Bender assembly code to create a mixer.
So I set out to use pbjAS to recreate Tinics code with a dynamic number of channels.
The result is this class:
package com.blixtsystems.audio
{
import flash.display.Shader;
import flash.display.ShaderJob;
import flash.utils.ByteArray;
import pbjAS.ops.OpAdd;
import pbjAS.ops.OpMul;
import pbjAS.ops.OpSampleNearest;
import pbjAS.params.Parameter;
import pbjAS.params.Texture;
import pbjAS.PBJ;
import pbjAS.PBJAssembler;
import pbjAS.PBJChannel;
import pbjAS.PBJParam;
import pbjAS.PBJType;
import pbjAS.regs.RFloat;
/**
* Shader to mix audio with a dynamic number of channels
* @author leo@blixtsystems.com
*/
public class MixerShader
{
private var _bufferSize:int;
private var _pbj:PBJ = new PBJ();
private var _shader:Shader;
private var _buffer:Vector.<ByteArray> = new Vector.<ByteArray>();
private var _numTracks:int;
/**
* Constructor
* @param numTracks track count
*/
public function MixerShader(numTracks:int, bufferSize:int=2048)
{
_numTracks = numTracks;
_bufferSize = bufferSize;
}
/*-----------------------------------------------------------
Public methods
-------------------------------------------------------------*/
/**
* Mix audio
* @param data ByteArray in which to store the result, probably SampleDataEvent.data
*/
public function mix(data:ByteArray):void
{
var mixerJob:ShaderJob = new ShaderJob(_shader, data, 1024, _bufferSize/1024);
mixerJob.start(true);
}
/*-----------------------------------------------------------
Private methods
-------------------------------------------------------------*/
private function assembleShader():void
{
var channels:Array = [PBJChannel.R, PBJChannel.G];
var chanStr:String = "rg";
_pbj.version = 1;
_pbj.name = "SoundMixer";
_pbj.parameters =
[
new PBJParam
(
"_OutCoord",
new Parameter
(
PBJType.TFloat2,
false,
new RFloat( 0, channels)
)
)
];
_pbj.code =
[
new OpSampleNearest
(
new RFloat(1, channels),
new RFloat(0, channels),
0
),
new OpMul
(
new RFloat(1, channels),
new RFloat(3, channels)
)
];
var i:int;
for (i = 0; i < _numTracks; i++)
{
_pbj.parameters.push
(
new PBJParam
(
"track" + i,
new Texture(2, i)
)
);
}
for (i = 0; i < _numTracks; i++)
{
_pbj.parameters.push
(
new PBJParam
(
"volume" + i,
new Parameter
(
PBJType.TFloat2,
false,
new RFloat(i + 3, channels)
)
)
);
}
for (i = 0; i < _numTracks-1; i++)
{
_pbj.code.push
(
new OpSampleNearest
(
new RFloat(2, channels),
new RFloat(0, channels),
i+1
),
new OpMul
(
new RFloat(2, channels),
new RFloat(i+4, channels)
),
new OpAdd
(
new RFloat(1, channels),
new RFloat(2, channels)
)
);
}
_pbj.parameters.push
(
new PBJParam
(
"output",
new Parameter
(
PBJType.TFloat2,
true,
new RFloat(1, channels)
)
)
);
var pbjBytes:ByteArray = PBJAssembler.assemble(_pbj);
_shader = new Shader(pbjBytes);
_buffer = new Vector.<ByteArray>(_numTracks);
// initialize the shader inputs
for (i = 0; i < _numTracks; i++) {
_buffer[i] = new ByteArray();
_buffer[i].length = _bufferSize * 4 * 2;
_shader.data["track" + i]["width"] = 1024;
_shader.data["track" + i]["height"] = _bufferSize / 1024;
_shader.data["track" + i]["input"] = _buffer[i];
_shader.data["volume" + i]["value"] = [1, 1];
}
}
/*-----------------------------------------------------------
Getters/Setters
-------------------------------------------------------------*/
public function get numTracks():int { return _numTracks; }
public function set numTracks(value:int):void
{
// needs to be at least one input, and no point reassembling pbj if track count has not changed
if (_numTracks < 1 && _numTracks == value) return;
_numTracks = value;
assembleShader();
}
public function get buffer():Vector.<ByteArray> { return _buffer; }
}
}
To use it you need to download pjbAS from James Ward and include the swc in your project.
Then in your audio engine do the following:
static public const BUFFER_SIZE:int = 2048;
// number of tracks (minimum 1)
private var _numTracks:int = 1;
// instantiate mixer shader
private var _mixerShader:MixerShader;
_mixerShader = new MixerShader(_numTracks, BUFFER_SIZE);
// here you have your sound objects stored
// adding or removing objects will change number of tracks in the shader
public var sounds:Vector.<Sound> = new Vector.<Sound>();
// SampleDataEvent callback
private function samplesCallback(e:SampleDataEvent):void
{
_numTracks = sounds.length;
// update number of track for the shader, causing pbj to recompile
_mixerShader.numTracks = _numTracks;
// extract audio data into the shader buffers
for (var i:int = 0; i < _numTracks; i++)
{
_mixerShader.buffer[i].position = 0;
sound[i].extract(_mixerShader.buffer[i], BUFFER_SIZE);
_mixerShader.buffer[i].position = 0;
}
// do shader job
_mixerShader.mix(e.data);
}
According to Tinic Uro, the number of inputs in a shader is limited to 15, so adding more tracks than that will probably not work.
In my tests, playing back eight tracks using pure AS3 will often cause CPU peak usage over 16% of one 2.3 GHz core on my quad-core machine with the debug version of the Flash Player.
If I instead use Pixel Bender the peaks are rarely above 6%. Adding or removing tracks which causes the pbj to recompile, does not cause any noticeable spikes in CPU usage.
So using Pixel Bender will cut CPU usage to around 1/3 on my machine!
Big thanks to Tinic Uro, Nicolas Cannasse and James Ward who made it easy to accomplish!
Related posts:
Awesome stuff! Thanks for using pbjAS!
-James
is ther a way do detect when the shader has completed and then save the bytearray using filerefrence
Should not be a problem. MixerShader.mix() will take a ByteArray as argument, so instead of using SampleDataEvent.data you can use your own ByteArray to store the data.
I would process a chunk of samples on each enterframe to make sure the flash player does not freeze up when processing many samples. Then you can either just do a check in your enterframe to see if there is sufficient audio remaining to fill a whole buffer, or have MixerShader dispatch an event when data passed to mix function is not full length of the buffersize.
i am building an application that mix audio and export it to mp3 . like th avyary audio editor can you help me with some resources ?
thanks in advance
The way I have done when rendering mixes is to simply send metadata to the server and then use Ecasound or SoX to render the mix and LAME to encode the mp3.
Sending multiple tracks as wav to the client is slow, and when mixing with mp3 and then encoding the mix once again you loose quality. Also rendering and encoding on the server is faster than on the client, and you can easily provide encoding in different quality and formats.
Might not be ideal in all scenarios, but could be an approach worth considering.
can you give a snippet how to implement the event complete in the MixShader
Okay I’m convniced. Let’s put it to action.
So excited I found this atilrce as it made things much quicker!
hello !
first of all – great work ! (thanks for the efford)
My problem (please have a short look at it and tell me if you can help me with it)
background:
I record audio via an air application and since the input is mono I do not store the same sample two times but just once (to keep memory and file size low)
when playing the audio via the SampleDataEvent I do something like this
private function handleSoundData(e:SampleDataEvent):void {
var waxedByteArray:ByteArray = getSamples(_sampleCounter); // returns a bytearray with length 2048 * 4
waxedByteArray.position = 0;
while (waxedByteArray.bytesAvailable) {
var singleChannel:Number = waxedByteArray.readFloat();
e.data.writeFloat(singleChannel);
e.data.writeFloat(singleChannel);
}
}
Question:
Basically my ByteArray is mono (no pairs of left and right float values) – I just read the samples / float values one by one and write them two times back into the SampleDataEvent.data ByteArray
Your implementation of the MixShader handles stereo – what whould I have to change to allow it to handle my mono ByteArray ?
thanks in advance – hope you can help mw with that – and if not still thanks a lot for your examples !
kind regards,
m.
I did try to make a mono version myself a while back, but could not figure out a way since PixelBender does not accept single channel inputs.
In the scenario I had I was working with mono mp3 as the source, and I came to the conclusion that I could not make any gains by mixing in mono. However, using an existing ByteArray with a mono signal it might be more efficient doing straight mixing with AS, but I’m not sure that is the case and would try filling the mixer shader buffer with duplicated values to see how that fares compared to using pure AS.
thanks for your suggestion
mixing in actionscript still got my the CPU usage of my application up tp 15 – 25 percent for 3 audiofiles
instead I implemented a littel prechaching mechanism where I created a byteArray with double the amount of samples of my mono byteArray and passed theses blocks into your MixShader – doing so the CPU usage seldom exceeds 5 – 7%
pixelbender (especially with the possibility to create shaders on-the-fly) seems to be an interesting option to do large / repeating calculations indeed !
thanks for your assistance !
stay well,
m.
Hi. You made a awesome class. I’m using it for a sampler app. I have a bunch of buttons. 6 that play only once, and 6 that play in a loop. First I had the problem that the sounds that should loop weren’t actually looping. So I implemented some onComplete listeners on the sounds that should loop so when it’s triggered it removes it from the _buffer and destroys the current sound object, and then make a new sound object and adds it to the _buffer.
Now everything works but there is a gap between the end/removing of the sound and the creation and loading of a new one. It’s not big but it is kind of irritating.
So my question is, is there any way to loop the sound that is added to the shader buffer? Any advice is appreciated.
Or is there a way to offset the sound a few milliseconds in advance?