Bug 1869043 allow a device to be specified with MediaTrack::AddAudioOutput() r=padenot

Output to secondary devices is supported through CrossGraphReceivers on other
graphs created when required.

Differential Revision: https://phabricator.services.mozilla.com/D198229
This commit is contained in:
Karl Tomlinson 2024-01-12 19:08:57 +00:00
parent 28fcad562f
commit 7b15ffca4e
9 changed files with 353 additions and 67 deletions

View File

@ -29,6 +29,7 @@ class AudioDeviceInfo final : public nsIAudioDeviceInfo {
AudioDeviceID DeviceID() const;
const nsString& Name() const;
uint32_t DefaultRate() const { return mDefaultRate; }
uint32_t MaxChannels() const;
uint32_t Type() const;
uint32_t State() const;

View File

@ -22,7 +22,7 @@ RefPtr<GenericPromise> AudioStreamTrack::AddAudioOutput(
UniquePtr<CrossGraphPort> manager;
if (!aSink || !(manager = CrossGraphPort::Connect(this, aSink, mWindow))) {
// We are setting the default output device.
mTrack->AddAudioOutput(aKey);
mTrack->AddAudioOutput(aKey, nullptr);
return GenericPromise::CreateAndResolve(true, __func__);
}

View File

@ -70,7 +70,8 @@ UniquePtr<CrossGraphPort> CrossGraphPort::Connect(
}
void CrossGraphPort::AddAudioOutput(void* aKey) {
mReceiver->AddAudioOutput(aKey);
mReceiver->AddAudioOutput(aKey, mReceiver->Graph()->PrimaryOutputDeviceID(),
0);
}
void CrossGraphPort::RemoveAudioOutput(void* aKey) {

View File

@ -853,9 +853,11 @@ void MediaTrackGraphImpl::CloseAudioInputImpl(DeviceInputTrack* aTrack) {
void MediaTrackGraphImpl::UnregisterAllAudioOutputs(MediaTrack* aTrack) {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
mAudioOutputs.RemoveElementsBy([aTrack](const TrackAndVolume& aOutput) {
return aOutput.mTrack == aTrack;
mOutputDevices.RemoveElementsBy([&](OutputDeviceEntry& aDeviceRef) {
aDeviceRef.mTrackOutputs.RemoveElement(aTrack);
// mReceiver is null for the primary output device, which is retained for
// AudioCallbackDriver output even when no tracks have audio outputs.
return aDeviceRef.mTrackOutputs.IsEmpty() && aDeviceRef.mReceiver;
});
}
@ -1434,19 +1436,29 @@ void MediaTrackGraphImpl::Process(MixerCallbackReceiver* aMixerReceiver) {
}
mProcessedTime = mStateComputedTime;
if (aMixerReceiver) {
MOZ_ASSERT(mRealtime, "If there's a mixer, this graph must be realtime");
MOZ_ASSERT(CurrentDriver()->AsAudioCallbackDriver(),
"Driver must be AudioCallbackDriver if aMixerReceiver");
// Use the number of channel the driver expects: this is the number of
// channel that can be output by the underlying system level audio stream.
uint32_t outputChannelCount =
CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
for (const auto& outputDeviceEntry : mOutputDevices) {
uint32_t outputChannelCount;
if (!outputDeviceEntry.mReceiver) { // primary output
if (!aMixerReceiver) {
// Running off a system clock driver. No need to mix output.
continue;
}
MOZ_ASSERT(CurrentDriver()->AsAudioCallbackDriver(),
"Driver must be AudioCallbackDriver if aMixerReceiver");
// Use the number of channel the driver expects: this is the number of
// channel that can be output by the underlying system level audio stream.
outputChannelCount =
CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
} else {
outputChannelCount = AudioOutputChannelCount(outputDeviceEntry);
}
MOZ_ASSERT(mRealtime,
"If there's an output device, this graph must be realtime");
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) {
for (const auto& t : outputDeviceEntry.mTrackOutputs) {
TrackTime ticksPlayedForThisTrack =
PlayAudio(t, oldProcessedTime, outputChannelCount);
if (ticksPlayed == 0) {
@ -1465,7 +1477,12 @@ void MediaTrackGraphImpl::Process(MixerCallbackReceiver* aMixerReceiver) {
mMixer.Mix(nullptr, outputChannelCount,
mStateComputedTime - oldProcessedTime, mSampleRate);
}
aMixerReceiver->MixerCallback(mMixer.MixedChunk(), mSampleRate);
AudioChunk* outputChunk = mMixer.MixedChunk();
if (!outputDeviceEntry.mReceiver) { // primary output
aMixerReceiver->MixerCallback(outputChunk, mSampleRate);
} else {
outputDeviceEntry.mReceiver->EnqueueAudio(*outputChunk);
}
}
if (!allBlockedForever) {
@ -2263,13 +2280,25 @@ TrackTime MediaTrack::GetEnd() const {
return mSegment ? mSegment->GetDuration() : 0;
}
void MediaTrack::AddAudioOutput(void* aKey) {
void MediaTrack::AddAudioOutput(void* aKey, const AudioDeviceInfo* aSink) {
MOZ_ASSERT(NS_IsMainThread());
AudioDeviceID deviceID = nullptr;
TrackRate preferredSampleRate = 0;
if (aSink) {
deviceID = aSink->DeviceID();
preferredSampleRate = static_cast<TrackRate>(aSink->DefaultRate());
}
AddAudioOutput(aKey, deviceID, preferredSampleRate);
}
void MediaTrack::AddAudioOutput(void* aKey, CubebUtils::AudioDeviceID aDeviceID,
TrackRate aPreferredSampleRate) {
MOZ_ASSERT(NS_IsMainThread());
if (mMainThreadDestroyed) {
return;
}
LOG(LogLevel::Info, ("MediaTrack %p adding AudioOutput", this));
GraphImpl()->RegisterAudioOutput(this, aKey);
GraphImpl()->RegisterAudioOutput(this, aKey, aDeviceID, aPreferredSampleRate);
}
void MediaTrackGraphImpl::SetAudioOutputVolume(MediaTrack* aTrack, void* aKey,
@ -2278,7 +2307,7 @@ void MediaTrackGraphImpl::SetAudioOutputVolume(MediaTrack* aTrack, void* aKey,
for (auto& params : mAudioOutputParams) {
if (params.mKey == aKey && aTrack == params.mTrack) {
params.mVolume = aVolume;
UpdateAudioOutput(aTrack);
UpdateAudioOutput(aTrack, params.mDeviceID);
return;
}
}
@ -2301,13 +2330,18 @@ void MediaTrack::RemoveAudioOutput(void* aKey) {
GraphImpl()->UnregisterAudioOutput(this, aKey);
}
void MediaTrackGraphImpl::RegisterAudioOutput(MediaTrack* aTrack, void* aKey) {
void MediaTrackGraphImpl::RegisterAudioOutput(
MediaTrack* aTrack, void* aKey, CubebUtils::AudioDeviceID aDeviceID,
TrackRate aPreferredSampleRate) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mAudioOutputParams.Contains(TrackAndKey{aTrack, aKey}));
mAudioOutputParams.EmplaceBack(TrackKeyAndVolume{aTrack, aKey, 1.f});
IncrementOutputDeviceRefCnt(aDeviceID, aPreferredSampleRate);
UpdateAudioOutput(aTrack);
mAudioOutputParams.EmplaceBack(
TrackKeyDeviceAndVolume{aTrack, aKey, aDeviceID, 1.f});
UpdateAudioOutput(aTrack, aDeviceID);
}
void MediaTrackGraphImpl::UnregisterAudioOutput(MediaTrack* aTrack,
@ -2316,43 +2350,110 @@ void MediaTrackGraphImpl::UnregisterAudioOutput(MediaTrack* aTrack,
size_t index = mAudioOutputParams.IndexOf(TrackAndKey{aTrack, aKey});
MOZ_ASSERT(index != mAudioOutputParams.NoIndex);
AudioDeviceID deviceID = mAudioOutputParams[index].mDeviceID;
mAudioOutputParams.UnorderedRemoveElementAt(index);
UpdateAudioOutput(aTrack);
UpdateAudioOutput(aTrack, deviceID);
DecrementOutputDeviceRefCnt(deviceID);
}
void MediaTrackGraphImpl::UpdateAudioOutput(MediaTrack* aTrack) {
void MediaTrackGraphImpl::UpdateAudioOutput(MediaTrack* aTrack,
AudioDeviceID aDeviceID) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!aTrack->IsDestroyed());
float volume = 0.f;
bool found = false;
for (const auto& params : mAudioOutputParams) {
if (params.mTrack == aTrack) {
if (params.mTrack == aTrack && params.mDeviceID == aDeviceID) {
volume += params.mVolume;
found = true;
}
}
QueueControlMessageWithNoShutdown([track = RefPtr{aTrack}, volume, found] {
TRACE("MediaTrack::UpdateAudioOutput ControlMessage");
MediaTrackGraphImpl* graph = track->GraphImpl();
auto& audioOutputsRef = graph->mAudioOutputs;
if (found) {
for (auto& outputRef : audioOutputsRef) {
if (outputRef.mTrack == track) {
outputRef.mVolume = volume;
return;
QueueControlMessageWithNoShutdown(
// track has a strong reference to this.
[track = RefPtr{aTrack}, aDeviceID, volume, found] {
TRACE("MediaTrack::UpdateAudioOutput ControlMessage");
MediaTrackGraphImpl* graph = track->GraphImpl();
auto& outputDevicesRef = graph->mOutputDevices;
size_t deviceIndex = outputDevicesRef.IndexOf(aDeviceID);
MOZ_ASSERT(deviceIndex != outputDevicesRef.NoIndex);
auto& deviceOutputsRef = outputDevicesRef[deviceIndex].mTrackOutputs;
if (found) {
for (auto& outputRef : deviceOutputsRef) {
if (outputRef.mTrack == track) {
outputRef.mVolume = volume;
return;
}
}
deviceOutputsRef.EmplaceBack(TrackAndVolume{track, volume});
} else {
DebugOnly<bool> removed = deviceOutputsRef.RemoveElement(track);
MOZ_ASSERT(removed);
// mOutputDevices[0] is retained for AudioCallbackDriver output even
// when no tracks have audio outputs.
if (deviceIndex != 0 && deviceOutputsRef.IsEmpty()) {
// The device is no longer in use.
outputDevicesRef.UnorderedRemoveElementAt(deviceIndex);
}
}
}
audioOutputsRef.EmplaceBack(TrackAndVolume{track, volume});
} else {
DebugOnly<bool> removed = audioOutputsRef.RemoveElement(track);
MOZ_ASSERT(removed);
});
}
void MediaTrackGraphImpl::IncrementOutputDeviceRefCnt(
AudioDeviceID aDeviceID, TrackRate aPreferredSampleRate) {
MOZ_ASSERT(NS_IsMainThread());
for (auto& elementRef : mOutputDeviceRefCnts) {
if (elementRef.mDeviceID == aDeviceID) {
++elementRef.mRefCnt;
return;
}
}
MOZ_ASSERT(aDeviceID != mPrimaryOutputDeviceID,
"mOutputDeviceRefCnts should always have the primary device");
// Need to add an output device.
// Output via another graph for this device.
// This sample rate is not exposed to content.
TrackRate sampleRate =
aPreferredSampleRate != 0
? aPreferredSampleRate
: static_cast<TrackRate>(CubebUtils::PreferredSampleRate(
/*aShouldResistFingerprinting*/ false));
MediaTrackGraph* newGraph = MediaTrackGraphImpl::GetInstance(
MediaTrackGraph::AUDIO_THREAD_DRIVER, mWindowID, sampleRate, aDeviceID,
GetMainThreadSerialEventTarget());
// CreateCrossGraphReceiver wants the sample rate of this graph.
RefPtr receiver = newGraph->CreateCrossGraphReceiver(mSampleRate);
receiver->AddAudioOutput(nullptr, aDeviceID, sampleRate);
mOutputDeviceRefCnts.EmplaceBack(
DeviceReceiverAndCount{aDeviceID, receiver, 1});
QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, aDeviceID,
receiver = std::move(receiver)]() mutable {
TRACE("MediaTrackGraph add output device ControlMessage");
MOZ_ASSERT(!mOutputDevices.Contains(aDeviceID));
mOutputDevices.EmplaceBack(
OutputDeviceEntry{aDeviceID, std::move(receiver)});
});
}
void MediaTrackGraphImpl::DecrementOutputDeviceRefCnt(AudioDeviceID aDeviceID) {
MOZ_ASSERT(NS_IsMainThread());
size_t index = mOutputDeviceRefCnts.IndexOf(aDeviceID);
MOZ_ASSERT(index != mOutputDeviceRefCnts.NoIndex);
// mOutputDeviceRefCnts[0] is retained for consistency with
// mOutputDevices[0], which is retained for AudioCallbackDriver output even
// when no tracks have audio outputs.
if (--mOutputDeviceRefCnts[index].mRefCnt == 0 && index != 0) {
mOutputDeviceRefCnts[index].mReceiver->Destroy();
mOutputDeviceRefCnts.UnorderedRemoveElementAt(index);
}
}
void MediaTrack::Suspend() {
// This can happen if this method has been called asynchronously, and the
// track has been destroyed since then.
@ -3262,6 +3363,13 @@ MediaTrackGraphImpl::MediaTrackGraphImpl(
mMainThreadGraphTime(0, "MediaTrackGraphImpl::mMainThreadGraphTime"),
mAudioOutputLatency(0.0),
mMaxOutputChannelCount(std::min(8u, CubebUtils::MaxNumberOfChannels())) {
// The primary output device always exists because an AudioCallbackDriver
// may exist, and want to be fed data, even when no tracks have audio
// outputs.
mOutputDeviceRefCnts.EmplaceBack(
DeviceReceiverAndCount{aPrimaryOutputDeviceID, nullptr, 0});
mOutputDevices.EmplaceBack(OutputDeviceEntry{aPrimaryOutputDeviceID});
bool failedToGetShutdownBlocker = false;
if (!IsNonRealtime()) {
failedToGetShutdownBlocker = !AddShutdownBlocker();
@ -3615,12 +3723,14 @@ void MediaTrackGraphImpl::RemoveTrack(MediaTrack* aTrack) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_DIAGNOSTIC_ASSERT(mMainThreadTrackCount > 0);
mAudioOutputParams.RemoveElementsBy([&](const TrackKeyAndVolume& aElement) {
if (aElement.mTrack != aTrack) {
return false;
};
return true;
});
mAudioOutputParams.RemoveElementsBy(
[&](const TrackKeyDeviceAndVolume& aElement) {
if (aElement.mTrack != aTrack) {
return false;
};
DecrementOutputDeviceRefCnt(aElement.mDeviceID);
return true;
});
if (--mMainThreadTrackCount == 0) {
LOG(LogLevel::Info, ("MediaTrackGraph %p, last track %p removed from "
@ -3837,19 +3947,26 @@ auto MediaTrackGraph::ApplyAudioContextOperation(
}
uint32_t MediaTrackGraphImpl::PrimaryOutputChannelCount() const {
MOZ_ASSERT(!mOutputDevices[0].mReceiver);
return AudioOutputChannelCount(mOutputDevices[0]);
}
uint32_t MediaTrackGraphImpl::AudioOutputChannelCount(
const OutputDeviceEntry& aDevice) const {
MOZ_ASSERT(OnGraphThread());
// The audio output channel count for a graph is the maximum of the output
// channel count of all the tracks that are in mAudioOutputs, or the max audio
// output channel count the machine can do, whichever is smaller.
uint32_t channelCount = 0;
for (const auto& output : mAudioOutputs) {
for (const auto& output : aDevice.mTrackOutputs) {
channelCount = std::max(channelCount, output.mTrack->NumberOfChannels());
}
channelCount = std::min(channelCount, mMaxOutputChannelCount);
if (channelCount) {
return channelCount;
} else {
if (CurrentDriver()->AsAudioCallbackDriver()) {
// null aDevice.mReceiver indicates the primary graph output device.
if (!aDevice.mReceiver && CurrentDriver()->AsAudioCallbackDriver()) {
return CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
}
return 2;

View File

@ -269,7 +269,9 @@ class MediaTrack : public mozilla::LinkedListElement<MediaTrack> {
void SetGraphImpl(MediaTrackGraph* aGraph);
// Control API.
void AddAudioOutput(void* aKey);
void AddAudioOutput(void* aKey, const AudioDeviceInfo* aSink);
void AddAudioOutput(void* aKey, CubebUtils::AudioDeviceID aDeviceID,
TrackRate aPreferredSampleRate);
void SetAudioOutputVolume(void* aKey, float aVolume);
void RemoveAudioOutput(void* aKey);
// Explicitly suspend. Useful for example if a media element is pausing

View File

@ -203,14 +203,25 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
std::forward<Function>(aFunction))));
}
/* Add or remove an audio output for this track. At most one output may be
* registered per key. */
void RegisterAudioOutput(MediaTrack* aTrack, void* aKey);
* registered per key. aPreferredSampleRate is the rate preferred by the
* output device; it may be zero to indicate the preferred rate for the
* default device; it is unused when aDeviceID is the graph's primary output.
*/
void RegisterAudioOutput(MediaTrack* aTrack, void* aKey,
CubebUtils::AudioDeviceID aDeviceID,
TrackRate aPreferredSampleRate);
void UnregisterAudioOutput(MediaTrack* aTrack, void* aKey);
void SetAudioOutputVolume(MediaTrack* aTrack, void* aKey, float aVolume);
/* Send a control message to update mAudioOutputs for main thread changes to
/* Manage the creation and destruction of CrossGraphReceivers.
* aPreferredSampleRate is the rate preferred by the output device. */
void IncrementOutputDeviceRefCnt(CubebUtils::AudioDeviceID aDeviceID,
TrackRate aPreferredSampleRate);
void DecrementOutputDeviceRefCnt(CubebUtils::AudioDeviceID aDeviceID);
/* Send a control message to update mOutputDevices for main thread changes to
* mAudioOutputParams. */
void UpdateAudioOutput(MediaTrack* aTrack);
void UpdateAudioOutput(MediaTrack* aTrack,
CubebUtils::AudioDeviceID aDeviceID);
/**
* Dispatches a runnable from any thread to the correct main thread for this
* MediaTrackGraph.
@ -515,9 +526,16 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
mTrackOrderDirty = true;
}
private:
// Get the current maximum channel count required for a device.
// aDevice is an element of mOutputDevices. Graph thread only.
struct OutputDeviceEntry;
uint32_t AudioOutputChannelCount(const OutputDeviceEntry& aDevice) const;
// Get the current maximum channel count for audio output through an
// AudioCallbackDriver. Graph thread only.
uint32_t PrimaryOutputChannelCount() const;
public:
// Set a new maximum channel count. Graph thread only.
void SetMaxOutputChannelCount(uint32_t aMaxChannelCount);
@ -982,28 +1000,52 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
/**
* Main thread unordered record of audio outputs, keyed by Track and output
* key. Used to record the volumes corresponding to each key. An array is
* used as a simple hash table, on the assumption that the number of outputs
* is small.
* key. Used to determine when an output device is no longer in use and to
* record the volumes corresponding to each key. An array is used as a
* simple hash table, on the assumption that the number of outputs is small.
*/
struct TrackAndKey {
MOZ_UNSAFE_REF("struct exists only if track exists") MediaTrack* mTrack;
void* mKey;
};
struct TrackKeyAndVolume {
struct TrackKeyDeviceAndVolume {
MOZ_UNSAFE_REF("struct exists only if track exists")
MediaTrack* const mTrack;
void* const mKey;
const CubebUtils::AudioDeviceID mDeviceID;
float mVolume;
bool operator==(const TrackAndKey& aTrackAndKey) const {
return mTrack == aTrackAndKey.mTrack && mKey == aTrackAndKey.mKey;
}
};
nsTArray<TrackKeyAndVolume> mAudioOutputParams;
nsTArray<TrackKeyDeviceAndVolume> mAudioOutputParams;
/**
* Mapping from MediaTrack to volume for all tracks that have their audio
* output mixed and written to an audio output device. Graph thread.
* Main thread record of which audio output devices are active, keyed by
* AudioDeviceID, and their CrossGraphReceivers if any.
* mOutputDeviceRefCnts[0] always exists and corresponds to the primary
* audio output device, which an AudioCallbackDriver will use if active.
* mCount may be zero for the first entry only. */
struct DeviceReceiverAndCount {
const CubebUtils::AudioDeviceID mDeviceID;
// For secondary devices, mReceiver receives audio output.
// Null for the primary output device, fed by an AudioCallbackDriver.
const RefPtr<CrossGraphReceiver> mReceiver;
size_t mRefCnt; // number of mAudioOutputParams entries with this device
bool operator==(CubebUtils::AudioDeviceID aDeviceID) const {
return mDeviceID == aDeviceID;
}
};
nsTArray<DeviceReceiverAndCount> mOutputDeviceRefCnts;
/**
* Graph thread record of devices to which audio outputs are mixed, keyed by
* AudioDeviceID. All tracks that have an audio output to each device are
* grouped for mixing their outputs to a single stream.
* mOutputDevices[0] always exists and corresponds to the primary audio
* output device, which an AudioCallbackDriver will use if active.
* An AudioCallbackDriver may be active when no audio outputs have audio
* outputs.
*/
struct TrackAndVolume {
MOZ_UNSAFE_REF("struct exists only if track exists")
@ -1012,7 +1054,22 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
bool operator==(const MediaTrack* aTrack) const { return mTrack == aTrack; }
};
nsTArray<TrackAndVolume> mAudioOutputs;
struct OutputDeviceEntry {
const CubebUtils::AudioDeviceID mDeviceID;
// For secondary devices, mReceiver receives audio output.
// Null for the primary output device, fed by an AudioCallbackDriver.
const RefPtr<CrossGraphReceiver> mReceiver;
/**
* Mapping from MediaTrack to volume for all tracks that have their audio
* output mixed and written to this output device.
*/
nsTArray<TrackAndVolume> mTrackOutputs;
bool operator==(CubebUtils::AudioDeviceID aDeviceID) const {
return mDeviceID == aDeviceID;
}
};
nsTArray<OutputDeviceEntry> mOutputDevices;
/**
* Global volume scale. Used when running tests so that the output is not too

View File

@ -209,7 +209,9 @@ class MockCubebStream {
MediaEventSource<nsCString>& NameSetEvent();
MediaEventSource<cubeb_state>& StateEvent();
MediaEventSource<uint32_t>& FramesProcessedEvent();
// Notified when frames are processed after first non-silent output
MediaEventSource<uint32_t>& FramesVerifiedEvent();
// Notified when the stream is Stop()ed
MediaEventSource<std::tuple<uint64_t, float, uint32_t>>&
OutputVerificationEvent();
MediaEventSource<void>& ErrorForcedEvent();

View File

@ -1189,7 +1189,7 @@ TEST(TestAudioTrackGraph, ErrorCallback)
processingTrack->ConnectDeviceInput(deviceId, listener,
PRINCIPAL_HANDLE_NONE);
EXPECT_EQ(processingTrack->DeviceId().value(), deviceId);
processingTrack->AddAudioOutput(reinterpret_cast<void*>(1));
processingTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
return graph->NotifyWhenDeviceStarted(processingTrack);
});
@ -1248,7 +1248,7 @@ TEST(TestAudioTrackGraph, AudioProcessingTrack)
processingTrack = AudioProcessingTrack::Create(graph);
outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
outputTrack->QueueSetAutoend(false);
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
port = outputTrack->AllocateInputPort(processingTrack);
/* Primary graph: Open Audio Input through SourceMediaTrack */
listener = new AudioInputProcessing(2);
@ -1339,7 +1339,7 @@ TEST(TestAudioTrackGraph, ReConnectDeviceInput)
processingTrack = AudioProcessingTrack::Create(graph);
outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
outputTrack->QueueSetAutoend(false);
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
port = outputTrack->AllocateInputPort(processingTrack);
listener = new AudioInputProcessing(2);
processingTrack->SetInputProcessing(listener);
@ -1495,7 +1495,7 @@ TEST(TestAudioTrackGraph, AudioProcessingTrackDisabling)
processingTrack = AudioProcessingTrack::Create(graph);
outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
outputTrack->QueueSetAutoend(false);
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
port = outputTrack->AllocateInputPort(processingTrack);
/* Primary graph: Open Audio Input through SourceMediaTrack */
listener = new AudioInputProcessing(2);
@ -2410,7 +2410,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
/*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1),
GetMainThreadSerialEventTarget());
const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
const CubebUtils::AudioDeviceID inputDeviceId = (CubebUtils::AudioDeviceID)1;
RefPtr<AudioProcessingTrack> processingTrack;
RefPtr<AudioInputProcessing> listener;
@ -2424,7 +2424,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
processingTrack->SetInputProcessing(listener);
processingTrack->GraphImpl()->AppendMessage(
MakeUnique<StartInputProcessing>(processingTrack, listener));
processingTrack->ConnectDeviceInput(deviceId, listener,
processingTrack->ConnectDeviceInput(inputDeviceId, listener,
PRINCIPAL_HANDLE_NONE);
primaryFallbackListener = new OnFallbackListener(processingTrack);
processingTrack->AddListener(primaryFallbackListener);
@ -2455,7 +2455,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
/* How the input track connects to another ProcessedMediaTrack.
* Check in MediaManager how it is connected to AudioStreamTrack. */
port = transmitter->AllocateInputPort(processingTrack);
receiver->AddAudioOutput((void*)1);
receiver->AddAudioOutput((void*)1, partner->PrimaryOutputDeviceID(), 0);
partnerFallbackListener = new OnFallbackListener(receiver);
receiver->AddListener(partnerFallbackListener);
@ -2622,6 +2622,112 @@ TEST(TestAudioTrackGraph, CrossGraphPortUnderrun)
TestCrossGraphPort(52110, 17781, 1.01, 30, 1);
TestCrossGraphPort(52110, 17781, 1.03, 40, 3);
}
TEST(TestAudioTrackGraph, SecondaryOutputDevice)
{
MockCubeb* cubeb = new MockCubeb();
CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
const TrackRate primaryRate = 48000;
const TrackRate secondaryRate = 44100; // for secondary output device
MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
MediaTrackGraph::SYSTEM_THREAD_DRIVER,
/*Window ID*/ 1, primaryRate, nullptr, GetMainThreadSerialEventTarget());
RefPtr<AudioProcessingTrack> processingTrack;
RefPtr<AudioInputProcessing> listener;
DispatchFunction([&] {
/* Create an input track and connect it to a device */
processingTrack = AudioProcessingTrack::Create(graph);
listener = new AudioInputProcessing(2);
processingTrack->GraphImpl()->AppendMessage(
MakeUnique<SetPassThrough>(processingTrack, listener, true));
processingTrack->SetInputProcessing(listener);
processingTrack->GraphImpl()->AppendMessage(
MakeUnique<StartInputProcessing>(processingTrack, listener));
processingTrack->ConnectDeviceInput(nullptr, listener,
PRINCIPAL_HANDLE_NONE);
});
RefPtr<SmartMockCubebStream> primaryStream =
WaitFor(cubeb->StreamInitEvent());
const void* secondaryDeviceID = CubebUtils::AudioDeviceID(2);
DispatchFunction([&] {
processingTrack->AddAudioOutput(nullptr, secondaryDeviceID, secondaryRate);
processingTrack->SetAudioOutputVolume(nullptr, 0.f);
});
RefPtr<SmartMockCubebStream> secondaryStream =
WaitFor(cubeb->StreamInitEvent());
EXPECT_EQ(secondaryStream->GetOutputDeviceID(), secondaryDeviceID);
EXPECT_EQ(static_cast<TrackRate>(secondaryStream->SampleRate()),
secondaryRate);
nsIThread* currentThread = NS_GetCurrentThread();
uint32_t audioFrames = 0; // excludes pre-silence
MediaEventListener audioListener =
secondaryStream->FramesVerifiedEvent().Connect(
currentThread, [&](uint32_t aFrames) { audioFrames += aFrames; });
// Wait for 100ms of pre-silence to verify that SetAudioOutputVolume() is
// effective.
uint32_t processedFrames = 0;
WaitUntil(secondaryStream->FramesProcessedEvent(), [&](uint32_t aFrames) {
processedFrames += aFrames;
return processedFrames > static_cast<uint32_t>(secondaryRate / 10);
});
EXPECT_EQ(audioFrames, 0U) << "audio frames at zero volume";
secondaryStream->SetOutputRecordingEnabled(true);
DispatchFunction(
[&] { processingTrack->SetAudioOutputVolume(nullptr, 1.f); });
// Wait for enough audio after initial silence to check the frequency.
SpinEventLoopUntil("200ms of audio"_ns, [&] {
return audioFrames > static_cast<uint32_t>(secondaryRate / 5);
});
audioListener.Disconnect();
// Stop recording now so as not to record the discontinuity when the
// CrossGraphReceiver is removed from the secondary graph before its
// AudioCallbackDriver is stopped.
secondaryStream->SetOutputRecordingEnabled(false);
DispatchFunction([&] { processingTrack->RemoveAudioOutput(nullptr); });
WaitFor(secondaryStream->OutputVerificationEvent());
// The frequency from OutputVerificationEvent() is estimated by
// AudioVerifier from a zero-crossing count. When the discontinuity from
// the volume change is resampled, the discontinuity presents as
// oscillations, which increase the zero-crossing count and corrupt the
// frequency estimate. Trim off sufficient leading from the output to
// remove this discontinuity.
uint32_t channelCount = secondaryStream->OutputChannels();
nsTArray<AudioDataValue> output = secondaryStream->TakeRecordedOutput();
size_t leadingIndex = 0;
for (; leadingIndex < output.Length() && output[leadingIndex] == 0.f;
leadingIndex += channelCount) {
};
leadingIndex += 10 * channelCount; // skip discontinuity oscillations
EXPECT_LT(leadingIndex, output.Length());
auto trimmed = Span(output).From(std::min(leadingIndex, output.Length()));
size_t frameCount = trimmed.Length() / channelCount;
uint32_t inputFrequency = primaryStream->InputFrequency();
AudioVerifier<AudioDataValue> verifier(secondaryRate, inputFrequency);
verifier.AppendDataInterleaved(trimmed.Elements(), frameCount, channelCount);
EXPECT_EQ(verifier.EstimatedFreq(), inputFrequency);
// AudioVerifier considers the previous value before the initial sample to
// be zero and so considers any initial sample >> 0 to be a discontinuity.
EXPECT_EQ(verifier.CountDiscontinuities(), 1U);
DispatchFunction([&] {
// Clean up
processingTrack->GraphImpl()->AppendMessage(
MakeUnique<StopInputProcessing>(processingTrack, listener));
processingTrack->DisconnectDeviceInput();
processingTrack->Destroy();
});
WaitFor(primaryStream->OutputVerificationEvent());
}
#endif // MOZ_WEBRTC
#undef Invoke

View File

@ -315,7 +315,7 @@ AudioDestinationNode::AudioDestinationNode(AudioContext* aContext,
mTrack = AudioNodeTrack::Create(aContext, engine, kTrackFlags, graph);
mTrack->AddMainThreadListener(this);
// null key is fine: only one output per mTrack
mTrack->AddAudioOutput(nullptr);
mTrack->AddAudioOutput(nullptr, nullptr);
}
void AudioDestinationNode::Init() {