// // redbooksound.cpp // // SoundEngine support for redbook audio. // #include "pch.h" #include "soundbase.h" #include "redbooksound.h" #include "ds3dutil.h" using std::list; namespace SoundEngine { // // A class dedicated to controling the volume of the CD player(s) on a system // class CDVolume { public: enum { c_nMaxChannels = 8 }; private: struct MixerLineData { HMIXEROBJ hmixer; DWORD cChannels; MIXERCONTROL mixercontrol; MIXERCONTROLDETAILS_UNSIGNED vmixercontrolsOld[c_nMaxChannels]; }; public: list m_listMixerLines; CDVolume() { UINT uMaxDevices = mixerGetNumDevs(); // find all of the CD line controls and store their ID and starting volume for (UINT uDeviceID = 0; uDeviceID < uMaxDevices; ++uDeviceID) { HMIXEROBJ hmixer; // // open the mixer in question // if (MMSYSERR_NOERROR != mixerOpen((LPHMIXER)&hmixer, uDeviceID, NULL, NULL, MIXER_OBJECTF_MIXER)) { debugf("Failed to open mixer %d\n", uDeviceID); continue; } // // look for a mixer line attached to a CD // MIXERLINE mixerline; mixerline.cbStruct = sizeof(mixerline); mixerline.dwComponentType = MIXERLINE_COMPONENTTYPE_SRC_COMPACTDISC; if (MMSYSERR_NOERROR != mixerGetLineInfo(hmixer, &mixerline, MIXER_GETLINEINFOF_COMPONENTTYPE) || mixerline.cControls == 0) { debugf("Failed to find CD line on mixer %d\n", uDeviceID); mixerClose((HMIXER)hmixer); continue; } // // look for a volume control for that mixer line // MIXERLINECONTROLS mixerlinecontrols; MixerLineData mixerlinedata; mixerlinecontrols.cbStruct = sizeof(mixerlinecontrols); mixerlinecontrols.dwLineID = mixerline.dwLineID; mixerlinecontrols.dwControlType = MIXERCONTROL_CONTROLTYPE_VOLUME; mixerlinecontrols.cControls = 1; mixerlinecontrols.cbmxctrl = sizeof(MIXERCONTROL); mixerlinecontrols.pamxctrl = &(mixerlinedata.mixercontrol); mixerlinedata.hmixer = hmixer; mixerlinedata.cChannels = mixerline.cChannels; if (MMSYSERR_NOERROR != mixerGetLineControls(hmixer, &mixerlinecontrols, MIXER_GETLINECONTROLSF_ONEBYTYPE)) { debugf("Failed to find CD volume fader on mixer %d\n", uDeviceID); mixerClose((HMIXER)hmixer); continue; } // don't try to use more than 8 channels (not likely to be a problem) if (mixerlinedata.cChannels > c_nMaxChannels) mixerlinedata.cChannels = 1; // // Get the initial volume settings (so we can restore them when we are done) // MIXERCONTROLDETAILS mixercontroldetails; mixercontroldetails.cbStruct = sizeof(MIXERCONTROLDETAILS); mixercontroldetails.dwControlID = mixerlinedata.mixercontrol.dwControlID; mixercontroldetails.cChannels = mixerlinedata.cChannels; mixercontroldetails.cMultipleItems = 0; mixercontroldetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED); mixercontroldetails.paDetails = &(mixerlinedata.vmixercontrolsOld); if (MMSYSERR_NOERROR != mixerGetControlDetails(hmixer, &mixercontroldetails, MIXER_GETCONTROLDETAILSF_VALUE)) { debugf("Failed to get previous volume levels for mixer %d\n", uDeviceID); mixerClose((HMIXER)hmixer); continue; } // add this to the list of volume controls m_listMixerLines.push_back(mixerlinedata); } } ~CDVolume() { // restore the volume settings for all of the CD players while (!m_listMixerLines.empty()) { MixerLineData& mixerlinedata = m_listMixerLines.back(); MIXERCONTROLDETAILS mixercontroldetails; mixercontroldetails.cbStruct = sizeof(MIXERCONTROLDETAILS); mixercontroldetails.dwControlID = mixerlinedata.mixercontrol.dwControlID; mixercontroldetails.cChannels = mixerlinedata.cChannels; mixercontroldetails.cMultipleItems = 0; mixercontroldetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED); mixercontroldetails.paDetails = &(mixerlinedata.vmixercontrolsOld); ZVerify(mixerSetControlDetails(mixerlinedata.hmixer, &mixercontroldetails, MIXER_SETCONTROLDETAILSF_VALUE) == MMSYSERR_NOERROR); ZVerify(mixerClose((HMIXER)mixerlinedata.hmixer) == MMSYSERR_NOERROR); m_listMixerLines.pop_back(); } } HRESULT SetGain(float fGain) { if (fGain > 0.0f) { return E_INVALIDARG; } const float fMinGain = -40; float fClippedGain = max(fMinGain, fGain); // set the volume on every CD player (since we can't map to the right one) // restore the volume settings for all of the CD players std::list::iterator mixerline; for (mixerline = m_listMixerLines.begin(); mixerline != m_listMixerLines.end(); ++mixerline) { MixerLineData& mixerlinedata = *mixerline; // translate the gain to a linear volume setting. MIXERCONTROLDETAILS_UNSIGNED volume; volume.dwValue = (DWORD)(mixerlinedata.mixercontrol.Bounds.dwMinimum + (mixerlinedata.mixercontrol.Bounds.dwMaximum - mixerlinedata.mixercontrol.Bounds.dwMinimum) * (1 - fClippedGain/fMinGain)); // set the volume for this control MIXERCONTROLDETAILS mixercontroldetails; mixercontroldetails.cbStruct = sizeof(MIXERCONTROLDETAILS); mixercontroldetails.dwControlID = mixerlinedata.mixercontrol.dwControlID; mixercontroldetails.cChannels = 1; mixercontroldetails.cMultipleItems = 0; mixercontroldetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED); mixercontroldetails.paDetails = &volume; ZVerify(mixerSetControlDetails(mixerlinedata.hmixer, &mixercontroldetails, MIXER_SETCONTROLDETAILSF_VALUE) == MMSYSERR_NOERROR); } return S_OK; } }; // // An implementation of a generic disk player wrapper that wraps a CD player // class DiskPlayerImpl : public IDiskPlayer, private WorkerThread { UINT m_idDevice; // the device name ZString m_strElementName; // a critical section controling access to the queued track and current track CriticalSection m_csTrack; enum { trackEmpty = -1, trackStop = 0 }; // the next track to play (trackEmpty if the queue is empty, or trackStop // if a stop is queued). volatile int m_nQueuedTrack; // the current track of the CD player (or trackStop if the cd player is stopped). volatile int m_nCurrentTrack; CDVolume m_cdvolume; public: DiskPlayerImpl() : m_nQueuedTrack(trackEmpty), m_nCurrentTrack(trackStop) { }; ~DiskPlayerImpl() { StopThread(); } HRESULT Init(const ZString& strDevice) { DWORD dwError; // try to open the device MCI_OPEN_PARMS mciOpenParms; DWORD dwFlags; mciOpenParms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_CD_AUDIO; dwFlags = MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID; // translate the device name into an element name, if appropriate if (!strDevice.IsEmpty()) { m_strElementName = TranslateElementName(strDevice); mciOpenParms.lpstrElementName = m_strElementName; dwFlags |= MCI_OPEN_ELEMENT; } // try opening it to make sure it exists dwError = mciSendCommand(NULL, MCI_OPEN, dwFlags, (UINT_PTR)&mciOpenParms); if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Open failed for CD Audio device '%': %s\n", (const char*)strDevice, cbError); return E_FAIL; } mciSendCommand(mciOpenParms.wDeviceID, MCI_CLOSE, 0, NULL); // start the background (io) thread StartThread(THREAD_PRIORITY_NORMAL, 200); return S_OK; } void ThreadInit() { DWORD dwError; // try to open the device MCI_OPEN_PARMS mciOpenParms; DWORD dwFlags; ZString strElementName; mciOpenParms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_CD_AUDIO; dwFlags = MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID; if (!m_strElementName.IsEmpty()) { mciOpenParms.lpstrElementName = m_strElementName; dwFlags |= MCI_OPEN_ELEMENT; } dwError = mciSendCommand(NULL, MCI_OPEN, dwFlags, (UINT_PTR)&mciOpenParms); if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Open failed for CD Audio device '%': %s\n", (const char*)m_strElementName, cbError); } m_idDevice = mciOpenParms.wDeviceID; // Set the time format to track/minute/second/frame (TMSF). MCI_SET_PARMS mciSetParms; mciSetParms.dwTimeFormat = MCI_FORMAT_TMSF; dwError = mciSendCommand(m_idDevice, MCI_SET, MCI_SET_TIME_FORMAT, (UINT_PTR)&mciSetParms); if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Set format failed for CD Audio device: %s\n", cbError); } } // called for background thread; changes state (since that can involve // slow blocking calls) and updates the state (since the state is read // frequently and querying the device is a bit slow) bool ThreadIteration() { // fetch the current requested state and copy it to the current state int nRequestedTrack; int nOldTrack; { CriticalSectionLock lock(m_csTrack); nOldTrack = m_nCurrentTrack; nRequestedTrack = m_nQueuedTrack; if (nRequestedTrack != trackEmpty) m_nCurrentTrack = nRequestedTrack; m_nQueuedTrack = trackEmpty; } // if there is a new state requested, make that change if (nRequestedTrack != trackEmpty) { if (nRequestedTrack == trackStop) StopImpl(); else PlayImpl(nRequestedTrack); } // otherwise, just update the current state else { if ((nOldTrack != trackStop) && (IsPlayingImpl() != S_OK)) { CriticalSectionLock lock(m_csTrack); m_nCurrentTrack = trackStop; } } return true; } void ThreadCleanup() { StopImpl(); mciSendCommand(m_idDevice, MCI_CLOSE, 0, NULL); } ZString TranslateElementName(const ZString& strDevice) { if (strDevice.Find(':') != -1) { return strDevice; } else { // get a list of all of the drives on the system char cTemp; int nDrivesStringLength = GetLogicalDriveStrings(1, &cTemp); if (nDrivesStringLength == 0) { ZError("Error getting drives list\n"); return strDevice; } char* cbDrives = (char*)_alloca(nDrivesStringLength); nDrivesStringLength = GetLogicalDriveStrings(nDrivesStringLength, cbDrives); if (nDrivesStringLength == 0) { ZError("Error getting drives list\n"); return strDevice; } // search through the list of drives looking for a CD-ROM who's volume // label matches strDevice while (cbDrives[0] != '\0') { const int c_nVolumeNameLength = 1024; char cbVolumeName[c_nVolumeNameLength]; if (GetDriveType(cbDrives) == DRIVE_CDROM && GetVolumeInformation(cbDrives, cbVolumeName, c_nVolumeNameLength, NULL, NULL, NULL, NULL, 0)) { if (_stricmp(strDevice, cbVolumeName) == 0) { return cbDrives; } } cbDrives += strlen(cbDrives) + 1; } return strDevice; } } // plays one track virtual HRESULT Play(int nTrack) { CriticalSectionLock lock(m_csTrack); if (nTrack <= 0) { return E_INVALIDARG; } m_nQueuedTrack = nTrack; return S_OK; } // stops the CD player virtual HRESULT Stop() { CriticalSectionLock lock(m_csTrack); m_nQueuedTrack = trackStop; return S_OK; } // returns S_OK if the CD player is playing, S_FALSE otherwise virtual HRESULT IsPlaying() { CriticalSectionLock lock(m_csTrack); int nTrack; if (m_nQueuedTrack != trackEmpty) { nTrack = m_nQueuedTrack; } else { nTrack = m_nCurrentTrack; } return (nTrack == trackStop) ? S_FALSE : S_OK; } // returns the current track of the CD player virtual HRESULT GetCurrentTrack(int& nTrack) { CriticalSectionLock lock(m_csTrack); if (m_nQueuedTrack != trackEmpty) { nTrack = m_nQueuedTrack; } else { nTrack = m_nCurrentTrack; } return (nTrack != trackStop) ? S_OK : E_FAIL; } // sets the gain on the CD player, from 0 to -100 dB virtual HRESULT SetGain(float fGain) { return m_cdvolume.SetGain(fGain); }; // plays one track (blocking) HRESULT PlayImpl(int nTrack) { MCI_PLAY_PARMS mciPlayParms; mciPlayParms.dwFrom = MCI_MAKE_TMSF(nTrack, 0, 0, 0); mciPlayParms.dwTo = MCI_MAKE_TMSF(nTrack + 1, 0, 0, 0); DWORD dwError = mciSendCommand(m_idDevice, MCI_PLAY, MCI_FROM | MCI_TO, (UINT_PTR)&mciPlayParms); // if the track is out of range, retry without the stop point in case // this is the last track on the CD. (review: we could store it, but // this case handles switching the CD) if (dwError == MCIERR_OUTOFRANGE) dwError = mciSendCommand(m_idDevice, MCI_PLAY, MCI_FROM, (UINT_PTR)&mciPlayParms); // this is the highest track on the CD if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Play track %d failed for CD Audio device: %s\n", nTrack, cbError); return E_FAIL; } return S_OK; }; // stops the CD player (blocking) HRESULT StopImpl() { DWORD dwError = mciSendCommand(m_idDevice, MCI_STOP, 0, NULL); if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Stop failed for CD Audio device: %s\n", cbError); return E_FAIL; } return S_OK; }; // returns S_OK if the CD player is playing, S_FALSE otherwise (blocking) HRESULT IsPlayingImpl() { MCI_STATUS_PARMS mciStatusParams; mciStatusParams.dwItem = MCI_STATUS_MODE; DWORD dwError = mciSendCommand(m_idDevice, MCI_STATUS, MCI_STATUS_ITEM, (UINT_PTR)&mciStatusParams); if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Status:Mode failed for CD Audio device: %s\n", cbError); return E_FAIL; } return (mciStatusParams.dwReturn == MCI_MODE_PLAY) ? S_OK : S_FALSE; }; // returns the current track of the CD player (blocking) HRESULT GetCurrentTrackImpl(int& nTrack) { MCI_STATUS_PARMS mciStatusParams; mciStatusParams.dwItem = MCI_STATUS_CURRENT_TRACK; DWORD dwError = mciSendCommand(m_idDevice, MCI_STATUS, MCI_STATUS_ITEM, (UINT_PTR)&mciStatusParams); if (dwError) { char cbError[256]; mciGetErrorString(dwError, cbError, 256); debugf("Status:Track failed for CD Audio device: %s\n", cbError); return E_FAIL; } nTrack = mciStatusParams.dwReturn; return S_OK; }; }; // Creates a new disk player object (CD, minidisk, etc.). strDevice can be // empty (choose any device), a path ("E:\"), or a volume label. HRESULT CreateDiskPlayer(TRef& pdiskplayer, const ZString& strDevice) { TRef pdiskplayerimpl = new DiskPlayerImpl(); HRESULT hr = pdiskplayerimpl->Init(strDevice); if (SUCCEEDED(hr)) pdiskplayer = pdiskplayerimpl; return hr; }; class DummyDiskPlayer : public IDiskPlayer { public: // plays one track virtual HRESULT Play(int nTrack) { return S_OK; }; // stops the CD player virtual HRESULT Stop() { return S_OK; }; // returns S_OK if the CD player is playing virtual HRESULT IsPlaying() { return S_FALSE; }; // returns the current track of the CD player virtual HRESULT GetCurrentTrack(int& nTrack) { nTrack = 1; return S_OK; }; // sets the gain on the CD player, from 0 to -100 dB virtual HRESULT SetGain(float fGain) { return S_OK; }; }; // Creates a new disk player object that only has stubs for each of the calls HRESULT CreateDummyDiskPlayer(TRef& pdiskplayer) { pdiskplayer = new DummyDiskPlayer(); return S_OK; } // a template for redbook audio class RedbookSoundTemplate : public ISoundTemplate { private: TRef m_pdiskplayer; int m_nTrack; // // Playback controls // class RedbookSoundInstance : public ISoundInstance { TRef m_pdiskplayer; int m_nTrack; public: RedbookSoundInstance(TRef pdiskplayer, int nTrack) : m_pdiskplayer(pdiskplayer), m_nTrack(nTrack) { pdiskplayer->Play(nTrack); } // Stops the sound. If bForceNow is true the sound will stop ASAP, // possibly popping. If it is false some sounds may play a trail-off // sound or fade away. virtual HRESULT Stop(bool bForceNow = false) { HRESULT hr = IsPlaying(); if (hr == S_OK) { return m_pdiskplayer->Stop(); } else if (SUCCEEDED(hr)) return S_OK; else return hr; } // returns S_OK if the sound is currently playing, S_FALSE otherwise. virtual HRESULT IsPlaying() { HRESULT hr = m_pdiskplayer->IsPlaying(); if (hr == S_OK) { // it's playing, but is it playing our track or something else? int nTrack; hr = m_pdiskplayer->GetCurrentTrack(nTrack); if (FAILED(hr)) return hr; return (nTrack == m_nTrack) ? S_OK : S_FALSE; } else return hr; } // Gets an event which fires when the sound finishes playing (for any // reason) virtual IEventSource* GetFinishEventSource() { ZError("NYI"); return NULL; } // Gets an interface for tweaking the sound, if supported, NULL otherwise. virtual TRef GetISoundTweakable() { return NULL; } virtual TRef GetISoundTweakable3D() { return NULL; } }; public: // tries to initialize the object with the given file. HRESULT Init(TRef pdiskplayer, int nTrack) { if (!pdiskplayer) { ZAssert(false); return E_POINTER; } if (nTrack <= 0) { ZAssert(false); return E_INVALIDARG; } m_pdiskplayer = pdiskplayer; m_nTrack = nTrack; return S_OK; }; // Creates a new instance of the given sound virtual HRESULT CreateSound(TRef& psoundNew, ISoundBufferSource* pbufferSource, ISoundPositionSource* psource = NULL) { if (!pbufferSource) { ZAssert(false); return E_POINTER; } if (psource) { ZAssert(false); return E_NOTIMPL; } psoundNew = new RedbookSoundInstance(m_pdiskplayer, m_nTrack); return S_OK; } }; // creates a sound template representing a redbook audio track HRESULT CreateRedbookSoundTemplate(TRef& pstDest, TRef pdiskplayer, int nTrack) { TRef ptemplate = new RedbookSoundTemplate(); HRESULT hr = ptemplate->Init(pdiskplayer, nTrack); if (ZSucceeded(hr)) pstDest = ptemplate; return hr; } };