Bug 1869043 pull mixed audio from AudioMixer instead of using a callback r=padenot

AudioCallbackDriver is the only client to benefit from a callback.

StartMixing() is now required if reusing the AudioMixer because getting the
mixed data after mixing no longer collapses the duration to zero.

Differential Revision: https://phabricator.services.mozilla.com/D198221
This commit is contained in:
Karl Tomlinson 2024-01-12 01:32:50 +00:00
parent 7fa20ab1f1
commit 0a8a3cbddb
11 changed files with 93 additions and 143 deletions

View File

@ -34,13 +34,9 @@ AudioCaptureTrack::AudioCaptureTrack(TrackRate aRate)
mStarted(false) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_COUNT_CTOR(AudioCaptureTrack);
mMixer.AddCallback(WrapNotNull(this));
}
AudioCaptureTrack::~AudioCaptureTrack() {
MOZ_COUNT_DTOR(AudioCaptureTrack);
mMixer.RemoveCallback(this);
}
AudioCaptureTrack::~AudioCaptureTrack() { MOZ_COUNT_DTOR(AudioCaptureTrack); }
void AudioCaptureTrack::Start() {
QueueControlMessageWithNoShutdown(
@ -87,8 +83,10 @@ void AudioCaptureTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
}
toMix.Mix(mMixer, MONO, Graph()->GraphRate());
}
// This calls MixerCallback below
mMixer.FinishMixing();
AudioChunk* mixed = mMixer.MixedChunk();
MOZ_ASSERT(mixed->ChannelCount() == MONO);
// Now we have mixed data, simply append it.
GetData<AudioSegment>()->AppendAndConsumeChunk(std::move(*mixed));
}
}
@ -96,10 +94,4 @@ uint32_t AudioCaptureTrack::NumberOfChannels() const {
return GetData<AudioSegment>()->MaxChannelCount();
}
void AudioCaptureTrack::MixerCallback(AudioChunk* aMixedBuffer,
uint32_t aSampleRate) {
MOZ_ASSERT(aMixedBuffer->ChannelCount() == MONO);
// Now we have mixed data, simply append it.
GetData<AudioSegment>()->AppendAndConsumeChunk(std::move(*aMixedBuffer));
}
} // namespace mozilla

View File

