/** * @file streamingaudio_fmodstudio.cpp * @brief LLStreamingAudio_FMODSTUDIO implementation * * $LicenseInfo:firstyear=2009&license=viewergpl$ * * Copyright (c) 2009, Linden Research, Inc. * * Second Life Viewer Source Code * The source code in this file ("Source Code") is provided by Linden Lab * to you under the terms of the GNU General Public License, version 2.0 * ("GPL"), unless you have obtained a separate licensing agreement * ("Other License"), formally executed by you and Linden Lab. Terms of * the GPL can be found in doc/GPL-license.txt in this distribution, or * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2 * * There are special exceptions to the terms and conditions of the GPL as * it is applied to this Source Code. View the full text of the exception * in the file doc/FLOSS-exception.txt in this software distribution, or * online at * http://secondlifegrid.net/programs/open_source/licensing/flossexception * * By copying, modifying or distributing this software, you acknowledge * that you have read and understood your obligations described above, * and agree to abide by those obligations. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. * $/LicenseInfo$ */ #include "linden_common.h" #include "llmath.h" #include "llthread.h" #include "fmod.hpp" #include "fmod_errors.h" #include "llstreamingaudio_fmodstudio.h" inline bool Check_FMOD_Error(FMOD_RESULT result, const char *string) { if (result == FMOD_OK) return false; LL_WARNS("AudioImpl") << string << " Error: " << FMOD_ErrorString(result) << LL_ENDL; return true; } class LLAudioStreamManagerFMODSTUDIO { public: LLAudioStreamManagerFMODSTUDIO(FMOD::System *system, FMOD::ChannelGroup *group, const std::string& url); FMOD::Channel* startStream(); bool stopStream(); // Returns true if the stream was successfully stopped. bool ready(); const std::string& getURL() { return mInternetStreamURL; } FMOD_RESULT getOpenState(FMOD_OPENSTATE& openstate, unsigned int* percentbuffered = NULL, bool* starving = NULL, bool* diskbusy = NULL); protected: FMOD::System* mSystem; FMOD::Channel* mStreamChannel; FMOD::Sound* mInternetStream; FMOD::ChannelGroup* mChannelGroup; bool mReady; std::string mInternetStreamURL; }; LLGlobalMutex gWaveDataMutex; //Just to be extra strict. const U32 WAVE_BUFFER_SIZE = 1024; U32 gWaveBufferMinSize = 0; F32 gWaveDataBuffer[WAVE_BUFFER_SIZE] = { 0.f }; U32 gWaveDataBufferSize = 0; FMOD_RESULT F_CALLBACK waveDataCallback(FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int *outchannels) { if (!length || !inchannels) return FMOD_OK; memcpy(outbuffer, inbuffer, length * inchannels * sizeof(float)); static std::vector local_buf; if (local_buf.size() < length) local_buf.resize(length, 0.f); for (U32 i = 0; i < length; ++i) { F32 total = 0.f; for (S32 j = 0; j < inchannels; ++j) { total += inbuffer[i*inchannels + j]; } local_buf[i] = total / inchannels; } { LLMutexLock lock(gWaveDataMutex); for (U32 i = length; i > 0; --i) { if (++gWaveDataBufferSize > WAVE_BUFFER_SIZE) { if (gWaveBufferMinSize) memcpy(gWaveDataBuffer + WAVE_BUFFER_SIZE - gWaveBufferMinSize, gWaveDataBuffer, gWaveBufferMinSize * sizeof(float)); gWaveDataBufferSize = 1 + gWaveBufferMinSize; } gWaveDataBuffer[WAVE_BUFFER_SIZE - gWaveDataBufferSize] = local_buf[i - 1]; } } return FMOD_OK; } //--------------------------------------------------------------------------- // Internet Streaming //--------------------------------------------------------------------------- LLStreamingAudio_FMODSTUDIO::LLStreamingAudio_FMODSTUDIO(FMOD::System *system) : mSystem(system), mCurrentInternetStreamp(NULL), mFMODInternetStreamChannelp(NULL), mGain(1.0f), mMetaData(NULL), mStreamGroup(NULL), mStreamDSP(NULL) { FMOD_RESULT result; // Number of milliseconds of audio to buffer for the audio card. // Must be larger than the usual Second Life frame stutter time. const U32 buffer_seconds = 10; //sec const U32 estimated_bitrate = 128; //kbit/sec result = mSystem->setStreamBufferSize(estimated_bitrate * buffer_seconds * 128/*bytes/kbit*/, FMOD_TIMEUNIT_RAWBYTES); Check_FMOD_Error(result, "FMOD::System::setStreamBufferSize"); // Here's where we set the size of the network buffer and some buffering // parameters. In this case we want a network buffer of 16k, we want it // to prebuffer 40% of that when we first connect, and we want it // to rebuffer 80% of that whenever we encounter a buffer underrun. // Leave the net buffer properties at the default. //FSOUND_Stream_Net_SetBufferProperties(20000, 40, 80); Check_FMOD_Error(mSystem->createChannelGroup("stream", &mStreamGroup), "FMOD::System::createChannelGroup"); FMOD_DSP_DESCRIPTION dspdesc; memset(&dspdesc, 0, sizeof(FMOD_DSP_DESCRIPTION)); //Zero out everything dspdesc.pluginsdkversion = FMOD_PLUGIN_SDK_VERSION; strncpy(dspdesc.name, "Waveform", sizeof(dspdesc.name)); dspdesc.numoutputbuffers = 1; dspdesc.read = &waveDataCallback; //Assign callback. Check_FMOD_Error(system->createDSP(&dspdesc, &mStreamDSP), "FMOD::System::createDSP"); } LLStreamingAudio_FMODSTUDIO::~LLStreamingAudio_FMODSTUDIO() { stop(); for (U32 i = 0; i < 100; ++i) { if (releaseDeadStreams()) break; ms_sleep(10); } cleanupWaveData(); } void LLStreamingAudio_FMODSTUDIO::start(const std::string& url) { //if (!mInited) //{ // LL_WARNS() << "startInternetStream before audio initialized" << LL_ENDL; // return; //} // "stop" stream but don't clear url, etc. in case url == mInternetStreamURL stop(); if (!url.empty()) { if(mDeadStreams.empty()) { LL_INFOS() << "Starting internet stream: " << url << LL_ENDL; mCurrentInternetStreamp = new LLAudioStreamManagerFMODSTUDIO(mSystem, mStreamGroup, url); mURL = url; mMetaData = new LLSD; } else { LL_INFOS() << "Deferring stream load until buffer release: " << url << LL_ENDL; mPendingURL = url; } } else { LL_INFOS() << "Set internet stream to null" << LL_ENDL; mURL.clear(); } } enum utf_endian_type_t { UTF16LE, UTF16BE, UTF16 }; std::string utf16input_to_utf8(char* input, U32 len, utf_endian_type_t type) { if (type == UTF16) { type = UTF16BE; //Default if (len > 2) { //Parse and strip BOM. if ((input[0] == 0xFE && input[1] == 0xFF) || (input[0] == 0xFF && input[1] == 0xFE)) { input += 2; len -= 2; type = input[0] == 0xFE ? UTF16BE : UTF16LE; } } } llutf16string out_16((utf16strtype*)input, len / 2); if (len % 2) { out_16.push_back((input)[len - 1] << 8); } if (type == UTF16BE) { for (llutf16string::iterator i = out_16.begin(); i < out_16.end(); ++i) { llutf16string::value_type v = *i; *i = ((v & 0x00FF) << 8) | ((v & 0xFF00) >> 8); } } return utf16str_to_utf8str(out_16); } void LLStreamingAudio_FMODSTUDIO::update() { if (!releaseDeadStreams()) { llassert_always(mCurrentInternetStreamp == NULL); return; } if(!mPendingURL.empty()) { llassert_always(mCurrentInternetStreamp == NULL); LL_INFOS() << "Starting internet stream: " << mPendingURL << LL_ENDL; mCurrentInternetStreamp = new LLAudioStreamManagerFMODSTUDIO(mSystem,mStreamGroup, mPendingURL); mURL = mPendingURL; mMetaData = new LLSD; mPendingURL.clear(); } // Don't do anything if there are no streams playing if (!mCurrentInternetStreamp) { return; } unsigned int progress; bool starving; bool diskbusy; FMOD_OPENSTATE open_state; FMOD_RESULT result = mCurrentInternetStreamp->getOpenState(open_state, &progress, &starving, &diskbusy); if (result != FMOD_OK || open_state == FMOD_OPENSTATE_ERROR) { stop(); return; } else if (open_state == FMOD_OPENSTATE_READY) { // Stream is live // start the stream if it's ready if (!mFMODInternetStreamChannelp && (mFMODInternetStreamChannelp = mCurrentInternetStreamp->startStream())) { // Reset volume to previously set volume setGain(getGain()); if (mStreamDSP) { Check_FMOD_Error(mFMODInternetStreamChannelp->addDSP(FMOD_CHANNELCONTROL_DSP_TAIL, mStreamDSP), "FMOD::Channel::addDSP"); } Check_FMOD_Error(mFMODInternetStreamChannelp->setPaused(false), "FMOD::Channel::setPaused"); } } if(mFMODInternetStreamChannelp) { if(!mMetaData) mMetaData = new LLSD; FMOD::Sound *sound = NULL; if(mFMODInternetStreamChannelp->getCurrentSound(&sound) == FMOD_OK && sound) { FMOD_TAG tag; S32 tagcount, dirtytagcount; if(sound->getNumTags(&tagcount, &dirtytagcount) == FMOD_OK && dirtytagcount) { mMetaData->clear(); for(S32 i = 0; i < tagcount; ++i) { if(sound->getTag(NULL, i, &tag)!=FMOD_OK) continue; std::string name = tag.name; switch(tag.type) //Crappy tag translate table. { case(FMOD_TAGTYPE_ID3V2): if (!LLStringUtil::compareInsensitive(name, "TIT2")) name = "TITLE"; else if(name == "TPE1") name = "ARTIST"; break; case(FMOD_TAGTYPE_ASF): if (!LLStringUtil::compareInsensitive(name, "Title")) name = "TITLE"; else if (!LLStringUtil::compareInsensitive(name, "WM/AlbumArtist")) name = "ARTIST"; break; case(FMOD_TAGTYPE_FMOD): if (!LLStringUtil::compareInsensitive(name, "Sample Rate Change")) { LL_INFOS() << "Stream forced changing sample rate to " << *((float *)tag.data) << LL_ENDL; Check_FMOD_Error(mFMODInternetStreamChannelp->setFrequency(*((float *)tag.data)), "FMOD::Channel::setFrequency"); } continue; default: if (!LLStringUtil::compareInsensitive(name, "TITLE") || !LLStringUtil::compareInsensitive(name, "ARTIST")) LLStringUtil::toUpper(name); break; } switch(tag.datatype) { case(FMOD_TAGDATATYPE_INT): (*mMetaData)[name]=*(LLSD::Integer*)(tag.data); LL_INFOS() << tag.name << ": " << *(int*)(tag.data) << LL_ENDL; break; case(FMOD_TAGDATATYPE_FLOAT): (*mMetaData)[name]=*(LLSD::Float*)(tag.data); LL_INFOS() << tag.name << ": " << *(float*)(tag.data) << LL_ENDL; break; case(FMOD_TAGDATATYPE_STRING): { std::string out = rawstr_to_utf8(std::string((char*)tag.data,tag.datalen)); if (out.length() && out[out.size() - 1] == 0) out.erase(out.size() - 1); (*mMetaData)[name]=out; LL_INFOS() << tag.name << "(RAW): " << out << LL_ENDL; } break; case(FMOD_TAGDATATYPE_STRING_UTF8) : { U8 offs = 0; if (tag.datalen > 3 && ((unsigned char*)tag.data)[0] == 0xEF && ((unsigned char*)tag.data)[1] == 0xBB && ((unsigned char*)tag.data)[2] == 0xBF) offs = 3; std::string out((char*)tag.data + offs, tag.datalen - offs); if (out.length() && out[out.size() - 1] == 0) out.erase(out.size() - 1); (*mMetaData)[name] = out; LL_INFOS() << tag.name << "(UTF8): " << out << LL_ENDL; } break; case(FMOD_TAGDATATYPE_STRING_UTF16): { std::string out = utf16input_to_utf8((char*)tag.data, tag.datalen, UTF16); if (out.length() && out[out.size() - 1] == 0) out.erase(out.size() - 1); (*mMetaData)[name] = out; LL_INFOS() << tag.name << "(UTF16): " << out << LL_ENDL; } break; case(FMOD_TAGDATATYPE_STRING_UTF16BE): { std::string out = utf16input_to_utf8((char*)tag.data, tag.datalen, UTF16BE); if (out.length() && out[out.size() - 1] == 0) out.erase(out.size() - 1); (*mMetaData)[name] = out; LL_INFOS() << tag.name << "(UTF16BE): " << out << LL_ENDL; } default: break; } } } if(starving) { bool paused = false; if (mFMODInternetStreamChannelp->getPaused(&paused) == FMOD_OK && !paused) { LL_INFOS() << "Stream starvation detected! Pausing stream until buffer nearly full." << LL_ENDL; LL_INFOS() << " (diskbusy="<setPaused(true), "FMOD::Channel::setPaused"); } } else if(progress > 80) { Check_FMOD_Error(mFMODInternetStreamChannelp->setPaused(false), "FMOD::Channel::setPaused"); } } } } void LLStreamingAudio_FMODSTUDIO::stop() { mPendingURL.clear(); if(mMetaData) { delete mMetaData; mMetaData = NULL; } if (mFMODInternetStreamChannelp) { Check_FMOD_Error(mFMODInternetStreamChannelp->setPaused(true), "FMOD::Channel::setPaused"); Check_FMOD_Error(mFMODInternetStreamChannelp->setPriority(0), "FMOD::Channel::setPriority"); if (mStreamDSP) { Check_FMOD_Error(mFMODInternetStreamChannelp->removeDSP(mStreamDSP), "FMOD::Channel::removeDSP"); } mFMODInternetStreamChannelp = NULL; } if (mCurrentInternetStreamp) { LL_INFOS() << "Stopping internet stream: " << mCurrentInternetStreamp->getURL() << LL_ENDL; if (mCurrentInternetStreamp->stopStream()) { delete mCurrentInternetStreamp; } else { LL_WARNS() << "Pushing stream to dead list: " << mCurrentInternetStreamp->getURL() << LL_ENDL; mDeadStreams.push_back(mCurrentInternetStreamp); } mCurrentInternetStreamp = NULL; //mURL.clear(); } } void LLStreamingAudio_FMODSTUDIO::pause(int pauseopt) { if (pauseopt < 0) { pauseopt = mCurrentInternetStreamp ? 1 : 0; } if (pauseopt) { if (mCurrentInternetStreamp) { stop(); } } else { start(getURL()); } } // A stream is "playing" if it has been requested to start. That // doesn't necessarily mean audio is coming out of the speakers. int LLStreamingAudio_FMODSTUDIO::isPlaying() { if (mCurrentInternetStreamp) { return 1; // Active and playing } else if (!mURL.empty() || !mPendingURL.empty()) { return 2; // "Paused" } else { return 0; } } F32 LLStreamingAudio_FMODSTUDIO::getGain() { return mGain; } std::string LLStreamingAudio_FMODSTUDIO::getURL() { return mURL; } void LLStreamingAudio_FMODSTUDIO::setGain(F32 vol) { mGain = vol; if (mFMODInternetStreamChannelp) { vol = llclamp(vol * vol, 0.f, 1.f); //should vol be squared here? Check_FMOD_Error(mFMODInternetStreamChannelp->setVolume(vol), "FMOD::Channel::setVolume"); } } /*virtual*/ bool LLStreamingAudio_FMODSTUDIO::getWaveData(float* arr, S32 count, S32 stride/*=1*/) { if (count > (WAVE_BUFFER_SIZE / 2)) LL_ERRS("AudioImpl") << "Count=" << count << " exceeds WAVE_BUFFER_SIZE/2=" << WAVE_BUFFER_SIZE << LL_ENDL; if(!mFMODInternetStreamChannelp || !mCurrentInternetStreamp) return false; bool muted = false; FMOD_RESULT res = mFMODInternetStreamChannelp->getMute(&muted); if(res != FMOD_OK || muted) return false; { U32 buff_size; { LLMutexLock lock(gWaveDataMutex); gWaveBufferMinSize = count; buff_size = gWaveDataBufferSize; if (!buff_size) return false; memcpy(arr, gWaveDataBuffer + WAVE_BUFFER_SIZE - buff_size, llmin(U32(count), buff_size) * sizeof(float)); } if (buff_size < U32(count)) memset(arr + buff_size, 0, (count - buff_size) * sizeof(float)); } return true; } /////////////////////////////////////////////////////// // manager of possibly-multiple internet audio streams LLAudioStreamManagerFMODSTUDIO::LLAudioStreamManagerFMODSTUDIO(FMOD::System *system, FMOD::ChannelGroup *group, const std::string& url) : mSystem(system), mStreamChannel(NULL), mInternetStream(NULL), mChannelGroup(group), mReady(false) { mInternetStreamURL = url; FMOD_RESULT result = mSystem->createStream(url.c_str(), FMOD_2D | FMOD_NONBLOCKING | FMOD_IGNORETAGS, 0, &mInternetStream); if (result!= FMOD_OK) { LL_WARNS() << "Couldn't open fmod stream, error " << FMOD_ErrorString(result) << LL_ENDL; mReady = false; return; } mReady = true; } FMOD::Channel *LLAudioStreamManagerFMODSTUDIO::startStream() { // We need a live and opened stream before we try and play it. FMOD_OPENSTATE open_state; if (getOpenState(open_state) != FMOD_OK || open_state != FMOD_OPENSTATE_READY) { LL_WARNS() << "No internet stream to start playing!" << LL_ENDL; return NULL; } if(mStreamChannel) return mStreamChannel; //Already have a channel for this stream. Check_FMOD_Error(mSystem->playSound(mInternetStream, mChannelGroup, true, &mStreamChannel), "FMOD::System::playSound"); return mStreamChannel; } bool LLAudioStreamManagerFMODSTUDIO::stopStream() { if (mInternetStream) { bool close = true; FMOD_OPENSTATE open_state; if (getOpenState(open_state) == FMOD_OK) { switch (open_state) { case FMOD_OPENSTATE_CONNECTING: close = false; break; default: close = true; } } if (close && mInternetStream->release() == FMOD_OK) { mStreamChannel = NULL; mInternetStream = NULL; return true; } else { return false; } } else { return true; } } FMOD_RESULT LLAudioStreamManagerFMODSTUDIO::getOpenState(FMOD_OPENSTATE& state, unsigned int* percentbuffered, bool* starving, bool* diskbusy) { if (!mInternetStream) return FMOD_ERR_INVALID_HANDLE; FMOD_RESULT result = mInternetStream->getOpenState(&state, percentbuffered, starving, diskbusy); Check_FMOD_Error(result, "FMOD::Sound::getOpenState"); return result; } void LLStreamingAudio_FMODSTUDIO::setBufferSizes(U32 streambuffertime, U32 decodebuffertime) { Check_FMOD_Error(mSystem->setStreamBufferSize(streambuffertime / 1000 * 128 * 128, FMOD_TIMEUNIT_RAWBYTES), "FMOD::System::setStreamBufferSize"); FMOD_ADVANCEDSETTINGS settings; memset(&settings,0,sizeof(settings)); settings.cbSize=sizeof(settings); settings.defaultDecodeBufferSize = decodebuffertime;//ms Check_FMOD_Error(mSystem->setAdvancedSettings(&settings), "FMOD::System::setAdvancedSettings"); } bool LLStreamingAudio_FMODSTUDIO::releaseDeadStreams() { // Kill dead internet streams, if possible std::list::iterator iter; for (iter = mDeadStreams.begin(); iter != mDeadStreams.end();) { LLAudioStreamManagerFMODSTUDIO *streamp = *iter; if (streamp->stopStream()) { LL_INFOS() << "Closed dead stream" << LL_ENDL; delete streamp; mDeadStreams.erase(iter++); } else { iter++; } } return mDeadStreams.empty(); } void LLStreamingAudio_FMODSTUDIO::cleanupWaveData() { if (mStreamGroup) { Check_FMOD_Error(mStreamGroup->release(), "FMOD::ChannelGroup::release"); mStreamGroup = NULL; } if(mStreamDSP) Check_FMOD_Error(mStreamDSP->release(), "FMOD::DSP::release"); mStreamDSP = NULL; }