/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-11 by Raw Material Software Ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License (Version 2), as published by the Free Software Foundation. A copy of the license is included in the JUCE distribution, or can be found online at www.gnu.org/licenses. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ #ifndef WASAPI_ENABLE_LOGGING #define WASAPI_ENABLE_LOGGING 0 #endif //============================================================================== namespace WasapiClasses { void logFailure (HRESULT hr) { (void) hr; #if WASAPI_ENABLE_LOGGING if (FAILED (hr)) { String e; e << Time::getCurrentTime().toString (true, true, true, true) << " -- WASAPI error: "; switch (hr) { case E_POINTER: e << "E_POINTER"; break; case E_INVALIDARG: e << "E_INVALIDARG"; break; case AUDCLNT_E_NOT_INITIALIZED: e << "AUDCLNT_E_NOT_INITIALIZED"; break; case AUDCLNT_E_ALREADY_INITIALIZED: e << "AUDCLNT_E_ALREADY_INITIALIZED"; break; case AUDCLNT_E_WRONG_ENDPOINT_TYPE: e << "AUDCLNT_E_WRONG_ENDPOINT_TYPE"; break; case AUDCLNT_E_DEVICE_INVALIDATED: e << "AUDCLNT_E_DEVICE_INVALIDATED"; break; case AUDCLNT_E_NOT_STOPPED: e << "AUDCLNT_E_NOT_STOPPED"; break; case AUDCLNT_E_BUFFER_TOO_LARGE: e << "AUDCLNT_E_BUFFER_TOO_LARGE"; break; case AUDCLNT_E_OUT_OF_ORDER: e << "AUDCLNT_E_OUT_OF_ORDER"; break; case AUDCLNT_E_UNSUPPORTED_FORMAT: e << "AUDCLNT_E_UNSUPPORTED_FORMAT"; break; case AUDCLNT_E_INVALID_SIZE: e << "AUDCLNT_E_INVALID_SIZE"; break; case AUDCLNT_E_DEVICE_IN_USE: e << "AUDCLNT_E_DEVICE_IN_USE"; break; case AUDCLNT_E_BUFFER_OPERATION_PENDING: e << "AUDCLNT_E_BUFFER_OPERATION_PENDING"; break; case AUDCLNT_E_THREAD_NOT_REGISTERED: e << "AUDCLNT_E_THREAD_NOT_REGISTERED"; break; case AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED: e << "AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED"; break; case AUDCLNT_E_ENDPOINT_CREATE_FAILED: e << "AUDCLNT_E_ENDPOINT_CREATE_FAILED"; break; case AUDCLNT_E_SERVICE_NOT_RUNNING: e << "AUDCLNT_E_SERVICE_NOT_RUNNING"; break; case AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED: e << "AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED"; break; case AUDCLNT_E_EXCLUSIVE_MODE_ONLY: e << "AUDCLNT_E_EXCLUSIVE_MODE_ONLY"; break; case AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL: e << "AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL"; break; case AUDCLNT_E_EVENTHANDLE_NOT_SET: e << "AUDCLNT_E_EVENTHANDLE_NOT_SET"; break; case AUDCLNT_E_INCORRECT_BUFFER_SIZE: e << "AUDCLNT_E_INCORRECT_BUFFER_SIZE"; break; case AUDCLNT_E_BUFFER_SIZE_ERROR: e << "AUDCLNT_E_BUFFER_SIZE_ERROR"; break; case AUDCLNT_S_BUFFER_EMPTY: e << "AUDCLNT_S_BUFFER_EMPTY"; break; case AUDCLNT_S_THREAD_ALREADY_REGISTERED: e << "AUDCLNT_S_THREAD_ALREADY_REGISTERED"; break; default: e << String::toHexString ((int) hr); break; } DBG (e); jassertfalse; } #endif } #undef check bool check (HRESULT hr) { logFailure (hr); return SUCCEEDED (hr); } //============================================================================== String getDeviceID (IMMDevice* const device) { String s; WCHAR* deviceId = nullptr; if (check (device->GetId (&deviceId))) { s = String (deviceId); CoTaskMemFree (deviceId); } return s; } EDataFlow getDataFlow (const ComSmartPtr& device) { EDataFlow flow = eRender; ComSmartPtr endPoint; if (check (device.QueryInterface (endPoint))) (void) check (endPoint->GetDataFlow (&flow)); return flow; } int refTimeToSamples (const REFERENCE_TIME& t, const double sampleRate) noexcept { return roundDoubleToInt (sampleRate * ((double) t) * 0.0000001); } void copyWavFormat (WAVEFORMATEXTENSIBLE& dest, const WAVEFORMATEX* const src) noexcept { memcpy (&dest, src, src->wFormatTag == WAVE_FORMAT_EXTENSIBLE ? sizeof (WAVEFORMATEXTENSIBLE) : sizeof (WAVEFORMATEX)); } //============================================================================== class WASAPIDeviceBase { public: WASAPIDeviceBase (const ComSmartPtr & device_, const bool useExclusiveMode_) : device (device_), sampleRate (0), defaultSampleRate (0), numChannels (0), actualNumChannels (0), minBufferSize (0), defaultBufferSize (0), latencySamples (0), useExclusiveMode (useExclusiveMode_), sampleRateHasChanged (false) { clientEvent = CreateEvent (0, false, false, _T("JuceWASAPI")); ComSmartPtr tempClient (createClient()); if (tempClient == nullptr) return; REFERENCE_TIME defaultPeriod, minPeriod; if (! check (tempClient->GetDevicePeriod (&defaultPeriod, &minPeriod))) return; WAVEFORMATEX* mixFormat = nullptr; if (! check (tempClient->GetMixFormat (&mixFormat))) return; WAVEFORMATEXTENSIBLE format; copyWavFormat (format, mixFormat); CoTaskMemFree (mixFormat); actualNumChannels = numChannels = format.Format.nChannels; defaultSampleRate = format.Format.nSamplesPerSec; minBufferSize = refTimeToSamples (minPeriod, defaultSampleRate); defaultBufferSize = refTimeToSamples (defaultPeriod, defaultSampleRate); rates.addUsingDefaultSort (defaultSampleRate); static const double ratesToTest[] = { 44100.0, 48000.0, 88200.0, 96000.0 }; for (int i = 0; i < numElementsInArray (ratesToTest); ++i) { if (ratesToTest[i] == defaultSampleRate) continue; format.Format.nSamplesPerSec = (DWORD) roundDoubleToInt (ratesToTest[i]); if (SUCCEEDED (tempClient->IsFormatSupported (useExclusiveMode ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED, (WAVEFORMATEX*) &format, 0))) if (! rates.contains (ratesToTest[i])) rates.addUsingDefaultSort (ratesToTest[i]); } } ~WASAPIDeviceBase() { device = nullptr; CloseHandle (clientEvent); } bool isOk() const noexcept { return defaultBufferSize > 0 && defaultSampleRate > 0; } bool openClient (const double newSampleRate, const BigInteger& newChannels) { sampleRate = newSampleRate; channels = newChannels; channels.setRange (actualNumChannels, channels.getHighestBit() + 1 - actualNumChannels, false); numChannels = channels.getHighestBit() + 1; if (numChannels == 0) return true; client = createClient(); if (client != nullptr && (tryInitialisingWithFormat (true, 4) || tryInitialisingWithFormat (false, 4) || tryInitialisingWithFormat (false, 3) || tryInitialisingWithFormat (false, 2))) { sampleRateHasChanged = false; channelMaps.clear(); for (int i = 0; i <= channels.getHighestBit(); ++i) if (channels[i]) channelMaps.add (i); REFERENCE_TIME latency; if (check (client->GetStreamLatency (&latency))) latencySamples = refTimeToSamples (latency, sampleRate); (void) check (client->GetBufferSize (&actualBufferSize)); createSessionEventCallback(); return check (client->SetEventHandle (clientEvent)); } return false; } void closeClient() { if (client != nullptr) client->Stop(); deleteSessionEventCallback(); client = nullptr; ResetEvent (clientEvent); } void deviceSampleRateChanged() { sampleRateHasChanged = true; } //============================================================================== ComSmartPtr device; ComSmartPtr client; double sampleRate, defaultSampleRate; int numChannels, actualNumChannels; int minBufferSize, defaultBufferSize, latencySamples; const bool useExclusiveMode; Array rates; HANDLE clientEvent; BigInteger channels; Array channelMaps; UINT32 actualBufferSize; int bytesPerSample; bool sampleRateHasChanged; virtual void updateFormat (bool isFloat) = 0; private: //============================================================================== class SessionEventCallback : public ComBaseClassHelper { public: SessionEventCallback (WASAPIDeviceBase& owner_) : owner (owner_) {} JUCE_COMRESULT OnDisplayNameChanged (LPCWSTR, LPCGUID) { return S_OK; } JUCE_COMRESULT OnIconPathChanged (LPCWSTR, LPCGUID) { return S_OK; } JUCE_COMRESULT OnSimpleVolumeChanged (float, BOOL, LPCGUID) { return S_OK; } JUCE_COMRESULT OnChannelVolumeChanged (DWORD, float*, DWORD, LPCGUID) { return S_OK; } JUCE_COMRESULT OnGroupingParamChanged (LPCGUID, LPCGUID) { return S_OK; } JUCE_COMRESULT OnStateChanged (AudioSessionState) { return S_OK; } JUCE_COMRESULT OnSessionDisconnected (AudioSessionDisconnectReason reason) { if (reason == DisconnectReasonFormatChanged) owner.deviceSampleRateChanged(); return S_OK; } private: WASAPIDeviceBase& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SessionEventCallback); }; ComSmartPtr audioSessionControl; ComSmartPtr sessionEventCallback; void createSessionEventCallback() { deleteSessionEventCallback(); client->GetService (__uuidof (IAudioSessionControl), (void**) audioSessionControl.resetAndGetPointerAddress()); if (audioSessionControl != nullptr) { sessionEventCallback = new SessionEventCallback (*this); audioSessionControl->RegisterAudioSessionNotification (sessionEventCallback); sessionEventCallback->Release(); // (required because ComBaseClassHelper objects are constructed with a ref count of 1) } } void deleteSessionEventCallback() { if (audioSessionControl != nullptr && sessionEventCallback != nullptr) audioSessionControl->UnregisterAudioSessionNotification (sessionEventCallback); audioSessionControl = nullptr; sessionEventCallback = nullptr; } //============================================================================== const ComSmartPtr createClient() { ComSmartPtr client; if (device != nullptr) { HRESULT hr = device->Activate (__uuidof (IAudioClient), CLSCTX_INPROC_SERVER, 0, (void**) client.resetAndGetPointerAddress()); logFailure (hr); } return client; } bool tryInitialisingWithFormat (const bool useFloat, const int bytesPerSampleToTry) { WAVEFORMATEXTENSIBLE format = { 0 }; if (numChannels <= 2 && bytesPerSampleToTry <= 2) { format.Format.wFormatTag = WAVE_FORMAT_PCM; } else { format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; format.Format.cbSize = sizeof (WAVEFORMATEXTENSIBLE) - sizeof (WAVEFORMATEX); } format.Format.nSamplesPerSec = (DWORD) roundDoubleToInt (sampleRate); format.Format.nChannels = (WORD) numChannels; format.Format.wBitsPerSample = (WORD) (8 * bytesPerSampleToTry); format.Format.nAvgBytesPerSec = (DWORD) (format.Format.nSamplesPerSec * numChannels * bytesPerSampleToTry); format.Format.nBlockAlign = (WORD) (numChannels * bytesPerSampleToTry); format.SubFormat = useFloat ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM; format.Samples.wValidBitsPerSample = format.Format.wBitsPerSample; switch (numChannels) { case 1: format.dwChannelMask = SPEAKER_FRONT_CENTER; break; case 2: format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; break; case 4: format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; break; case 6: format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; break; case 8: format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_FRONT_RIGHT_OF_CENTER; break; default: break; } WAVEFORMATEXTENSIBLE* nearestFormat = nullptr; HRESULT hr = client->IsFormatSupported (useExclusiveMode ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED, (WAVEFORMATEX*) &format, useExclusiveMode ? nullptr : (WAVEFORMATEX**) &nearestFormat); logFailure (hr); if (hr == S_FALSE && format.Format.nSamplesPerSec == nearestFormat->Format.nSamplesPerSec) { copyWavFormat (format, (WAVEFORMATEX*) nearestFormat); hr = S_OK; } CoTaskMemFree (nearestFormat); REFERENCE_TIME defaultPeriod = 0, minPeriod = 0; if (useExclusiveMode) check (client->GetDevicePeriod (&defaultPeriod, &minPeriod)); GUID session; if (hr == S_OK && check (client->Initialize (useExclusiveMode ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, defaultPeriod, defaultPeriod, (WAVEFORMATEX*) &format, &session))) { actualNumChannels = format.Format.nChannels; const bool isFloat = format.Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE && format.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; bytesPerSample = format.Format.wBitsPerSample / 8; updateFormat (isFloat); return true; } return false; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIDeviceBase); }; //============================================================================== class WASAPIInputDevice : public WASAPIDeviceBase { public: WASAPIInputDevice (const ComSmartPtr & device_, const bool useExclusiveMode_) : WASAPIDeviceBase (device_, useExclusiveMode_), reservoir (1, 1) { } ~WASAPIInputDevice() { close(); } bool open (const double newSampleRate, const BigInteger& newChannels) { reservoirSize = 0; reservoirCapacity = 16384; reservoir.setSize (actualNumChannels * reservoirCapacity * sizeof (float)); return openClient (newSampleRate, newChannels) && (numChannels == 0 || check (client->GetService (__uuidof (IAudioCaptureClient), (void**) captureClient.resetAndGetPointerAddress()))); } void close() { closeClient(); captureClient = nullptr; reservoir.setSize (0); } template void updateFormatWithType (SourceType*) { typedef AudioData::Pointer NativeType; converter = new AudioData::ConverterInstance , NativeType> (actualNumChannels, 1); } void updateFormat (bool isFloat) { if (isFloat) updateFormatWithType ((AudioData::Float32*) 0); else if (bytesPerSample == 4) updateFormatWithType ((AudioData::Int32*) 0); else if (bytesPerSample == 3) updateFormatWithType ((AudioData::Int24*) 0); else updateFormatWithType ((AudioData::Int16*) 0); } void copyBuffers (float** destBuffers, int numDestBuffers, int bufferSize, Thread& thread) { if (numChannels <= 0) return; int offset = 0; while (bufferSize > 0) { if (reservoirSize > 0) // There's stuff in the reservoir, so use that... { const int samplesToDo = jmin (bufferSize, (int) reservoirSize); for (int i = 0; i < numDestBuffers; ++i) converter->convertSamples (destBuffers[i] + offset, 0, reservoir.getData(), channelMaps.getUnchecked(i), samplesToDo); bufferSize -= samplesToDo; offset += samplesToDo; reservoirSize = 0; } else { UINT32 packetLength = 0; if (! check (captureClient->GetNextPacketSize (&packetLength))) break; if (packetLength == 0) { if (thread.threadShouldExit() || WaitForSingleObject (clientEvent, 1000) == WAIT_TIMEOUT) break; continue; } uint8* inputData; UINT32 numSamplesAvailable; DWORD flags; if (check (captureClient->GetBuffer (&inputData, &numSamplesAvailable, &flags, 0, 0))) { const int samplesToDo = jmin (bufferSize, (int) numSamplesAvailable); for (int i = 0; i < numDestBuffers; ++i) converter->convertSamples (destBuffers[i] + offset, 0, inputData, channelMaps.getUnchecked(i), samplesToDo); bufferSize -= samplesToDo; offset += samplesToDo; if (samplesToDo < (int) numSamplesAvailable) { reservoirSize = jmin ((int) (numSamplesAvailable - samplesToDo), reservoirCapacity); memcpy ((uint8*) reservoir.getData(), inputData + bytesPerSample * actualNumChannels * samplesToDo, (size_t) (bytesPerSample * actualNumChannels * reservoirSize)); } captureClient->ReleaseBuffer (numSamplesAvailable); } } } } ComSmartPtr captureClient; MemoryBlock reservoir; int reservoirSize, reservoirCapacity; ScopedPointer converter; private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIInputDevice); }; //============================================================================== class WASAPIOutputDevice : public WASAPIDeviceBase { public: WASAPIOutputDevice (const ComSmartPtr & device_, const bool useExclusiveMode_) : WASAPIDeviceBase (device_, useExclusiveMode_) { } ~WASAPIOutputDevice() { close(); } bool open (const double newSampleRate, const BigInteger& newChannels) { return openClient (newSampleRate, newChannels) && (numChannels == 0 || check (client->GetService (__uuidof (IAudioRenderClient), (void**) renderClient.resetAndGetPointerAddress()))); } void close() { closeClient(); renderClient = nullptr; } template void updateFormatWithType (DestType*) { typedef AudioData::Pointer NativeType; converter = new AudioData::ConverterInstance > (1, actualNumChannels); } void updateFormat (bool isFloat) { if (isFloat) updateFormatWithType ((AudioData::Float32*) 0); else if (bytesPerSample == 4) updateFormatWithType ((AudioData::Int32*) 0); else if (bytesPerSample == 3) updateFormatWithType ((AudioData::Int24*) 0); else updateFormatWithType ((AudioData::Int16*) 0); } void copyBuffers (const float** const srcBuffers, const int numSrcBuffers, int bufferSize, Thread& thread) { if (numChannels <= 0) return; int offset = 0; while (bufferSize > 0) { UINT32 padding = 0; if (! check (client->GetCurrentPadding (&padding))) return; int samplesToDo = useExclusiveMode ? bufferSize : jmin ((int) (actualBufferSize - padding), bufferSize); if (samplesToDo <= 0) { if (thread.threadShouldExit() || WaitForSingleObject (clientEvent, 1000) == WAIT_TIMEOUT) break; continue; } uint8* outputData = nullptr; if (check (renderClient->GetBuffer ((UINT32) samplesToDo, &outputData))) { for (int i = 0; i < numSrcBuffers; ++i) converter->convertSamples (outputData, channelMaps.getUnchecked(i), srcBuffers[i] + offset, 0, samplesToDo); renderClient->ReleaseBuffer ((UINT32) samplesToDo, 0); offset += samplesToDo; bufferSize -= samplesToDo; } } } ComSmartPtr renderClient; ScopedPointer converter; private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIOutputDevice); }; //============================================================================== class WASAPIAudioIODevice : public AudioIODevice, public Thread { public: WASAPIAudioIODevice (const String& deviceName, const String& outputDeviceId_, const String& inputDeviceId_, const bool useExclusiveMode_) : AudioIODevice (deviceName, "Windows Audio"), Thread ("Juce WASAPI"), outputDeviceId (outputDeviceId_), inputDeviceId (inputDeviceId_), useExclusiveMode (useExclusiveMode_), isOpen_ (false), isStarted (false), currentBufferSizeSamples (0), currentSampleRate (0), callback (nullptr) { } ~WASAPIAudioIODevice() { close(); } bool initialise() { latencyIn = latencyOut = 0; Array ratesIn, ratesOut; if (createDevices()) { jassert (inputDevice != nullptr || outputDevice != nullptr); if (inputDevice != nullptr && outputDevice != nullptr) { defaultSampleRate = jmin (inputDevice->defaultSampleRate, outputDevice->defaultSampleRate); minBufferSize = jmin (inputDevice->minBufferSize, outputDevice->minBufferSize); defaultBufferSize = jmax (inputDevice->defaultBufferSize, outputDevice->defaultBufferSize); sampleRates = inputDevice->rates; sampleRates.removeValuesNotIn (outputDevice->rates); } else { WASAPIDeviceBase* d = inputDevice != nullptr ? static_cast (inputDevice) : static_cast (outputDevice); defaultSampleRate = d->defaultSampleRate; minBufferSize = d->minBufferSize; defaultBufferSize = d->defaultBufferSize; sampleRates = d->rates; } bufferSizes.addUsingDefaultSort (defaultBufferSize); if (minBufferSize != defaultBufferSize) bufferSizes.addUsingDefaultSort (minBufferSize); int n = 64; for (int i = 0; i < 40; ++i) { if (n >= minBufferSize && n <= 2048 && ! bufferSizes.contains (n)) bufferSizes.addUsingDefaultSort (n); n += (n < 512) ? 32 : (n < 1024 ? 64 : 128); } return true; } return false; } StringArray getOutputChannelNames() { StringArray outChannels; if (outputDevice != nullptr) for (int i = 1; i <= outputDevice->actualNumChannels; ++i) outChannels.add ("Output channel " + String (i)); return outChannels; } StringArray getInputChannelNames() { StringArray inChannels; if (inputDevice != nullptr) for (int i = 1; i <= inputDevice->actualNumChannels; ++i) inChannels.add ("Input channel " + String (i)); return inChannels; } int getNumSampleRates() { return sampleRates.size(); } double getSampleRate (int index) { return sampleRates [index]; } int getNumBufferSizesAvailable() { return bufferSizes.size(); } int getBufferSizeSamples (int index) { return bufferSizes [index]; } int getDefaultBufferSize() { return defaultBufferSize; } int getCurrentBufferSizeSamples() { return currentBufferSizeSamples; } double getCurrentSampleRate() { return currentSampleRate; } int getCurrentBitDepth() { return 32; } int getOutputLatencyInSamples() { return latencyOut; } int getInputLatencyInSamples() { return latencyIn; } BigInteger getActiveOutputChannels() const { return outputDevice != nullptr ? outputDevice->channels : BigInteger(); } BigInteger getActiveInputChannels() const { return inputDevice != nullptr ? inputDevice->channels : BigInteger(); } String getLastError() { return lastError; } String open (const BigInteger& inputChannels, const BigInteger& outputChannels, double sampleRate, int bufferSizeSamples) { close(); lastError = String::empty; if (sampleRates.size() == 0 && inputDevice != nullptr && outputDevice != nullptr) { lastError = "The input and output devices don't share a common sample rate!"; return lastError; } currentBufferSizeSamples = bufferSizeSamples <= 0 ? defaultBufferSize : jmax (bufferSizeSamples, minBufferSize); currentSampleRate = sampleRate > 0 ? sampleRate : defaultSampleRate; if (inputDevice != nullptr && ! inputDevice->open (currentSampleRate, inputChannels)) { lastError = "Couldn't open the input device!"; return lastError; } if (outputDevice != nullptr && ! outputDevice->open (currentSampleRate, outputChannels)) { close(); lastError = "Couldn't open the output device!"; return lastError; } if (inputDevice != nullptr) ResetEvent (inputDevice->clientEvent); if (outputDevice != nullptr) ResetEvent (outputDevice->clientEvent); startThread (8); Thread::sleep (5); if (inputDevice != nullptr && inputDevice->client != nullptr) { latencyIn = (int) (inputDevice->latencySamples + currentBufferSizeSamples); if (! check (inputDevice->client->Start())) { close(); lastError = "Couldn't start the input device!"; return lastError; } } if (outputDevice != nullptr && outputDevice->client != nullptr) { latencyOut = (int) (outputDevice->latencySamples + currentBufferSizeSamples); if (! check (outputDevice->client->Start())) { close(); lastError = "Couldn't start the output device!"; return lastError; } } isOpen_ = true; return lastError; } void close() { stop(); signalThreadShouldExit(); if (inputDevice != nullptr) SetEvent (inputDevice->clientEvent); if (outputDevice != nullptr) SetEvent (outputDevice->clientEvent); stopThread (5000); if (inputDevice != nullptr) inputDevice->close(); if (outputDevice != nullptr) outputDevice->close(); isOpen_ = false; } bool isOpen() { return isOpen_ && isThreadRunning(); } bool isPlaying() { return isStarted && isOpen_ && isThreadRunning(); } void start (AudioIODeviceCallback* call) { if (isOpen_ && call != nullptr && ! isStarted) { if (! isThreadRunning()) { // something's gone wrong and the thread's stopped.. isOpen_ = false; return; } call->audioDeviceAboutToStart (this); const ScopedLock sl (startStopLock); callback = call; isStarted = true; } } void stop() { if (isStarted) { AudioIODeviceCallback* const callbackLocal = callback; { const ScopedLock sl (startStopLock); isStarted = false; } if (callbackLocal != nullptr) callbackLocal->audioDeviceStopped(); } } void setMMThreadPriority() { DynamicLibrary dll ("avrt.dll"); JUCE_LOAD_WINAPI_FUNCTION (dll, AvSetMmThreadCharacteristicsW, avSetMmThreadCharacteristics, HANDLE, (LPCWSTR, LPDWORD)) JUCE_LOAD_WINAPI_FUNCTION (dll, AvSetMmThreadPriority, avSetMmThreadPriority, HANDLE, (HANDLE, AVRT_PRIORITY)) if (avSetMmThreadCharacteristics != 0 && avSetMmThreadPriority != 0) { DWORD dummy = 0; HANDLE h = avSetMmThreadCharacteristics (L"Pro Audio", &dummy); if (h != 0) avSetMmThreadPriority (h, AVRT_PRIORITY_NORMAL); } } void run() { setMMThreadPriority(); const int bufferSize = currentBufferSizeSamples; const int numInputBuffers = getActiveInputChannels().countNumberOfSetBits(); const int numOutputBuffers = getActiveOutputChannels().countNumberOfSetBits(); bool sampleRateChanged = false; AudioSampleBuffer ins (jmax (1, numInputBuffers), bufferSize + 32); AudioSampleBuffer outs (jmax (1, numOutputBuffers), bufferSize + 32); float** const inputBuffers = ins.getArrayOfChannels(); float** const outputBuffers = outs.getArrayOfChannels(); ins.clear(); while (! threadShouldExit()) { if (inputDevice != nullptr) { inputDevice->copyBuffers (inputBuffers, numInputBuffers, bufferSize, *this); if (threadShouldExit()) break; if (inputDevice->sampleRateHasChanged) sampleRateChanged = true; } JUCE_TRY { const ScopedLock sl (startStopLock); if (isStarted) callback->audioDeviceIOCallback (const_cast (inputBuffers), numInputBuffers, outputBuffers, numOutputBuffers, bufferSize); else outs.clear(); } JUCE_CATCH_EXCEPTION if (outputDevice != nullptr) { outputDevice->copyBuffers (const_cast (outputBuffers), numOutputBuffers, bufferSize, *this); if (outputDevice->sampleRateHasChanged) sampleRateChanged = true; } if (sampleRateChanged) { // xxx one of the devices has had its sample rate changed externally.. not 100% sure how // to handle this.. } } } //============================================================================== String outputDeviceId, inputDeviceId; String lastError; private: // Device stats... ScopedPointer inputDevice; ScopedPointer outputDevice; const bool useExclusiveMode; double defaultSampleRate; int minBufferSize, defaultBufferSize; int latencyIn, latencyOut; Array sampleRates; Array bufferSizes; // Active state... bool isOpen_, isStarted; int currentBufferSizeSamples; double currentSampleRate; AudioIODeviceCallback* callback; CriticalSection startStopLock; //============================================================================== bool createDevices() { ComSmartPtr enumerator; if (! check (enumerator.CoCreateInstance (__uuidof (MMDeviceEnumerator)))) return false; ComSmartPtr deviceCollection; if (! check (enumerator->EnumAudioEndpoints (eAll, DEVICE_STATE_ACTIVE, deviceCollection.resetAndGetPointerAddress()))) return false; UINT32 numDevices = 0; if (! check (deviceCollection->GetCount (&numDevices))) return false; for (UINT32 i = 0; i < numDevices; ++i) { ComSmartPtr device; if (! check (deviceCollection->Item (i, device.resetAndGetPointerAddress()))) continue; const String deviceId (getDeviceID (device)); if (deviceId.isEmpty()) continue; const EDataFlow flow = getDataFlow (device); if (deviceId == inputDeviceId && flow == eCapture) inputDevice = new WASAPIInputDevice (device, useExclusiveMode); else if (deviceId == outputDeviceId && flow == eRender) outputDevice = new WASAPIOutputDevice (device, useExclusiveMode); } return (outputDeviceId.isEmpty() || (outputDevice != nullptr && outputDevice->isOk())) && (inputDeviceId.isEmpty() || (inputDevice != nullptr && inputDevice->isOk())); } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIAudioIODevice); }; //============================================================================== class WASAPIAudioIODeviceType : public AudioIODeviceType, private DeviceChangeDetector { public: WASAPIAudioIODeviceType() : AudioIODeviceType ("Windows Audio"), DeviceChangeDetector (L"Windows Audio"), hasScanned (false) { } //============================================================================== void scanForDevices() { hasScanned = true; outputDeviceNames.clear(); inputDeviceNames.clear(); outputDeviceIds.clear(); inputDeviceIds.clear(); scan (outputDeviceNames, inputDeviceNames, outputDeviceIds, inputDeviceIds); } StringArray getDeviceNames (bool wantInputNames) const { jassert (hasScanned); // need to call scanForDevices() before doing this return wantInputNames ? inputDeviceNames : outputDeviceNames; } int getDefaultDeviceIndex (bool /*forInput*/) const { jassert (hasScanned); // need to call scanForDevices() before doing this return 0; } int getIndexOfDevice (AudioIODevice* device, bool asInput) const { jassert (hasScanned); // need to call scanForDevices() before doing this WASAPIAudioIODevice* const d = dynamic_cast (device); return d == nullptr ? -1 : (asInput ? inputDeviceIds.indexOf (d->inputDeviceId) : outputDeviceIds.indexOf (d->outputDeviceId)); } bool hasSeparateInputsAndOutputs() const { return true; } AudioIODevice* createDevice (const String& outputDeviceName, const String& inputDeviceName) { jassert (hasScanned); // need to call scanForDevices() before doing this const bool useExclusiveMode = false; ScopedPointer device; const int outputIndex = outputDeviceNames.indexOf (outputDeviceName); const int inputIndex = inputDeviceNames.indexOf (inputDeviceName); if (outputIndex >= 0 || inputIndex >= 0) { device = new WASAPIAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName, outputDeviceIds [outputIndex], inputDeviceIds [inputIndex], useExclusiveMode); if (! device->initialise()) device = nullptr; } return device.release(); } //============================================================================== StringArray outputDeviceNames, outputDeviceIds; StringArray inputDeviceNames, inputDeviceIds; private: bool hasScanned; //============================================================================== static String getDefaultEndpoint (IMMDeviceEnumerator* const enumerator, const bool forCapture) { String s; IMMDevice* dev = nullptr; if (check (enumerator->GetDefaultAudioEndpoint (forCapture ? eCapture : eRender, eMultimedia, &dev))) { WCHAR* deviceId = nullptr; if (check (dev->GetId (&deviceId))) { s = deviceId; CoTaskMemFree (deviceId); } dev->Release(); } return s; } //============================================================================== void scan (StringArray& outputDeviceNames, StringArray& inputDeviceNames, StringArray& outputDeviceIds, StringArray& inputDeviceIds) { ComSmartPtr enumerator; if (! check (enumerator.CoCreateInstance (__uuidof (MMDeviceEnumerator)))) return; const String defaultRenderer (getDefaultEndpoint (enumerator, false)); const String defaultCapture (getDefaultEndpoint (enumerator, true)); ComSmartPtr deviceCollection; UINT32 numDevices = 0; if (! (check (enumerator->EnumAudioEndpoints (eAll, DEVICE_STATE_ACTIVE, deviceCollection.resetAndGetPointerAddress())) && check (deviceCollection->GetCount (&numDevices)))) return; for (UINT32 i = 0; i < numDevices; ++i) { ComSmartPtr device; if (! check (deviceCollection->Item (i, device.resetAndGetPointerAddress()))) continue; DWORD state = 0; if (! (check (device->GetState (&state)) && state == DEVICE_STATE_ACTIVE)) continue; const String deviceId (getDeviceID (device)); String name; { ComSmartPtr properties; if (! check (device->OpenPropertyStore (STGM_READ, properties.resetAndGetPointerAddress()))) continue; PROPVARIANT value; PropVariantInit (&value); if (check (properties->GetValue (PKEY_Device_FriendlyName, &value))) name = value.pwszVal; PropVariantClear (&value); } const EDataFlow flow = getDataFlow (device); if (flow == eRender) { const int index = (deviceId == defaultRenderer) ? 0 : -1; outputDeviceIds.insert (index, deviceId); outputDeviceNames.insert (index, name); } else if (flow == eCapture) { const int index = (deviceId == defaultCapture) ? 0 : -1; inputDeviceIds.insert (index, deviceId); inputDeviceNames.insert (index, name); } } inputDeviceNames.appendNumbersToDuplicates (false, false); outputDeviceNames.appendNumbersToDuplicates (false, false); } //============================================================================== void systemDeviceChanged() { StringArray newOutNames, newInNames, newOutIds, newInIds; scan (newOutNames, newInNames, newOutIds, newInIds); if (newOutNames != outputDeviceNames || newInNames != inputDeviceNames || newOutIds != outputDeviceIds || newInIds != inputDeviceIds) { hasScanned = true; outputDeviceNames = newOutNames; inputDeviceNames = newInNames; outputDeviceIds = newOutIds; inputDeviceIds = newInIds; callDeviceChangeListeners(); } } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIAudioIODeviceType); }; } //============================================================================== AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_WASAPI() { if (SystemStats::getOperatingSystemType() >= SystemStats::WinVista) return new WasapiClasses::WASAPIAudioIODeviceType(); return nullptr; }