300 lines
6.9 KiB
Haxe
300 lines
6.9 KiB
Haxe
package aura.channels;
|
|
|
|
import haxe.ds.Vector;
|
|
|
|
#if cpp
|
|
import sys.thread.Mutex;
|
|
#end
|
|
|
|
import aura.channels.BaseChannel.BaseChannelHandle;
|
|
import aura.threading.BufferCache;
|
|
import aura.threading.Message;
|
|
import aura.types.AudioBuffer;
|
|
import aura.utils.Profiler;
|
|
|
|
|
|
/**
|
|
Main-thread handle to a `MixChannel` in the audio thread.
|
|
**/
|
|
class MixChannelHandle extends BaseChannelHandle {
|
|
#if AURA_DEBUG
|
|
public var name: String = "";
|
|
public var inputHandles: Array<BaseChannelHandle> = new Array();
|
|
#end
|
|
|
|
public inline function getNumInputs(): Int {
|
|
return getMixChannel().getNumInputs();
|
|
}
|
|
|
|
/**
|
|
Adds an input channel. Returns `true` if adding the channel was
|
|
successful, `false` if the amount of input channels is already maxed
|
|
out.
|
|
**/
|
|
inline function addInputChannel(channelHandle: BaseChannelHandle): Bool {
|
|
assert(Error, channelHandle != null, "channelHandle must not be null");
|
|
|
|
final foundChannel = getMixChannel().addInputChannel(channelHandle.channel);
|
|
#if AURA_DEBUG
|
|
if (foundChannel) inputHandles.push(channelHandle);
|
|
#end
|
|
return foundChannel;
|
|
}
|
|
|
|
/**
|
|
Removes an input channel from this `MixChannel`.
|
|
**/
|
|
inline function removeInputChannel(channelHandle: BaseChannelHandle) {
|
|
#if AURA_DEBUG
|
|
inputHandles.remove(channelHandle);
|
|
#end
|
|
getMixChannel().removeInputChannel(channelHandle.channel);
|
|
}
|
|
|
|
inline function getMixChannel(): MixChannel {
|
|
return cast this.channel;
|
|
}
|
|
|
|
#if AURA_DEBUG
|
|
public override function getDebugAttrs(): Map<String, String> {
|
|
return super.getDebugAttrs().mergeIntoThis([
|
|
"Name" => name,
|
|
"Num inserts" => Std.string(@:privateAccess channel.inserts.length),
|
|
]);
|
|
}
|
|
#end
|
|
}
|
|
|
|
|
|
/**
|
|
A channel that mixes together the output of multiple input channels.
|
|
**/
|
|
@:access(aura.dsp.DSP)
|
|
class MixChannel extends BaseChannel {
|
|
#if cpp
|
|
static var mutex: Mutex = new Mutex();
|
|
#end
|
|
|
|
/**
|
|
The amount of inputs a MixChannel can hold. Set this value via
|
|
`Aura.init(channelSize)`.
|
|
**/
|
|
static var channelSize: Int;
|
|
|
|
var inputChannels: Vector<BaseChannel>;
|
|
var numUsedInputs: Int = 0;
|
|
|
|
/**
|
|
Temporary copy of inputChannels for thread safety.
|
|
**/
|
|
var inputChannelsCopy: Vector<BaseChannel>;
|
|
|
|
public function new() {
|
|
inputChannels = new Vector<BaseChannel>(channelSize);
|
|
|
|
// Make sure super.isPlayable() is true until we find better semantics
|
|
// for MixChannel.play()/pause()/stop()
|
|
this.finished = false;
|
|
}
|
|
|
|
/**
|
|
Adds an input channel. Returns `true` if adding the channel was
|
|
successful, `false` if the amount of input channels is already maxed
|
|
out.
|
|
**/
|
|
public function addInputChannel(channel: BaseChannel): Bool {
|
|
var foundChannel = false;
|
|
|
|
#if cpp
|
|
mutex.acquire();
|
|
#end
|
|
|
|
for (i in 0...MixChannel.channelSize) {
|
|
if (inputChannels[i] == null) { // || inputChannels[i].finished) {
|
|
inputChannels[i] = channel;
|
|
numUsedInputs++;
|
|
channel.setTreeLevel(this.treeLevel + 1);
|
|
|
|
foundChannel = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateChannelsCopy();
|
|
|
|
#if cpp
|
|
mutex.release();
|
|
#end
|
|
|
|
return foundChannel;
|
|
}
|
|
|
|
public function removeInputChannel(channel: BaseChannel) {
|
|
#if cpp
|
|
mutex.acquire();
|
|
#end
|
|
|
|
for (i in 0...MixChannel.channelSize) {
|
|
if (inputChannels[i] == channel) {
|
|
inputChannels[i] = null;
|
|
numUsedInputs--;
|
|
break;
|
|
}
|
|
}
|
|
|
|
updateChannelsCopy();
|
|
|
|
#if cpp
|
|
mutex.release();
|
|
#end
|
|
}
|
|
|
|
public inline function getNumInputs() {
|
|
return numUsedInputs;
|
|
}
|
|
|
|
/**
|
|
Copy the references to the inputs channels for thread safety. This
|
|
function does not acquire any additional mutexes.
|
|
@see `MixChannel.inputChannelsCopy`
|
|
**/
|
|
inline function updateChannelsCopy() {
|
|
inputChannelsCopy = inputChannels.copy();
|
|
|
|
// TODO: Streaming
|
|
// for (i in 0...channelCount) {
|
|
// internalStreamChannels[i] = streamChannels[i];
|
|
// }
|
|
}
|
|
|
|
override function isPlayable(): Bool {
|
|
// TODO: be more intelligent here and actually check inputs?
|
|
return super.isPlayable() && numUsedInputs != 0;
|
|
}
|
|
|
|
override function setTreeLevel(level: Int) {
|
|
this.treeLevel = level;
|
|
for (inputChannel in inputChannels) {
|
|
if (inputChannel != null) {
|
|
inputChannel.setTreeLevel(level + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
override function synchronize() {
|
|
for (inputChannel in inputChannels) {
|
|
if (inputChannel != null) {
|
|
inputChannel.synchronize();
|
|
}
|
|
}
|
|
super.synchronize();
|
|
}
|
|
|
|
function nextSamples(requestedSamples: AudioBuffer, sampleRate: Hertz): Void {
|
|
Profiler.event();
|
|
|
|
if (numUsedInputs == 0) {
|
|
requestedSamples.clear();
|
|
return;
|
|
}
|
|
|
|
final inputBuffer = BufferCache.getTreeBuffer(treeLevel, requestedSamples.numChannels, requestedSamples.channelLength);
|
|
if (inputBuffer == null) {
|
|
requestedSamples.clear();
|
|
return;
|
|
}
|
|
|
|
var first = true;
|
|
var foundPlayableInput = false;
|
|
for (channel in inputChannelsCopy) {
|
|
if (channel == null || !channel.isPlayable()) {
|
|
continue;
|
|
}
|
|
foundPlayableInput = true;
|
|
|
|
channel.nextSamples(inputBuffer, sampleRate);
|
|
|
|
if (first) {
|
|
// To prevent feedback loops, the input buffer has to be cleared
|
|
// before all inputs are added to it. To not waste calculations,
|
|
// we do not clear the buffer here but instead just override
|
|
// the previous sample cache.
|
|
for (i in 0...requestedSamples.rawData.length) {
|
|
requestedSamples.rawData[i] = inputBuffer.rawData[i];
|
|
}
|
|
first = false;
|
|
}
|
|
else {
|
|
for (i in 0...requestedSamples.rawData.length) {
|
|
requestedSamples.rawData[i] += inputBuffer.rawData[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// for (channel in internalStreamChannels) {
|
|
// if (channel == null || !channel.isPlayable())
|
|
// continue;
|
|
// foundPlayableInput = true;
|
|
// channel.nextSamples(inputBuffer, samples, buffer.samplesPerSecond);
|
|
// for (i in 0...samples) {
|
|
// sampleCacheAccumulated[i] += inputBuffer[i] * channel.volume;
|
|
// }
|
|
// }
|
|
|
|
if (!foundPlayableInput) {
|
|
// Didn't read from input channels, clear possible garbage values
|
|
requestedSamples.clear();
|
|
return;
|
|
}
|
|
|
|
// Apply volume of this channel
|
|
final stepVol = pVolume.getLerpStepSize(requestedSamples.channelLength);
|
|
for (c in 0...requestedSamples.numChannels) {
|
|
final channelView = requestedSamples.getChannelView(c);
|
|
|
|
for (i in 0...requestedSamples.channelLength) {
|
|
channelView[i] *= pVolume.currentValue;
|
|
pVolume.currentValue += stepVol;
|
|
}
|
|
pVolume.currentValue = pVolume.lastValue;
|
|
}
|
|
|
|
pVolume.updateLast();
|
|
|
|
processInserts(requestedSamples);
|
|
}
|
|
|
|
/**
|
|
Calls `play()` for all input channels.
|
|
**/
|
|
public function play(retrigger: Bool): Void {
|
|
for (inputChannel in inputChannels) {
|
|
if (inputChannel != null) {
|
|
inputChannel.play(retrigger);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Calls `pause()` for all input channels.
|
|
**/
|
|
public function pause(): Void {
|
|
for (inputChannel in inputChannels) {
|
|
if (inputChannel != null) {
|
|
inputChannel.pause();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Calls `stop()` for all input channels.
|
|
**/
|
|
public function stop(): Void {
|
|
for (inputChannel in inputChannels) {
|
|
if (inputChannel != null) {
|
|
inputChannel.stop();
|
|
}
|
|
}
|
|
}
|
|
}
|