/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AudioChannelService.h" #include "AudioChannelServiceChild.h" #include "base/basictypes.h" #include "mozilla/Services.h" #include "mozilla/StaticPtr.h" #include "mozilla/unused.h" #include "mozilla/Util.h" #include "mozilla/dom/ContentParent.h" #include "nsThreadUtils.h" #include "nsHashPropertyBag.h" #include "nsComponentManagerUtils.h" #include "nsServiceManagerUtils.h" #ifdef MOZ_WIDGET_GONK #include "nsJSUtils.h" #include "nsCxPusher.h" #include "nsIAudioManager.h" #define NS_AUDIOMANAGER_CONTRACTID "@mozilla.org/telephony/audiomanager;1" #endif using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::hal; StaticRefPtr gAudioChannelService; // static AudioChannelService* AudioChannelService::GetAudioChannelService() { MOZ_ASSERT(NS_IsMainThread()); if (XRE_GetProcessType() != GeckoProcessType_Default) { return AudioChannelServiceChild::GetAudioChannelService(); } // If we already exist, exit early if (gAudioChannelService) { return gAudioChannelService; } // Create new instance, register, return nsRefPtr service = new AudioChannelService(); NS_ENSURE_TRUE(service, nullptr); gAudioChannelService = service; return gAudioChannelService; } void AudioChannelService::Shutdown() { if (XRE_GetProcessType() != GeckoProcessType_Default) { return AudioChannelServiceChild::Shutdown(); } if (gAudioChannelService) { gAudioChannelService = nullptr; } } NS_IMPL_ISUPPORTS2(AudioChannelService, nsIObserver, nsITimerCallback) AudioChannelService::AudioChannelService() : mCurrentHigherChannel(AUDIO_CHANNEL_LAST) , mCurrentVisibleHigherChannel(AUDIO_CHANNEL_LAST) , mActiveContentChildIDsFrozen(false) , mDefChannelChildID(CONTENT_PROCESS_ID_UNKNOWN) { if (XRE_GetProcessType() == GeckoProcessType_Default) { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, "ipc:content-shutdown", false); #ifdef MOZ_WIDGET_GONK // To monitor the volume settings based on audio channel. obs->AddObserver(this, "mozsettings-changed", false); #endif } } } AudioChannelService::~AudioChannelService() { } void AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent, AudioChannelType aType, bool aWithVideo) { MOZ_ASSERT(aType != AUDIO_CHANNEL_DEFAULT); AudioChannelAgentData* data = new AudioChannelAgentData(aType, true /* aElementHidden */, AUDIO_CHANNEL_STATE_MUTED /* aState */, aWithVideo); mAgents.Put(aAgent, data); RegisterType(aType, CONTENT_PROCESS_ID_MAIN, aWithVideo); } void AudioChannelService::RegisterType(AudioChannelType aType, uint64_t aChildID, bool aWithVideo) { AudioChannelInternalType type = GetInternalType(aType, true); mChannelCounters[type].AppendElement(aChildID); if (XRE_GetProcessType() == GeckoProcessType_Default) { // Since there is another telephony registered, we can unregister old one // immediately. if (mDeferTelChannelTimer && aType == AUDIO_CHANNEL_TELEPHONY) { mDeferTelChannelTimer->Cancel(); mDeferTelChannelTimer = nullptr; UnregisterTypeInternal(aType, mTimerElementHidden, mTimerChildID, false); } if (aWithVideo) { mWithVideoChildIDs.AppendElement(aChildID); } // In order to avoid race conditions, it's safer to notify any existing // agent any time a new one is registered. SendAudioChannelChangedNotification(aChildID); Notify(); } } void AudioChannelService::UnregisterAudioChannelAgent(AudioChannelAgent* aAgent) { nsAutoPtr data; mAgents.RemoveAndForget(aAgent, data); if (data) { UnregisterType(data->mType, data->mElementHidden, CONTENT_PROCESS_ID_MAIN, data->mWithVideo); } } void AudioChannelService::UnregisterType(AudioChannelType aType, bool aElementHidden, uint64_t aChildID, bool aWithVideo) { // There are two reasons to defer the decrease of telephony channel. // 1. User can have time to remove device from his ear before music resuming. // 2. Give BT SCO to be disconnected before starting to connect A2DP. if (XRE_GetProcessType() == GeckoProcessType_Default && aType == AUDIO_CHANNEL_TELEPHONY && (mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY_HIDDEN].Length() + mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY].Length()) == 1) { mTimerElementHidden = aElementHidden; mTimerChildID = aChildID; mDeferTelChannelTimer = do_CreateInstance("@mozilla.org/timer;1"); mDeferTelChannelTimer->InitWithCallback(this, 1500, nsITimer::TYPE_ONE_SHOT); return; } UnregisterTypeInternal(aType, aElementHidden, aChildID, aWithVideo); } void AudioChannelService::UnregisterTypeInternal(AudioChannelType aType, bool aElementHidden, uint64_t aChildID, bool aWithVideo) { // The array may contain multiple occurrence of this appId but // this should remove only the first one. AudioChannelInternalType type = GetInternalType(aType, aElementHidden); MOZ_ASSERT(mChannelCounters[type].Contains(aChildID)); mChannelCounters[type].RemoveElement(aChildID); // In order to avoid race conditions, it's safer to notify any existing // agent any time a new one is registered. if (XRE_GetProcessType() == GeckoProcessType_Default) { // We only remove ChildID when it is in the foreground. // If in the background, we kept ChildID for allowing it to play next song. if (aType == AUDIO_CHANNEL_CONTENT && mActiveContentChildIDs.Contains(aChildID) && !aElementHidden && !mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].Contains(aChildID)) { mActiveContentChildIDs.RemoveElement(aChildID); } if (aWithVideo) { MOZ_ASSERT(mWithVideoChildIDs.Contains(aChildID)); mWithVideoChildIDs.RemoveElement(aChildID); } SendAudioChannelChangedNotification(aChildID); Notify(); } } void AudioChannelService::UpdateChannelType(AudioChannelType aType, uint64_t aChildID, bool aElementHidden, bool aElementWasHidden) { // Calculate the new and old internal type and update the hashtable if needed. AudioChannelInternalType newType = GetInternalType(aType, aElementHidden); AudioChannelInternalType oldType = GetInternalType(aType, aElementWasHidden); if (newType != oldType) { mChannelCounters[newType].AppendElement(aChildID); MOZ_ASSERT(mChannelCounters[oldType].Contains(aChildID)); mChannelCounters[oldType].RemoveElement(aChildID); } } AudioChannelState AudioChannelService::GetState(AudioChannelAgent* aAgent, bool aElementHidden) { AudioChannelAgentData* data; if (!mAgents.Get(aAgent, &data)) { return AUDIO_CHANNEL_STATE_MUTED; } bool oldElementHidden = data->mElementHidden; // Update visibility. data->mElementHidden = aElementHidden; data->mState = GetStateInternal(data->mType, CONTENT_PROCESS_ID_MAIN, aElementHidden, oldElementHidden); return data->mState; } AudioChannelState AudioChannelService::GetStateInternal(AudioChannelType aType, uint64_t aChildID, bool aElementHidden, bool aElementWasHidden) { UpdateChannelType(aType, aChildID, aElementHidden, aElementWasHidden); // Calculating the new and old type and update the hashtable if needed. AudioChannelInternalType newType = GetInternalType(aType, aElementHidden); AudioChannelInternalType oldType = GetInternalType(aType, aElementWasHidden); // If the audio content channel is visible, let's remember this ChildID. if (newType == AUDIO_CHANNEL_INT_CONTENT && oldType == AUDIO_CHANNEL_INT_CONTENT_HIDDEN) { if (mActiveContentChildIDsFrozen) { mActiveContentChildIDsFrozen = false; mActiveContentChildIDs.Clear(); } if (!mActiveContentChildIDs.Contains(aChildID)) { mActiveContentChildIDs.AppendElement(aChildID); } } else if (newType == AUDIO_CHANNEL_INT_CONTENT_HIDDEN && oldType == AUDIO_CHANNEL_INT_CONTENT && !mActiveContentChildIDsFrozen) { // If nothing is visible, the list has to been frozen. // Or if there is still any one with other ChildID in foreground then // it should be removed from list and left other ChildIDs in the foreground // to keep playing. Finally only last one childID which go to background // will be in list. if (mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty()) { mActiveContentChildIDsFrozen = true; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].Contains(aChildID)) { MOZ_ASSERT(mActiveContentChildIDs.Contains(aChildID)); mActiveContentChildIDs.RemoveElement(aChildID); } } else if (newType == AUDIO_CHANNEL_INT_NORMAL && oldType == AUDIO_CHANNEL_INT_NORMAL_HIDDEN && mWithVideoChildIDs.Contains(aChildID)) { if (mActiveContentChildIDsFrozen) { mActiveContentChildIDsFrozen = false; mActiveContentChildIDs.Clear(); } } if (newType != oldType && (aType == AUDIO_CHANNEL_CONTENT || (aType == AUDIO_CHANNEL_NORMAL && mWithVideoChildIDs.Contains(aChildID)))) { Notify(); } SendAudioChannelChangedNotification(aChildID); // Let play any visible audio channel. if (!aElementHidden) { if (CheckVolumeFadedCondition(newType, aElementHidden)) { return AUDIO_CHANNEL_STATE_FADED; } return AUDIO_CHANNEL_STATE_NORMAL; } // We are not visible, maybe we have to mute. if (newType == AUDIO_CHANNEL_INT_NORMAL_HIDDEN || (newType == AUDIO_CHANNEL_INT_CONTENT_HIDDEN && !mActiveContentChildIDs.Contains(aChildID))) { return AUDIO_CHANNEL_STATE_MUTED; } // After checking the condition on normal & content channel, if the state // is not on muted then checking other higher channels type here. if (ChannelsActiveWithHigherPriorityThan(newType)) { MOZ_ASSERT(newType != AUDIO_CHANNEL_INT_NORMAL_HIDDEN); if (CheckVolumeFadedCondition(newType, aElementHidden)) { return AUDIO_CHANNEL_STATE_FADED; } return AUDIO_CHANNEL_STATE_MUTED; } return AUDIO_CHANNEL_STATE_NORMAL; } bool AudioChannelService::CheckVolumeFadedCondition(AudioChannelInternalType aType, bool aElementHidden) { // Only normal & content channels are considered if (aType > AUDIO_CHANNEL_INT_CONTENT_HIDDEN) { return false; } // Consider that audio from notification is with short duration // so just fade the volume not pause it if (mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION].IsEmpty() && mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN].IsEmpty()) { return false; } // Since this element is on the foreground, it can be allowed to play always. // So return true directly when there is any notification channel alive. if (aElementHidden == false) { return true; } // If element is on the background, it is possible paused by channels higher // then notification. for (int i = AUDIO_CHANNEL_INT_LAST - 1; i != AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN; --i) { if (!mChannelCounters[i].IsEmpty()) { return false; } } return true; } bool AudioChannelService::ContentOrNormalChannelIsActive() { return !mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty() || !mChannelCounters[AUDIO_CHANNEL_INT_CONTENT_HIDDEN].IsEmpty() || !mChannelCounters[AUDIO_CHANNEL_INT_NORMAL].IsEmpty(); } bool AudioChannelService::ProcessContentOrNormalChannelIsActive(uint64_t aChildID) { return mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].Contains(aChildID) || mChannelCounters[AUDIO_CHANNEL_INT_CONTENT_HIDDEN].Contains(aChildID) || mChannelCounters[AUDIO_CHANNEL_INT_NORMAL].Contains(aChildID); } void AudioChannelService::SetDefaultVolumeControlChannel(AudioChannelType aType, bool aHidden) { SetDefaultVolumeControlChannelInternal(aType, aHidden, CONTENT_PROCESS_ID_MAIN); } void AudioChannelService::SetDefaultVolumeControlChannelInternal( AudioChannelType aType, bool aHidden, uint64_t aChildID) { if (XRE_GetProcessType() != GeckoProcessType_Default) { return; } // If this child is in the background and mDefChannelChildID is set to // others then it means other child in the foreground already set it's // own default channel already. if (!aHidden && mDefChannelChildID != aChildID) { return; } mDefChannelChildID = aChildID; nsString channelName; channelName.AssignASCII(ChannelName(aType)); nsCOMPtr obs = mozilla::services::GetObserverService(); obs->NotifyObservers(nullptr, "default-volume-channel-changed", channelName.get()); } void AudioChannelService::SendAudioChannelChangedNotification(uint64_t aChildID) { if (XRE_GetProcessType() != GeckoProcessType_Default) { return; } nsRefPtr props = new nsHashPropertyBag(); props->SetPropertyAsUint64(NS_LITERAL_STRING("childID"), aChildID); nsCOMPtr obs = mozilla::services::GetObserverService(); obs->NotifyObservers(static_cast(props), "audio-channel-process-changed", nullptr); // Calculating the most important active channel. AudioChannelType higher = AUDIO_CHANNEL_LAST; // Top-Down in the hierarchy for visible elements if (!mChannelCounters[AUDIO_CHANNEL_INT_PUBLICNOTIFICATION].IsEmpty()) { higher = AUDIO_CHANNEL_PUBLICNOTIFICATION; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_RINGER].IsEmpty()) { higher = AUDIO_CHANNEL_RINGER; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY].IsEmpty()) { higher = AUDIO_CHANNEL_TELEPHONY; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_ALARM].IsEmpty()) { higher = AUDIO_CHANNEL_ALARM; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION].IsEmpty()) { higher = AUDIO_CHANNEL_NOTIFICATION; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_CONTENT].IsEmpty()) { higher = AUDIO_CHANNEL_CONTENT; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_NORMAL].IsEmpty()) { higher = AUDIO_CHANNEL_NORMAL; } AudioChannelType visibleHigher = higher; // Top-Down in the hierarchy for non-visible elements if (higher == AUDIO_CHANNEL_LAST) { if (!mChannelCounters[AUDIO_CHANNEL_INT_PUBLICNOTIFICATION_HIDDEN].IsEmpty()) { higher = AUDIO_CHANNEL_PUBLICNOTIFICATION; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_RINGER_HIDDEN].IsEmpty()) { higher = AUDIO_CHANNEL_RINGER; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_TELEPHONY_HIDDEN].IsEmpty()) { higher = AUDIO_CHANNEL_TELEPHONY; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_ALARM_HIDDEN].IsEmpty()) { higher = AUDIO_CHANNEL_ALARM; } else if (!mChannelCounters[AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN].IsEmpty()) { higher = AUDIO_CHANNEL_NOTIFICATION; } // There is only one Child can play content channel in the background. // And need to check whether there is any content channels under playing // now. else if (!mActiveContentChildIDs.IsEmpty() && mChannelCounters[AUDIO_CHANNEL_INT_CONTENT_HIDDEN].Contains( mActiveContentChildIDs[0])) { higher = AUDIO_CHANNEL_CONTENT; } } if (higher != mCurrentHigherChannel) { mCurrentHigherChannel = higher; nsString channelName; if (mCurrentHigherChannel != AUDIO_CHANNEL_LAST) { channelName.AssignASCII(ChannelName(mCurrentHigherChannel)); } else { channelName.AssignLiteral("none"); } obs->NotifyObservers(nullptr, "audio-channel-changed", channelName.get()); } if (visibleHigher != mCurrentVisibleHigherChannel) { mCurrentVisibleHigherChannel = visibleHigher; nsString channelName; if (mCurrentVisibleHigherChannel != AUDIO_CHANNEL_LAST) { channelName.AssignASCII(ChannelName(mCurrentVisibleHigherChannel)); } else { channelName.AssignLiteral("none"); } obs->NotifyObservers(nullptr, "visible-audio-channel-changed", channelName.get()); } } PLDHashOperator AudioChannelService::NotifyEnumerator(AudioChannelAgent* aAgent, AudioChannelAgentData* aData, void* aUnused) { MOZ_ASSERT(aAgent); aAgent->NotifyAudioChannelStateChanged(); return PL_DHASH_NEXT; } void AudioChannelService::Notify() { MOZ_ASSERT(NS_IsMainThread()); // Notify any agent for the main process. mAgents.EnumerateRead(NotifyEnumerator, nullptr); // Notify for the child processes. nsTArray children; ContentParent::GetAll(children); for (uint32_t i = 0; i < children.Length(); i++) { unused << children[i]->SendAudioChannelNotify(); } } NS_IMETHODIMP AudioChannelService::Notify(nsITimer* aTimer) { UnregisterTypeInternal(AUDIO_CHANNEL_TELEPHONY, mTimerElementHidden, mTimerChildID, false); mDeferTelChannelTimer = nullptr; return NS_OK; } bool AudioChannelService::ChannelsActiveWithHigherPriorityThan( AudioChannelInternalType aType) { for (int i = AUDIO_CHANNEL_INT_LAST - 1; i != AUDIO_CHANNEL_INT_CONTENT_HIDDEN; --i) { if (i == aType) { return false; } if (!mChannelCounters[i].IsEmpty()) { return true; } } return false; } const char* AudioChannelService::ChannelName(AudioChannelType aType) { static struct { int32_t type; const char* value; } ChannelNameTable[] = { { AUDIO_CHANNEL_NORMAL, "normal" }, { AUDIO_CHANNEL_CONTENT, "content" }, { AUDIO_CHANNEL_NOTIFICATION, "notification" }, { AUDIO_CHANNEL_ALARM, "alarm" }, { AUDIO_CHANNEL_TELEPHONY, "telephony" }, { AUDIO_CHANNEL_RINGER, "ringer" }, { AUDIO_CHANNEL_PUBLICNOTIFICATION, "publicnotification" }, { -1, "unknown" } }; for (int i = AUDIO_CHANNEL_NORMAL; ; ++i) { if (ChannelNameTable[i].type == aType || ChannelNameTable[i].type == -1) { return ChannelNameTable[i].value; } } NS_NOTREACHED("Execution should not reach here!"); return nullptr; } NS_IMETHODIMP AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic, const PRUnichar* aData) { if (!strcmp(aTopic, "ipc:content-shutdown")) { nsCOMPtr props = do_QueryInterface(aSubject); if (!props) { NS_WARNING("ipc:content-shutdown message without property bag as subject"); return NS_OK; } int32_t index; uint64_t childID = 0; nsresult rv = props->GetPropertyAsUint64(NS_LITERAL_STRING("childID"), &childID); if (NS_SUCCEEDED(rv)) { for (int32_t type = AUDIO_CHANNEL_INT_NORMAL; type < AUDIO_CHANNEL_INT_LAST; ++type) { while ((index = mChannelCounters[type].IndexOf(childID)) != -1) { mChannelCounters[type].RemoveElementAt(index); } } while ((index = mActiveContentChildIDs.IndexOf(childID)) != -1) { mActiveContentChildIDs.RemoveElementAt(index); } while ((index = mWithVideoChildIDs.IndexOf(childID)) != -1) { mWithVideoChildIDs.RemoveElementAt(index); } // We don't have to remove the agents from the mAgents hashtable because if // that table contains only agents running on the same process. SendAudioChannelChangedNotification(childID); Notify(); if (mDefChannelChildID == childID) { SetDefaultVolumeControlChannelInternal(AUDIO_CHANNEL_DEFAULT, false, childID); mDefChannelChildID = CONTENT_PROCESS_ID_UNKNOWN; } } else { NS_WARNING("ipc:content-shutdown message without childID property"); } } #ifdef MOZ_WIDGET_GONK // To process the volume control on each audio channel according to // change of settings else if (!strcmp(aTopic, "mozsettings-changed")) { AutoSafeJSContext cx; nsDependentString dataStr(aData); JS::Rooted val(cx); if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || !val.isObject()) { return NS_OK; } JS::Rooted obj(cx, &val.toObject()); JS::Rooted key(cx); if (!JS_GetProperty(cx, obj, "key", &key) || !key.isString()) { return NS_OK; } JS::RootedString jsKey(cx, JS_ValueToString(cx, key)); if (!jsKey) { return NS_OK; } nsDependentJSString keyStr; if (!keyStr.init(cx, jsKey) || keyStr.Find("audio.volume.", 0, false)) { return NS_OK; } JS::Rooted value(cx); if (!JS_GetProperty(cx, obj, "value", &value) || !value.isInt32()) { return NS_OK; } nsCOMPtr audioManager = do_GetService(NS_AUDIOMANAGER_CONTRACTID); NS_ENSURE_TRUE(audioManager, NS_OK); int32_t index = value.toInt32(); if (keyStr.EqualsLiteral("audio.volume.content")) { audioManager->SetAudioChannelVolume(AUDIO_CHANNEL_CONTENT, index); } else if (keyStr.EqualsLiteral("audio.volume.notification")) { audioManager->SetAudioChannelVolume(AUDIO_CHANNEL_NOTIFICATION, index); } else if (keyStr.EqualsLiteral("audio.volume.alarm")) { audioManager->SetAudioChannelVolume(AUDIO_CHANNEL_ALARM, index); } else if (keyStr.EqualsLiteral("audio.volume.telephony")) { audioManager->SetAudioChannelVolume(AUDIO_CHANNEL_TELEPHONY, index); } else { MOZ_ASSERT("unexpected audio channel for volume control"); } } #endif return NS_OK; } AudioChannelService::AudioChannelInternalType AudioChannelService::GetInternalType(AudioChannelType aType, bool aElementHidden) { switch (aType) { case AUDIO_CHANNEL_NORMAL: return aElementHidden ? AUDIO_CHANNEL_INT_NORMAL_HIDDEN : AUDIO_CHANNEL_INT_NORMAL; case AUDIO_CHANNEL_CONTENT: return aElementHidden ? AUDIO_CHANNEL_INT_CONTENT_HIDDEN : AUDIO_CHANNEL_INT_CONTENT; case AUDIO_CHANNEL_NOTIFICATION: return aElementHidden ? AUDIO_CHANNEL_INT_NOTIFICATION_HIDDEN : AUDIO_CHANNEL_INT_NOTIFICATION; case AUDIO_CHANNEL_ALARM: return aElementHidden ? AUDIO_CHANNEL_INT_ALARM_HIDDEN : AUDIO_CHANNEL_INT_ALARM; case AUDIO_CHANNEL_TELEPHONY: return aElementHidden ? AUDIO_CHANNEL_INT_TELEPHONY_HIDDEN : AUDIO_CHANNEL_INT_TELEPHONY; case AUDIO_CHANNEL_RINGER: return aElementHidden ? AUDIO_CHANNEL_INT_RINGER_HIDDEN : AUDIO_CHANNEL_INT_RINGER; case AUDIO_CHANNEL_PUBLICNOTIFICATION: return aElementHidden ? AUDIO_CHANNEL_INT_PUBLICNOTIFICATION_HIDDEN : AUDIO_CHANNEL_INT_PUBLICNOTIFICATION; case AUDIO_CHANNEL_LAST: default: break; } MOZ_CRASH("unexpected audio channel type"); }