@ -18,8 +18,7 @@ class DOMMediaStream;
/**
* See MediaTrackGraph::CreateAudioCaptureTrack.
*/
class AudioCaptureTrack : public ProcessedMediaTrack,
public MixerCallbackReceiver {
class AudioCaptureTrack : public ProcessedMediaTrack {
public:
explicit AudioCaptureTrack(TrackRate aRate);
virtual ~AudioCaptureTrack();
@ -31,7 +30,6 @@ class AudioCaptureTrack : public ProcessedMediaTrack,
uint32_t NumberOfChannels() const override;
protected:
void MixerCallback(AudioChunk* aMixedBuffer, uint32_t aSampleRate) override;
AudioMixer mMixer;
bool mStarted;
bool mTrackCreated;

View File

@ -10,7 +10,6 @@
#include "AudioSegment.h"
#include "AudioStream.h"
#include "nsTArray.h"
#include "mozilla/LinkedList.h"
#include "mozilla/NotNull.h"
#include "mozilla/PodOperations.h"
@ -31,7 +30,7 @@ struct MixerCallbackReceiver {
* length, sample rate, sample format and channel count. This class works with
* planar buffers.
*
* When all the tracks have been mixed, calling FinishMixing will call back with
* When all the tracks have been mixed, calling MixedChunk() will provide
* a buffer containing the mixed audio data.
*
* This class is not thread safe.
@ -40,12 +39,7 @@ class AudioMixer {
public:
AudioMixer() { mChunk.mBufferFormat = AUDIO_OUTPUT_FORMAT; }
~AudioMixer() {
MixerCallback* cb;
while ((cb = mCallbacks.popFirst())) {
delete cb;
}
}
~AudioMixer() = default;
void StartMixing() {
mChunk.mDuration = 0;
@ -53,18 +47,13 @@ class AudioMixer {
}
/* Get the data from the mixer. This is supposed to be called when all the
* tracks have been mixed in. The caller should not hold onto the data. */
void FinishMixing() {
* tracks have been mixed in. The caller MAY modify the chunk but MUST clear
* mBuffer if its data needs to survive the next call to Mix(). */
AudioChunk* MixedChunk() {
MOZ_ASSERT(mSampleRate, "Mix not called for this cycle?");
for (MixerCallback* cb = mCallbacks.getFirst(); cb != nullptr;
cb = cb->getNext()) {
MixerCallbackReceiver* receiver = cb->mReceiver;
MOZ_ASSERT(receiver);
receiver->MixerCallback(&mChunk, mSampleRate);
}
mChunk.mDuration = 0;
mSampleRate = 0;
}
return &mChunk;
};
/* Add a buffer to the mix. The buffer can be null if there's nothing to mix
* but the callback is still needed. */
@ -91,32 +80,6 @@ class AudioMixer {
}
}
void AddCallback(NotNull<MixerCallbackReceiver*> aReceiver) {
mCallbacks.insertBack(new MixerCallback(aReceiver));
}
bool FindCallback(MixerCallbackReceiver* aReceiver) {
for (MixerCallback* cb = mCallbacks.getFirst(); cb != nullptr;
cb = cb->getNext()) {
if (cb->mReceiver == aReceiver) {
return true;
}
}
return false;
}
bool RemoveCallback(MixerCallbackReceiver* aReceiver) {
for (MixerCallback* cb = mCallbacks.getFirst(); cb != nullptr;
cb = cb->getNext()) {
if (cb->mReceiver == aReceiver) {
cb->remove();
delete cb;
return true;
}
}
return false;
}
private:
void EnsureCapacityAndSilence() {
uint32_t sampleCount = mChunk.mDuration * mChunk.ChannelCount();
@ -136,15 +99,6 @@ class AudioMixer {
PodZero(mChunk.ChannelDataForWrite<AudioDataValue>(0), sampleCount);
}
class MixerCallback : public LinkedListElement<MixerCallback> {
public:
explicit MixerCallback(NotNull<MixerCallbackReceiver*> aReceiver)
: mReceiver(aReceiver) {}
NotNull<MixerCallbackReceiver*> mReceiver;
};
/* Function that is called when the mixing is done. */
LinkedList<MixerCallback> mCallbacks;
/* Buffer containing the mixed audio data. */
AudioChunk mChunk;
/* Size allocated for mChunk.mBuffer. */

View File

@ -356,15 +356,15 @@ class AudioCallbackDriver::FallbackWrapper : public GraphInterface {
#endif
IterationResult OneIteration(GraphTime aStateComputedEnd,
GraphTime aIterationEnd,
AudioMixer* aMixer) override {
MOZ_ASSERT(!aMixer);
MixerCallbackReceiver* aMixerReceiver) override {
MOZ_ASSERT(!aMixerReceiver);
#ifdef DEBUG
AutoInCallback aic(mOwner);
#endif
IterationResult result =
mGraph->OneIteration(aStateComputedEnd, aIterationEnd, aMixer);
mGraph->OneIteration(aStateComputedEnd, aIterationEnd, aMixerReceiver);
AudioStreamState audioState = mOwner->mAudioStreamState;
@ -475,8 +475,6 @@ AudioCallbackDriver::AudioCallbackDriver(
} else {
mInputDevicePreference = CUBEB_DEVICE_PREF_ALL;
}
mMixer.AddCallback(WrapNotNull(this));
}
AudioCallbackDriver::~AudioCallbackDriver() {
@ -948,7 +946,7 @@ long AudioCallbackDriver::DataCallback(const AudioDataValue* aInputBuffer,
}
IterationResult result =
Graph()->OneIteration(nextStateComputedTime, mIterationEnd, &mMixer);
Graph()->OneIteration(nextStateComputedTime, mIterationEnd, this);
mStateComputedTime = nextStateComputedTime;

View File

@ -193,11 +193,11 @@ struct GraphInterface : public nsISupports {
* plug/unplug etc. This can be called on any thread, and posts a message to
* the main thread so that it can post a message to the graph thread. */
virtual void DeviceChanged() = 0;
/* Called by GraphDriver to iterate the graph. Output from the graph gets
* mixed into aMixer, if it is non-null. */
virtual IterationResult OneIteration(GraphTime aStateComputedEnd,
GraphTime aIterationEnd,
AudioMixer* aMixer) = 0;
/* Called by GraphDriver to iterate the graph. Mixed audio output from the
* graph is passed into aMixerReceiver, if it is non-null. */
virtual IterationResult OneIteration(
GraphTime aStateComputedEnd, GraphTime aIterationEnd,
MixerCallbackReceiver* aMixerReceiver) = 0;
#ifdef DEBUG
/* True if we're on aDriver's thread, or if we're on mGraphRunner's thread
* and mGraphRunner is currently run by aDriver. */
@ -719,9 +719,6 @@ class AudioCallbackDriver : public GraphDriver, public MixerCallbackReceiver {
* must run serially for access to mAudioStream. */
const RefPtr<SharedThreadPool> mCubebOperationThread;
cubeb_device_pref mInputDevicePreference;
/* The mixer that the graph mixes into during an iteration. Audio thread only.
*/
AudioMixer mMixer;
/* Contains the id of the audio thread, from profiler_current_thread_id. */
std::atomic<ProfilerThreadId> mAudioThreadId;
/* This allows implementing AutoInCallback. This is equal to the current

View File

@ -60,12 +60,14 @@ void GraphRunner::Shutdown() {
}
auto GraphRunner::OneIteration(GraphTime aStateTime, GraphTime aIterationEnd,
AudioMixer* aMixer) -> IterationResult {
MixerCallbackReceiver* aMixerReceiver)
-> IterationResult {
TRACE("GraphRunner::OneIteration");
MonitorAutoLock lock(mMonitor);
MOZ_ASSERT(mThreadState == ThreadState::Wait);
mIterationState = Some(IterationState(aStateTime, aIterationEnd, aMixer));
mIterationState =
Some(IterationState(aStateTime, aIterationEnd, aMixerReceiver));
#ifdef DEBUG
if (const auto* audioDriver =
@ -136,9 +138,9 @@ NS_IMETHODIMP GraphRunner::Run() {
}
MOZ_DIAGNOSTIC_ASSERT(mIterationState.isSome());
TRACE("GraphRunner::Run");
mIterationResult = mGraph->OneIterationImpl(mIterationState->StateTime(),
mIterationState->IterationEnd(),
mIterationState->Mixer());
mIterationResult = mGraph->OneIterationImpl(
mIterationState->StateTime(), mIterationState->IterationEnd(),
mIterationState->MixerReceiver());
// Signal that mIterationResult was updated
mThreadState = ThreadState::Wait;
mMonitor.Notify();

View File

@ -36,7 +36,7 @@ class GraphRunner final : public Runnable {
* the iteration there.
*/
IterationResult OneIteration(GraphTime aStateTime, GraphTime aIterationEnd,
AudioMixer* aMixer);
MixerCallbackReceiver* aMixerReceiver);
/**
* Runs mGraph until it shuts down.
@ -64,18 +64,18 @@ class GraphRunner final : public Runnable {
class IterationState {
GraphTime mStateTime;
GraphTime mIterationEnd;
AudioMixer* MOZ_NON_OWNING_REF mMixer;
MixerCallbackReceiver* MOZ_NON_OWNING_REF mMixerReceiver;
public:
IterationState(GraphTime aStateTime, GraphTime aIterationEnd,
AudioMixer* aMixer)
MixerCallbackReceiver* aMixerReceiver)
: mStateTime(aStateTime),
mIterationEnd(aIterationEnd),
mMixer(aMixer) {}
mMixerReceiver(aMixerReceiver) {}
IterationState& operator=(const IterationState& aOther) = default;
GraphTime StateTime() const { return mStateTime; }
GraphTime IterationEnd() const { return mIterationEnd; }
AudioMixer* Mixer() const { return mMixer; }
MixerCallbackReceiver* MixerReceiver() const { return mMixerReceiver; }
};
// Monitor used for yielding mThread through Wait(), and scheduling mThread

View File

@ -653,12 +653,10 @@ void MediaTrackGraphImpl::UpdateTrackOrder() {
MOZ_ASSERT(orderedTrackCount == mFirstCycleBreaker);
}
TrackTime MediaTrackGraphImpl::PlayAudio(AudioMixer* aMixer,
const TrackKeyAndVolume& aTkv,
TrackTime MediaTrackGraphImpl::PlayAudio(const TrackKeyAndVolume& aTkv,
GraphTime aPlayedTime) {
MOZ_ASSERT(OnGraphThread());
MOZ_ASSERT(mRealtime, "Should only attempt to play audio in realtime mode");
MOZ_ASSERT(aMixer, "Can only play audio if there's a mixer");
TrackTime ticksWritten = 0;
@ -745,7 +743,7 @@ TrackTime MediaTrackGraphImpl::PlayAudio(AudioMixer* aMixer,
} else {
outputChannels = AudioOutputChannelCount();
}
output.Mix(*aMixer, outputChannels, mSampleRate);
output.Mix(mMixer, outputChannels, mSampleRate);
}
return ticksWritten;
}
@ -1428,7 +1426,7 @@ void MediaTrackGraphImpl::UpdateGraph(GraphTime aEndBlockingDecisions) {
}
}
void MediaTrackGraphImpl::Process(AudioMixer* aMixer) {
void MediaTrackGraphImpl::Process(MixerCallbackReceiver* aMixerReceiver) {
TRACE("MTG::Process");
MOZ_ASSERT(OnGraphThread());
// Play track contents.
@ -1477,15 +1475,14 @@ void MediaTrackGraphImpl::Process(AudioMixer* aMixer) {
}
mProcessedTime = mStateComputedTime;
if (aMixer) {
if (aMixerReceiver) {
MOZ_ASSERT(mRealtime, "If there's a mixer, this graph must be realtime");
aMixer->StartMixing();
mMixer.StartMixing();
// This is the number of frames that are written to the output buffer, for
// this iteration.
TrackTime ticksPlayed = 0;
for (auto& t : mAudioOutputs) {
TrackTime ticksPlayedForThisTrack =
PlayAudio(aMixer, t, oldProcessedTime);
TrackTime ticksPlayedForThisTrack = PlayAudio(t, oldProcessedTime);
if (ticksPlayed == 0) {
ticksPlayed = ticksPlayedForThisTrack;
} else {
@ -1499,12 +1496,11 @@ void MediaTrackGraphImpl::Process(AudioMixer* aMixer) {
// Nothing was played, so the mixer doesn't know how many frames were
// processed. We still tell it so AudioCallbackDriver knows how much has
// been processed. (bug 1406027)
aMixer->Mix(
nullptr,
CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount(),
mStateComputedTime - oldProcessedTime, mSampleRate);
mMixer.Mix(nullptr,
CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount(),
mStateComputedTime - oldProcessedTime, mSampleRate);
}
aMixer->FinishMixing();
aMixerReceiver->MixerCallback(mMixer.MixedChunk(), mSampleRate);
}
if (!allBlockedForever) {
@ -1541,18 +1537,19 @@ bool MediaTrackGraphImpl::UpdateMainThreadState() {
auto MediaTrackGraphImpl::OneIteration(GraphTime aStateTime,
GraphTime aIterationEnd,
AudioMixer* aMixer) -> IterationResult {
MixerCallbackReceiver* aMixerReceiver)
-> IterationResult {
if (mGraphRunner) {
return mGraphRunner->OneIteration(aStateTime, aIterationEnd, aMixer);
return mGraphRunner->OneIteration(aStateTime, aIterationEnd,
aMixerReceiver);
}
return OneIterationImpl(aStateTime, aIterationEnd, aMixer);
return OneIterationImpl(aStateTime, aIterationEnd, aMixerReceiver);
}
auto MediaTrackGraphImpl::OneIterationImpl(GraphTime aStateTime,
GraphTime aIterationEnd,
AudioMixer* aMixer)
-> IterationResult {
auto MediaTrackGraphImpl::OneIterationImpl(
GraphTime aStateTime, GraphTime aIterationEnd,
MixerCallbackReceiver* aMixerReceiver) -> IterationResult {
TRACE("MTG::OneIterationImpl");
mIterationEndTime = aIterationEnd;
@ -1596,7 +1593,7 @@ auto MediaTrackGraphImpl::OneIterationImpl(GraphTime aStateTime,
mStateComputedTime = stateTime;
GraphTime oldProcessedTime = mProcessedTime;
Process(aMixer);
Process(aMixerReceiver);
MOZ_ASSERT(mProcessedTime == stateTime);
UpdateCurrentTimeForTracks(oldProcessedTime);

View File

@ -258,17 +258,18 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
* Proxy method called by GraphDriver to iterate the graph.
* If this graph was created with GraphRunType SINGLE_THREAD, mGraphRunner
* will take care of calling OneIterationImpl from its thread. Otherwise,
* OneIterationImpl is called directly. Output from the graph gets mixed into
* aMixer, if it is non-null.
* OneIterationImpl is called directly. Mixed audio output from the graph is
* passed into aMixerReceiver, if it is non-null.
*/
IterationResult OneIteration(GraphTime aStateTime, GraphTime aIterationEnd,
AudioMixer* aMixer) override;
MixerCallbackReceiver* aMixerReceiver) override;
/**
* Returns true if this MediaTrackGraph should keep running
*/
IterationResult OneIterationImpl(GraphTime aStateTime,
GraphTime aIterationEnd, AudioMixer* aMixer);
GraphTime aIterationEnd,
MixerCallbackReceiver* aMixerReceiver);
/**
* Called from the driver, when the graph thread is about to stop, to tell
@ -340,7 +341,7 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
* Do all the processing and play the audio and video, from
* mProcessedTime to mStateComputedTime.
*/
void Process(AudioMixer* aMixer);
void Process(MixerCallbackReceiver* aMixerReceiver);
/**
* For use during ProcessedMediaTrack::ProcessInput() or
@ -430,8 +431,7 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
void* mKey;
float mVolume;
};
TrackTime PlayAudio(AudioMixer* aMixer, const TrackKeyAndVolume& aTkv,
GraphTime aPlayedTime);
TrackTime PlayAudio(const TrackKeyAndVolume& aTkv, GraphTime aPlayedTime);
/* Runs off a message on the graph thread when something requests audio from
* an input audio device of ID aID, and delivers the input audio frames to
@ -1049,6 +1049,12 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
* Manage the native or non-native input device in graph. Graph thread only.
*/
DeviceInputTrackManager mDeviceInputTrackManagerGraphThread;
/**
* The mixer that the graph mixes into during an iteration. This is here
* rather than on the stack so that its buffer is not allocated each
* iteration. Graph thread only.
*/
AudioMixer mMixer;
};
} // namespace mozilla

View File

@ -35,14 +35,13 @@ class MockGraphInterface : public GraphInterface {
/* OneIteration cannot be mocked because IterationResult is non-memmovable and
* cannot be passed as a parameter, which GMock does internally. */
IterationResult OneIteration(GraphTime aStateComputedTime, GraphTime,
AudioMixer* aMixer) {
MixerCallbackReceiver* aMixerReceiver) {
GraphDriver* driver = mCurrentDriver;
if (aMixer) {
aMixer->StartMixing();
aMixer->Mix(nullptr,
driver->AsAudioCallbackDriver()->OutputChannelCount(),
aStateComputedTime - mStateComputedTime, mSampleRate);
aMixer->FinishMixing();
if (aMixerReceiver) {
mMixer.StartMixing();
mMixer.Mix(nullptr, driver->AsAudioCallbackDriver()->OutputChannelCount(),
aStateComputedTime - mStateComputedTime, mSampleRate);
aMixerReceiver->MixerCallback(mMixer.MixedChunk(), mSampleRate);
}
if (aStateComputedTime != mStateComputedTime) {
mFramesIteratedEvent.Notify(aStateComputedTime - mStateComputedTime);
@ -93,6 +92,7 @@ class MockGraphInterface : public GraphInterface {
Atomic<bool> mKeepProcessing{true};
Atomic<GraphDriver*> mNextDriver{nullptr};
MediaEventProducer<uint32_t> mFramesIteratedEvent;
AudioMixer mMixer;
virtual ~MockGraphInterface() = default;
};

View File

@ -83,20 +83,19 @@ TEST(AudioMixer, Test)
{
int iterations = 2;
mozilla::AudioMixer mixer;
mixer.AddCallback(WrapNotNull(&consumer));
fprintf(stderr, "Test AudioMixer constant buffer length.\n");
while (iterations--) {
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
}
}
{
mozilla::AudioMixer mixer;
mixer.AddCallback(WrapNotNull(&consumer));
fprintf(stderr, "Test AudioMixer variable buffer length.\n");
@ -106,27 +105,30 @@ TEST(AudioMixer, Test)
FillBuffer(b, CHANNEL_LENGTH / 2, GetHighValue<AudioDataValue>());
FillBuffer(b + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
GetHighValue<AudioDataValue>());
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>());
FillBuffer(a + CHANNEL_LENGTH, CHANNEL_LENGTH,
GetHighValue<AudioDataValue>());
FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>());
FillBuffer(b + CHANNEL_LENGTH, CHANNEL_LENGTH,
GetLowValue<AudioDataValue>());
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
FillBuffer(a, CHANNEL_LENGTH / 2, GetLowValue<AudioDataValue>());
FillBuffer(a + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
GetLowValue<AudioDataValue>());
FillBuffer(b, CHANNEL_LENGTH / 2, GetHighValue<AudioDataValue>());
FillBuffer(b + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
GetHighValue<AudioDataValue>());
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
}
FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>());
@ -134,37 +136,41 @@ TEST(AudioMixer, Test)
{
mozilla::AudioMixer mixer;
mixer.AddCallback(WrapNotNull(&consumer));
fprintf(stderr, "Test AudioMixer variable channel count.\n");
mixer.StartMixing();
mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
mixer.StartMixing();
mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
mixer.StartMixing();
mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
}
{
mozilla::AudioMixer mixer;
mixer.AddCallback(WrapNotNull(&consumer));
fprintf(stderr, "Test AudioMixer variable stream count.\n");
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
mixer.StartMixing();
mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
mixer.FinishMixing();
consumer.MixerCallback(mixer.MixedChunk(), AUDIO_RATE);
}
}