Fix bug 100472 -- play sounds asynchronously on Mac, cache sounds using necko, and deal with lots of different sound formats. r=pinkerton, sr=sspitzer.

This commit is contained in:
sfraser%netscape.com 2001-10-11 00:34:34 +00:00
Родитель de3f0803a8
Коммит 36c079c27b
2 изменённых файлов: 686 добавлений и 87 удалений

Просмотреть файл

@ -20,6 +20,7 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Simon Fraser <sfraser@netscape.com>
* Pierre Phaneuf <pp@ludusdesign.com>
*
* Alternatively, the contents of this file may be used under the terms of
@ -39,149 +40,741 @@
#include "nscore.h"
#include "nsIAllocator.h"
#include "plstr.h"
#include "nsVoidArray.h"
#include "nsSound.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "prmem.h"
#include "nsGfxUtils.h"
#include "nsIStreamLoader.h"
#include "nsICacheService.h"
#include "nsICacheSession.h"
#include "nsICacheEntryDescriptor.h"
#include "nsICachingChannel.h"
#include "nsITimer.h"
#include "nsITimerCallback.h"
#include <Gestalt.h>
#include <Sound.h>
#include <Movies.h>
#include <QuickTimeComponents.h>
NS_IMPL_ISUPPORTS2(nsSound, nsISound, nsIStreamLoaderObserver);
#include "nsSound.h"
//#define SOUND_DEBUG
#pragma mark nsSoundRequest
class nsSoundRequest : public nsIStreamLoaderObserver,
public nsITimerCallback
{
public:
nsSoundRequest();
virtual ~nsSoundRequest();
NS_DECL_ISUPPORTS
NS_DECL_NSISTREAMLOADEROBSERVER
nsresult Init(nsISound* aSound, nsIURL *aURL);
nsresult PlaySound();
// nsITimerCallback
NS_IMETHOD_(void) Notify(nsITimer *timer);
static nsSoundRequest* GetFromISupports(nsISupports* inSupports);
protected:
OSType GetFileFormat(const char* inData, long inDataSize, const nsACString& contentType);
OSErr ImportMovie(Handle inDataHandle, long inDataSize, const nsACString& contentType);
PRBool HaveQuickTime();
nsresult Cleanup();
void DisposeMovieData();
PRBool IsAnyMoviePlaying();
OSErr TaskActiveMovies(PRBool *outAllMoviesDone);
static PRBool TaskOneMovie(Movie inMovie); // return true if done
protected:
nsCOMPtr<nsISound> mSound; // back ptr, owned and released when play done
nsCOMPtr<nsITimer> mTimer;
Movie mMovie; // the original movie, kept around as long as this request is cached
Handle mDataHandle; // data handle, has to persist for the lifetime of any movies
// depending on it
nsVoidArray mMovies; // list of playing movie clones, which are transient.
};
#pragma mark -
static PRUint32
SecondsFromPRTime(PRTime prTime)
{
PRInt64 microSecondsPerSecond, intermediateResult;
PRUint32 seconds;
LL_I2L(microSecondsPerSecond, PR_USEC_PER_SEC);
LL_DIV(intermediateResult, prTime, microSecondsPerSecond);
LL_L2UI(seconds, intermediateResult);
return seconds;
}
#pragma mark -
////////////////////////////////////////////////////////////////////////
nsSound::nsSound()
{
NS_INIT_REFCNT();
#ifdef SOUND_DEBUG
printf("%%%%%%%% Made nsSound\n");
#endif
}
nsSound::~nsSound()
{
#ifdef SOUND_DEBUG
printf("%%%%%%%% Deleted nsSound\n");
#endif
}
NS_METHOD nsSound::Beep()
NS_IMPL_ISUPPORTS1(nsSound, nsISound);
NS_METHOD
nsSound::Beep()
{
::SysBeep(1);
return NS_OK;
}
NS_IMETHODIMP
nsSound::PlaySystemSound(const char *aSoundAlias)
{
return Beep();
}
// this currently does no caching of the sound buffer. It should
NS_METHOD
nsSound::Play(nsIURL *aURL)
{
NS_ENSURE_ARG(aURL);
nsresult rv;
// try to get from cache
nsCOMPtr<nsISupports> requestSupports;
(void)GetSoundFromCache(aURL, getter_AddRefs(requestSupports));
if (requestSupports)
{
nsSoundRequest* cachedRequest = nsSoundRequest::GetFromISupports(requestSupports);
// if it was cached, start playing right away
cachedRequest->PlaySound();
}
else
{
nsSoundRequest* soundRequest;
NS_NEWXPCOM(soundRequest, nsSoundRequest);
if (!soundRequest)
return NS_ERROR_OUT_OF_MEMORY;
requestSupports = NS_STATIC_CAST(nsIStreamLoaderObserver*, soundRequest);
nsresult rv = soundRequest->Init(this, aURL);
if (NS_FAILED(rv))
return rv;
}
rv = AddRequest(requestSupports);
if (NS_FAILED(rv))
return rv;
return NS_OK;
}
#define kReadBufferSize (4 * 1024)
// this currently does no cacheing of the sound buffer. It should
NS_METHOD nsSound::Play(nsIURL *aURL)
nsresult
nsSound::AddRequest(nsISupports* aSoundRequest)
{
// only add if not already in the list
PRInt32 index = mSoundRequests.IndexOf(aSoundRequest);
if (index == -1)
{
nsresult appended = mSoundRequests.AppendElement(aSoundRequest);
if (!appended)
return NS_ERROR_FAILURE;
}
return NS_OK;
}
nsresult
nsSound::RemoveRequest(nsISupports* aSoundRequest)
{
nsresult removed = mSoundRequests.RemoveElement(aSoundRequest);
if (!removed)
return NS_ERROR_FAILURE;
return NS_OK;
}
nsresult
nsSound::GetCacheSession(nsICacheSession** outCacheSession)
{
nsresult rv;
nsCOMPtr<nsICacheService> cacheService = do_GetService("@mozilla.org/network/cache-service;1", &rv);
if (NS_FAILED(rv)) return rv;
return cacheService->CreateSession("sound",
nsICache::NOT_STREAM_BASED,
PR_FALSE, outCacheSession);
}
nsresult
nsSound::GetSoundFromCache(nsIURI* inURI, nsISupports** outSound)
{
nsresult rv;
nsXPIDLCString uriSpec;
inURI->GetSpec(getter_Copies(uriSpec));
nsCOMPtr<nsICacheSession> cacheSession;
rv = GetCacheSession(getter_AddRefs(cacheSession));
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsICacheEntryDescriptor> entry;
rv = cacheSession->OpenCacheEntry(uriSpec, nsICache::ACCESS_READ, nsICache::BLOCKING, getter_AddRefs(entry));
#ifdef SOUND_DEBUG
printf("Got sound from cache with rv %ld\n", rv);
#endif
if (NS_FAILED(rv)) return rv;
return entry->GetCacheElement(outSound);
}
nsresult
nsSound::PutSoundInCache(nsIChannel* inChannel, PRUint32 inDataSize, nsISupports* inSound)
{
nsresult rv;
NS_ENSURE_ARG(inChannel && inSound);
nsCOMPtr<nsIURI> uri;
inChannel->GetOriginalURI(getter_AddRefs(uri));
if (!uri) return NS_ERROR_FAILURE;
nsXPIDLCString uriSpec;
rv = uri->GetSpec(getter_Copies(uriSpec));
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsICacheSession> cacheSession;
rv = GetCacheSession(getter_AddRefs(cacheSession));
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsICacheEntryDescriptor> entry;
rv = cacheSession->OpenCacheEntry(uriSpec, nsICache::ACCESS_WRITE, nsICache::BLOCKING, getter_AddRefs(entry));
#ifdef SOUND_DEBUG
printf("Put sound in cache with rv %ld\n", rv);
#endif
if (NS_FAILED(rv)) return rv;
rv = entry->SetCacheElement(inSound);
if (NS_FAILED(rv)) return rv;
rv = entry->SetDataSize(inDataSize);
if (NS_FAILED(rv)) return rv;
PRUint32 expirationTime = 0;
// try to get the expiration time from the URI load
nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(inChannel);
if (cachingChannel)
{
nsCOMPtr<nsISupports> cacheToken;
cachingChannel->GetCacheToken(getter_AddRefs(cacheToken));
nsCOMPtr<nsICacheEntryInfo> cacheEntryInfo = do_QueryInterface(cacheToken);
if (cacheEntryInfo)
{
cacheEntryInfo->GetExpirationTime(&expirationTime);
}
}
if (expirationTime == 0)
{
// set it to some reasonable default, like now + 24 hours
expirationTime = SecondsFromPRTime(PR_Now()) + 60 * 60 * 24;
}
rv = entry->SetExpirationTime(expirationTime);
if (NS_FAILED(rv)) return rv;
return entry->MarkValid();
}
#pragma mark -
NS_IMPL_ISUPPORTS2(nsSoundRequest, nsIStreamLoaderObserver, nsITimerCallback);
////////////////////////////////////////////////////////////////////////
nsSoundRequest::nsSoundRequest()
: mMovie(nsnull)
, mDataHandle(nsnull)
{
NS_INIT_REFCNT();
#ifdef SOUND_DEBUG
printf("%%%%%%%% Made nsSoundRequest\n");
#endif
}
nsSoundRequest::~nsSoundRequest()
{
#ifdef SOUND_DEBUG
printf("%%%%%%%% Deleted nsSoundRequest\n");
#endif
DisposeMovieData();
}
nsresult
nsSoundRequest::Init(nsISound* aSound, nsIURL *aURL)
{
NS_ENSURE_ARG(aURL && aSound);
mSound = aSound;
// if quicktime is not installed, we can't do anything
if (!HaveQuickTime())
return NS_ERROR_NOT_IMPLEMENTED;
nsCOMPtr<nsIStreamLoader> loader;
nsresult rv = NS_NewStreamLoader(getter_AddRefs(loader), aURL, this);
return rv;
#ifdef SOUND_DEBUG
printf("%%%%%%%% Playing nsSound\n");
#endif
NS_ASSERTION(mMovie == nsnull, "nsSound being played twice");
nsCOMPtr<nsIStreamLoader> streamLoader;
return NS_NewStreamLoader(getter_AddRefs(streamLoader), aURL, NS_STATIC_CAST(nsIStreamLoaderObserver*, this));
}
NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader *aLoader,
NS_IMETHODIMP
nsSoundRequest::OnStreamComplete(nsIStreamLoader *aLoader,
nsISupports *context,
nsresult aStatus,
PRUint32 stringLen,
const char *stringData)
{
NS_ENSURE_ARG(aLoader);
if (NS_FAILED(aStatus))
return NS_ERROR_FAILURE;
// we should really verify that this is a .wav file
nsXPIDLCString contentType;
nsCOMPtr<nsIRequest> request;
aLoader->GetRequest(getter_AddRefs(request));
nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
if (channel)
channel->GetContentType(getter_Copies(contentType));
// we could use a Pointer data handler type, and avoid this
// allocation/copy, in QuickTime 5 and above.
OSErr err;
Handle dataHandle = ::TempNewHandle(stringLen, &err);
if (!dataHandle) return NS_ERROR_OUT_OF_MEMORY;
mDataHandle = ::TempNewHandle(stringLen, &err);
if (!mDataHandle) return NS_ERROR_OUT_OF_MEMORY;
BlockMoveData(stringData, *dataHandle, stringLen);
::BlockMoveData(stringData, *mDataHandle, stringLen);
nsresult rv = PlaySound(dataHandle, stringLen);
NS_ASSERTION(mMovie == nsnull, "nsSoundRequest has a movie already");
err = ImportMovie(mDataHandle, stringLen, contentType);
if (err != noErr) {
Cleanup();
return NS_ERROR_FAILURE;
}
::DisposeHandle(dataHandle);
return rv;
nsSound* macSound = NS_REINTERPRET_CAST(nsSound*, mSound.get());
NS_ASSERTION(macSound, "Should have nsSound here");
// put it in the cache. Not vital that this succeeds.
// for the data size we just use the string data, since the movie simply wraps this
// (we have to keep the handle around until the movies are done playing)
nsresult rv = macSound->PutSoundInCache(channel, stringLen, NS_STATIC_CAST(nsIStreamLoaderObserver*, this));
NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to put sound in cache");
return PlaySound();
}
nsresult nsSound::PlaySound(Handle waveDataHandle, long waveDataSize)
OSType
nsSoundRequest::GetFileFormat(const char* inData, long inDataSize, const nsACString& contentType)
{
OSType fileFormat = kQTFileTypeMovie; // Default to just treating it like a movie.
// Hopefully QuickTime will be able to import it.
if (inDataSize >= 16)
{
// look for WAVE
const char* dataPtr = inData;
if (*(OSType *)dataPtr == 'RIFF')
{
dataPtr += 4; // skip RIFF
dataPtr += 4; // skip length bytes
if (*(OSType *)dataPtr == 'WAVE')
return kQTFileTypeWave;
}
// look for AIFF
dataPtr = inData;
if (*(OSType *)dataPtr == 'FORM')
{
dataPtr += 4; // skip FORM
dataPtr += 4; // skip length bytes
if (*(OSType *)dataPtr == 'AIFF')
return kQTFileTypeAIFF;
if (*(OSType *)dataPtr == 'AIFC')
return kQTFileTypeAIFC;
}
}
if (inDataSize >= 4)
{
// look for midi
if (*(OSType *)inData == 'MThd')
return kQTFileTypeMIDI;
// look for µLaw/Next-Sun file format (.au)
if (*(OSType *)inData == '.snd')
return kQTFileTypeMuLaw;
}
// MP3 files have a complex format that is not easily sniffed. Just go by
// MIME type.
if (contentType.Equals("audio/mpeg") ||
contentType.Equals("audio/mp3") ||
contentType.Equals("audio/mpeg3") ||
contentType.Equals("audio/x-mpeg3") ||
contentType.Equals("audio/x-mp3") ||
contentType.Equals("audio/x-mpeg3"))
{
fileFormat = 'MP3 '; // not sure why there is no enum for this
}
return fileFormat;
}
nsresult
nsSoundRequest::PlaySound()
{
nsresult rv;
// we'll have a timer already if the sound is still playing from a previous
// request. In that case, we cloen the movie into a new one, so we can play it
// again from the start.
if (!mTimer)
{
mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); // release previous timer, if any
if (NS_FAILED(rv)) {
Cleanup();
return rv;
}
const PRInt32 kMovieTimerInterval = 250; // 250 milliseconds
rv = mTimer->Init(NS_STATIC_CAST(nsITimerCallback*, this), kMovieTimerInterval,
NS_PRIORITY_NORMAL, NS_TYPE_REPEATING_PRECISE);
if (NS_FAILED(rv)) {
Cleanup();
return rv;
}
}
Movie movieToPlay = mMovie;
if (!::IsMovieDone(mMovie)) // if the current movie is still playing, clone it
{
Movie newMovie = ::NewMovie(0);
if (!newMovie) return NS_ERROR_FAILURE;
// note that this copies refs, not all the data. So it should be fast
OSErr err = ::InsertMovieSegment(mMovie, newMovie, 0, ::GetMovieDuration(mMovie), 0);
if (err != noErr)
{
::DisposeMovie(newMovie);
return NS_ERROR_FAILURE;
}
// append it to the array
PRBool appended = mMovies.AppendElement((void *)newMovie);
if (!appended)
{
::DisposeMovie(newMovie);
return NS_ERROR_FAILURE;
}
movieToPlay = newMovie;
}
::SetMovieVolume(movieToPlay, kFullVolume);
::GoToBeginningOfMovie(movieToPlay);
::StartMovie(movieToPlay);
::MoviesTask(movieToPlay, 0);
#ifdef SOUND_DEBUG
printf("Starting movie playback\n");
#endif
return NS_OK;
}
NS_IMETHODIMP_(void)
nsSoundRequest::Notify(nsITimer *timer)
{
if (!mMovie)
{
NS_ASSERTION(0, "nsSoundRequest has no movie in timer callback");
return;
}
#ifdef SOUND_DEBUG
printf("In movie timer callback\n");
#endif
PRBool moviesDone;
TaskActiveMovies(&moviesDone);
// we're done for now. Remember that this nsSoundRequest might be in the cache,
// so won't necessarily go away.
if (moviesDone)
Cleanup();
}
OSErr
nsSoundRequest::ImportMovie(Handle inDataHandle, long inDataSize, const nsACString& contentType)
{
Handle dataRef = nil;
Movie movie = nil;
MovieImportComponent miComponent = nil;
Track targetTrack = nil;
TimeValue addedDuration = 0;
long outFlags = 0;
OSErr err = noErr;
ComponentResult compErr = noErr;
err = ::PtrToHand(&waveDataHandle, &dataRef, sizeof(Handle));
if (err != noErr) goto bail;
miComponent = ::OpenDefaultComponent(MovieImportType, kQTFileTypeWave);
if (!miComponent) {
err = paramErr;
goto bail;
}
movie = ::NewMovie(0);
if (!movie) {
err = paramErr;
goto bail;
}
compErr = ::MovieImportDataRef(miComponent,
dataRef,
HandleDataHandlerSubType,
movie,
nil,
&targetTrack,
nil,
&addedDuration,
movieImportCreateTrack,
&outFlags);
if (compErr != noErr) {
err = compErr;
goto bail;
}
::SetMovieVolume(movie, kFullVolume);
::GoToBeginningOfMovie(movie);
::StartMovie(movie);
while (! ::IsMovieDone(movie))
OSType fileFormat;
{
::MoviesTask(movie, 0);
err = ::GetMoviesError();
StHandleLocker locker(inDataHandle);
fileFormat = GetFileFormat(*inDataHandle, inDataSize, contentType);
}
bail: // gasp, a goto label
err = ::PtrToHand(&inDataHandle, &dataRef, sizeof(Handle));
if (err != noErr)
return err;
{
MovieImportComponent miComponent = ::OpenDefaultComponent(MovieImportType, fileFormat);
Track targetTrack = nil;
TimeValue addedDuration = 0;
long outFlags = 0;
ComponentResult compErr = noErr;
if (!miComponent) {
err = paramErr;
goto bail;
}
NS_ASSERTION(mMovie == nsnull, "nsSoundRequest already has movie");
mMovie = ::NewMovie(0);
if (!mMovie) {
err = ::GetMoviesError();
goto bail;
}
compErr = ::MovieImportDataRef(miComponent,
dataRef,
HandleDataHandlerSubType,
mMovie,
nil,
&targetTrack,
nil,
&addedDuration,
movieImportCreateTrack,
&outFlags);
if (compErr != noErr) {
::DisposeMovie(mMovie);
mMovie = nil;
err = compErr;
goto bail;
}
// ensure that the track never draws on screen, otherwise we might be
// suspecptible to spoofing attacks
Rect movieRect = {0};
::SetMovieBox(mMovie, &movieRect);
::GoToEndOfMovie(mMovie); // simplifies the logic in PlaySound()
bail:
if (miComponent)
::CloseComponent(miComponent);
}
if (dataRef)
::DisposeHandle(dataRef);
return err;
}
if (miComponent)
::CloseComponent(miComponent);
nsresult
nsSoundRequest::Cleanup()
{
nsresult rv = NS_OK;
#ifdef SOUND_DEBUG
printf("Movie playback done\n");
#endif
// kill the timer
if (mTimer) {
mTimer->Cancel();
mTimer = nsnull;
}
NS_ASSERTION(mMovies.Count() == 0, "Should not have any movies running here");
if (movie)
::DisposeMovie(movie);
// remove from parent array. This could be the last ref, so we might be deleted here.
if (mSound.get())
{
nsSound* macSound = NS_REINTERPRET_CAST(nsSound*, mSound.get());
rv = macSound->RemoveRequest(NS_STATIC_CAST(nsIStreamLoaderObserver*, this));
mSound = nsnull;
}
return rv;
}
return (err == noErr) ? NS_OK : NS_ERROR_FAILURE;
void
nsSoundRequest::DisposeMovieData()
{
for (PRInt32 i = 0; i < mMovies.Count(); i ++)
{
Movie thisMovie = (Movie)mMovies.ElementAt(i);
::DisposeMovie(mMovie);
}
mMovies.Clear();
if (mMovie) {
::DisposeMovie(mMovie);
mMovie = nsnull;
}
if (mDataHandle) {
::DisposeHandle(mDataHandle);
mDataHandle = nsnull;
}
}
PRBool nsSound::HaveQuickTime()
PRBool
nsSoundRequest::TaskOneMovie(Movie inMovie) // return true if done
{
PRBool movieDone = PR_FALSE;
ComponentResult status = ::GetMovieStatus(inMovie, nil);
NS_ASSERTION(status == noErr, "Movie bad");
if (status != noErr) {
::StopMovie(inMovie);
movieDone = PR_TRUE;
}
movieDone |= ::IsMovieDone(inMovie);
if (!movieDone)
::MoviesTask(inMovie, 0);
return movieDone;
}
nsSoundRequest*
nsSoundRequest::GetFromISupports(nsISupports* inSupports)
{
if (!inSupports) return nsnull;
// test to see if this is really a nsSoundRequest by trying a QI to both
// interfaces we support
nsCOMPtr<nsIStreamLoaderObserver> loaderObserver = do_QueryInterface(inSupports);
nsCOMPtr<nsITimerCallback> timerCallback = do_QueryInterface(inSupports);
if (!loaderObserver || !timerCallback) return nsnull;
return NS_REINTERPRET_CAST(nsSoundRequest*, inSupports);
}
OSErr
nsSoundRequest::TaskActiveMovies(PRBool *outAllMoviesDone)
{
PRBool allMoviesDone = PR_FALSE;
allMoviesDone = TaskOneMovie(mMovie);
PRInt32 initMovieCount = mMovies.Count();
PRInt32 curIndex = 0;
for (PRInt32 i = 0; i < initMovieCount; i ++)
{
Movie thisMovie = (Movie)mMovies.ElementAt(curIndex);
PRBool thisMovieDone = TaskOneMovie(thisMovie);
if (thisMovieDone) // remove finished movies from the array
{
mMovies.RemoveElementAt(curIndex);
::DisposeMovie(thisMovie);
// curIndex doesn't change
}
else
{
curIndex ++;
}
allMoviesDone &= thisMovieDone;
}
*outAllMoviesDone = allMoviesDone;
return noErr;
}
PRBool
nsSoundRequest::IsAnyMoviePlaying()
{
if (!::IsMovieDone(mMovie))
return PR_TRUE;
for (PRInt32 i = 0; i < mMovies.Count(); i ++)
{
Movie thisMovie = (Movie)mMovies.ElementAt(i);
if (!::IsMovieDone(thisMovie))
return PR_TRUE;
}
return PR_FALSE;
}
PRBool
nsSoundRequest::HaveQuickTime()
{
long gestResult;
OSErr err = Gestalt (gestaltQuickTime, &gestResult);
return (err == noErr) && ((long)EnterMovies != kUnresolvedCFragSymbolAddress);
}
NS_IMETHODIMP nsSound::PlaySystemSound(const char *aSoundAlias)
{
return Beep();
}

Просмотреть файл

@ -39,26 +39,32 @@
#define __nsSound_h__
#include "nsISound.h"
#include "nsIStreamLoader.h"
#include "nsSupportsArray.h"
#include <MacTypes.h>
class nsIURI;
class nsIChannel;
class nsICacheSession;
class nsSound : public nsISound,
public nsIStreamLoaderObserver
class nsSound : public nsISound
{
public:
nsSound();
virtual ~nsSound();
NS_DECL_ISUPPORTS
NS_DECL_NSISOUND
NS_DECL_NSISTREAMLOADEROBSERVER
nsSound();
virtual ~nsSound();
nsresult AddRequest(nsISupports* aSoundRequest);
nsresult RemoveRequest(nsISupports* aSoundRequest);
nsresult GetCacheSession(nsICacheSession** outCacheSession);
nsresult GetSoundFromCache(nsIURI* inURI, nsISupports** outSound);
nsresult PutSoundInCache(nsIChannel* inChannel, PRUint32 inDataSize, nsISupports* inSound);
protected:
nsresult PlaySound(Handle waveDataHandle, long waveDataSize);
PRBool HaveQuickTime();
nsSupportsArray mSoundRequests; // array of outstanding/playing sounds
};