From 5c33ed6271a6d1300e9e0fee4a9e1cd52cf16b65 Mon Sep 17 00:00:00 2001 From: falkTX Date: Wed, 16 Jan 2019 13:19:03 +0100 Subject: [PATCH] Revert "Remove juce_audio_devices" This reverts commit ff1bec463509d8142d3f1f70b8bdd25a872e4f2e. Signed-off-by: falkTX --- source/modules/juce_audio_devices/Makefile | 125 + .../audio_io/juce_AudioDeviceManager.cpp | 1018 ++++++++ .../audio_io/juce_AudioDeviceManager.h | 532 +++++ .../audio_io/juce_AudioIODevice.cpp | 45 + .../audio_io/juce_AudioIODevice.h | 319 +++ .../audio_io/juce_AudioIODeviceType.cpp | 81 + .../audio_io/juce_AudioIODeviceType.h | 178 ++ .../audio_io/juce_SystemAudioVolume.h | 57 + .../juce_audio_devices/juce_audio_devices.cpp | 225 ++ .../juce_audio_devices/juce_audio_devices.h | 155 ++ .../midi_io/juce_MidiInput.h | 176 ++ .../midi_io/juce_MidiMessageCollector.cpp | 158 ++ .../midi_io/juce_MidiMessageCollector.h | 103 + .../midi_io/juce_MidiOutput.cpp | 176 ++ .../midi_io/juce_MidiOutput.h | 143 ++ .../native/juce_MidiDataConcatenator.h | 191 ++ .../native/juce_android_Audio.cpp | 497 ++++ .../native/juce_android_Midi.cpp | 364 +++ .../native/juce_android_OpenSL.cpp | 1321 +++++++++++ .../native/juce_ios_Audio.cpp | 1206 ++++++++++ .../native/juce_ios_Audio.h | 93 + .../native/juce_linux_ALSA.cpp | 1314 +++++++++++ .../native/juce_linux_JackAudio.cpp | 624 +++++ .../native/juce_linux_Midi.cpp | 617 +++++ .../native/juce_mac_CoreAudio.cpp | 2073 +++++++++++++++++ .../native/juce_mac_CoreMidi.cpp | 562 +++++ .../native/juce_win32_ASIO.cpp | 1649 +++++++++++++ .../native/juce_win32_DirectSound.cpp | 1301 +++++++++++ .../native/juce_win32_Midi.cpp | 1232 ++++++++++ .../native/juce_win32_WASAPI.cpp | 1731 ++++++++++++++ .../sources/juce_AudioSourcePlayer.cpp | 185 ++ .../sources/juce_AudioSourcePlayer.h | 111 + .../sources/juce_AudioTransportSource.cpp | 286 +++ .../sources/juce_AudioTransportSource.h | 176 ++ 34 files changed, 19024 insertions(+) create mode 100644 source/modules/juce_audio_devices/Makefile create mode 100644 source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp create mode 100644 source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.h create mode 100644 source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.cpp create mode 100644 source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.h create mode 100644 source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp create mode 100644 source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h create mode 100644 source/modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h create mode 100644 source/modules/juce_audio_devices/juce_audio_devices.cpp create mode 100644 source/modules/juce_audio_devices/juce_audio_devices.h create mode 100644 source/modules/juce_audio_devices/midi_io/juce_MidiInput.h create mode 100644 source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp create mode 100644 source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h create mode 100644 source/modules/juce_audio_devices/midi_io/juce_MidiOutput.cpp create mode 100644 source/modules/juce_audio_devices/midi_io/juce_MidiOutput.h create mode 100644 source/modules/juce_audio_devices/native/juce_MidiDataConcatenator.h create mode 100644 source/modules/juce_audio_devices/native/juce_android_Audio.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_android_Midi.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_android_OpenSL.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_ios_Audio.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_ios_Audio.h create mode 100644 source/modules/juce_audio_devices/native/juce_linux_ALSA.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_linux_JackAudio.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_linux_Midi.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_mac_CoreAudio.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_win32_ASIO.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_win32_Midi.cpp create mode 100644 source/modules/juce_audio_devices/native/juce_win32_WASAPI.cpp create mode 100644 source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.cpp create mode 100644 source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.h create mode 100644 source/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp create mode 100644 source/modules/juce_audio_devices/sources/juce_AudioTransportSource.h diff --git a/source/modules/juce_audio_devices/Makefile b/source/modules/juce_audio_devices/Makefile new file mode 100644 index 000000000..4910fd058 --- /dev/null +++ b/source/modules/juce_audio_devices/Makefile @@ -0,0 +1,125 @@ +#!/usr/bin/make -f +# Makefile for juce_audio_devices # +# ------------------------------- # +# Created by falkTX +# + +CWD=../.. +MODULENAME=juce_audio_devices +include ../Makefile.mk + +# ---------------------------------------------------------------------------------------------------------------------------- + +BUILD_CXX_FLAGS += $(JUCE_AUDIO_DEVICES_FLAGS) -I.. + +ifeq ($(WIN32),true) +BUILD_CXX_FLAGS += -Wno-missing-field-initializers +# BUILD_CXX_FLAGS += -I$(CWD)/includes/asio +BUILD_CXX_FLAGS += -I$(CWD)/modules/rtaudio/include +endif + +# ---------------------------------------------------------------------------------------------------------------------------- + +ifeq ($(MACOS),true) +OBJS = $(OBJDIR)/$(MODULENAME).mm.o +OBJS_posix32 = $(OBJDIR)/$(MODULENAME).mm.posix32.o +OBJS_posix64 = $(OBJDIR)/$(MODULENAME).mm.posix64.o +else +OBJS = $(OBJDIR)/$(MODULENAME).cpp.o +OBJS_posix32 = $(OBJDIR)/$(MODULENAME).cpp.posix32.o +OBJS_posix64 = $(OBJDIR)/$(MODULENAME).cpp.posix64.o +endif +OBJS_win32 = $(OBJDIR)/$(MODULENAME).cpp.win32.o +OBJS_win64 = $(OBJDIR)/$(MODULENAME).cpp.win64.o + +# ---------------------------------------------------------------------------------------------------------------------------- + +all: $(MODULEDIR)/$(MODULENAME).a +posix32: $(MODULEDIR)/$(MODULENAME).posix32.a +posix64: $(MODULEDIR)/$(MODULENAME).posix64.a +win32: $(MODULEDIR)/$(MODULENAME).win32.a +win64: $(MODULEDIR)/$(MODULENAME).win64.a + +# ---------------------------------------------------------------------------------------------------------------------------- + +clean: + rm -f $(OBJDIR)/*.o $(MODULEDIR)/$(MODULENAME)*.a + +debug: + $(MAKE) DEBUG=true + +# ---------------------------------------------------------------------------------------------------------------------------- + +$(MODULEDIR)/$(MODULENAME).a: $(OBJS) + -@mkdir -p $(MODULEDIR) + @echo "Creating $(MODULENAME).a" + @rm -f $@ + @$(AR) crs $@ $^ + +$(MODULEDIR)/$(MODULENAME).posix32.a: $(OBJS_posix32) + -@mkdir -p $(MODULEDIR) + @echo "Creating $(MODULENAME).posix32.a" + @rm -f $@ + @$(AR) crs $@ $^ + +$(MODULEDIR)/$(MODULENAME).posix64.a: $(OBJS_posix64) + -@mkdir -p $(MODULEDIR) + @echo "Creating $(MODULENAME).posix64.a" + @rm -f $@ + @$(AR) crs $@ $^ + +$(MODULEDIR)/$(MODULENAME).win32.a: $(OBJS_win32) + -@mkdir -p $(MODULEDIR) + @echo "Creating $(MODULENAME).win32.a" + @rm -f $@ + @$(AR) crs $@ $^ + +$(MODULEDIR)/$(MODULENAME).win64.a: $(OBJS_win64) + -@mkdir -p $(MODULEDIR) + @echo "Creating $(MODULENAME).win64.a" + @rm -f $@ + @$(AR) crs $@ $^ + +# ---------------------------------------------------------------------------------------------------------------------------- + +$(OBJDIR)/$(MODULENAME).cpp.o: $(MODULENAME).cpp + -@mkdir -p $(OBJDIR) + @echo "Compiling $<" + @$(CXX) $< $(BUILD_CXX_FLAGS) -c -o $@ + +$(OBJDIR)/$(MODULENAME).cpp.%32.o: $(MODULENAME).cpp + -@mkdir -p $(OBJDIR) + @echo "Compiling $< (32bit)" + @$(CXX) $< $(BUILD_CXX_FLAGS) $(32BIT_FLAGS) -c -o $@ + +$(OBJDIR)/$(MODULENAME).cpp.%64.o: $(MODULENAME).cpp + -@mkdir -p $(OBJDIR) + @echo "Compiling $< (64bit)" + @$(CXX) $< $(BUILD_CXX_FLAGS) $(64BIT_FLAGS) -c -o $@ + +# ---------------------------------------------------------------------------------------------------------------------------- + +$(OBJDIR)/$(MODULENAME).mm.o: $(MODULENAME).cpp + -@mkdir -p $(OBJDIR) + @echo "Compiling $<" + @$(CXX) $< $(BUILD_CXX_FLAGS) -ObjC++ -c -o $@ + +$(OBJDIR)/$(MODULENAME).mm.%32.o: $(MODULENAME).cpp + -@mkdir -p $(OBJDIR) + @echo "Compiling $< (32bit)" + @$(CXX) $< $(BUILD_CXX_FLAGS) $(32BIT_FLAGS) -ObjC++ -c -o $@ + +$(OBJDIR)/$(MODULENAME).mm.%64.o: $(MODULENAME).cpp + -@mkdir -p $(OBJDIR) + @echo "Compiling $< (64bit)" + @$(CXX) $< $(BUILD_CXX_FLAGS) $(64BIT_FLAGS) -ObjC++ -c -o $@ + +# ---------------------------------------------------------------------------------------------------------------------------- + +-include $(OBJS:%.o=%.d) +-include $(OBJS_posix32:%.o=%.d) +-include $(OBJS_posix64:%.o=%.d) +-include $(OBJS_win32:%.o=%.d) +-include $(OBJS_win64:%.o=%.d) + +# ---------------------------------------------------------------------------------------------------------------------------- diff --git a/source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp b/source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp new file mode 100644 index 000000000..1cc237e58 --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp @@ -0,0 +1,1018 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +AudioDeviceManager::AudioDeviceSetup::AudioDeviceSetup() + : sampleRate (0), + bufferSize (0), + useDefaultInputChannels (true), + useDefaultOutputChannels (true) +{ +} + +bool AudioDeviceManager::AudioDeviceSetup::operator== (const AudioDeviceManager::AudioDeviceSetup& other) const +{ + return outputDeviceName == other.outputDeviceName + && inputDeviceName == other.inputDeviceName + && sampleRate == other.sampleRate + && bufferSize == other.bufferSize + && inputChannels == other.inputChannels + && useDefaultInputChannels == other.useDefaultInputChannels + && outputChannels == other.outputChannels + && useDefaultOutputChannels == other.useDefaultOutputChannels; +} + +//============================================================================== +class AudioDeviceManager::CallbackHandler : public AudioIODeviceCallback, + public MidiInputCallback, + public AudioIODeviceType::Listener +{ +public: + CallbackHandler (AudioDeviceManager& adm) noexcept : owner (adm) {} + +private: + void audioDeviceIOCallback (const float** ins, int numIns, float** outs, int numOuts, int numSamples) override + { + owner.audioDeviceIOCallbackInt (ins, numIns, outs, numOuts, numSamples); + } + + void audioDeviceAboutToStart (AudioIODevice* device) override + { + owner.audioDeviceAboutToStartInt (device); + } + + void audioDeviceStopped() override + { + owner.audioDeviceStoppedInt(); + } + + void audioDeviceError (const String& message) override + { + owner.audioDeviceErrorInt (message); + } + + void handleIncomingMidiMessage (MidiInput* source, const MidiMessage& message) override + { + owner.handleIncomingMidiMessageInt (source, message); + } + + void audioDeviceListChanged() override + { + owner.audioDeviceListChanged(); + } + + AudioDeviceManager& owner; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CallbackHandler) +}; + +//============================================================================== +AudioDeviceManager::AudioDeviceManager() + : numInputChansNeeded (0), + numOutputChansNeeded (2), + listNeedsScanning (true), + testSoundPosition (0), + cpuUsageMs (0), + timeToCpuScale (0) +{ + callbackHandler = new CallbackHandler (*this); +} + +AudioDeviceManager::~AudioDeviceManager() +{ + currentAudioDevice = nullptr; + defaultMidiOutput = nullptr; +} + +//============================================================================== +void AudioDeviceManager::createDeviceTypesIfNeeded() +{ + if (availableDeviceTypes.size() == 0) + { + OwnedArray types; + createAudioDeviceTypes (types); + + for (int i = 0; i < types.size(); ++i) + addAudioDeviceType (types.getUnchecked(i)); + + types.clear (false); + + if (AudioIODeviceType* first = availableDeviceTypes.getFirst()) + currentDeviceType = first->getTypeName(); + } +} + +const OwnedArray& AudioDeviceManager::getAvailableDeviceTypes() +{ + scanDevicesIfNeeded(); + return availableDeviceTypes; +} + +void AudioDeviceManager::audioDeviceListChanged() +{ + if (currentAudioDevice != nullptr) + { + currentSetup.sampleRate = currentAudioDevice->getCurrentSampleRate(); + currentSetup.bufferSize = currentAudioDevice->getCurrentBufferSizeSamples(); + currentSetup.inputChannels = currentAudioDevice->getActiveInputChannels(); + currentSetup.outputChannels = currentAudioDevice->getActiveOutputChannels(); + } + + sendChangeMessage(); +} + +//============================================================================== +static void addIfNotNull (OwnedArray& list, AudioIODeviceType* const device) +{ + if (device != nullptr) + list.add (device); +} + +void AudioDeviceManager::createAudioDeviceTypes (OwnedArray& list) +{ + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (false)); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_WASAPI (true)); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_DirectSound()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_ASIO()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_CoreAudio()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_iOSAudio()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_JACK()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_ALSA()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_OpenSLES()); + addIfNotNull (list, AudioIODeviceType::createAudioIODeviceType_Android()); +} + +void AudioDeviceManager::addAudioDeviceType (AudioIODeviceType* newDeviceType) +{ + if (newDeviceType != nullptr) + { + jassert (lastDeviceTypeConfigs.size() == availableDeviceTypes.size()); + availableDeviceTypes.add (newDeviceType); + lastDeviceTypeConfigs.add (new AudioDeviceSetup()); + + newDeviceType->addListener (callbackHandler); + } +} + +static bool deviceListContains (AudioIODeviceType* type, bool isInput, const String& name) +{ + StringArray devices (type->getDeviceNames (isInput)); + + for (int i = devices.size(); --i >= 0;) + if (devices[i].trim().equalsIgnoreCase (name.trim())) + return true; + + return false; +} + +//============================================================================== +String AudioDeviceManager::initialise (const int numInputChannelsNeeded, + const int numOutputChannelsNeeded, + const XmlElement* const xml, + const bool selectDefaultDeviceOnFailure, + const String& preferredDefaultDeviceName, + const AudioDeviceSetup* preferredSetupOptions) +{ + scanDevicesIfNeeded(); + + numInputChansNeeded = numInputChannelsNeeded; + numOutputChansNeeded = numOutputChannelsNeeded; + + if (xml != nullptr && xml->hasTagName ("DEVICESETUP")) + return initialiseFromXML (*xml, selectDefaultDeviceOnFailure, + preferredDefaultDeviceName, preferredSetupOptions); + + return initialiseDefault (preferredDefaultDeviceName, preferredSetupOptions); +} + +String AudioDeviceManager::initialiseDefault (const String& preferredDefaultDeviceName, + const AudioDeviceSetup* preferredSetupOptions) +{ + AudioDeviceSetup setup; + + if (preferredSetupOptions != nullptr) + { + setup = *preferredSetupOptions; + } + else if (preferredDefaultDeviceName.isNotEmpty()) + { + for (int j = availableDeviceTypes.size(); --j >= 0;) + { + AudioIODeviceType* const type = availableDeviceTypes.getUnchecked(j); + + const StringArray outs (type->getDeviceNames (false)); + + for (int i = 0; i < outs.size(); ++i) + { + if (outs[i].matchesWildcard (preferredDefaultDeviceName, true)) + { + setup.outputDeviceName = outs[i]; + break; + } + } + + const StringArray ins (type->getDeviceNames (true)); + + for (int i = 0; i < ins.size(); ++i) + { + if (ins[i].matchesWildcard (preferredDefaultDeviceName, true)) + { + setup.inputDeviceName = ins[i]; + break; + } + } + } + } + + insertDefaultDeviceNames (setup); + return setAudioDeviceSetup (setup, false); +} + +String AudioDeviceManager::initialiseFromXML (const XmlElement& xml, + const bool selectDefaultDeviceOnFailure, + const String& preferredDefaultDeviceName, + const AudioDeviceSetup* preferredSetupOptions) +{ + lastExplicitSettings = new XmlElement (xml); + + String error; + AudioDeviceSetup setup; + + if (preferredSetupOptions != nullptr) + setup = *preferredSetupOptions; + + if (xml.getStringAttribute ("audioDeviceName").isNotEmpty()) + { + setup.inputDeviceName = setup.outputDeviceName + = xml.getStringAttribute ("audioDeviceName"); + } + else + { + setup.inputDeviceName = xml.getStringAttribute ("audioInputDeviceName"); + setup.outputDeviceName = xml.getStringAttribute ("audioOutputDeviceName"); + } + + currentDeviceType = xml.getStringAttribute ("deviceType"); + + if (findType (currentDeviceType) == nullptr) + { + if (AudioIODeviceType* const type = findType (setup.inputDeviceName, setup.outputDeviceName)) + currentDeviceType = type->getTypeName(); + else if (availableDeviceTypes.size() > 0) + currentDeviceType = availableDeviceTypes.getUnchecked(0)->getTypeName(); + } + + setup.bufferSize = xml.getIntAttribute ("audioDeviceBufferSize", setup.bufferSize); + setup.sampleRate = xml.getDoubleAttribute ("audioDeviceRate", setup.sampleRate); + + setup.inputChannels .parseString (xml.getStringAttribute ("audioDeviceInChans", "11"), 2); + setup.outputChannels.parseString (xml.getStringAttribute ("audioDeviceOutChans", "11"), 2); + + setup.useDefaultInputChannels = ! xml.hasAttribute ("audioDeviceInChans"); + setup.useDefaultOutputChannels = ! xml.hasAttribute ("audioDeviceOutChans"); + + error = setAudioDeviceSetup (setup, true); + + midiInsFromXml.clear(); + + forEachXmlChildElementWithTagName (xml, c, "MIDIINPUT") + midiInsFromXml.add (c->getStringAttribute ("name")); + + const StringArray allMidiIns (MidiInput::getDevices()); + + for (int i = allMidiIns.size(); --i >= 0;) + setMidiInputEnabled (allMidiIns[i], midiInsFromXml.contains (allMidiIns[i])); + + if (error.isNotEmpty() && selectDefaultDeviceOnFailure) + error = initialise (numInputChansNeeded, numOutputChansNeeded, + nullptr, false, preferredDefaultDeviceName); + + setDefaultMidiOutput (xml.getStringAttribute ("defaultMidiOutput")); + + return error; +} + +String AudioDeviceManager::initialiseWithDefaultDevices (int numInputChannelsNeeded, + int numOutputChannelsNeeded) +{ + lastExplicitSettings = nullptr; + + return initialise (numInputChannelsNeeded, numOutputChannelsNeeded, + nullptr, false, String(), nullptr); +} + +void AudioDeviceManager::insertDefaultDeviceNames (AudioDeviceSetup& setup) const +{ + if (AudioIODeviceType* type = getCurrentDeviceTypeObject()) + { + if (setup.outputDeviceName.isEmpty()) + setup.outputDeviceName = type->getDeviceNames (false) [type->getDefaultDeviceIndex (false)]; + + if (setup.inputDeviceName.isEmpty()) + setup.inputDeviceName = type->getDeviceNames (true) [type->getDefaultDeviceIndex (true)]; + } +} + +XmlElement* AudioDeviceManager::createStateXml() const +{ + return lastExplicitSettings.createCopy(); +} + +//============================================================================== +void AudioDeviceManager::scanDevicesIfNeeded() +{ + if (listNeedsScanning) + { + listNeedsScanning = false; + + createDeviceTypesIfNeeded(); + + for (int i = availableDeviceTypes.size(); --i >= 0;) + availableDeviceTypes.getUnchecked(i)->scanForDevices(); + } +} + +AudioIODeviceType* AudioDeviceManager::findType (const String& typeName) +{ + scanDevicesIfNeeded(); + + for (int i = availableDeviceTypes.size(); --i >= 0;) + if (availableDeviceTypes.getUnchecked(i)->getTypeName() == typeName) + return availableDeviceTypes.getUnchecked(i); + + return nullptr; +} + +AudioIODeviceType* AudioDeviceManager::findType (const String& inputName, const String& outputName) +{ + scanDevicesIfNeeded(); + + for (int i = availableDeviceTypes.size(); --i >= 0;) + { + AudioIODeviceType* const type = availableDeviceTypes.getUnchecked(i); + + if ((inputName.isNotEmpty() && deviceListContains (type, true, inputName)) + || (outputName.isNotEmpty() && deviceListContains (type, false, outputName))) + { + return type; + } + } + + return nullptr; +} + +void AudioDeviceManager::getAudioDeviceSetup (AudioDeviceSetup& setup) const +{ + setup = currentSetup; +} + +void AudioDeviceManager::deleteCurrentDevice() +{ + currentAudioDevice = nullptr; + currentSetup.inputDeviceName.clear(); + currentSetup.outputDeviceName.clear(); +} + +void AudioDeviceManager::setCurrentAudioDeviceType (const String& type, + const bool treatAsChosenDevice) +{ + for (int i = 0; i < availableDeviceTypes.size(); ++i) + { + if (availableDeviceTypes.getUnchecked(i)->getTypeName() == type + && currentDeviceType != type) + { + if (currentAudioDevice != nullptr) + { + closeAudioDevice(); + Thread::sleep (1500); // allow a moment for OS devices to sort themselves out, to help + // avoid things like DirectSound/ASIO clashes + } + + currentDeviceType = type; + + AudioDeviceSetup s (*lastDeviceTypeConfigs.getUnchecked(i)); + insertDefaultDeviceNames (s); + + setAudioDeviceSetup (s, treatAsChosenDevice); + + sendChangeMessage(); + break; + } + } +} + +AudioIODeviceType* AudioDeviceManager::getCurrentDeviceTypeObject() const +{ + for (int i = 0; i < availableDeviceTypes.size(); ++i) + if (availableDeviceTypes.getUnchecked(i)->getTypeName() == currentDeviceType) + return availableDeviceTypes.getUnchecked(i); + + return availableDeviceTypes[0]; +} + +String AudioDeviceManager::setAudioDeviceSetup (const AudioDeviceSetup& newSetup, + const bool treatAsChosenDevice) +{ + jassert (&newSetup != ¤tSetup); // this will have no effect + + if (newSetup == currentSetup && currentAudioDevice != nullptr) + return {}; + + if (! (newSetup == currentSetup)) + sendChangeMessage(); + + stopDevice(); + + const String newInputDeviceName (numInputChansNeeded == 0 ? String() : newSetup.inputDeviceName); + const String newOutputDeviceName (numOutputChansNeeded == 0 ? String() : newSetup.outputDeviceName); + + String error; + AudioIODeviceType* type = getCurrentDeviceTypeObject(); + + if (type == nullptr || (newInputDeviceName.isEmpty() && newOutputDeviceName.isEmpty())) + { + deleteCurrentDevice(); + + if (treatAsChosenDevice) + updateXml(); + + return {}; + } + + if (currentSetup.inputDeviceName != newInputDeviceName + || currentSetup.outputDeviceName != newOutputDeviceName + || currentAudioDevice == nullptr) + { + deleteCurrentDevice(); + scanDevicesIfNeeded(); + + if (newOutputDeviceName.isNotEmpty() && ! deviceListContains (type, false, newOutputDeviceName)) + return "No such device: " + newOutputDeviceName; + + if (newInputDeviceName.isNotEmpty() && ! deviceListContains (type, true, newInputDeviceName)) + return "No such device: " + newInputDeviceName; + + currentAudioDevice = type->createDevice (newOutputDeviceName, newInputDeviceName); + + if (currentAudioDevice == nullptr) + error = "Can't open the audio device!\n\n" + "This may be because another application is currently using the same device - " + "if so, you should close any other applications and try again!"; + else + error = currentAudioDevice->getLastError(); + + if (error.isNotEmpty()) + { + deleteCurrentDevice(); + return error; + } + + if (newSetup.useDefaultInputChannels) + { + inputChannels.clear(); + inputChannels.setRange (0, numInputChansNeeded, true); + } + + if (newSetup.useDefaultOutputChannels) + { + outputChannels.clear(); + outputChannels.setRange (0, numOutputChansNeeded, true); + } + + if (newInputDeviceName.isEmpty()) inputChannels.clear(); + if (newOutputDeviceName.isEmpty()) outputChannels.clear(); + } + + if (! newSetup.useDefaultInputChannels) inputChannels = newSetup.inputChannels; + if (! newSetup.useDefaultOutputChannels) outputChannels = newSetup.outputChannels; + + currentSetup = newSetup; + + currentSetup.sampleRate = chooseBestSampleRate (newSetup.sampleRate); + currentSetup.bufferSize = chooseBestBufferSize (newSetup.bufferSize); + + error = currentAudioDevice->open (inputChannels, + outputChannels, + currentSetup.sampleRate, + currentSetup.bufferSize); + + if (error.isEmpty()) + { + currentDeviceType = currentAudioDevice->getTypeName(); + + currentAudioDevice->start (callbackHandler); + + currentSetup.sampleRate = currentAudioDevice->getCurrentSampleRate(); + currentSetup.bufferSize = currentAudioDevice->getCurrentBufferSizeSamples(); + currentSetup.inputChannels = currentAudioDevice->getActiveInputChannels(); + currentSetup.outputChannels = currentAudioDevice->getActiveOutputChannels(); + + for (int i = 0; i < availableDeviceTypes.size(); ++i) + if (availableDeviceTypes.getUnchecked (i)->getTypeName() == currentDeviceType) + *(lastDeviceTypeConfigs.getUnchecked (i)) = currentSetup; + + if (treatAsChosenDevice) + updateXml(); + } + else + { + deleteCurrentDevice(); + } + + return error; +} + +double AudioDeviceManager::chooseBestSampleRate (double rate) const +{ + jassert (currentAudioDevice != nullptr); + + const Array rates (currentAudioDevice->getAvailableSampleRates()); + + if (rate > 0 && rates.contains (rate)) + return rate; + + rate = currentAudioDevice->getCurrentSampleRate(); + + if (rate > 0 && rates.contains (rate)) + return rate; + + double lowestAbove44 = 0.0; + + for (int i = rates.size(); --i >= 0;) + { + const double sr = rates[i]; + + if (sr >= 44100.0 && (lowestAbove44 < 1.0 || sr < lowestAbove44)) + lowestAbove44 = sr; + } + + if (lowestAbove44 > 0.0) + return lowestAbove44; + + return rates[0]; +} + +int AudioDeviceManager::chooseBestBufferSize (int bufferSize) const +{ + jassert (currentAudioDevice != nullptr); + + if (bufferSize > 0 && currentAudioDevice->getAvailableBufferSizes().contains (bufferSize)) + return bufferSize; + + return currentAudioDevice->getDefaultBufferSize(); +} + +void AudioDeviceManager::stopDevice() +{ + if (currentAudioDevice != nullptr) + currentAudioDevice->stop(); + + testSound = nullptr; +} + +void AudioDeviceManager::closeAudioDevice() +{ + stopDevice(); + currentAudioDevice = nullptr; +} + +void AudioDeviceManager::restartLastAudioDevice() +{ + if (currentAudioDevice == nullptr) + { + if (currentSetup.inputDeviceName.isEmpty() + && currentSetup.outputDeviceName.isEmpty()) + { + // This method will only reload the last device that was running + // before closeAudioDevice() was called - you need to actually open + // one first, with setAudioDevice(). + jassertfalse; + return; + } + + AudioDeviceSetup s (currentSetup); + setAudioDeviceSetup (s, false); + } +} + +void AudioDeviceManager::updateXml() +{ + lastExplicitSettings = new XmlElement ("DEVICESETUP"); + + lastExplicitSettings->setAttribute ("deviceType", currentDeviceType); + lastExplicitSettings->setAttribute ("audioOutputDeviceName", currentSetup.outputDeviceName); + lastExplicitSettings->setAttribute ("audioInputDeviceName", currentSetup.inputDeviceName); + + if (currentAudioDevice != nullptr) + { + lastExplicitSettings->setAttribute ("audioDeviceRate", currentAudioDevice->getCurrentSampleRate()); + + if (currentAudioDevice->getDefaultBufferSize() != currentAudioDevice->getCurrentBufferSizeSamples()) + lastExplicitSettings->setAttribute ("audioDeviceBufferSize", currentAudioDevice->getCurrentBufferSizeSamples()); + + if (! currentSetup.useDefaultInputChannels) + lastExplicitSettings->setAttribute ("audioDeviceInChans", currentSetup.inputChannels.toString (2)); + + if (! currentSetup.useDefaultOutputChannels) + lastExplicitSettings->setAttribute ("audioDeviceOutChans", currentSetup.outputChannels.toString (2)); + } + + for (int i = 0; i < enabledMidiInputs.size(); ++i) + lastExplicitSettings->createNewChildElement ("MIDIINPUT") + ->setAttribute ("name", enabledMidiInputs[i]->getName()); + + if (midiInsFromXml.size() > 0) + { + // Add any midi devices that have been enabled before, but which aren't currently + // open because the device has been disconnected. + const StringArray availableMidiDevices (MidiInput::getDevices()); + + for (int i = 0; i < midiInsFromXml.size(); ++i) + if (! availableMidiDevices.contains (midiInsFromXml[i], true)) + lastExplicitSettings->createNewChildElement ("MIDIINPUT") + ->setAttribute ("name", midiInsFromXml[i]); + } + + if (defaultMidiOutputName.isNotEmpty()) + lastExplicitSettings->setAttribute ("defaultMidiOutput", defaultMidiOutputName); +} + +//============================================================================== +void AudioDeviceManager::addAudioCallback (AudioIODeviceCallback* newCallback) +{ + { + const ScopedLock sl (audioCallbackLock); + if (callbacks.contains (newCallback)) + return; + } + + if (currentAudioDevice != nullptr && newCallback != nullptr) + newCallback->audioDeviceAboutToStart (currentAudioDevice); + + const ScopedLock sl (audioCallbackLock); + callbacks.add (newCallback); +} + +void AudioDeviceManager::removeAudioCallback (AudioIODeviceCallback* callbackToRemove) +{ + if (callbackToRemove != nullptr) + { + bool needsDeinitialising = currentAudioDevice != nullptr; + + { + const ScopedLock sl (audioCallbackLock); + + needsDeinitialising = needsDeinitialising && callbacks.contains (callbackToRemove); + callbacks.removeFirstMatchingValue (callbackToRemove); + } + + if (needsDeinitialising) + callbackToRemove->audioDeviceStopped(); + } +} + +void AudioDeviceManager::audioDeviceIOCallbackInt (const float** inputChannelData, + int numInputChannels, + float** outputChannelData, + int numOutputChannels, + int numSamples) +{ + const ScopedLock sl (audioCallbackLock); + + inputLevelMeter.updateLevel (inputChannelData, numInputChannels, numSamples); + outputLevelMeter.updateLevel (const_cast (outputChannelData), numOutputChannels, numSamples); + + if (callbacks.size() > 0) + { + const double callbackStartTime = Time::getMillisecondCounterHiRes(); + + tempBuffer.setSize (jmax (1, numOutputChannels), jmax (1, numSamples), false, false, true); + + callbacks.getUnchecked(0)->audioDeviceIOCallback (inputChannelData, numInputChannels, + outputChannelData, numOutputChannels, numSamples); + + float** const tempChans = tempBuffer.getArrayOfWritePointers(); + + for (int i = callbacks.size(); --i > 0;) + { + callbacks.getUnchecked(i)->audioDeviceIOCallback (inputChannelData, numInputChannels, + tempChans, numOutputChannels, numSamples); + + for (int chan = 0; chan < numOutputChannels; ++chan) + { + if (const float* const src = tempChans [chan]) + if (float* const dst = outputChannelData [chan]) + for (int j = 0; j < numSamples; ++j) + dst[j] += src[j]; + } + } + + const double msTaken = Time::getMillisecondCounterHiRes() - callbackStartTime; + const double filterAmount = 0.2; + cpuUsageMs += filterAmount * (msTaken - cpuUsageMs); + + if (msTaken > msPerBlock) + xruns++; + } + else + { + for (int i = 0; i < numOutputChannels; ++i) + zeromem (outputChannelData[i], sizeof (float) * (size_t) numSamples); + } + + if (testSound != nullptr) + { + const int numSamps = jmin (numSamples, testSound->getNumSamples() - testSoundPosition); + const float* const src = testSound->getReadPointer (0, testSoundPosition); + + for (int i = 0; i < numOutputChannels; ++i) + for (int j = 0; j < numSamps; ++j) + outputChannelData [i][j] += src[j]; + + testSoundPosition += numSamps; + if (testSoundPosition >= testSound->getNumSamples()) + testSound = nullptr; + } +} + +void AudioDeviceManager::audioDeviceAboutToStartInt (AudioIODevice* const device) +{ + cpuUsageMs = 0; + xruns = 0; + + const double sampleRate = device->getCurrentSampleRate(); + const int blockSize = device->getCurrentBufferSizeSamples(); + + if (sampleRate > 0.0 && blockSize > 0) + { + msPerBlock = 1000.0 * blockSize / sampleRate; + timeToCpuScale = (msPerBlock > 0.0) ? (1.0 / msPerBlock) : 0.0; + } + + { + const ScopedLock sl (audioCallbackLock); + for (int i = callbacks.size(); --i >= 0;) + callbacks.getUnchecked(i)->audioDeviceAboutToStart (device); + } + + sendChangeMessage(); +} + +void AudioDeviceManager::audioDeviceStoppedInt() +{ + cpuUsageMs = 0; + timeToCpuScale = 0; + xruns = 0; + sendChangeMessage(); + + const ScopedLock sl (audioCallbackLock); + for (int i = callbacks.size(); --i >= 0;) + callbacks.getUnchecked(i)->audioDeviceStopped(); +} + +void AudioDeviceManager::audioDeviceErrorInt (const String& message) +{ + const ScopedLock sl (audioCallbackLock); + for (int i = callbacks.size(); --i >= 0;) + callbacks.getUnchecked(i)->audioDeviceError (message); +} + +double AudioDeviceManager::getCpuUsage() const +{ + return jlimit (0.0, 1.0, timeToCpuScale * cpuUsageMs); +} + +//============================================================================== +void AudioDeviceManager::setMidiInputEnabled (const String& name, const bool enabled) +{ + if (enabled != isMidiInputEnabled (name)) + { + if (enabled) + { + const int index = MidiInput::getDevices().indexOf (name); + + if (index >= 0) + { + if (MidiInput* const midiIn = MidiInput::openDevice (index, callbackHandler)) + { + enabledMidiInputs.add (midiIn); + midiIn->start(); + } + } + } + else + { + for (int i = enabledMidiInputs.size(); --i >= 0;) + if (enabledMidiInputs[i]->getName() == name) + enabledMidiInputs.remove (i); + } + + updateXml(); + sendChangeMessage(); + } +} + +bool AudioDeviceManager::isMidiInputEnabled (const String& name) const +{ + for (int i = enabledMidiInputs.size(); --i >= 0;) + if (enabledMidiInputs[i]->getName() == name) + return true; + + return false; +} + +void AudioDeviceManager::addMidiInputCallback (const String& name, MidiInputCallback* callbackToAdd) +{ + removeMidiInputCallback (name, callbackToAdd); + + if (name.isEmpty() || isMidiInputEnabled (name)) + { + const ScopedLock sl (midiCallbackLock); + + MidiCallbackInfo mc; + mc.deviceName = name; + mc.callback = callbackToAdd; + midiCallbacks.add (mc); + } +} + +void AudioDeviceManager::removeMidiInputCallback (const String& name, MidiInputCallback* callbackToRemove) +{ + for (int i = midiCallbacks.size(); --i >= 0;) + { + const MidiCallbackInfo& mc = midiCallbacks.getReference(i); + + if (mc.callback == callbackToRemove && mc.deviceName == name) + { + const ScopedLock sl (midiCallbackLock); + midiCallbacks.remove (i); + } + } +} + +void AudioDeviceManager::handleIncomingMidiMessageInt (MidiInput* source, const MidiMessage& message) +{ + if (! message.isActiveSense()) + { + const ScopedLock sl (midiCallbackLock); + + for (int i = 0; i < midiCallbacks.size(); ++i) + { + const MidiCallbackInfo& mc = midiCallbacks.getReference(i); + + if (mc.deviceName.isEmpty() || mc.deviceName == source->getName()) + mc.callback->handleIncomingMidiMessage (source, message); + } + } +} + +//============================================================================== +void AudioDeviceManager::setDefaultMidiOutput (const String& deviceName) +{ + if (defaultMidiOutputName != deviceName) + { + Array oldCallbacks; + + { + const ScopedLock sl (audioCallbackLock); + oldCallbacks.swapWith (callbacks); + } + + if (currentAudioDevice != nullptr) + for (int i = oldCallbacks.size(); --i >= 0;) + oldCallbacks.getUnchecked(i)->audioDeviceStopped(); + + defaultMidiOutput = nullptr; + defaultMidiOutputName = deviceName; + + if (deviceName.isNotEmpty()) + defaultMidiOutput = MidiOutput::openDevice (MidiOutput::getDevices().indexOf (deviceName)); + + if (currentAudioDevice != nullptr) + for (int i = oldCallbacks.size(); --i >= 0;) + oldCallbacks.getUnchecked(i)->audioDeviceAboutToStart (currentAudioDevice); + + { + const ScopedLock sl (audioCallbackLock); + oldCallbacks.swapWith (callbacks); + } + + updateXml(); + sendChangeMessage(); + } +} + +//============================================================================== +AudioDeviceManager::LevelMeter::LevelMeter() noexcept : level() {} + +void AudioDeviceManager::LevelMeter::updateLevel (const float* const* channelData, int numChannels, int numSamples) noexcept +{ + if (enabled.get() != 0 && numChannels > 0) + { + for (int j = 0; j < numSamples; ++j) + { + float s = 0; + + for (int i = 0; i < numChannels; ++i) + s += std::abs (channelData[i][j]); + + s /= (float) numChannels; + + const double decayFactor = 0.99992; + + if (s > level) + level = s; + else if (level > 0.001f) + level *= decayFactor; + else + level = 0; + } + } + else + { + level = 0; + } +} + +void AudioDeviceManager::LevelMeter::setEnabled (bool shouldBeEnabled) noexcept +{ + enabled.set (shouldBeEnabled ? 1 : 0); + level = 0; +} + +double AudioDeviceManager::LevelMeter::getCurrentLevel() const noexcept +{ + jassert (enabled.get() != 0); // you need to call setEnabled (true) before using this! + return level; +} + +void AudioDeviceManager::playTestSound() +{ + { // cunningly nested to swap, unlock and delete in that order. + ScopedPointer oldSound; + + { + const ScopedLock sl (audioCallbackLock); + oldSound = testSound; + } + } + + testSoundPosition = 0; + + if (currentAudioDevice != nullptr) + { + const double sampleRate = currentAudioDevice->getCurrentSampleRate(); + const int soundLength = (int) sampleRate; + + const double frequency = 440.0; + const float amplitude = 0.5f; + + const double phasePerSample = double_Pi * 2.0 / (sampleRate / frequency); + + AudioSampleBuffer* const newSound = new AudioSampleBuffer (1, soundLength); + + for (int i = 0; i < soundLength; ++i) + newSound->setSample (0, i, amplitude * (float) std::sin (i * phasePerSample)); + + newSound->applyGainRamp (0, 0, soundLength / 10, 0.0f, 1.0f); + newSound->applyGainRamp (0, soundLength - soundLength / 4, soundLength / 4, 1.0f, 0.0f); + + const ScopedLock sl (audioCallbackLock); + testSound = newSound; + } +} + +int AudioDeviceManager::getXRunCount() const noexcept +{ + auto deviceXRuns = (currentAudioDevice != nullptr ? currentAudioDevice->getXRunCount() : -1); + return (deviceXRuns >= 0 ? deviceXRuns : xruns); +} + +double AudioDeviceManager::getCurrentInputLevel() const noexcept { return inputLevelMeter.getCurrentLevel(); } +double AudioDeviceManager::getCurrentOutputLevel() const noexcept { return outputLevelMeter.getCurrentLevel(); } + +void AudioDeviceManager::enableInputLevelMeasurement (bool enable) noexcept { inputLevelMeter.setEnabled (enable); } +void AudioDeviceManager::enableOutputLevelMeasurement (bool enable) noexcept { outputLevelMeter.setEnabled (enable); } + +} // namespace juce diff --git a/source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.h b/source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.h new file mode 100644 index 000000000..6e5d68254 --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.h @@ -0,0 +1,532 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Manages the state of some audio and midi i/o devices. + + This class keeps tracks of a currently-selected audio device, through + with which it continuously streams data from an audio callback, as well as + one or more midi inputs. + + The idea is that your application will create one global instance of this object, + and let it take care of creating and deleting specific types of audio devices + internally. So when the device is changed, your callbacks will just keep running + without having to worry about this. + + The manager can save and reload all of its device settings as XML, which + makes it very easy for you to save and reload the audio setup of your + application. + + And to make it easy to let the user change its settings, there's a component + to do just that - the AudioDeviceSelectorComponent class, which contains a set of + device selection/sample-rate/latency controls. + + To use an AudioDeviceManager, create one, and use initialise() to set it up. Then + call addAudioCallback() to register your audio callback with it, and use that to process + your audio data. + + The manager also acts as a handy hub for incoming midi messages, allowing a + listener to register for messages from either a specific midi device, or from whatever + the current default midi input device is. The listener then doesn't have to worry about + re-registering with different midi devices if they are changed or deleted. + + And yet another neat trick is that amount of CPU time being used is measured and + available with the getCpuUsage() method. + + The AudioDeviceManager is a ChangeBroadcaster, and will send a change message to + listeners whenever one of its settings is changed. + + @see AudioDeviceSelectorComponent, AudioIODevice, AudioIODeviceType +*/ +class JUCE_API AudioDeviceManager : public ChangeBroadcaster +{ +public: + //============================================================================== + /** Creates a default AudioDeviceManager. + + Initially no audio device will be selected. You should call the initialise() method + and register an audio callback with setAudioCallback() before it'll be able to + actually make any noise. + */ + AudioDeviceManager(); + + /** Destructor. */ + ~AudioDeviceManager(); + + //============================================================================== + /** + This structure holds a set of properties describing the current audio setup. + + An AudioDeviceManager uses this class to save/load its current settings, and to + specify your preferred options when opening a device. + + @see AudioDeviceManager::setAudioDeviceSetup(), AudioDeviceManager::initialise() + */ + struct JUCE_API AudioDeviceSetup + { + /** Creates an AudioDeviceSetup object. + + The default constructor sets all the member variables to indicate default values. + You can then fill-in any values you want to before passing the object to + AudioDeviceManager::initialise(). + */ + AudioDeviceSetup(); + + bool operator== (const AudioDeviceSetup& other) const; + + /** The name of the audio device used for output. + The name has to be one of the ones listed by the AudioDeviceManager's currently + selected device type. + This may be the same as the input device. + An empty string indicates the default device. + */ + String outputDeviceName; + + /** The name of the audio device used for input. + This may be the same as the output device. + An empty string indicates the default device. + */ + String inputDeviceName; + + /** The current sample rate. + This rate is used for both the input and output devices. + A value of 0 indicates that you don't care what rate is used, and the + device will choose a sensible rate for you. + */ + double sampleRate; + + /** The buffer size, in samples. + This buffer size is used for both the input and output devices. + A value of 0 indicates the default buffer size. + */ + int bufferSize; + + /** The set of active input channels. + The bits that are set in this array indicate the channels of the + input device that are active. + If useDefaultInputChannels is true, this value is ignored. + */ + BigInteger inputChannels; + + /** If this is true, it indicates that the inputChannels array + should be ignored, and instead, the device's default channels + should be used. + */ + bool useDefaultInputChannels; + + /** The set of active output channels. + The bits that are set in this array indicate the channels of the + input device that are active. + If useDefaultOutputChannels is true, this value is ignored. + */ + BigInteger outputChannels; + + /** If this is true, it indicates that the outputChannels array + should be ignored, and instead, the device's default channels + should be used. + */ + bool useDefaultOutputChannels; + }; + + + //============================================================================== + /** Opens a set of audio devices ready for use. + + This will attempt to open either a default audio device, or one that was + previously saved as XML. + + @param numInputChannelsNeeded the maximum number of input channels your app would like to + use (the actual number of channels opened may be less than + the number requested) + @param numOutputChannelsNeeded the maximum number of output channels your app would like to + use (the actual number of channels opened may be less than + the number requested) + @param savedState either a previously-saved state that was produced + by createStateXml(), or nullptr if you want the manager + to choose the best device to open. + @param selectDefaultDeviceOnFailure if true, then if the device specified in the XML + fails to open, then a default device will be used + instead. If false, then on failure, no device is + opened. + @param preferredDefaultDeviceName if this is not empty, and there's a device with this + name, then that will be used as the default device + (assuming that there wasn't one specified in the XML). + The string can actually be a simple wildcard, containing "*" + and "?" characters + @param preferredSetupOptions if this is non-null, the structure will be used as the + set of preferred settings when opening the device. If you + use this parameter, the preferredDefaultDeviceName + field will be ignored + + @returns an error message if anything went wrong, or an empty string if it worked ok. + */ + String initialise (int numInputChannelsNeeded, + int numOutputChannelsNeeded, + const XmlElement* savedState, + bool selectDefaultDeviceOnFailure, + const String& preferredDefaultDeviceName = String(), + const AudioDeviceSetup* preferredSetupOptions = nullptr); + + /** Resets everything to a default device setup, clearing any stored settings. */ + String initialiseWithDefaultDevices (int numInputChannelsNeeded, + int numOutputChannelsNeeded); + + /** Returns some XML representing the current state of the manager. + + This stores the current device, its samplerate, block size, etc, and + can be restored later with initialise(). + + Note that this can return a null pointer if no settings have been explicitly changed + (i.e. if the device manager has just been left in its default state). + */ + XmlElement* createStateXml() const; + + //============================================================================== + /** Returns the current device properties that are in use. + @see setAudioDeviceSetup + */ + void getAudioDeviceSetup (AudioDeviceSetup& result) const; + + /** Changes the current device or its settings. + + If you want to change a device property, like the current sample rate or + block size, you can call getAudioDeviceSetup() to retrieve the current + settings, then tweak the appropriate fields in the AudioDeviceSetup structure, + and pass it back into this method to apply the new settings. + + @param newSetup the settings that you'd like to use + @param treatAsChosenDevice if this is true and if the device opens correctly, these new + settings will be taken as having been explicitly chosen by the + user, and the next time createStateXml() is called, these settings + will be returned. If it's false, then the device is treated as a + temporary or default device, and a call to createStateXml() will + return either the last settings that were made with treatAsChosenDevice + as true, or the last XML settings that were passed into initialise(). + @returns an error message if anything went wrong, or an empty string if it worked ok. + + @see getAudioDeviceSetup + */ + String setAudioDeviceSetup (const AudioDeviceSetup& newSetup, + bool treatAsChosenDevice); + + + /** Returns the currently-active audio device. */ + AudioIODevice* getCurrentAudioDevice() const noexcept { return currentAudioDevice; } + + /** Returns the type of audio device currently in use. + @see setCurrentAudioDeviceType + */ + String getCurrentAudioDeviceType() const { return currentDeviceType; } + + /** Returns the currently active audio device type object. + Don't keep a copy of this pointer - it's owned by the device manager and could + change at any time. + */ + AudioIODeviceType* getCurrentDeviceTypeObject() const; + + /** Changes the class of audio device being used. + + This switches between, e.g. ASIO and DirectSound. On the Mac you probably won't ever call + this because there's only one type: CoreAudio. + + For a list of types, see getAvailableDeviceTypes(). + */ + void setCurrentAudioDeviceType (const String& type, + bool treatAsChosenDevice); + + /** Closes the currently-open device. + You can call restartLastAudioDevice() later to reopen it in the same state + that it was just in. + */ + void closeAudioDevice(); + + /** Tries to reload the last audio device that was running. + + Note that this only reloads the last device that was running before + closeAudioDevice() was called - it doesn't reload any kind of saved-state, + and can only be called after a device has been opened with SetAudioDevice(). + + If a device is already open, this call will do nothing. + */ + void restartLastAudioDevice(); + + //============================================================================== + /** Registers an audio callback to be used. + + The manager will redirect callbacks from whatever audio device is currently + in use to all registered callback objects. If more than one callback is + active, they will all be given the same input data, and their outputs will + be summed. + + If necessary, this method will invoke audioDeviceAboutToStart() on the callback + object before returning. + + To remove a callback, use removeAudioCallback(). + */ + void addAudioCallback (AudioIODeviceCallback* newCallback); + + /** Deregisters a previously added callback. + + If necessary, this method will invoke audioDeviceStopped() on the callback + object before returning. + + @see addAudioCallback + */ + void removeAudioCallback (AudioIODeviceCallback* callback); + + //============================================================================== + /** Returns the average proportion of available CPU being spent inside the audio callbacks. + @returns A value between 0 and 1.0 to indicate the approximate proportion of CPU + time spent in the callbacks. + */ + double getCpuUsage() const; + + //============================================================================== + /** Enables or disables a midi input device. + + The list of devices can be obtained with the MidiInput::getDevices() method. + + Any incoming messages from enabled input devices will be forwarded on to all the + listeners that have been registered with the addMidiInputCallback() method. They + can either register for messages from a particular device, or from just the + "default" midi input. + + Routing the midi input via an AudioDeviceManager means that when a listener + registers for the default midi input, this default device can be changed by the + manager without the listeners having to know about it or re-register. + + It also means that a listener can stay registered for a midi input that is disabled + or not present, so that when the input is re-enabled, the listener will start + receiving messages again. + + @see addMidiInputCallback, isMidiInputEnabled + */ + void setMidiInputEnabled (const String& midiInputDeviceName, bool enabled); + + /** Returns true if a given midi input device is being used. + @see setMidiInputEnabled + */ + bool isMidiInputEnabled (const String& midiInputDeviceName) const; + + /** Registers a listener for callbacks when midi events arrive from a midi input. + + The device name can be empty to indicate that it wants to receive all incoming + events from all the enabled MIDI inputs. Or it can be the name of one of the + MIDI input devices if it just wants the events from that device. (see + MidiInput::getDevices() for the list of device names). + + Only devices which are enabled (see the setMidiInputEnabled() method) will have their + events forwarded on to listeners. + */ + void addMidiInputCallback (const String& midiInputDeviceName, + MidiInputCallback* callback); + + /** Removes a listener that was previously registered with addMidiInputCallback(). */ + void removeMidiInputCallback (const String& midiInputDeviceName, + MidiInputCallback* callback); + + //============================================================================== + /** Sets a midi output device to use as the default. + + The list of devices can be obtained with the MidiOutput::getDevices() method. + + The specified device will be opened automatically and can be retrieved with the + getDefaultMidiOutput() method. + + Pass in an empty string to deselect all devices. For the default device, you + can use MidiOutput::getDevices() [MidiOutput::getDefaultDeviceIndex()]. + + @see getDefaultMidiOutput, getDefaultMidiOutputName + */ + void setDefaultMidiOutput (const String& deviceName); + + /** Returns the name of the default midi output. + @see setDefaultMidiOutput, getDefaultMidiOutput + */ + const String& getDefaultMidiOutputName() const noexcept { return defaultMidiOutputName; } + + /** Returns the current default midi output device. + If no device has been selected, or the device can't be opened, this will return nullptr. + @see getDefaultMidiOutputName + */ + MidiOutput* getDefaultMidiOutput() const noexcept { return defaultMidiOutput; } + + /** Returns a list of the types of device supported. */ + const OwnedArray& getAvailableDeviceTypes(); + + //============================================================================== + /** Creates a list of available types. + + This will add a set of new AudioIODeviceType objects to the specified list, to + represent each available types of device. + + You can override this if your app needs to do something specific, like avoid + using DirectSound devices, etc. + */ + virtual void createAudioDeviceTypes (OwnedArray& types); + + /** Adds a new device type to the list of types. + The manager will take ownership of the object that is passed-in. + */ + void addAudioDeviceType (AudioIODeviceType* newDeviceType); + + //============================================================================== + /** Plays a beep through the current audio device. + + This is here to allow the audio setup UI panels to easily include a "test" + button so that the user can check where the audio is coming from. + */ + void playTestSound(); + + //============================================================================== + /** Turns on level-measuring for input channels. + @see getCurrentInputLevel() + */ + void enableInputLevelMeasurement (bool enableMeasurement) noexcept; + + /** Turns on level-measuring for output channels. + @see getCurrentOutputLevel() + */ + void enableOutputLevelMeasurement (bool enableMeasurement) noexcept; + + /** Returns the current input level. + To use this, you must first enable it by calling enableInputLevelMeasurement(). + @see enableInputLevelMeasurement() + */ + double getCurrentInputLevel() const noexcept; + + /** Returns the current output level. + To use this, you must first enable it by calling enableOutputLevelMeasurement(). + @see enableOutputLevelMeasurement() + */ + double getCurrentOutputLevel() const noexcept; + + /** Returns the a lock that can be used to synchronise access to the audio callback. + Obviously while this is locked, you're blocking the audio thread from running, so + it must only be used for very brief periods when absolutely necessary. + */ + CriticalSection& getAudioCallbackLock() noexcept { return audioCallbackLock; } + + /** Returns the a lock that can be used to synchronise access to the midi callback. + Obviously while this is locked, you're blocking the midi system from running, so + it must only be used for very brief periods when absolutely necessary. + */ + CriticalSection& getMidiCallbackLock() noexcept { return midiCallbackLock; } + + //============================================================================== + /** Returns the number of under- or over runs reported. + + This method will use the underlying device's native getXRunCount if it supports + it. Otherwise it will estimate the number of under-/overruns by measuring the + time it spent in the audio callback. + */ + int getXRunCount() const noexcept; + +private: + //============================================================================== + OwnedArray availableDeviceTypes; + OwnedArray lastDeviceTypeConfigs; + + AudioDeviceSetup currentSetup; + ScopedPointer currentAudioDevice; + Array callbacks; + int numInputChansNeeded, numOutputChansNeeded; + String currentDeviceType; + BigInteger inputChannels, outputChannels; + ScopedPointer lastExplicitSettings; + mutable bool listNeedsScanning; + AudioSampleBuffer tempBuffer; + + struct MidiCallbackInfo + { + String deviceName; + MidiInputCallback* callback; + }; + + StringArray midiInsFromXml; + OwnedArray enabledMidiInputs; + Array midiCallbacks; + + String defaultMidiOutputName; + ScopedPointer defaultMidiOutput; + CriticalSection audioCallbackLock, midiCallbackLock; + + ScopedPointer testSound; + int testSoundPosition; + + double cpuUsageMs, timeToCpuScale, msPerBlock; + int xruns; + + struct LevelMeter + { + LevelMeter() noexcept; + void updateLevel (const float* const*, int numChannels, int numSamples) noexcept; + void setEnabled (bool) noexcept; + double getCurrentLevel() const noexcept; + + Atomic enabled; + double level; + }; + + LevelMeter inputLevelMeter, outputLevelMeter; + + //============================================================================== + class CallbackHandler; + friend class CallbackHandler; + friend struct ContainerDeletePolicy; + ScopedPointer callbackHandler; + + void audioDeviceIOCallbackInt (const float** inputChannelData, int totalNumInputChannels, + float** outputChannelData, int totalNumOutputChannels, int numSamples); + void audioDeviceAboutToStartInt (AudioIODevice*); + void audioDeviceStoppedInt(); + void audioDeviceErrorInt (const String&); + void handleIncomingMidiMessageInt (MidiInput*, const MidiMessage&); + void audioDeviceListChanged(); + + String restartDevice (int blockSizeToUse, double sampleRateToUse, + const BigInteger& ins, const BigInteger& outs); + void stopDevice(); + + void updateXml(); + + void createDeviceTypesIfNeeded(); + void scanDevicesIfNeeded(); + void deleteCurrentDevice(); + double chooseBestSampleRate (double preferred) const; + int chooseBestBufferSize (int preferred) const; + void insertDefaultDeviceNames (AudioDeviceSetup&) const; + String initialiseDefault (const String& preferredDefaultDeviceName, const AudioDeviceSetup*); + String initialiseFromXML (const XmlElement&, bool selectDefaultDeviceOnFailure, + const String& preferredDefaultDeviceName, const AudioDeviceSetup*); + + AudioIODeviceType* findType (const String& inputName, const String& outputName); + AudioIODeviceType* findType (const String& typeName); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioDeviceManager) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.cpp b/source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.cpp new file mode 100644 index 000000000..935de58d0 --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.cpp @@ -0,0 +1,45 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +AudioIODevice::AudioIODevice (const String& deviceName, const String& deviceTypeName) + : name (deviceName), typeName (deviceTypeName) +{ +} + +AudioIODevice::~AudioIODevice() {} + +void AudioIODeviceCallback::audioDeviceError (const String&) {} +bool AudioIODevice::setAudioPreprocessingEnabled (bool) { return false; } +bool AudioIODevice::hasControlPanel() const { return false; } +int AudioIODevice::getXRunCount() const noexcept { return -1; } + +bool AudioIODevice::showControlPanel() +{ + jassertfalse; // this should only be called for devices which return true from + // their hasControlPanel() method. + return false; +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.h b/source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.h new file mode 100644 index 000000000..a1599f4d2 --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_AudioIODevice.h @@ -0,0 +1,319 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class AudioIODevice; + + +//============================================================================== +/** + One of these is passed to an AudioIODevice object to stream the audio data + in and out. + + The AudioIODevice will repeatedly call this class's audioDeviceIOCallback() + method on its own high-priority audio thread, when it needs to send or receive + the next block of data. + + @see AudioIODevice, AudioDeviceManager +*/ +class JUCE_API AudioIODeviceCallback +{ +public: + /** Destructor. */ + virtual ~AudioIODeviceCallback() {} + + /** Processes a block of incoming and outgoing audio data. + + The subclass's implementation should use the incoming audio for whatever + purposes it needs to, and must fill all the output channels with the next + block of output data before returning. + + The channel data is arranged with the same array indices as the channel name + array returned by AudioIODevice::getOutputChannelNames(), but those channels + that aren't specified in AudioIODevice::open() will have a null pointer for their + associated channel, so remember to check for this. + + @param inputChannelData a set of arrays containing the audio data for each + incoming channel - this data is valid until the function + returns. There will be one channel of data for each input + channel that was enabled when the audio device was opened + (see AudioIODevice::open()) + @param numInputChannels the number of pointers to channel data in the + inputChannelData array. + @param outputChannelData a set of arrays which need to be filled with the data + that should be sent to each outgoing channel of the device. + There will be one channel of data for each output channel + that was enabled when the audio device was opened (see + AudioIODevice::open()) + The initial contents of the array is undefined, so the + callback function must fill all the channels with zeros if + its output is silence. Failing to do this could cause quite + an unpleasant noise! + @param numOutputChannels the number of pointers to channel data in the + outputChannelData array. + @param numSamples the number of samples in each channel of the input and + output arrays. The number of samples will depend on the + audio device's buffer size and will usually remain constant, + although this isn't guaranteed, so make sure your code can + cope with reasonable changes in the buffer size from one + callback to the next. + */ + virtual void audioDeviceIOCallback (const float** inputChannelData, + int numInputChannels, + float** outputChannelData, + int numOutputChannels, + int numSamples) = 0; + + /** Called to indicate that the device is about to start calling back. + + This will be called just before the audio callbacks begin, either when this + callback has just been added to an audio device, or after the device has been + restarted because of a sample-rate or block-size change. + + You can use this opportunity to find out the sample rate and block size + that the device is going to use by calling the AudioIODevice::getCurrentSampleRate() + and AudioIODevice::getCurrentBufferSizeSamples() on the supplied pointer. + + @param device the audio IO device that will be used to drive the callback. + Note that if you're going to store this this pointer, it is + only valid until the next time that audioDeviceStopped is called. + */ + virtual void audioDeviceAboutToStart (AudioIODevice* device) = 0; + + /** Called to indicate that the device has stopped. */ + virtual void audioDeviceStopped() = 0; + + /** This can be overridden to be told if the device generates an error while operating. + Be aware that this could be called by any thread! And not all devices perform + this callback. + */ + virtual void audioDeviceError (const String& errorMessage); +}; + + +//============================================================================== +/** + Base class for an audio device with synchronised input and output channels. + + Subclasses of this are used to implement different protocols such as DirectSound, + ASIO, CoreAudio, etc. + + To create one of these, you'll need to use the AudioIODeviceType class - see the + documentation for that class for more info. + + For an easier way of managing audio devices and their settings, have a look at the + AudioDeviceManager class. + + @see AudioIODeviceType, AudioDeviceManager +*/ +class JUCE_API AudioIODevice +{ +public: + /** Destructor. */ + virtual ~AudioIODevice(); + + //============================================================================== + /** Returns the device's name, (as set in the constructor). */ + const String& getName() const noexcept { return name; } + + /** Returns the type of the device. + + E.g. "CoreAudio", "ASIO", etc. - this comes from the AudioIODeviceType that created it. + */ + const String& getTypeName() const noexcept { return typeName; } + + //============================================================================== + /** Returns the names of all the available output channels on this device. + To find out which of these are currently in use, call getActiveOutputChannels(). + */ + virtual StringArray getOutputChannelNames() = 0; + + /** Returns the names of all the available input channels on this device. + To find out which of these are currently in use, call getActiveInputChannels(). + */ + virtual StringArray getInputChannelNames() = 0; + + //============================================================================== + /** Returns the set of sample-rates this device supports. + @see getCurrentSampleRate + */ + virtual Array getAvailableSampleRates() = 0; + + /** Returns the set of buffer sizes that are available. + @see getCurrentBufferSizeSamples, getDefaultBufferSize + */ + virtual Array getAvailableBufferSizes() = 0; + + /** Returns the default buffer-size to use. + @returns a number of samples + @see getAvailableBufferSizes + */ + virtual int getDefaultBufferSize() = 0; + + //============================================================================== + /** Tries to open the device ready to play. + + @param inputChannels a BigInteger in which a set bit indicates that the corresponding + input channel should be enabled + @param outputChannels a BigInteger in which a set bit indicates that the corresponding + output channel should be enabled + @param sampleRate the sample rate to try to use - to find out which rates are + available, see getAvailableSampleRates() + @param bufferSizeSamples the size of i/o buffer to use - to find out the available buffer + sizes, see getAvailableBufferSizes() + @returns an error description if there's a problem, or an empty string if it succeeds in + opening the device + @see close + */ + virtual String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate, + int bufferSizeSamples) = 0; + + /** Closes and releases the device if it's open. */ + virtual void close() = 0; + + /** Returns true if the device is still open. + + A device might spontaneously close itself if something goes wrong, so this checks if + it's still open. + */ + virtual bool isOpen() = 0; + + /** Starts the device actually playing. + + This must be called after the device has been opened. + + @param callback the callback to use for streaming the data. + @see AudioIODeviceCallback, open + */ + virtual void start (AudioIODeviceCallback* callback) = 0; + + /** Stops the device playing. + + Once a device has been started, this will stop it. Any pending calls to the + callback class will be flushed before this method returns. + */ + virtual void stop() = 0; + + /** Returns true if the device is still calling back. + + The device might mysteriously stop, so this checks whether it's + still playing. + */ + virtual bool isPlaying() = 0; + + /** Returns the last error that happened if anything went wrong. */ + virtual String getLastError() = 0; + + //============================================================================== + /** Returns the buffer size that the device is currently using. + + If the device isn't actually open, this value doesn't really mean much. + */ + virtual int getCurrentBufferSizeSamples() = 0; + + /** Returns the sample rate that the device is currently using. + + If the device isn't actually open, this value doesn't really mean much. + */ + virtual double getCurrentSampleRate() = 0; + + /** Returns the device's current physical bit-depth. + + If the device isn't actually open, this value doesn't really mean much. + */ + virtual int getCurrentBitDepth() = 0; + + /** Returns a mask showing which of the available output channels are currently + enabled. + @see getOutputChannelNames + */ + virtual BigInteger getActiveOutputChannels() const = 0; + + /** Returns a mask showing which of the available input channels are currently + enabled. + @see getInputChannelNames + */ + virtual BigInteger getActiveInputChannels() const = 0; + + /** Returns the device's output latency. + + This is the delay in samples between a callback getting a block of data, and + that data actually getting played. + */ + virtual int getOutputLatencyInSamples() = 0; + + /** Returns the device's input latency. + + This is the delay in samples between some audio actually arriving at the soundcard, + and the callback getting passed this block of data. + */ + virtual int getInputLatencyInSamples() = 0; + + + //============================================================================== + /** True if this device can show a pop-up control panel for editing its settings. + + This is generally just true of ASIO devices. If true, you can call showControlPanel() + to display it. + */ + virtual bool hasControlPanel() const; + + /** Shows a device-specific control panel if there is one. + + This should only be called for devices which return true from hasControlPanel(). + */ + virtual bool showControlPanel(); + + /** On devices which support it, this allows automatic gain control or other + mic processing to be disabled. + If the device doesn't support this operation, it'll return false. + */ + virtual bool setAudioPreprocessingEnabled (bool shouldBeEnabled); + + //============================================================================== + /** Returns the number of under- or over runs reported by the OS since + playback/recording has started. + + This number may be different than determining the Xrun count manually (by + measuring the time spent in the audio callback) as the OS may be doing + some buffering internally - especially on mobile devices. + + Returns -1 if playback/recording has not started yet or if getting the underrun + count is not supported for this device (Android SDK 23 and lower). + */ + virtual int getXRunCount() const noexcept; + + //============================================================================== +protected: + /** Creates a device, setting its name and type member variables. */ + AudioIODevice (const String& deviceName, + const String& typeName); + + /** @internal */ + String name, typeName; +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp b/source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp new file mode 100644 index 000000000..838aaa654 --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp @@ -0,0 +1,81 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +AudioIODeviceType::AudioIODeviceType (const String& name) + : typeName (name) +{ +} + +AudioIODeviceType::~AudioIODeviceType() +{ +} + +//============================================================================== +void AudioIODeviceType::addListener (Listener* l) { listeners.add (l); } +void AudioIODeviceType::removeListener (Listener* l) { listeners.remove (l); } + +void AudioIODeviceType::callDeviceChangeListeners() +{ + listeners.call (&AudioIODeviceType::Listener::audioDeviceListChanged); +} + +//============================================================================== +#if ! JUCE_MAC +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_CoreAudio() { return nullptr; } +#endif + +#if ! JUCE_IOS +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_iOSAudio() { return nullptr; } +#endif + +#if ! (JUCE_WINDOWS && JUCE_WASAPI) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_WASAPI (bool) { return nullptr; } +#endif + +#if ! (JUCE_WINDOWS && JUCE_DIRECTSOUND) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_DirectSound() { return nullptr; } +#endif + +#if ! (JUCE_WINDOWS && JUCE_ASIO) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_ASIO() { return nullptr; } +#endif + +#if ! (JUCE_LINUX && JUCE_ALSA) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_ALSA() { return nullptr; } +#endif + +#if ! (JUCE_LINUX && JUCE_JACK) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_JACK() { return nullptr; } +#endif + +#if ! JUCE_ANDROID +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Android() { return nullptr; } +#endif + +#if ! (JUCE_ANDROID && JUCE_USE_ANDROID_OPENSLES) +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_OpenSLES() { return nullptr; } +#endif + +} // namespace juce diff --git a/source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h b/source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h new file mode 100644 index 000000000..c11f711a4 --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h @@ -0,0 +1,178 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Represents a type of audio driver, such as DirectSound, ASIO, CoreAudio, etc. + + To get a list of available audio driver types, use the AudioDeviceManager::createAudioDeviceTypes() + method. Each of the objects returned can then be used to list the available + devices of that type. E.g. + @code + OwnedArray types; + myAudioDeviceManager.createAudioDeviceTypes (types); + + for (int i = 0; i < types.size(); ++i) + { + String typeName (types[i]->getTypeName()); // This will be things like "DirectSound", "CoreAudio", etc. + + types[i]->scanForDevices(); // This must be called before getting the list of devices + + StringArray deviceNames (types[i]->getDeviceNames()); // This will now return a list of available devices of this type + + for (int j = 0; j < deviceNames.size(); ++j) + { + AudioIODevice* device = types[i]->createDevice (deviceNames [j]); + + ... + } + } + @endcode + + For an easier way of managing audio devices and their settings, have a look at the + AudioDeviceManager class. + + @see AudioIODevice, AudioDeviceManager +*/ +class JUCE_API AudioIODeviceType +{ +public: + //============================================================================== + /** Returns the name of this type of driver that this object manages. + + This will be something like "DirectSound", "ASIO", "CoreAudio", "ALSA", etc. + */ + const String& getTypeName() const noexcept { return typeName; } + + //============================================================================== + /** Refreshes the object's cached list of known devices. + + This must be called at least once before calling getDeviceNames() or any of + the other device creation methods. + */ + virtual void scanForDevices() = 0; + + /** Returns the list of available devices of this type. + + The scanForDevices() method must have been called to create this list. + + @param wantInputNames only really used by DirectSound where devices are split up + into inputs and outputs, this indicates whether to use + the input or output name to refer to a pair of devices. + */ + virtual StringArray getDeviceNames (bool wantInputNames = false) const = 0; + + /** Returns the name of the default device. + + This will be one of the names from the getDeviceNames() list. + + @param forInput if true, this means that a default input device should be + returned; if false, it should return the default output + */ + virtual int getDefaultDeviceIndex (bool forInput) const = 0; + + /** Returns the index of a given device in the list of device names. + If asInput is true, it shows the index in the inputs list, otherwise it + looks for it in the outputs list. + */ + virtual int getIndexOfDevice (AudioIODevice* device, bool asInput) const = 0; + + /** Returns true if two different devices can be used for the input and output. + */ + virtual bool hasSeparateInputsAndOutputs() const = 0; + + /** Creates one of the devices of this type. + + The deviceName must be one of the strings returned by getDeviceNames(), and + scanForDevices() must have been called before this method is used. + */ + virtual AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) = 0; + + //============================================================================== + /** + A class for receiving events when audio devices are inserted or removed. + + You can register an AudioIODeviceType::Listener with an~AudioIODeviceType object + using the AudioIODeviceType::addListener() method, and it will be called when + devices of that type are added or removed. + + @see AudioIODeviceType::addListener, AudioIODeviceType::removeListener + */ + class Listener + { + public: + virtual ~Listener() {} + + /** Called when the list of available audio devices changes. */ + virtual void audioDeviceListChanged() = 0; + }; + + /** Adds a listener that will be called when this type of device is added or + removed from the system. + */ + void addListener (Listener* listener); + + /** Removes a listener that was previously added with addListener(). */ + void removeListener (Listener* listener); + + //============================================================================== + /** Destructor. */ + virtual ~AudioIODeviceType(); + + //============================================================================== + /** Creates a CoreAudio device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_CoreAudio(); + /** Creates an iOS device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_iOSAudio(); + /** Creates a WASAPI device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_WASAPI (bool exclusiveMode); + /** Creates a DirectSound device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_DirectSound(); + /** Creates an ASIO device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_ASIO(); + /** Creates an ALSA device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_ALSA(); + /** Creates a JACK device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_JACK(); + /** Creates an Android device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_Android(); + /** Creates an Android OpenSLES device type if it's available on this platform, or returns null. */ + static AudioIODeviceType* createAudioIODeviceType_OpenSLES(); + +protected: + explicit AudioIODeviceType (const String& typeName); + + /** Synchronously calls all the registered device list change listeners. */ + void callDeviceChangeListeners(); + +private: + String typeName; + ListenerList listeners; + + JUCE_DECLARE_NON_COPYABLE (AudioIODeviceType) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h b/source/modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h new file mode 100644 index 000000000..bf5c44c6a --- /dev/null +++ b/source/modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h @@ -0,0 +1,57 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Contains functions to control the system's master volume. +*/ +class JUCE_API SystemAudioVolume +{ +public: + //============================================================================== + /** Returns the operating system's current volume level in the range 0 to 1.0 */ + static float JUCE_CALLTYPE getGain(); + + /** Attempts to set the operating system's current volume level. + @param newGain the level, between 0 and 1.0 + @returns true if the operation succeeds + */ + static bool JUCE_CALLTYPE setGain (float newGain); + + /** Returns true if the system's audio output is currently muted. */ + static bool JUCE_CALLTYPE isMuted(); + + /** Attempts to mute the operating system's audio output. + @param shouldBeMuted true if you want it to be muted + @returns true if the operation succeeds + */ + static bool JUCE_CALLTYPE setMuted (bool shouldBeMuted); + +private: + SystemAudioVolume(); // Don't instantiate this class, just call its static fns. + JUCE_DECLARE_NON_COPYABLE (SystemAudioVolume) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/juce_audio_devices.cpp b/source/modules/juce_audio_devices/juce_audio_devices.cpp new file mode 100644 index 000000000..6bdd8fe06 --- /dev/null +++ b/source/modules/juce_audio_devices/juce_audio_devices.cpp @@ -0,0 +1,225 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#ifdef JUCE_AUDIO_DEVICES_H_INCLUDED + /* When you add this cpp file to your project, you mustn't include it in a file where you've + already included any other headers - just put it inside a file on its own, possibly with your config + flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix + header files that the compiler may be using. + */ + #error "Incorrect use of JUCE cpp file" +#endif + +#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1 +#define JUCE_CORE_INCLUDE_COM_SMART_PTR 1 +#define JUCE_CORE_INCLUDE_JNI_HELPERS 1 +#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1 +#define JUCE_EVENTS_INCLUDE_WIN32_MESSAGE_WINDOW 1 + +#ifndef JUCE_USE_WINRT_MIDI + #define JUCE_USE_WINRT_MIDI 0 +#endif + +#if JUCE_USE_WINRT_MIDI + #define JUCE_EVENTS_INCLUDE_WINRT_WRAPPER 1 +#endif + +#include "juce_audio_devices.h" + +//============================================================================== +#if JUCE_MAC + #define Point CarbonDummyPointName + #define Component CarbonDummyCompName + #import + #import + #import + #undef Point + #undef Component + +#elif JUCE_IOS + #import + #import + #import + + #if TARGET_OS_SIMULATOR + #import + #endif + +//============================================================================== +#elif JUCE_WINDOWS + #if JUCE_WASAPI + #include + #endif + + #if JUCE_USE_WINRT_MIDI + /* If you cannot find any of the header files below then you are probably + attempting to use the Windows 10 Bluetooth Low Energy API. For this to work you + need to install version 10.0.14393.0 of the Windows Standalone SDK and add the + path to the WinRT headers to your build system. This path should have the form + "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt". + + Also please note that Microsoft's Bluetooth MIDI stack has multiple issues, so + this API is EXPERIMENTAL - use at your own risk! + */ + #include + #include + #include + #include + #if JUCE_MSVC + #pragma warning (push) + #pragma warning (disable: 4467) + #endif + #include + #if JUCE_MSVC + #pragma warning (pop) + #endif + #endif + + #if JUCE_ASIO + /* This is very frustrating - we only need to use a handful of definitions from + a couple of the header files in Steinberg's ASIO SDK, and it'd be easy to copy + about 30 lines of code into this cpp file to create a fully stand-alone ASIO + implementation... + + ..unfortunately that would break Steinberg's license agreement for use of + their SDK, so I'm not allowed to do this. + + This means that anyone who wants to use JUCE's ASIO abilities will have to: + + 1) Agree to Steinberg's licensing terms and download the ASIO SDK + (see http://www.steinberg.net/en/company/developers.html). + + 2) Enable this code with a global definition #define JUCE_ASIO 1. + + 3) Make sure that your header search path contains the iasiodrv.h file that + comes with the SDK. (Only about a handful of the SDK header files are actually + needed - so to simplify things, you could just copy these into your JUCE directory). + */ + #include + #endif + +//============================================================================== +#elif JUCE_LINUX + #if JUCE_ALSA + /* Got an include error here? If so, you've either not got ALSA installed, or you've + not got your paths set up correctly to find its header files. + + The package you need to install to get ASLA support is "libasound2-dev". + + If you don't have the ALSA library and don't want to build Juce with audio support, + just set the JUCE_ALSA flag to 0. + */ + #include + #endif + + #if JUCE_JACK + /* Got an include error here? If so, you've either not got jack-audio-connection-kit + installed, or you've not got your paths set up correctly to find its header files. + + The package you need to install to get JACK support is "libjack-dev". + + If you don't have the jack-audio-connection-kit library and don't want to build + Juce with low latency audio support, just set the JUCE_JACK flag to 0. + */ + #include + #endif + #undef SIZEOF + +//============================================================================== +#elif JUCE_ANDROID + + #if JUCE_USE_ANDROID_OPENSLES + #include + #include + #include + #endif + +#endif + +#include "audio_io/juce_AudioDeviceManager.cpp" +#include "audio_io/juce_AudioIODevice.cpp" +#include "audio_io/juce_AudioIODeviceType.cpp" +#include "midi_io/juce_MidiMessageCollector.cpp" +#include "midi_io/juce_MidiOutput.cpp" +#include "sources/juce_AudioSourcePlayer.cpp" +#include "sources/juce_AudioTransportSource.cpp" +#include "native/juce_MidiDataConcatenator.h" + +//============================================================================== +#if JUCE_MAC + #include "native/juce_mac_CoreAudio.cpp" + #include "native/juce_mac_CoreMidi.cpp" + +//============================================================================== +#elif JUCE_IOS + #include "native/juce_ios_Audio.cpp" + #include "native/juce_mac_CoreMidi.cpp" + +//============================================================================== +#elif JUCE_WINDOWS + + #if JUCE_WASAPI + #include "native/juce_win32_WASAPI.cpp" + #endif + + #if JUCE_DIRECTSOUND + #include "native/juce_win32_DirectSound.cpp" + #endif + + #include "native/juce_win32_Midi.cpp" + + #if JUCE_ASIO + #include "native/juce_win32_ASIO.cpp" + #endif + +//============================================================================== +#elif JUCE_LINUX + #if JUCE_ALSA + #include "native/juce_linux_ALSA.cpp" + #endif + + #include "native/juce_linux_Midi.cpp" + + #if JUCE_JACK + #include "native/juce_linux_JackAudio.cpp" + #endif + +//============================================================================== +#elif JUCE_ANDROID + #include "native/juce_android_Audio.cpp" + #include "native/juce_android_Midi.cpp" + + #if JUCE_USE_ANDROID_OPENSLES + #include "native/juce_android_OpenSL.cpp" + #endif +#endif + +#if ! JUCE_SYSTEMAUDIOVOL_IMPLEMENTED +namespace juce +{ + // None of these methods are available. (On Windows you might need to enable WASAPI for this) + float JUCE_CALLTYPE SystemAudioVolume::getGain() { jassertfalse; return 0.0f; } + bool JUCE_CALLTYPE SystemAudioVolume::setGain (float) { jassertfalse; return false; } + bool JUCE_CALLTYPE SystemAudioVolume::isMuted() { jassertfalse; return false; } + bool JUCE_CALLTYPE SystemAudioVolume::setMuted (bool) { jassertfalse; return false; } +} +#endif diff --git a/source/modules/juce_audio_devices/juce_audio_devices.h b/source/modules/juce_audio_devices/juce_audio_devices.h new file mode 100644 index 000000000..9b26405da --- /dev/null +++ b/source/modules/juce_audio_devices/juce_audio_devices.h @@ -0,0 +1,155 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +/******************************************************************************* + The block below describes the properties of this module, and is read by + the Projucer to automatically generate project code that uses it. + For details about the syntax and how to create or use a module, see the + JUCE Module Format.txt file. + + + BEGIN_JUCE_MODULE_DECLARATION + + ID: juce_audio_devices + vendor: juce + version: 5.1.2 + name: JUCE audio and MIDI I/O device classes + description: Classes to play and record from audio and MIDI I/O devices + website: http://www.juce.com/juce + license: ISC + + dependencies: juce_audio_basics, juce_events + OSXFrameworks: CoreAudio CoreMIDI AudioToolbox + iOSFrameworks: CoreAudio CoreMIDI AudioToolbox AVFoundation + linuxPackages: alsa + mingwLibs: winmm + + END_JUCE_MODULE_DECLARATION + +*******************************************************************************/ + + +#pragma once +#define JUCE_AUDIO_DEVICES_H_INCLUDED + +#include +#include + +#if JUCE_MODULE_AVAILABLE_juce_graphics +#include +#endif + +//============================================================================== +/** Config: JUCE_ASIO + Enables ASIO audio devices (MS Windows only). + Turning this on means that you'll need to have the Steinberg ASIO SDK installed + on your Windows build machine. + + See the comments in the ASIOAudioIODevice class's header file for more + info about this. +*/ +#ifndef JUCE_ASIO + #define JUCE_ASIO 0 +#endif + +/** Config: JUCE_WASAPI + Enables WASAPI audio devices (Windows Vista and above). See also the + JUCE_WASAPI_EXCLUSIVE flag. +*/ +#ifndef JUCE_WASAPI + #define JUCE_WASAPI 1 +#endif + +/** Config: JUCE_WASAPI_EXCLUSIVE + Enables WASAPI audio devices in exclusive mode (Windows Vista and above). +*/ +#ifndef JUCE_WASAPI_EXCLUSIVE + #define JUCE_WASAPI_EXCLUSIVE 0 +#endif + + +/** Config: JUCE_DIRECTSOUND + Enables DirectSound audio (MS Windows only). +*/ +#ifndef JUCE_DIRECTSOUND + #define JUCE_DIRECTSOUND 1 +#endif + +/** Config: JUCE_ALSA + Enables ALSA audio devices (Linux only). +*/ +#ifndef JUCE_ALSA + #define JUCE_ALSA 1 +#endif + +/** Config: JUCE_JACK + Enables JACK audio devices (Linux only). +*/ +#ifndef JUCE_JACK + #define JUCE_JACK 0 +#endif + +/** Config: JUCE_USE_ANDROID_OPENSLES + Enables OpenSLES devices (Android only). +*/ +#ifndef JUCE_USE_ANDROID_OPENSLES + #if JUCE_ANDROID_API_VERSION > 8 + #define JUCE_USE_ANDROID_OPENSLES 1 + #else + #define JUCE_USE_ANDROID_OPENSLES 0 + #endif +#endif + +/** Config: JUCE_USE_WINRT_MIDI + *** + EXPERIMENTAL - Microsoft's Bluetooth MIDI stack has multiple issues, + use at your own risk! + *** + + Enables the use of the Windows Runtime API for MIDI, which supports + Bluetooth Low Energy connections on computers with the Anniversary Update + of Windows 10. + + To compile with this flag requires version 10.0.14393.0 of the Windows + Standalone SDK and you must add the path to the WinRT headers. This path + should be something similar to + "C:\Program Files (x86)\Windows Kits\10\Include\10.0.14393.0\winrt". +*/ +#ifndef JUCE_USE_WINRT_MIDI + #define JUCE_USE_WINRT_MIDI 0 +#endif + + +//============================================================================== +#include "midi_io/juce_MidiInput.h" +#include "midi_io/juce_MidiMessageCollector.h" +#include "midi_io/juce_MidiOutput.h" +#include "audio_io/juce_AudioIODevice.h" +#include "audio_io/juce_AudioIODeviceType.h" +#include "audio_io/juce_SystemAudioVolume.h" +#include "sources/juce_AudioSourcePlayer.h" +#include "sources/juce_AudioTransportSource.h" +#include "audio_io/juce_AudioDeviceManager.h" + +#if JUCE_IOS + #include "native/juce_ios_Audio.h" +#endif diff --git a/source/modules/juce_audio_devices/midi_io/juce_MidiInput.h b/source/modules/juce_audio_devices/midi_io/juce_MidiInput.h new file mode 100644 index 000000000..52edd9fe0 --- /dev/null +++ b/source/modules/juce_audio_devices/midi_io/juce_MidiInput.h @@ -0,0 +1,176 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class MidiInput; + + +//============================================================================== +/** + Receives incoming messages from a physical MIDI input device. + + This class is overridden to handle incoming midi messages. See the MidiInput + class for more details. + + @see MidiInput +*/ +class JUCE_API MidiInputCallback +{ +public: + /** Destructor. */ + virtual ~MidiInputCallback() {} + + + /** Receives an incoming message. + + A MidiInput object will call this method when a midi event arrives. It'll be + called on a high-priority system thread, so avoid doing anything time-consuming + in here, and avoid making any UI calls. You might find the MidiBuffer class helpful + for queueing incoming messages for use later. + + @param source the MidiInput object that generated the message + @param message the incoming message. The message's timestamp is set to a value + equivalent to (Time::getMillisecondCounter() / 1000.0) to specify the + time when the message arrived. + */ + virtual void handleIncomingMidiMessage (MidiInput* source, + const MidiMessage& message) = 0; + + /** Notification sent each time a packet of a multi-packet sysex message arrives. + + If a long sysex message is broken up into multiple packets, this callback is made + for each packet that arrives until the message is finished, at which point + the normal handleIncomingMidiMessage() callback will be made with the entire + message. + + The message passed in will contain the start of a sysex, but won't be finished + with the terminating 0xf7 byte. + */ + virtual void handlePartialSysexMessage (MidiInput* source, + const uint8* messageData, + int numBytesSoFar, + double timestamp) + { + ignoreUnused (source, messageData, numBytesSoFar, timestamp); + } +}; + +//============================================================================== +/** + Represents a midi input device. + + To create one of these, use the static getDevices() method to find out what inputs are + available, and then use the openDevice() method to try to open one. + + @see MidiOutput +*/ +class JUCE_API MidiInput +{ +public: + //============================================================================== + /** Returns a list of the available midi input devices. + + You can open one of the devices by passing its index into the + openDevice() method. + + @see getDefaultDeviceIndex, openDevice + */ + static StringArray getDevices(); + + /** Returns the index of the default midi input device to use. + + This refers to the index in the list returned by getDevices(). + */ + static int getDefaultDeviceIndex(); + + /** Tries to open one of the midi input devices. + + This will return a MidiInput object if it manages to open it. You can then + call start() and stop() on this device, and delete it when no longer needed. + + If the device can't be opened, this will return a null pointer. + + @param deviceIndex the index of a device from the list returned by getDevices() + @param callback the object that will receive the midi messages from this device. + + @see MidiInputCallback, getDevices + */ + static MidiInput* openDevice (int deviceIndex, + MidiInputCallback* callback); + + #if JUCE_LINUX || JUCE_MAC || JUCE_IOS || DOXYGEN + /** This will try to create a new midi input device (Not available on Windows). + + This will attempt to create a new midi input device with the specified name, + for other apps to connect to. + + Returns nullptr if a device can't be created. + + @param deviceName the name to use for the new device + @param callback the object that will receive the midi messages from this device. + */ + static MidiInput* createNewDevice (const String& deviceName, + MidiInputCallback* callback); + #endif + + //============================================================================== + /** Destructor. */ + ~MidiInput(); + + /** Returns the name of this device. */ + const String& getName() const noexcept { return name; } + + /** Allows you to set a custom name for the device, in case you don't like the name + it was given when created. + */ + void setName (const String& newName) noexcept { name = newName; } + + //============================================================================== + /** Starts the device running. + + After calling this, the device will start sending midi messages to the + MidiInputCallback object that was specified when the openDevice() method + was called. + + @see stop + */ + void start(); + + /** Stops the device running. + @see start + */ + void stop(); + +private: + //============================================================================== + String name; + void* internal = nullptr; + + // The input objects are created with the openDevice() method. + explicit MidiInput (const String&); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInput) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp b/source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp new file mode 100644 index 000000000..50ca20569 --- /dev/null +++ b/source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp @@ -0,0 +1,158 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +MidiMessageCollector::MidiMessageCollector() +{ +} + +MidiMessageCollector::~MidiMessageCollector() +{ +} + +//============================================================================== +void MidiMessageCollector::reset (const double newSampleRate) +{ + jassert (newSampleRate > 0); + + const ScopedLock sl (midiCallbackLock); + #if JUCE_DEBUG + hasCalledReset = true; + #endif + sampleRate = newSampleRate; + incomingMessages.clear(); + lastCallbackTime = Time::getMillisecondCounterHiRes(); +} + +void MidiMessageCollector::addMessageToQueue (const MidiMessage& message) +{ + #if JUCE_DEBUG + jassert (hasCalledReset); // you need to call reset() to set the correct sample rate before using this object + #endif + + // the messages that come in here need to be time-stamped correctly - see MidiInput + // for details of what the number should be. + jassert (message.getTimeStamp() != 0); + + const ScopedLock sl (midiCallbackLock); + + auto sampleNumber = (int) ((message.getTimeStamp() - 0.001 * lastCallbackTime) * sampleRate); + + incomingMessages.addEvent (message, sampleNumber); + + // if the messages don't get used for over a second, we'd better + // get rid of any old ones to avoid the queue getting too big + if (sampleNumber > sampleRate) + incomingMessages.clear (0, sampleNumber - (int) sampleRate); +} + +void MidiMessageCollector::removeNextBlockOfMessages (MidiBuffer& destBuffer, + const int numSamples) +{ + #if JUCE_DEBUG + jassert (hasCalledReset); // you need to call reset() to set the correct sample rate before using this object + #endif + + jassert (numSamples > 0); + + auto timeNow = Time::getMillisecondCounterHiRes(); + auto msElapsed = timeNow - lastCallbackTime; + + const ScopedLock sl (midiCallbackLock); + lastCallbackTime = timeNow; + + if (! incomingMessages.isEmpty()) + { + int numSourceSamples = jmax (1, roundToInt (msElapsed * 0.001 * sampleRate)); + int startSample = 0; + int scale = 1 << 16; + + const uint8* midiData; + int numBytes, samplePosition; + + MidiBuffer::Iterator iter (incomingMessages); + + if (numSourceSamples > numSamples) + { + // if our list of events is longer than the buffer we're being + // asked for, scale them down to squeeze them all in.. + const int maxBlockLengthToUse = numSamples << 5; + + if (numSourceSamples > maxBlockLengthToUse) + { + startSample = numSourceSamples - maxBlockLengthToUse; + numSourceSamples = maxBlockLengthToUse; + iter.setNextSamplePosition (startSample); + } + + scale = (numSamples << 10) / numSourceSamples; + + while (iter.getNextEvent (midiData, numBytes, samplePosition)) + { + samplePosition = ((samplePosition - startSample) * scale) >> 10; + + destBuffer.addEvent (midiData, numBytes, + jlimit (0, numSamples - 1, samplePosition)); + } + } + else + { + // if our event list is shorter than the number we need, put them + // towards the end of the buffer + startSample = numSamples - numSourceSamples; + + while (iter.getNextEvent (midiData, numBytes, samplePosition)) + { + destBuffer.addEvent (midiData, numBytes, + jlimit (0, numSamples - 1, samplePosition + startSample)); + } + } + + incomingMessages.clear(); + } +} + +//============================================================================== +void MidiMessageCollector::handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) +{ + MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity)); + m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001); + + addMessageToQueue (m); +} + +void MidiMessageCollector::handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) +{ + MidiMessage m (MidiMessage::noteOff (midiChannel, midiNoteNumber, velocity)); + m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001); + + addMessageToQueue (m); +} + +void MidiMessageCollector::handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) +{ + addMessageToQueue (message); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h b/source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h new file mode 100644 index 000000000..aa8bcdbeb --- /dev/null +++ b/source/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h @@ -0,0 +1,103 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Collects incoming realtime MIDI messages and turns them into blocks suitable for + processing by a block-based audio callback. + + The class can also be used as either a MidiKeyboardStateListener or a MidiInputCallback + so it can easily use a midi input or keyboard component as its source. + + @see MidiMessage, MidiInput +*/ +class JUCE_API MidiMessageCollector : public MidiKeyboardStateListener, + public MidiInputCallback +{ +public: + //============================================================================== + /** Creates a MidiMessageCollector. */ + MidiMessageCollector(); + + /** Destructor. */ + ~MidiMessageCollector(); + + //============================================================================== + /** Clears any messages from the queue. + + You need to call this method before starting to use the collector, so that + it knows the correct sample rate to use. + */ + void reset (double sampleRate); + + /** Takes an incoming real-time message and adds it to the queue. + + The message's timestamp is taken, and it will be ready for retrieval as part + of the block returned by the next call to removeNextBlockOfMessages(). + + This method is fully thread-safe when overlapping calls are made with + removeNextBlockOfMessages(). + */ + void addMessageToQueue (const MidiMessage& message); + + /** Removes all the pending messages from the queue as a buffer. + + This will also correct the messages' timestamps to make sure they're in + the range 0 to numSamples - 1. + + This call should be made regularly by something like an audio processing + callback, because the time that it happens is used in calculating the + midi event positions. + + This method is fully thread-safe when overlapping calls are made with + addMessageToQueue(). + + Precondition: numSamples must be greater than 0. + */ + void removeNextBlockOfMessages (MidiBuffer& destBuffer, int numSamples); + + + //============================================================================== + /** @internal */ + void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + /** @internal */ + void handleIncomingMidiMessage (MidiInput*, const MidiMessage&) override; + +private: + //============================================================================== + double lastCallbackTime = 0; + CriticalSection midiCallbackLock; + MidiBuffer incomingMessages; + double sampleRate = 44100.0; + #if JUCE_DEBUG + bool hasCalledReset = false; + #endif + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiMessageCollector) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/midi_io/juce_MidiOutput.cpp b/source/modules/juce_audio_devices/midi_io/juce_MidiOutput.cpp new file mode 100644 index 000000000..36b7cf640 --- /dev/null +++ b/source/modules/juce_audio_devices/midi_io/juce_MidiOutput.cpp @@ -0,0 +1,176 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +struct MidiOutput::PendingMessage +{ + PendingMessage (const void* const data, const int len, const double timeStamp) + : message (data, len, timeStamp) + {} + + MidiMessage message; + PendingMessage* next; +}; + +MidiOutput::MidiOutput (const String& midiName) + : Thread ("midi out"), + internal (nullptr), + firstMessage (nullptr), + name (midiName) +{ +} + +void MidiOutput::sendBlockOfMessagesNow (const MidiBuffer& buffer) +{ + MidiBuffer::Iterator i (buffer); + MidiMessage message; + int samplePosition; // Note: not actually used, so no need to initialise. + + while (i.getNextEvent (message, samplePosition)) + sendMessageNow (message); +} + +void MidiOutput::sendBlockOfMessages (const MidiBuffer& buffer, + const double millisecondCounterToStartAt, + double samplesPerSecondForBuffer) +{ + // You've got to call startBackgroundThread() for this to actually work.. + jassert (isThreadRunning()); + + // this needs to be a value in the future - RTFM for this method! + jassert (millisecondCounterToStartAt > 0); + + const double timeScaleFactor = 1000.0 / samplesPerSecondForBuffer; + + MidiBuffer::Iterator i (buffer); + + const uint8* data; + int len, time; + + while (i.getNextEvent (data, len, time)) + { + const double eventTime = millisecondCounterToStartAt + timeScaleFactor * time; + + PendingMessage* const m = new PendingMessage (data, len, eventTime); + + const ScopedLock sl (lock); + + if (firstMessage == nullptr || firstMessage->message.getTimeStamp() > eventTime) + { + m->next = firstMessage; + firstMessage = m; + } + else + { + PendingMessage* mm = firstMessage; + + while (mm->next != nullptr && mm->next->message.getTimeStamp() <= eventTime) + mm = mm->next; + + m->next = mm->next; + mm->next = m; + } + } + + notify(); +} + +void MidiOutput::clearAllPendingMessages() +{ + const ScopedLock sl (lock); + + while (firstMessage != nullptr) + { + PendingMessage* const m = firstMessage; + firstMessage = firstMessage->next; + delete m; + } +} + +void MidiOutput::startBackgroundThread() +{ + startThread (9); +} + +void MidiOutput::stopBackgroundThread() +{ + stopThread (5000); +} + +void MidiOutput::run() +{ + while (! threadShouldExit()) + { + uint32 now = Time::getMillisecondCounter(); + uint32 eventTime = 0; + uint32 timeToWait = 500; + + PendingMessage* message; + + { + const ScopedLock sl (lock); + message = firstMessage; + + if (message != nullptr) + { + eventTime = (uint32) roundToInt (message->message.getTimeStamp()); + + if (eventTime > now + 20) + { + timeToWait = eventTime - (now + 20); + message = nullptr; + } + else + { + firstMessage = message->next; + } + } + } + + if (message != nullptr) + { + const ScopedPointer messageDeleter (message); + + if (eventTime > now) + { + Time::waitForMillisecondCounter (eventTime); + + if (threadShouldExit()) + break; + } + + if (eventTime > now - 200) + sendMessageNow (message->message); + } + else + { + jassert (timeToWait < 1000 * 30); + wait ((int) timeToWait); + } + } + + clearAllPendingMessages(); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/midi_io/juce_MidiOutput.h b/source/modules/juce_audio_devices/midi_io/juce_MidiOutput.h new file mode 100644 index 000000000..f05dc32f1 --- /dev/null +++ b/source/modules/juce_audio_devices/midi_io/juce_MidiOutput.h @@ -0,0 +1,143 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Controls a physical MIDI output device. + + To create one of these, use the static getDevices() method to get a list of the + available output devices, then use the openDevice() method to try to open one. + + @see MidiInput +*/ +class JUCE_API MidiOutput : private Thread +{ +public: + //============================================================================== + /** Returns a list of the available midi output devices. + + You can open one of the devices by passing its index into the + openDevice() method. + + @see getDefaultDeviceIndex, openDevice + */ + static StringArray getDevices(); + + /** Returns the index of the default midi output device to use. + + This refers to the index in the list returned by getDevices(). + */ + static int getDefaultDeviceIndex(); + + /** Tries to open one of the midi output devices. + + This will return a MidiOutput object if it manages to open it. You can then + send messages to this device, and delete it when no longer needed. + + If the device can't be opened, this will return a null pointer. + + @param deviceIndex the index of a device from the list returned by getDevices() + @see getDevices + */ + static MidiOutput* openDevice (int deviceIndex); + + + #if JUCE_LINUX || JUCE_MAC || JUCE_IOS || DOXYGEN + /** This will try to create a new midi output device (Not available on Windows). + + This will attempt to create a new midi output device that other apps can connect + to and use as their midi input. + + Returns nullptr if a device can't be created. + + @param deviceName the name to use for the new device + */ + static MidiOutput* createNewDevice (const String& deviceName); + #endif + + //============================================================================== + /** Destructor. */ + ~MidiOutput(); + + /** Returns the name of this device. */ + const String& getName() const noexcept { return name; } + + /** Sends out a MIDI message immediately. */ + void sendMessageNow (const MidiMessage& message); + + /** Sends out a sequence of MIDI messages immediately. */ + void sendBlockOfMessagesNow (const MidiBuffer& buffer); + + //============================================================================== + /** This lets you supply a block of messages that will be sent out at some point + in the future. + + The MidiOutput class has an internal thread that can send out timestamped + messages - this appends a set of messages to its internal buffer, ready for + sending. + + This will only work if you've already started the thread with startBackgroundThread(). + + A time is specified, at which the block of messages should be sent. This time uses + the same time base as Time::getMillisecondCounter(), and must be in the future. + + The samplesPerSecondForBuffer parameter indicates the number of samples per second + used by the MidiBuffer. Each event in a MidiBuffer has a sample position, and the + samplesPerSecondForBuffer value is needed to convert this sample position to a + real time. + */ + void sendBlockOfMessages (const MidiBuffer& buffer, + double millisecondCounterToStartAt, + double samplesPerSecondForBuffer); + + /** Gets rid of any midi messages that had been added by sendBlockOfMessages(). */ + void clearAllPendingMessages(); + + /** Starts up a background thread so that the device can send blocks of data. + Call this to get the device ready, before using sendBlockOfMessages(). + */ + void startBackgroundThread(); + + /** Stops the background thread, and clears any pending midi events. + @see startBackgroundThread + */ + void stopBackgroundThread(); + + +private: + //============================================================================== + void* internal = nullptr; + CriticalSection lock; + struct PendingMessage; + PendingMessage* firstMessage; + String name; + + MidiOutput (const String& midiName); // These objects are created with the openDevice() method. + void run() override; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutput) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_MidiDataConcatenator.h b/source/modules/juce_audio_devices/native/juce_MidiDataConcatenator.h new file mode 100644 index 000000000..1df491240 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_MidiDataConcatenator.h @@ -0,0 +1,191 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Helper class that takes chunks of incoming midi bytes, packages them into + messages, and dispatches them to a midi callback. +*/ +class MidiDataConcatenator +{ +public: + //============================================================================== + MidiDataConcatenator (int initialBufferSize) + : pendingData ((size_t) initialBufferSize) + { + } + + void reset() + { + pendingBytes = 0; + runningStatus = 0; + pendingDataTime = 0; + } + + template + void pushMidiData (const void* inputData, int numBytes, double time, + UserDataType* input, CallbackType& callback) + { + const uint8* d = static_cast (inputData); + + while (numBytes > 0) + { + if (pendingBytes > 0 || d[0] == 0xf0) + { + processSysex (d, numBytes, time, input, callback); + runningStatus = 0; + } + else + { + int len = 0; + uint8 data[3]; + + while (numBytes > 0) + { + // If there's a realtime message embedded in the middle of + // the normal message, handle it now.. + if (*d >= 0xf8 && *d <= 0xfe) + { + callback.handleIncomingMidiMessage (input, MidiMessage (*d++, time)); + --numBytes; + } + else + { + if (len == 0 && *d < 0x80 && runningStatus >= 0x80) + data[len++] = runningStatus; + + data[len++] = *d++; + --numBytes; + + const uint8 firstByte = data[0]; + + if (firstByte < 0x80 || firstByte == 0xf7) + { + len = 0; + break; // ignore this malformed MIDI message.. + } + + if (len >= MidiMessage::getMessageLengthFromFirstByte (firstByte)) + break; + } + } + + if (len > 0) + { + int used = 0; + const MidiMessage m (data, len, used, 0, time); + + if (used <= 0) + break; // malformed message.. + + jassert (used == len); + callback.handleIncomingMidiMessage (input, m); + runningStatus = data[0]; + } + } + } + } + +private: + template + void processSysex (const uint8*& d, int& numBytes, double time, + UserDataType* input, CallbackType& callback) + { + if (*d == 0xf0) + { + pendingBytes = 0; + pendingDataTime = time; + } + + pendingData.ensureSize ((size_t) (pendingBytes + numBytes), false); + uint8* totalMessage = static_cast (pendingData.getData()); + uint8* dest = totalMessage + pendingBytes; + + do + { + if (pendingBytes > 0 && *d >= 0x80) + { + if (*d == 0xf7) + { + *dest++ = *d++; + ++pendingBytes; + --numBytes; + break; + } + + if (*d >= 0xfa || *d == 0xf8) + { + callback.handleIncomingMidiMessage (input, MidiMessage (*d, time)); + ++d; + --numBytes; + } + else + { + pendingBytes = 0; + int used = 0; + const MidiMessage m (d, numBytes, used, 0, time); + + if (used > 0) + { + callback.handleIncomingMidiMessage (input, m); + numBytes -= used; + d += used; + } + + break; + } + } + else + { + *dest++ = *d++; + ++pendingBytes; + --numBytes; + } + } + while (numBytes > 0); + + if (pendingBytes > 0) + { + if (totalMessage [pendingBytes - 1] == 0xf7) + { + callback.handleIncomingMidiMessage (input, MidiMessage (totalMessage, pendingBytes, pendingDataTime)); + pendingBytes = 0; + } + else + { + callback.handlePartialSysexMessage (input, totalMessage, pendingBytes, pendingDataTime); + } + } + } + + MemoryBlock pendingData; + double pendingDataTime = 0; + int pendingBytes = 0; + uint8 runningStatus = 0; + + JUCE_DECLARE_NON_COPYABLE (MidiDataConcatenator) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_android_Audio.cpp b/source/modules/juce_audio_devices/native/juce_android_Audio.cpp new file mode 100644 index 000000000..42f6fdd98 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_android_Audio.cpp @@ -0,0 +1,497 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (getMinBufferSize, "getMinBufferSize", "(III)I") \ + STATICMETHOD (getNativeOutputSampleRate, "getNativeOutputSampleRate", "(I)I") \ + METHOD (constructor, "", "(IIIIII)V") \ + METHOD (getState, "getState", "()I") \ + METHOD (play, "play", "()V") \ + METHOD (stop, "stop", "()V") \ + METHOD (release, "release", "()V") \ + METHOD (flush, "flush", "()V") \ + METHOD (write, "write", "([SII)I") \ + +DECLARE_JNI_CLASS (AudioTrack, "android/media/AudioTrack"); +#undef JNI_CLASS_MEMBERS + +//============================================================================== +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (getMinBufferSize, "getMinBufferSize", "(III)I") \ + METHOD (constructor, "", "(IIIII)V") \ + METHOD (getState, "getState", "()I") \ + METHOD (startRecording, "startRecording", "()V") \ + METHOD (stop, "stop", "()V") \ + METHOD (read, "read", "([SII)I") \ + METHOD (release, "release", "()V") \ + +DECLARE_JNI_CLASS (AudioRecord, "android/media/AudioRecord"); +#undef JNI_CLASS_MEMBERS + +//============================================================================== +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICFIELD (SDK_INT, "SDK_INT", "I") \ + + DECLARE_JNI_CLASS (AndroidBuildVersion, "android/os/Build$VERSION"); +#undef JNI_CLASS_MEMBERS + +//============================================================================== +enum +{ + CHANNEL_OUT_STEREO = 12, + CHANNEL_IN_STEREO = 12, + CHANNEL_IN_MONO = 16, + ENCODING_PCM_16BIT = 2, + STREAM_MUSIC = 3, + MODE_STREAM = 1, + STATE_UNINITIALIZED = 0 +}; + +const char* const javaAudioTypeName = "Android Audio"; + +//============================================================================== +class AndroidAudioIODevice : public AudioIODevice, + public Thread +{ +public: + //============================================================================== + AndroidAudioIODevice (const String& deviceName) + : AudioIODevice (deviceName, javaAudioTypeName), + Thread ("audio"), + minBufferSizeOut (0), minBufferSizeIn (0), callback (0), sampleRate (0), + numClientInputChannels (0), numDeviceInputChannels (0), numDeviceInputChannelsAvailable (2), + numClientOutputChannels (0), numDeviceOutputChannels (0), + actualBufferSize (0), isRunning (false), + inputChannelBuffer (1, 1), + outputChannelBuffer (1, 1) + { + JNIEnv* env = getEnv(); + sampleRate = env->CallStaticIntMethod (AudioTrack, AudioTrack.getNativeOutputSampleRate, MODE_STREAM); + + minBufferSizeOut = (int) env->CallStaticIntMethod (AudioTrack, AudioTrack.getMinBufferSize, sampleRate, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT); + minBufferSizeIn = (int) env->CallStaticIntMethod (AudioRecord, AudioRecord.getMinBufferSize, sampleRate, CHANNEL_IN_STEREO, ENCODING_PCM_16BIT); + + if (minBufferSizeIn <= 0) + { + minBufferSizeIn = env->CallStaticIntMethod (AudioRecord, AudioRecord.getMinBufferSize, sampleRate, CHANNEL_IN_MONO, ENCODING_PCM_16BIT); + + if (minBufferSizeIn > 0) + numDeviceInputChannelsAvailable = 1; + else + numDeviceInputChannelsAvailable = 0; + } + + DBG ("Audio device - min buffers: " << minBufferSizeOut << ", " << minBufferSizeIn << "; " + << sampleRate << " Hz; input chans: " << numDeviceInputChannelsAvailable); + } + + ~AndroidAudioIODevice() + { + close(); + } + + StringArray getOutputChannelNames() override + { + StringArray s; + s.add ("Left"); + s.add ("Right"); + return s; + } + + StringArray getInputChannelNames() override + { + StringArray s; + + if (numDeviceInputChannelsAvailable == 2) + { + s.add ("Left"); + s.add ("Right"); + } + else if (numDeviceInputChannelsAvailable == 1) + { + s.add ("Audio Input"); + } + + return s; + } + + Array getAvailableSampleRates() override + { + Array r; + r.add ((double) sampleRate); + return r; + } + + Array getAvailableBufferSizes() override + { + Array b; + int n = 16; + + for (int i = 0; i < 50; ++i) + { + b.add (n); + n += n < 64 ? 16 + : (n < 512 ? 32 + : (n < 1024 ? 64 + : (n < 2048 ? 128 : 256))); + } + + return b; + } + + int getDefaultBufferSize() override { return 2048; } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double requestedSampleRate, + int bufferSize) override + { + close(); + + if (sampleRate != (int) requestedSampleRate) + return "Sample rate not allowed"; + + lastError.clear(); + int preferredBufferSize = (bufferSize <= 0) ? getDefaultBufferSize() : bufferSize; + + numDeviceInputChannels = 0; + numDeviceOutputChannels = 0; + + activeOutputChans = outputChannels; + activeOutputChans.setRange (2, activeOutputChans.getHighestBit(), false); + numClientOutputChannels = activeOutputChans.countNumberOfSetBits(); + + activeInputChans = inputChannels; + activeInputChans.setRange (2, activeInputChans.getHighestBit(), false); + numClientInputChannels = activeInputChans.countNumberOfSetBits(); + + actualBufferSize = preferredBufferSize; + inputChannelBuffer.setSize (2, actualBufferSize); + inputChannelBuffer.clear(); + outputChannelBuffer.setSize (2, actualBufferSize); + outputChannelBuffer.clear(); + + JNIEnv* env = getEnv(); + + if (numClientOutputChannels > 0) + { + numDeviceOutputChannels = 2; + outputDevice = GlobalRef (env->NewObject (AudioTrack, AudioTrack.constructor, + STREAM_MUSIC, sampleRate, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT, + (jint) (minBufferSizeOut * numDeviceOutputChannels * static_cast (sizeof (int16))), MODE_STREAM)); + + const bool supportsUnderrunCount = (getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT) >= 24); + getUnderrunCount = supportsUnderrunCount ? env->GetMethodID (AudioTrack, "getUnderrunCount", "()I") : 0; + + int outputDeviceState = env->CallIntMethod (outputDevice, AudioTrack.getState); + if (outputDeviceState > 0) + { + isRunning = true; + } + else + { + // failed to open the device + outputDevice.clear(); + lastError = "Error opening audio output device: android.media.AudioTrack failed with state = " + String (outputDeviceState); + } + } + + if (numClientInputChannels > 0 && numDeviceInputChannelsAvailable > 0) + { + if (! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) + { + // If you hit this assert, you probably forgot to get RuntimePermissions::recordAudio + // before trying to open an audio input device. This is not going to work! + jassertfalse; + + inputDevice.clear(); + lastError = "Error opening audio input device: the app was not granted android.permission.RECORD_AUDIO"; + } + else + { + numDeviceInputChannels = jmin (numClientInputChannels, numDeviceInputChannelsAvailable); + inputDevice = GlobalRef (env->NewObject (AudioRecord, AudioRecord.constructor, + 0 /* (default audio source) */, sampleRate, + numDeviceInputChannelsAvailable > 1 ? CHANNEL_IN_STEREO : CHANNEL_IN_MONO, + ENCODING_PCM_16BIT, + (jint) (minBufferSizeIn * numDeviceInputChannels * static_cast (sizeof (int16))))); + + int inputDeviceState = env->CallIntMethod (inputDevice, AudioRecord.getState); + if (inputDeviceState > 0) + { + isRunning = true; + } + else + { + // failed to open the device + inputDevice.clear(); + lastError = "Error opening audio input device: android.media.AudioRecord failed with state = " + String (inputDeviceState); + } + } + } + + if (isRunning) + { + if (outputDevice != nullptr) + env->CallVoidMethod (outputDevice, AudioTrack.play); + + if (inputDevice != nullptr) + env->CallVoidMethod (inputDevice, AudioRecord.startRecording); + + startThread (8); + } + else + { + closeDevices(); + } + + return lastError; + } + + void close() override + { + if (isRunning) + { + stopThread (2000); + isRunning = false; + closeDevices(); + } + } + + int getOutputLatencyInSamples() override { return (minBufferSizeOut * 3) / 4; } + int getInputLatencyInSamples() override { return (minBufferSizeIn * 3) / 4; } + bool isOpen() override { return isRunning; } + int getCurrentBufferSizeSamples() override { return actualBufferSize; } + int getCurrentBitDepth() override { return 16; } + double getCurrentSampleRate() override { return sampleRate; } + BigInteger getActiveOutputChannels() const override { return activeOutputChans; } + BigInteger getActiveInputChannels() const override { return activeInputChans; } + String getLastError() override { return lastError; } + bool isPlaying() override { return isRunning && callback != 0; } + + int getXRunCount() const noexcept override + { + if (outputDevice != nullptr && getUnderrunCount != 0) + return getEnv()->CallIntMethod (outputDevice, getUnderrunCount); + + return -1; + } + + void start (AudioIODeviceCallback* newCallback) override + { + if (isRunning && callback != newCallback) + { + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (this); + + const ScopedLock sl (callbackLock); + callback = newCallback; + } + } + + void stop() override + { + if (isRunning) + { + AudioIODeviceCallback* lastCallback; + + { + const ScopedLock sl (callbackLock); + lastCallback = callback; + callback = nullptr; + } + + if (lastCallback != nullptr) + lastCallback->audioDeviceStopped(); + } + } + + void run() override + { + JNIEnv* env = getEnv(); + jshortArray audioBuffer = env->NewShortArray (actualBufferSize * jmax (numDeviceOutputChannels, numDeviceInputChannels)); + + while (! threadShouldExit()) + { + if (inputDevice != nullptr) + { + jint numRead = env->CallIntMethod (inputDevice, AudioRecord.read, audioBuffer, 0, actualBufferSize * numDeviceInputChannels); + + if (numRead < actualBufferSize * numDeviceInputChannels) + { + DBG ("Audio read under-run! " << numRead); + } + + jshort* const src = env->GetShortArrayElements (audioBuffer, 0); + + for (int chan = 0; chan < inputChannelBuffer.getNumChannels(); ++chan) + { + AudioData::Pointer d (inputChannelBuffer.getWritePointer (chan)); + + if (chan < numDeviceInputChannels) + { + AudioData::Pointer s (src + chan, numDeviceInputChannels); + d.convertSamples (s, actualBufferSize); + } + else + { + d.clearSamples (actualBufferSize); + } + } + + env->ReleaseShortArrayElements (audioBuffer, src, 0); + } + + if (threadShouldExit()) + break; + + { + const ScopedLock sl (callbackLock); + + if (callback != nullptr) + { + callback->audioDeviceIOCallback (inputChannelBuffer.getArrayOfReadPointers(), numClientInputChannels, + outputChannelBuffer.getArrayOfWritePointers(), numClientOutputChannels, + actualBufferSize); + } + else + { + outputChannelBuffer.clear(); + } + } + + if (outputDevice != nullptr) + { + if (threadShouldExit()) + break; + + jshort* const dest = env->GetShortArrayElements (audioBuffer, 0); + + for (int chan = 0; chan < numDeviceOutputChannels; ++chan) + { + AudioData::Pointer d (dest + chan, numDeviceOutputChannels); + + const float* const sourceChanData = outputChannelBuffer.getReadPointer (jmin (chan, outputChannelBuffer.getNumChannels() - 1)); + AudioData::Pointer s (sourceChanData); + d.convertSamples (s, actualBufferSize); + } + + env->ReleaseShortArrayElements (audioBuffer, dest, 0); + jint numWritten = env->CallIntMethod (outputDevice, AudioTrack.write, audioBuffer, 0, actualBufferSize * numDeviceOutputChannels); + + if (numWritten < actualBufferSize * numDeviceOutputChannels) + { + DBG ("Audio write underrun! " << numWritten); + } + } + } + } + + int minBufferSizeOut, minBufferSizeIn; + +private: + //============================================================================== + CriticalSection callbackLock; + AudioIODeviceCallback* callback; + jint sampleRate; + int numClientInputChannels, numDeviceInputChannels, numDeviceInputChannelsAvailable; + int numClientOutputChannels, numDeviceOutputChannels; + int actualBufferSize; + bool isRunning; + String lastError; + BigInteger activeOutputChans, activeInputChans; + GlobalRef outputDevice, inputDevice; + AudioSampleBuffer inputChannelBuffer, outputChannelBuffer; + jmethodID getUnderrunCount = 0; + + void closeDevices() + { + if (outputDevice != nullptr) + { + outputDevice.callVoidMethod (AudioTrack.stop); + outputDevice.callVoidMethod (AudioTrack.release); + outputDevice.clear(); + } + + if (inputDevice != nullptr) + { + inputDevice.callVoidMethod (AudioRecord.stop); + inputDevice.callVoidMethod (AudioRecord.release); + inputDevice.clear(); + } + } + + JUCE_DECLARE_NON_COPYABLE (AndroidAudioIODevice) +}; + +//============================================================================== +class AndroidAudioIODeviceType : public AudioIODeviceType +{ +public: + AndroidAudioIODeviceType() : AudioIODeviceType (javaAudioTypeName) {} + + //============================================================================== + void scanForDevices() {} + StringArray getDeviceNames (bool) const { return StringArray (javaAudioTypeName); } + int getDefaultDeviceIndex (bool) const { return 0; } + int getIndexOfDevice (AudioIODevice* device, bool) const { return device != nullptr ? 0 : -1; } + bool hasSeparateInputsAndOutputs() const { return false; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + ScopedPointer dev; + + if (outputDeviceName.isNotEmpty() || inputDeviceName.isNotEmpty()) + { + dev = new AndroidAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName + : inputDeviceName); + + if (dev->getCurrentSampleRate() <= 0 || dev->getDefaultBufferSize() <= 0) + dev = nullptr; + } + + return dev.release(); + } + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AndroidAudioIODeviceType) +}; + + +//============================================================================== +extern bool isOpenSLAvailable(); + +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_Android() +{ + #if JUCE_USE_ANDROID_OPENSLES + if (isOpenSLAvailable()) + return nullptr; + #endif + + return new AndroidAudioIODeviceType(); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_android_Midi.cpp b/source/modules/juce_audio_devices/native/juce_android_Midi.cpp new file mode 100644 index 000000000..5afe6562c --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_android_Midi.cpp @@ -0,0 +1,364 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (getJuceAndroidMidiInputDevices, "getJuceAndroidMidiInputDevices", "()[Ljava/lang/String;") \ + METHOD (getJuceAndroidMidiOutputDevices, "getJuceAndroidMidiOutputDevices", "()[Ljava/lang/String;") \ + METHOD (openMidiInputPortWithJuceIndex, "openMidiInputPortWithJuceIndex", "(IJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceMidiPort;") \ + METHOD (openMidiOutputPortWithJuceIndex, "openMidiOutputPortWithJuceIndex", "(I)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceMidiPort;") \ + METHOD (getInputPortNameForJuceIndex, "getInputPortNameForJuceIndex", "(I)Ljava/lang/String;") \ + METHOD (getOutputPortNameForJuceIndex, "getOutputPortNameForJuceIndex", "(I)Ljava/lang/String;") + DECLARE_JNI_CLASS (MidiDeviceManager, JUCE_ANDROID_ACTIVITY_CLASSPATH "$MidiDeviceManager") +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (start, "start", "()V" )\ + METHOD (stop, "stop", "()V") \ + METHOD (close, "close", "()V") \ + METHOD (sendMidi, "sendMidi", "([BII)V") + DECLARE_JNI_CLASS (JuceMidiPort, JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceMidiPort") +#undef JNI_CLASS_MEMBERS + + +//============================================================================== +class AndroidMidiInput +{ +public: + AndroidMidiInput (MidiInput* midiInput, int portIdx, + juce::MidiInputCallback* midiInputCallback, jobject deviceManager) + : juceMidiInput (midiInput), + callback (midiInputCallback), + midiConcatenator (2048), + javaMidiDevice (getEnv()->CallObjectMethod (deviceManager, + MidiDeviceManager.openMidiInputPortWithJuceIndex, + (jint) portIdx, + (jlong) this)) + { + } + + ~AndroidMidiInput() + { + if (jobject d = javaMidiDevice.get()) + { + getEnv()->CallVoidMethod (d, JuceMidiPort.close); + javaMidiDevice.clear(); + } + } + + bool isOpen() const noexcept + { + return javaMidiDevice != nullptr; + } + + void start() + { + if (jobject d = javaMidiDevice.get()) + getEnv()->CallVoidMethod (d, JuceMidiPort.start); + } + + void stop() + { + if (jobject d = javaMidiDevice.get()) + getEnv()->CallVoidMethod (d, JuceMidiPort.stop); + + callback = nullptr; + } + + void receive (jbyteArray byteArray, jlong offset, jint len, jlong timestamp) + { + jassert (byteArray != nullptr); + jbyte* data = getEnv()->GetByteArrayElements (byteArray, nullptr); + + HeapBlock buffer (static_cast (len)); + std::memcpy (buffer.get(), data + offset, static_cast (len)); + + midiConcatenator.pushMidiData (buffer.get(), + len, static_cast (timestamp) * 1.0e-9, + juceMidiInput, *callback); + + getEnv()->ReleaseByteArrayElements (byteArray, data, 0); + } + +private: + MidiInput* juceMidiInput; + MidiInputCallback* callback; + MidiDataConcatenator midiConcatenator; + GlobalRef javaMidiDevice; +}; + +//============================================================================== +class AndroidMidiOutput +{ +public: + AndroidMidiOutput (jobject midiDevice) + : javaMidiDevice (midiDevice) + { + } + + ~AndroidMidiOutput() + { + if (jobject d = javaMidiDevice.get()) + { + getEnv()->CallVoidMethod (d, JuceMidiPort.close); + javaMidiDevice.clear(); + } + } + + void send (jbyteArray byteArray, jint offset, jint len) + { + if (jobject d = javaMidiDevice.get()) + getEnv()->CallVoidMethod (d, + JuceMidiPort.sendMidi, + byteArray, offset, len); + } + +private: + GlobalRef javaMidiDevice; +}; + +JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024JuceMidiInputPort), handleReceive, + void, (JNIEnv* env, jobject, jlong host, jbyteArray byteArray, + jint offset, jint count, jlong timestamp)) +{ + // Java may create a Midi thread which JUCE doesn't know about and this callback may be + // received on this thread. Java will have already created a JNI Env for this new thread, + // which we need to tell Juce about + setEnv (env); + + reinterpret_cast (host)->receive (byteArray, offset, count, timestamp); +} + +//============================================================================== +class AndroidMidiDeviceManager +{ +public: + AndroidMidiDeviceManager() + : deviceManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidMidiDeviceManager)) + { + } + + String getInputPortNameForJuceIndex (int idx) + { + if (jobject dm = deviceManager.get()) + { + LocalRef string ((jstring) getEnv()->CallObjectMethod (dm, MidiDeviceManager.getInputPortNameForJuceIndex, idx)); + return juceString (string); + } + + return {}; + } + + String getOutputPortNameForJuceIndex (int idx) + { + if (jobject dm = deviceManager.get()) + { + LocalRef string ((jstring) getEnv()->CallObjectMethod (dm, MidiDeviceManager.getOutputPortNameForJuceIndex, idx)); + return juceString (string); + } + + return {}; + } + + StringArray getDevices (bool input) + { + if (jobject dm = deviceManager.get()) + { + jobjectArray jDevices + = (jobjectArray) getEnv()->CallObjectMethod (dm, input ? MidiDeviceManager.getJuceAndroidMidiInputDevices + : MidiDeviceManager.getJuceAndroidMidiOutputDevices); + + // Create a local reference as converting this + // to a JUCE string will call into JNI + LocalRef devices (jDevices); + return javaStringArrayToJuce (devices); + } + + return {}; + } + + AndroidMidiInput* openMidiInputPortWithIndex (int idx, MidiInput* juceMidiInput, juce::MidiInputCallback* callback) + { + if (jobject dm = deviceManager.get()) + { + ScopedPointer androidMidiInput (new AndroidMidiInput (juceMidiInput, idx, callback, dm)); + + if (androidMidiInput->isOpen()) + return androidMidiInput.release(); + } + + return nullptr; + } + + AndroidMidiOutput* openMidiOutputPortWithIndex (int idx) + { + if (jobject dm = deviceManager.get()) + if (jobject javaMidiPort = getEnv()->CallObjectMethod (dm, MidiDeviceManager.openMidiOutputPortWithJuceIndex, (jint) idx)) + return new AndroidMidiOutput (javaMidiPort); + + return nullptr; + } + +private: + static StringArray javaStringArrayToJuce (jobjectArray jStrings) + { + StringArray retval; + + JNIEnv* env = getEnv(); + const int count = env->GetArrayLength (jStrings); + + for (int i = 0; i < count; ++i) + { + LocalRef string ((jstring) env->GetObjectArrayElement (jStrings, i)); + retval.add (juceString (string)); + } + + return retval; + } + + GlobalRef deviceManager; +}; + +//============================================================================== +StringArray MidiOutput::getDevices() +{ + AndroidMidiDeviceManager manager; + return manager.getDevices (false); +} + +int MidiOutput::getDefaultDeviceIndex() +{ + return 0; +} + +MidiOutput* MidiOutput::openDevice (int index) +{ + if (index < 0) + return nullptr; + + AndroidMidiDeviceManager manager; + + String midiOutputName = manager.getOutputPortNameForJuceIndex (index); + + if (midiOutputName.isEmpty()) + { + // you supplied an invalid device index! + jassertfalse; + return nullptr; + } + + if (AndroidMidiOutput* midiOutput = manager.openMidiOutputPortWithIndex (index)) + { + MidiOutput* retval = new MidiOutput (midiOutputName); + retval->internal = midiOutput; + + return retval; + } + + return nullptr; +} + +MidiOutput::~MidiOutput() +{ + stopBackgroundThread(); + + delete reinterpret_cast (internal); +} + +void MidiOutput::sendMessageNow (const MidiMessage& message) +{ + if (AndroidMidiOutput* androidMidi = reinterpret_cast(internal)) + { + JNIEnv* env = getEnv(); + const int messageSize = message.getRawDataSize(); + + LocalRef messageContent = LocalRef (env->NewByteArray (messageSize)); + jbyteArray content = messageContent.get(); + + jbyte* rawBytes = env->GetByteArrayElements (content, nullptr); + std::memcpy (rawBytes, message.getRawData(), static_cast (messageSize)); + env->ReleaseByteArrayElements (content, rawBytes, 0); + + androidMidi->send (content, (jint) 0, (jint) messageSize); + } +} + +//============================================================================== +MidiInput::MidiInput (const String& nm) : name (nm) +{ +} + +StringArray MidiInput::getDevices() +{ + AndroidMidiDeviceManager manager; + return manager.getDevices (true); +} + +int MidiInput::getDefaultDeviceIndex() +{ + return 0; +} + +MidiInput* MidiInput::openDevice (int index, juce::MidiInputCallback* callback) +{ + if (index < 0) + return nullptr; + + AndroidMidiDeviceManager manager; + + String midiInputName = manager.getInputPortNameForJuceIndex (index); + + if (midiInputName.isEmpty()) + { + // you supplied an invalid device index! + jassertfalse; + return nullptr; + } + + ScopedPointer midiInput (new MidiInput (midiInputName)); + + midiInput->internal = manager.openMidiInputPortWithIndex (index, midiInput, callback); + + return midiInput->internal != nullptr ? midiInput.release() + : nullptr; +} + +void MidiInput::start() +{ + if (AndroidMidiInput* mi = reinterpret_cast (internal)) + mi->start(); +} + +void MidiInput::stop() +{ + if (AndroidMidiInput* mi = reinterpret_cast (internal)) + mi->stop(); +} + +MidiInput::~MidiInput() +{ + delete reinterpret_cast (internal); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_android_OpenSL.cpp b/source/modules/juce_audio_devices/native/juce_android_OpenSL.cpp new file mode 100644 index 000000000..48cd2fe91 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_android_OpenSL.cpp @@ -0,0 +1,1321 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +#ifndef SL_ANDROID_DATAFORMAT_PCM_EX + #define SL_ANDROID_DATAFORMAT_PCM_EX ((SLuint32) 0x00000004) +#endif + +#ifndef SL_ANDROID_PCM_REPRESENTATION_FLOAT + #define SL_ANDROID_PCM_REPRESENTATION_FLOAT ((SLuint32) 0x00000003) +#endif + +#ifndef SL_ANDROID_RECORDING_PRESET_UNPROCESSED + #define SL_ANDROID_RECORDING_PRESET_UNPROCESSED ((SLuint32) 0x00000005) +#endif + +//============================================================================== +struct PCMDataFormatEx : SLDataFormat_PCM +{ + SLuint32 representation; +}; + +//============================================================================== +template struct IntfIID; +template <> struct IntfIID { static SLInterfaceID_ iid; }; +template <> struct IntfIID { static SLInterfaceID_ iid; }; +template <> struct IntfIID { static SLInterfaceID_ iid; }; +template <> struct IntfIID { static SLInterfaceID_ iid; }; +template <> struct IntfIID { static SLInterfaceID_ iid; }; +template <> struct IntfIID { static SLInterfaceID_ iid; }; +template <> struct IntfIID { static SLInterfaceID_ iid; }; + +SLInterfaceID_ IntfIID::iid = { 0x79216360, 0xddd7, 0x11db, 0xac16, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; +SLInterfaceID_ IntfIID::iid = { 0x8d97c260, 0xddd4, 0x11db, 0x958f, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; +SLInterfaceID_ IntfIID::iid = { 0x97750f60, 0xddd7, 0x11db, 0x92b1, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; +SLInterfaceID_ IntfIID::iid = { 0xef0bd9c0, 0xddd7, 0x11db, 0xbf49, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; +SLInterfaceID_ IntfIID::iid = { 0xc5657aa0, 0xdddb, 0x11db, 0x82f7, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; +SLInterfaceID_ IntfIID::iid = { 0x198e4940, 0xc5d7, 0x11df, 0xa2a6, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; +SLInterfaceID_ IntfIID::iid = { 0x89f6a7e0, 0xbeac, 0x11df, 0x8b5c, {0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b} }; + +//============================================================================== +// Some life-time and type management of OpenSL objects +class SlObjectRef +{ +public: + //============================================================================== + SlObjectRef() noexcept {} + SlObjectRef (const SlObjectRef& obj) noexcept : cb (obj.cb) {} + SlObjectRef (SlObjectRef&& obj) noexcept : cb (static_cast&&> (obj.cb)) { obj.cb = nullptr; } + explicit SlObjectRef (SLObjectItf o) : cb (new ControlBlock (o)) {} + + //============================================================================== + SlObjectRef& operator=(const SlObjectRef& r) noexcept { cb = r.cb; return *this; } + SlObjectRef& operator=(SlObjectRef&& r) noexcept { cb = static_cast&&> (r.cb); r.cb = nullptr; return *this; } + SlObjectRef& operator=(std::nullptr_t) noexcept { cb = nullptr; return *this; } + + //============================================================================== + const SLObjectItf_* const operator*() noexcept { return *cb->ptr.get(); } + SLObjectItf operator->() noexcept { return (cb == nullptr ? nullptr : cb->ptr.get()); } + operator SLObjectItf() noexcept { return (cb == nullptr ? nullptr : cb->ptr.get()); } + + //============================================================================== + bool operator== (nullptr_t) const noexcept { return (cb == nullptr || cb->ptr == nullptr); } + bool operator!= (nullptr_t) const noexcept { return (cb != nullptr && cb->ptr != nullptr); } +private: + + //============================================================================== + struct ControlBlock : ReferenceCountedObject { ScopedPointer ptr; ControlBlock() {} ControlBlock (SLObjectItf o) : ptr (o) {} }; + ReferenceCountedObjectPtr cb; +}; + +template +class SlRef : public SlObjectRef +{ +public: + //============================================================================== + SlRef() noexcept : type (nullptr) {} + SlRef (SlRef& r) noexcept : SlObjectRef (r), type (r.type) {} + SlRef (SlRef&& r) noexcept : SlObjectRef (static_cast (r)), type (r.type) { r.type = nullptr; } + + //============================================================================== + SlRef& operator= (const SlRef& r) noexcept { SlObjectRef::operator= (r); type = r.type; return *this; } + SlRef& operator= (SlRef&& r) noexcept { SlObjectRef::operator= (static_cast (r)); type = r.type; r.type = nullptr; return *this; } + SlRef& operator= (std::nullptr_t) noexcept { SlObjectRef::operator= (nullptr); type = nullptr; return *this; } + + //============================================================================== + T* const operator*() noexcept { return *type; } + T* const * operator->() noexcept { return type; } + operator T* const *() noexcept { return type; } + + //============================================================================== + static SlRef cast (SlObjectRef& base) { return SlRef (base); } + static SlRef cast (SlObjectRef&& base) { return SlRef (static_cast (base)); } +private: + //============================================================================== + SlRef (SlObjectRef& base) : SlObjectRef (base) + { + SLObjectItf obj = SlObjectRef::operator->(); + SLresult err = (*obj)->GetInterface (obj, &IntfIID::iid, &type); + if (type == nullptr || err != SL_RESULT_SUCCESS) + *this = nullptr; + } + + SlRef (SlObjectRef&& base) : SlObjectRef (static_cast (base)) + { + SLObjectItf obj = SlObjectRef::operator->(); + SLresult err = (*obj)->GetInterface (obj, &IntfIID::iid, &type); + base = nullptr; + + if (type == nullptr || err != SL_RESULT_SUCCESS) + *this = nullptr; + } + T* const * type; +}; + +template <> +struct ContainerDeletePolicy +{ + static void destroy (SLObjectItf object) + { + if (object != nullptr) + (*object)->Destroy (object); + } +}; + +//============================================================================== +template struct BufferHelpers {}; + +template <> +struct BufferHelpers +{ + enum { isFloatingPoint = 0 }; + + static void initPCMDataFormat (PCMDataFormatEx& dataFormat, int numChannels, double sampleRate) + { + dataFormat.formatType = SL_DATAFORMAT_PCM; + dataFormat.numChannels = (SLuint32) numChannels; + dataFormat.samplesPerSec = (SLuint32) (sampleRate * 1000); + dataFormat.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + dataFormat.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; + dataFormat.channelMask = (numChannels == 1) ? SL_SPEAKER_FRONT_CENTER : + (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT); + dataFormat.endianness = SL_BYTEORDER_LITTLEENDIAN; + dataFormat.representation = 0; + } + + static void prepareCallbackBuffer (AudioSampleBuffer&, int16*) {} + + static void convertFromOpenSL (const int16* srcInterleaved, AudioSampleBuffer& audioBuffer) + { + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (audioBuffer.getWritePointer (i)); + SrcSampleType srcData (srcInterleaved + i, audioBuffer.getNumChannels()); + dstData.convertSamples (srcData, audioBuffer.getNumSamples()); + } + } + + static void convertToOpenSL (const AudioSampleBuffer& audioBuffer, int16* dstInterleaved) + { + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (dstInterleaved + i, audioBuffer.getNumChannels()); + SrcSampleType srcData (audioBuffer.getReadPointer (i)); + + dstData.convertSamples (srcData, audioBuffer.getNumSamples()); + } + } + +}; + +template <> +struct BufferHelpers +{ + enum { isFloatingPoint = 1 }; + + static void initPCMDataFormat (PCMDataFormatEx& dataFormat, int numChannels, double sampleRate) + { + dataFormat.formatType = SL_ANDROID_DATAFORMAT_PCM_EX; + dataFormat.numChannels = (SLuint32) numChannels; + dataFormat.samplesPerSec = (SLuint32) (sampleRate * 1000); + dataFormat.bitsPerSample = 32; + dataFormat.containerSize = 32; + dataFormat.channelMask = (numChannels == 1) ? SL_SPEAKER_FRONT_CENTER : + (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT); + dataFormat.endianness = SL_BYTEORDER_LITTLEENDIAN; + dataFormat.representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT; + } + + static void prepareCallbackBuffer (AudioSampleBuffer& audioBuffer, float* native) + { + if (audioBuffer.getNumChannels() == 1) + audioBuffer.setDataToReferTo (&native, 1, audioBuffer.getNumSamples()); + } + + static void convertFromOpenSL (const float* srcInterleaved, AudioSampleBuffer& audioBuffer) + { + if (audioBuffer.getNumChannels() == 1) + { + jassert (srcInterleaved == audioBuffer.getWritePointer (0)); + return; + } + + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (audioBuffer.getWritePointer (i)); + SrcSampleType srcData (srcInterleaved + i, audioBuffer.getNumChannels()); + dstData.convertSamples (srcData, audioBuffer.getNumSamples()); + } + } + + static void convertToOpenSL (const AudioSampleBuffer& audioBuffer, float* dstInterleaved) + { + if (audioBuffer.getNumChannels() == 1) + { + jassert (dstInterleaved == audioBuffer.getReadPointer (0)); + return; + } + + for (int i = 0; i < audioBuffer.getNumChannels(); ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; + + DstSampleType dstData (dstInterleaved + i, audioBuffer.getNumChannels()); + SrcSampleType srcData (audioBuffer.getReadPointer (i)); + + dstData.convertSamples (srcData, audioBuffer.getNumSamples()); + } + } +}; + +class SLRealtimeThread; + +//============================================================================== +class OpenSLAudioIODevice : public AudioIODevice +{ +public: + //============================================================================== + template + class OpenSLSessionT; + + //============================================================================== + // CRTP + template + struct OpenSLQueueRunner + { + OpenSLQueueRunner (OpenSLSessionT& sessionToUse, int numChannelsToUse) + : owner (sessionToUse), + numChannels (numChannelsToUse), + nativeBuffer (static_cast (numChannels * owner.bufferSize * owner.numBuffers)), + scratchBuffer (numChannelsToUse, owner.bufferSize), + sampleBuffer (scratchBuffer.getArrayOfWritePointers(), numChannelsToUse, owner.bufferSize), + nextBlock (0), numBlocksOut (0) + {} + + ~OpenSLQueueRunner() + { + if (config != nullptr && javaProxy != nullptr) + { + javaProxy.clear(); + (*config)->ReleaseJavaProxy (config, SL_ANDROID_JAVA_PROXY_ROUTING); + } + } + + bool init() + { + runner = crtp().createPlayerOrRecorder(); + if (runner == nullptr) + return false; + + const bool supportsJavaProxy = (getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT) >= 24); + + if (supportsJavaProxy) + { + // may return nullptr on some platforms - that's ok + config = SlRef::cast (runner); + + if (config != nullptr) + { + jobject audioRoutingJni; + auto status = (*config)->AcquireJavaProxy (config, SL_ANDROID_JAVA_PROXY_ROUTING, + &audioRoutingJni); + + if (status == SL_RESULT_SUCCESS && audioRoutingJni != 0) + javaProxy = GlobalRef (audioRoutingJni); + } + } + + queue = SlRef::cast (runner); + if (queue == nullptr) + return false; + + return ((*queue)->RegisterCallback (queue, staticFinished, this) == SL_RESULT_SUCCESS); + } + + + void clear() + { + nextBlock.set (0); + numBlocksOut.set (0); + + zeromem (nativeBuffer.get(), static_cast (owner.bufferSize * numChannels * owner.numBuffers) * sizeof (T)); + scratchBuffer.clear(); + (*queue)->Clear (queue); + } + + void enqueueBuffer() + { + (*queue)->Enqueue (queue, getCurrentBuffer(), static_cast (getBufferSizeInSamples() * sizeof (T))); + ++numBlocksOut; + } + + bool isBufferAvailable() const { return (numBlocksOut.get() < owner.numBuffers); } + T* getNextBuffer() { nextBlock.set((nextBlock.get() + 1) % owner.numBuffers); return getCurrentBuffer(); } + T* getCurrentBuffer() { return nativeBuffer.get() + (static_cast (nextBlock.get()) * getBufferSizeInSamples()); } + size_t getBufferSizeInSamples() const { return static_cast (owner.bufferSize * numChannels); } + + void finished (SLAndroidSimpleBufferQueueItf) + { + attachAndroidJNI(); + + --numBlocksOut; + owner.doSomeWorkOnAudioThread(); + } + + static void staticFinished (SLAndroidSimpleBufferQueueItf caller, void *pContext) + { + reinterpret_cast (pContext)->finished (caller); + } + + // get the "this" pointer for CRTP + Child& crtp() { return * ((Child*) this); } + const Child& crtp() const { return * ((Child*) this); } + + OpenSLSessionT& owner; + + SlRef runner; + SlRef queue; + SlRef config; + GlobalRef javaProxy; + + int numChannels; + + HeapBlock nativeBuffer; + AudioSampleBuffer scratchBuffer, sampleBuffer; + + Atomic nextBlock, numBlocksOut; + }; + + //============================================================================== + template + struct OpenSLQueueRunnerPlayer : OpenSLQueueRunner, SLPlayItf_> + { + typedef OpenSLQueueRunner, SLPlayItf_> Base; + + enum { isPlayer = 1 }; + + OpenSLQueueRunnerPlayer (OpenSLSessionT& sessionToUse, int numChannelsToUse) + : Base (sessionToUse, numChannelsToUse) + {} + + SlRef createPlayerOrRecorder() + { + SLDataLocator_AndroidSimpleBufferQueue queueLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, static_cast (Base::owner.numBuffers)}; + SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, Base::owner.outputMix}; + + PCMDataFormatEx dataFormat; + BufferHelpers::initPCMDataFormat (dataFormat, Base::numChannels, Base::owner.sampleRate); + + SLDataSource source = {&queueLocator, &dataFormat}; + SLDataSink sink = {&outputMix, nullptr}; + + SLInterfaceID queueInterfaces[] = { &IntfIID::iid, &IntfIID::iid }; + SLboolean interfaceRequired[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE}; + + SLObjectItf obj = nullptr; + + SLresult status = (*Base::owner.engine)->CreateAudioPlayer (Base::owner.engine, &obj, &source, &sink, 2, queueInterfaces, interfaceRequired); + if (status != SL_RESULT_SUCCESS || obj == nullptr || (*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + if (obj != nullptr) + (*obj)->Destroy (obj); + + return SlRef(); + } + + return SlRef::cast (SlObjectRef (obj)); + } + + void setState (bool running) { (*Base::runner)->SetPlayState (Base::runner, running ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_STOPPED); } + }; + + template + struct OpenSLQueueRunnerRecorder : OpenSLQueueRunner, SLRecordItf_> + { + typedef OpenSLQueueRunner, SLRecordItf_> Base; + + enum { isPlayer = 0 }; + + OpenSLQueueRunnerRecorder (OpenSLSessionT& sessionToUse, int numChannelsToUse) + : Base (sessionToUse, numChannelsToUse) + {} + + SlRef createPlayerOrRecorder() + { + SLDataLocator_IODevice ioDeviceLocator = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr}; + SLDataLocator_AndroidSimpleBufferQueue queueLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, static_cast (Base::owner.numBuffers)}; + + PCMDataFormatEx dataFormat; + BufferHelpers::initPCMDataFormat (dataFormat, Base::numChannels, Base::owner.sampleRate); + + SLDataSource source = {&ioDeviceLocator, nullptr}; + SLDataSink sink = {&queueLocator, &dataFormat}; + + SLInterfaceID queueInterfaces[] = { &IntfIID::iid, &IntfIID::iid }; + SLboolean interfaceRequired[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE}; + + SLObjectItf obj = nullptr; + + SLresult status = (*Base::owner.engine)->CreateAudioRecorder (Base::owner.engine, &obj, &source, &sink, 2, queueInterfaces, interfaceRequired); + if (status != SL_RESULT_SUCCESS || obj == nullptr || (*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + if (obj != nullptr) + (*obj)->Destroy (obj); + + return SlRef(); + } + + SlRef recorder = SlRef::cast (SlObjectRef (obj)); + + return recorder; + } + + bool setAudioPreprocessingEnabled (bool shouldEnable) + { + if (Base::config != nullptr) + { + const bool supportsUnprocessed = (getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT) >= 25); + const SLuint32 recordingPresetValue + = (shouldEnable ? SL_ANDROID_RECORDING_PRESET_GENERIC + : (supportsUnprocessed ? SL_ANDROID_RECORDING_PRESET_UNPROCESSED + : SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION)); + + SLresult status = (*Base::config)->SetConfiguration (Base::config, SL_ANDROID_KEY_RECORDING_PRESET, + &recordingPresetValue, sizeof (recordingPresetValue)); + + return (status == SL_RESULT_SUCCESS); + } + + return false; + } + + void setState (bool running) { (*Base::runner)->SetRecordState (Base::runner, running ? SL_RECORDSTATE_RECORDING : SL_RECORDSTATE_STOPPED); } + }; + + //============================================================================== + class OpenSLSession + { + public: + OpenSLSession (DynamicLibrary& slLibraryToUse, + int numInputChannels, int numOutputChannels, + double samleRateToUse, int bufferSizeToUse, + int numBuffersToUse) + : inputChannels (numInputChannels), outputChannels (numOutputChannels), + sampleRate (samleRateToUse), bufferSize (bufferSizeToUse), numBuffers (numBuffersToUse), + running (false), audioProcessingEnabled (true), callback (nullptr) + { + jassert (numInputChannels > 0 || numOutputChannels > 0); + + if (CreateEngineFunc createEngine = (CreateEngineFunc) slLibraryToUse.getFunction ("slCreateEngine")) + { + SLObjectItf obj = nullptr; + + SLresult err = createEngine (&obj, 0, nullptr, 0, nullptr, nullptr); + if (err != SL_RESULT_SUCCESS || obj == nullptr || (*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + if (obj != nullptr) + (*obj)->Destroy (obj); + + return; + } + + engine = SlRef::cast (SlObjectRef (obj)); + } + + if (outputChannels > 0) + { + SLObjectItf obj = nullptr; + + SLresult err = (*engine)->CreateOutputMix (engine, &obj, 0, nullptr, nullptr); + if (err != SL_RESULT_SUCCESS || obj == nullptr || (*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + if (obj != nullptr) + (*obj)->Destroy (obj); + + return; + } + + outputMix = SlRef::cast (SlObjectRef (obj)); + } + } + + virtual ~OpenSLSession() {} + + virtual bool openedOK() const { return (engine != nullptr && (outputChannels == 0 || (outputMix != nullptr))); } + virtual void start() { stop(); jassert (callback.get() != nullptr); running = true; } + virtual void stop() { running = false; } + virtual bool setAudioPreprocessingEnabled (bool shouldEnable) = 0; + virtual bool supportsFloatingPoint() const noexcept = 0; + virtual int getXRunCount() const noexcept = 0; + + void setCallback (AudioIODeviceCallback* callbackToUse) + { + if (! running) + { + callback.set (callbackToUse); + return; + } + + // don't set callback to null! stop the playback instead! + jassert (callbackToUse != nullptr); + + // spin-lock until we can set the callback + while (true) + { + AudioIODeviceCallback* old = callback.get(); + if (old == callbackToUse) + break; + + if (callback.compareAndSetBool (callbackToUse, old)) + break; + + Thread::sleep (1); + } + } + + void process (const float** inputChannelData, float** outputChannelData) + { + if (AudioIODeviceCallback* cb = callback.exchange(nullptr)) + { + cb->audioDeviceIOCallback (inputChannelData, inputChannels, outputChannelData, outputChannels, bufferSize); + callback.set (cb); + } + else + { + for (int i = 0; i < outputChannels; ++i) + zeromem (outputChannelData[i], sizeof(float) * static_cast (bufferSize)); + } + } + + static OpenSLSession* create (DynamicLibrary& slLibrary, + int numInputChannels, int numOutputChannels, + double samleRateToUse, int bufferSizeToUse, + int numBuffersToUse); + + //============================================================================== + typedef SLresult (*CreateEngineFunc)(SLObjectItf*,SLuint32,const SLEngineOption*,SLuint32,const SLInterfaceID*,const SLboolean*); + + //============================================================================== + int inputChannels, outputChannels; + double sampleRate; + int bufferSize, numBuffers; + + bool running, audioProcessingEnabled; + + SlRef engine; + SlRef outputMix; + + Atomic callback; + }; + + template + class OpenSLSessionT : public OpenSLSession + { + public: + OpenSLSessionT (DynamicLibrary& slLibraryToUse, + int numInputChannels, int numOutputChannels, + double samleRateToUse, int bufferSizeToUse, + int numBuffersToUse) + : OpenSLSession (slLibraryToUse, numInputChannels, numOutputChannels, samleRateToUse, bufferSizeToUse, numBuffersToUse) + { + jassert (numInputChannels > 0 || numOutputChannels > 0); + + if (OpenSLSession::openedOK()) + { + if (inputChannels > 0) + { + recorder = new OpenSLQueueRunnerRecorder(*this, inputChannels); + + if (! recorder->init()) + { + recorder = nullptr; + return; + } + } + + if (outputChannels > 0) + { + player = new OpenSLQueueRunnerPlayer(*this, outputChannels); + + if (! player->init()) + { + player = nullptr; + return; + } + + const bool supportsUnderrunCount = (getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT) >= 24); + getUnderrunCount = supportsUnderrunCount ? getEnv()->GetMethodID (AudioTrack, "getUnderrunCount", "()I") : 0; + } + } + } + + bool openedOK() const override + { + return (OpenSLSession::openedOK() && (inputChannels == 0 || recorder != nullptr) + && (outputChannels == 0 || player != nullptr)); + } + + void start() override + { + OpenSLSession::start(); + + guard.set (0); + + if (inputChannels > 0) + recorder->clear(); + + if (outputChannels > 0) + player->clear(); + + // first enqueue all buffers + for (int i = 0; i < numBuffers; ++i) + doSomeWorkOnAudioThread(); + + if (inputChannels > 0) + recorder->setState (true); + + if (outputChannels > 0) + player->setState (true); + } + + void stop() override + { + OpenSLSession::stop(); + + while (! guard.compareAndSetBool (1, 0)) + Thread::sleep (1); + + if (inputChannels > 0) + recorder->setState (false); + + if (outputChannels > 0) + player->setState (false); + + guard.set (0); + } + + bool setAudioPreprocessingEnabled (bool shouldEnable) override + { + if (shouldEnable != audioProcessingEnabled) + { + audioProcessingEnabled = shouldEnable; + + if (recorder != nullptr) + return recorder->setAudioPreprocessingEnabled (audioProcessingEnabled); + } + + return true; + } + + int getXRunCount() const noexcept override + { + if (player != nullptr && player->javaProxy != nullptr && getUnderrunCount != 0) + return getEnv()->CallIntMethod (player->javaProxy, getUnderrunCount); + + return -1; + } + + bool supportsFloatingPoint() const noexcept override { return (BufferHelpers::isFloatingPoint != 0); } + + void doSomeWorkOnAudioThread() + { + // only the player or the recorder should enter this section at any time + if (guard.compareAndSetBool (1, 0)) + { + // are there enough buffers avaialable to process some audio + if ((inputChannels == 0 || recorder->isBufferAvailable()) && (outputChannels == 0 || player->isBufferAvailable())) + { + T* recorderBuffer = (inputChannels > 0 ? recorder->getNextBuffer() : nullptr); + T* playerBuffer = (outputChannels > 0 ? player->getNextBuffer() : nullptr); + + const float** inputChannelData = nullptr; + float** outputChannelData = nullptr; + + if (recorderBuffer != nullptr) + { + BufferHelpers::prepareCallbackBuffer (recorder->sampleBuffer, recorderBuffer); + BufferHelpers::convertFromOpenSL (recorderBuffer, recorder->sampleBuffer); + + inputChannelData = recorder->sampleBuffer.getArrayOfReadPointers(); + } + + if (playerBuffer != nullptr) + { + BufferHelpers::prepareCallbackBuffer (player->sampleBuffer, playerBuffer); + outputChannelData = player->sampleBuffer.getArrayOfWritePointers(); + } + + process (inputChannelData, outputChannelData); + + if (recorderBuffer != nullptr) + recorder->enqueueBuffer(); + + if (playerBuffer != nullptr) + { + BufferHelpers::convertToOpenSL (player->sampleBuffer, playerBuffer); + player->enqueueBuffer(); + } + } + + guard.set (0); + } + } + + //============================================================================== + ScopedPointer > player; + ScopedPointer > recorder; + Atomic guard; + jmethodID getUnderrunCount = 0; + }; + + //============================================================================== + OpenSLAudioIODevice (const String& deviceName) + : AudioIODevice (deviceName, openSLTypeName), + actualBufferSize (0), sampleRate (0), + audioProcessingEnabled (true), + callback (nullptr) + { + // OpenSL has piss-poor support for determining latency, so the only way I can find to + // get a number for this is by asking the AudioTrack/AudioRecord classes.. + AndroidAudioIODevice javaDevice (deviceName); + + // this is a total guess about how to calculate the latency, but seems to vaguely agree + // with the devices I've tested.. YMMV + inputLatency = (javaDevice.minBufferSizeIn * 2) / 3; + outputLatency = (javaDevice.minBufferSizeOut * 2) / 3; + + const int64 longestLatency = jmax (inputLatency, outputLatency); + const int64 totalLatency = inputLatency + outputLatency; + inputLatency = (int) ((longestLatency * inputLatency) / totalLatency) & ~15; + outputLatency = (int) ((longestLatency * outputLatency) / totalLatency) & ~15; + + bool success = slLibrary.open ("libOpenSLES.so"); + + // You can only create this class if you are sure that your hardware supports OpenSL + jassert (success); + ignoreUnused (success); + } + + ~OpenSLAudioIODevice() + { + close(); + } + + bool openedOk() const { return session != nullptr; } + + StringArray getOutputChannelNames() override + { + StringArray s; + s.add ("Left"); + s.add ("Right"); + return s; + } + + StringArray getInputChannelNames() override + { + StringArray s; + s.add ("Audio Input"); + return s; + } + + Array getAvailableSampleRates() override + { + //see https://developer.android.com/ndk/guides/audio/opensl-for-android.html + + static const double rates[] = { 8000.0, 11025.0, 12000.0, 16000.0, + 22050.0, 24000.0, 32000.0, 44100.0, 48000.0 }; + Array retval (rates, numElementsInArray (rates)); + + // make sure the native sample rate is pafrt of the list + double native = getNativeSampleRate(); + if (native != 0.0 && ! retval.contains (native)) + retval.add (native); + + return retval; + } + + Array getAvailableBufferSizes() override + { + // we need to offer the lowest possible buffer size which + // is the native buffer size + const int defaultNumMultiples = 8; + const int nativeBufferSize = getNativeBufferSize(); + + Array retval; + for (int i = 1; i < defaultNumMultiples; ++i) + retval.add (i * nativeBufferSize); + + return retval; + } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double requestedSampleRate, + int bufferSize) override + { + close(); + + lastError.clear(); + sampleRate = (int) requestedSampleRate; + + int preferredBufferSize = (bufferSize <= 0) ? getDefaultBufferSize() : bufferSize; + + activeOutputChans = outputChannels; + activeOutputChans.setRange (2, activeOutputChans.getHighestBit(), false); + int numOutputChannels = activeOutputChans.countNumberOfSetBits(); + + activeInputChans = inputChannels; + activeInputChans.setRange (1, activeInputChans.getHighestBit(), false); + int numInputChannels = activeInputChans.countNumberOfSetBits(); + + actualBufferSize = preferredBufferSize; + + const int audioBuffersToEnqueue = hasLowLatencyAudioPath() ? buffersToEnqueueForLowLatency + : buffersToEnqueueSlowAudio; + + if (numInputChannels > 0 && (! RuntimePermissions::isGranted (RuntimePermissions::recordAudio))) + { + // If you hit this assert, you probably forgot to get RuntimePermissions::recordAudio + // before trying to open an audio input device. This is not going to work! + jassertfalse; + lastError = "Error opening OpenSL input device: the app was not granted android.permission.RECORD_AUDIO"; + } + + session = OpenSLSession::create (slLibrary, numInputChannels, numOutputChannels, + sampleRate, actualBufferSize, audioBuffersToEnqueue); + if (session != nullptr) + session->setAudioPreprocessingEnabled (audioProcessingEnabled); + else + { + if (numInputChannels > 0 && numOutputChannels > 0 && RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) + { + // New versions of the Android emulator do not seem to support audio input anymore on OS X + activeInputChans = BigInteger(0); + numInputChannels = 0; + + session = OpenSLSession::create(slLibrary, numInputChannels, numOutputChannels, + sampleRate, actualBufferSize, audioBuffersToEnqueue); + } + } + + DBG ("OpenSL: numInputChannels = " << numInputChannels + << ", numOutputChannels = " << numOutputChannels + << ", nativeBufferSize = " << getNativeBufferSize() + << ", nativeSampleRate = " << getNativeSampleRate() + << ", actualBufferSize = " << actualBufferSize + << ", audioBuffersToEnqueue = " << audioBuffersToEnqueue + << ", sampleRate = " << sampleRate + << ", supportsFloatingPoint = " << (session != nullptr && session->supportsFloatingPoint() ? "true" : "false")); + + if (session == nullptr) + lastError = "Unknown error initializing opensl session"; + + deviceOpen = (session != nullptr); + return lastError; + } + + void close() override + { + stop(); + session = nullptr; + callback = nullptr; + } + + int getOutputLatencyInSamples() override { return outputLatency; } + int getInputLatencyInSamples() override { return inputLatency; } + bool isOpen() override { return deviceOpen; } + int getCurrentBufferSizeSamples() override { return actualBufferSize; } + int getCurrentBitDepth() override { return (session != nullptr && session->supportsFloatingPoint() ? 32 : 16); } + BigInteger getActiveOutputChannels() const override { return activeOutputChans; } + BigInteger getActiveInputChannels() const override { return activeInputChans; } + String getLastError() override { return lastError; } + bool isPlaying() override { return callback != nullptr; } + int getXRunCount() const noexcept override { return (session != nullptr ? session->getXRunCount() : -1); } + + int getDefaultBufferSize() override + { + // Only on a Pro-Audio device will we set the lowest possible buffer size + // by default. We need to be more conservative on other devices + // as they may be low-latency, but still have a crappy CPU. + return (isProAudioDevice() ? 1 : 6) + * defaultBufferSizeIsMultipleOfNative * getNativeBufferSize(); + } + + double getCurrentSampleRate() override + { + return (sampleRate == 0.0 ? getNativeSampleRate() : sampleRate); + } + + void start (AudioIODeviceCallback* newCallback) override + { + if (session != nullptr && callback != newCallback) + { + AudioIODeviceCallback* oldCallback = callback; + + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (this); + + if (oldCallback != nullptr) + { + // already running + if (newCallback == nullptr) + stop(); + else + session->setCallback (newCallback); + + oldCallback->audioDeviceStopped(); + } + else + { + jassert (newCallback != nullptr); + + // session hasn't started yet + session->setCallback (newCallback); + session->start(); + } + + callback = newCallback; + } + } + + void stop() override + { + if (session != nullptr && callback != nullptr) + { + callback = nullptr; + session->stop(); + session->setCallback (nullptr); + } + } + + bool setAudioPreprocessingEnabled (bool shouldAudioProcessingBeEnabled) override + { + audioProcessingEnabled = shouldAudioProcessingBeEnabled; + + if (session != nullptr) + session->setAudioPreprocessingEnabled (audioProcessingEnabled); + + return true; + } + + static const char* const openSLTypeName; + +private: + //============================================================================== + friend class SLRealtimeThread; + + //============================================================================== + DynamicLibrary slLibrary; + int actualBufferSize, sampleRate; + int inputLatency, outputLatency; + bool deviceOpen, audioProcessingEnabled; + String lastError; + BigInteger activeOutputChans, activeInputChans; + AudioIODeviceCallback* callback; + + ScopedPointer session; + + enum + { + // The number of buffers to enqueue needs to be at least two for the audio to use the low-latency + // audio path (see "Performance" section in ndk/docs/Additional_library_docs/opensles/index.html) + buffersToEnqueueForLowLatency = 4, + buffersToEnqueueSlowAudio = 8, + defaultBufferSizeIsMultipleOfNative = 1 + }; + + //============================================================================== + static String audioManagerGetProperty (const String& property) + { + const LocalRef jProperty (javaString (property)); + const LocalRef text ((jstring) android.activity.callObjectMethod (JuceAppActivity.audioManagerGetProperty, + jProperty.get())); + if (text.get() != 0) + return juceString (text); + + return {}; + } + + static bool androidHasSystemFeature (const String& property) + { + const LocalRef jProperty (javaString (property)); + return android.activity.callBooleanMethod (JuceAppActivity.hasSystemFeature, jProperty.get()); + } + + static double getNativeSampleRate() + { + return audioManagerGetProperty ("android.media.property.OUTPUT_SAMPLE_RATE").getDoubleValue(); + } + + static int getNativeBufferSize() + { + const int val = audioManagerGetProperty ("android.media.property.OUTPUT_FRAMES_PER_BUFFER").getIntValue(); + return val > 0 ? val : 512; + } + + static bool isProAudioDevice() + { + return androidHasSystemFeature ("android.hardware.audio.pro"); + } + + static bool hasLowLatencyAudioPath() + { + return androidHasSystemFeature ("android.hardware.audio.low_latency"); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenSLAudioIODevice) +}; + +OpenSLAudioIODevice::OpenSLSession* OpenSLAudioIODevice::OpenSLSession::create (DynamicLibrary& slLibrary, + int numInputChannels, int numOutputChannels, + double samleRateToUse, int bufferSizeToUse, + int numBuffersToUse) +{ + ScopedPointer retval; + auto sdkVersion = getEnv()->GetStaticIntField (AndroidBuildVersion, AndroidBuildVersion.SDK_INT); + + // SDK versions 21 and higher should natively support floating point... + if (sdkVersion >= 21) + { + retval = new OpenSLSessionT (slLibrary, numInputChannels, numOutputChannels, samleRateToUse, + bufferSizeToUse, numBuffersToUse); + + // ...however, some devices lie so re-try without floating point + if (retval != nullptr && (! retval->openedOK())) + retval = nullptr; + } + + if (retval == nullptr) + { + retval = new OpenSLSessionT (slLibrary, numInputChannels, numOutputChannels, samleRateToUse, + bufferSizeToUse, numBuffersToUse); + + if (retval != nullptr && (! retval->openedOK())) + retval = nullptr; + } + + return retval.release(); +} + +//============================================================================== +class OpenSLAudioDeviceType : public AudioIODeviceType +{ +public: + OpenSLAudioDeviceType() : AudioIODeviceType (OpenSLAudioIODevice::openSLTypeName) {} + + //============================================================================== + void scanForDevices() override {} + + StringArray getDeviceNames (bool) const override { return StringArray (OpenSLAudioIODevice::openSLTypeName); } + int getDefaultDeviceIndex (bool) const override { return 0; } + int getIndexOfDevice (AudioIODevice* device, bool) const override { return device != nullptr ? 0 : -1; } + bool hasSeparateInputsAndOutputs() const override { return false; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) override + { + ScopedPointer dev; + + if (outputDeviceName.isNotEmpty() || inputDeviceName.isNotEmpty()) + dev = new OpenSLAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName + : inputDeviceName); + + return dev.release(); + } + + static bool isOpenSLAvailable() + { + DynamicLibrary library; + return library.open ("libOpenSLES.so"); + } + +private: + + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenSLAudioDeviceType) +}; + +const char* const OpenSLAudioIODevice::openSLTypeName = "Android OpenSL"; + + +//============================================================================== +bool isOpenSLAvailable() { return OpenSLAudioDeviceType::isOpenSLAvailable(); } + +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_OpenSLES() +{ + return isOpenSLAvailable() ? new OpenSLAudioDeviceType() : nullptr; +} + +//============================================================================== +class SLRealtimeThread +{ +public: + static constexpr int numBuffers = 4; + + SLRealtimeThread() + { + if (auto createEngine = (OpenSLAudioIODevice::OpenSLSession::CreateEngineFunc) slLibrary.getFunction ("slCreateEngine")) + { + SLObjectItf obj = nullptr; + auto err = createEngine (&obj, 0, nullptr, 0, nullptr, nullptr); + + if (err != SL_RESULT_SUCCESS || obj == nullptr) + return; + + if ((*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + (*obj)->Destroy (obj); + return; + } + + engine = SlRef::cast (SlObjectRef (obj)); + + if (engine == nullptr) + { + (*obj)->Destroy (obj); + return; + } + + obj = nullptr; + err = (*engine)->CreateOutputMix (engine, &obj, 0, nullptr, nullptr); + + if (err != SL_RESULT_SUCCESS || obj == nullptr || (*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + (*obj)->Destroy (obj); + return; + } + + outputMix = SlRef::cast (SlObjectRef (obj)); + + if (outputMix == nullptr) + { + (*obj)->Destroy (obj); + return; + } + + SLDataLocator_AndroidSimpleBufferQueue queueLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, static_cast (numBuffers)}; + SLDataLocator_OutputMix outputMixLocator = {SL_DATALOCATOR_OUTPUTMIX, outputMix}; + + PCMDataFormatEx dataFormat; + BufferHelpers::initPCMDataFormat (dataFormat, 1, OpenSLAudioIODevice::getNativeSampleRate()); + + SLDataSource source = { &queueLocator, &dataFormat }; + SLDataSink sink = { &outputMixLocator, nullptr }; + + SLInterfaceID queueInterfaces[] = { &IntfIID::iid }; + SLboolean trueFlag = SL_BOOLEAN_TRUE; + + obj = nullptr; + err = (*engine)->CreateAudioPlayer (engine, &obj, &source, &sink, 1, queueInterfaces, &trueFlag); + + if (err != SL_RESULT_SUCCESS || obj == nullptr) + return; + + if ((*obj)->Realize (obj, 0) != SL_RESULT_SUCCESS) + { + (*obj)->Destroy (obj); + return; + } + + player = SlRef::cast (SlObjectRef (obj)); + + if (player == nullptr) + { + (*obj)->Destroy (obj); + return; + } + + queue = SlRef::cast (player); + if (queue == nullptr) + return; + + if ((*queue)->RegisterCallback (queue, staticFinished, this) != SL_RESULT_SUCCESS) + { + queue = nullptr; + return; + } + + pthread_cond_init (&threadReady, nullptr); + pthread_mutex_init (&threadReadyMutex, nullptr); + } + } + + bool isOK() const { return queue != nullptr; } + + pthread_t startThread (void* (*entry) (void*), void* userPtr) + { + memset (buffer.get(), 0, static_cast (sizeof (int16) * static_cast (bufferSize * numBuffers))); + + for (int i = 0; i < numBuffers; ++i) + { + int16* dst = buffer.get() + (bufferSize * i); + (*queue)->Enqueue (queue, dst, static_cast (static_cast (bufferSize) * sizeof (int16))); + } + + pthread_mutex_lock (&threadReadyMutex); + + threadEntryProc = entry; + threadUserPtr = userPtr; + + (*player)->SetPlayState (player, SL_PLAYSTATE_PLAYING); + + pthread_cond_wait (&threadReady, &threadReadyMutex); + pthread_mutex_unlock (&threadReadyMutex); + + return threadID; + } + + void finished() + { + if (threadEntryProc != nullptr) + { + pthread_mutex_lock (&threadReadyMutex); + + threadID = pthread_self(); + + pthread_cond_signal (&threadReady); + pthread_mutex_unlock (&threadReadyMutex); + + threadEntryProc (threadUserPtr); + threadEntryProc = nullptr; + + (*player)->SetPlayState (player, SL_PLAYSTATE_STOPPED); + MessageManager::callAsync ([this] () { delete this; }); + } + } + +private: + //============================================================================= + static void staticFinished (SLAndroidSimpleBufferQueueItf, void* context) + { + static_cast (context)->finished(); + } + + //============================================================================= + DynamicLibrary slLibrary { "libOpenSLES.so" }; + + SlRef engine; + SlRef outputMix; + SlRef player; + SlRef queue; + + int bufferSize = OpenSLAudioIODevice::getNativeBufferSize(); + HeapBlock buffer { HeapBlock (static_cast (1 * bufferSize * numBuffers)) }; + + void* (*threadEntryProc) (void*) = nullptr; + void* threadUserPtr = nullptr; + + pthread_cond_t threadReady; + pthread_mutex_t threadReadyMutex; + pthread_t threadID; +}; + +pthread_t juce_createRealtimeAudioThread (void* (*entry) (void*), void* userPtr) +{ + ScopedPointer thread (new SLRealtimeThread); + + if (! thread->isOK()) + return 0; + + pthread_t threadID = thread->startThread (entry, userPtr); + + // the thread will de-allocate itself + thread.release(); + + return threadID; +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_ios_Audio.cpp b/source/modules/juce_audio_devices/native/juce_ios_Audio.cpp new file mode 100644 index 000000000..432bb228b --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_ios_Audio.cpp @@ -0,0 +1,1206 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class iOSAudioIODevice; + +static const char* const iOSAudioDeviceName = "iOS Audio"; + +//============================================================================== +struct AudioSessionHolder : public AsyncUpdater +{ + AudioSessionHolder(); + ~AudioSessionHolder(); + + void handleAsyncUpdate() override; + + void handleStatusChange (bool enabled, const char* reason) const; + void handleRouteChange (const char* reason); + + CriticalSection routeChangeLock; + String lastRouteChangeReason; + Array activeDevices; + + id nativeSession; +}; + +static const char* getRoutingChangeReason (AVAudioSessionRouteChangeReason reason) noexcept +{ + switch (reason) + { + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: return "New device available"; + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: return "Old device unavailable"; + case AVAudioSessionRouteChangeReasonCategoryChange: return "Category change"; + case AVAudioSessionRouteChangeReasonOverride: return "Override"; + case AVAudioSessionRouteChangeReasonWakeFromSleep: return "Wake from sleep"; + case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: return "No suitable route for category"; + case AVAudioSessionRouteChangeReasonRouteConfigurationChange: return "Route configuration change"; + case AVAudioSessionRouteChangeReasonUnknown: + default: return "Unknown"; + } +} + +bool getNotificationValueForKey (NSNotification* notification, NSString* key, NSUInteger& value) noexcept +{ + if (notification != nil) + { + if (NSDictionary* userInfo = [notification userInfo]) + { + if (NSNumber* number = [userInfo objectForKey: key]) + { + value = [number unsignedIntegerValue]; + return true; + } + } + } + + jassertfalse; + return false; +} + +} // juce namespace + +//============================================================================== +@interface iOSAudioSessionNative : NSObject +{ +@private + juce::AudioSessionHolder* audioSessionHolder; +}; + +- (id) init: (juce::AudioSessionHolder*) holder; +- (void) dealloc; + +- (void) audioSessionChangedInterruptionType: (NSNotification*) notification; +- (void) handleMediaServicesReset; +- (void) handleMediaServicesLost; +- (void) handleRouteChange: (NSNotification*) notification; +@end + +@implementation iOSAudioSessionNative + +- (id) init: (juce::AudioSessionHolder*) holder +{ + self = [super init]; + + if (self != nil) + { + audioSessionHolder = holder; + + auto session = [AVAudioSession sharedInstance]; + auto centre = [NSNotificationCenter defaultCenter]; + + [centre addObserver: self + selector: @selector (audioSessionChangedInterruptionType:) + name: AVAudioSessionInterruptionNotification + object: session]; + + [centre addObserver: self + selector: @selector (handleMediaServicesLost) + name: AVAudioSessionMediaServicesWereLostNotification + object: session]; + + [centre addObserver: self + selector: @selector (handleMediaServicesReset) + name: AVAudioSessionMediaServicesWereResetNotification + object: session]; + + [centre addObserver: self + selector: @selector (handleRouteChange:) + name: AVAudioSessionRouteChangeNotification + object: session]; + } + else + { + jassertfalse; + } + + return self; +} + +- (void) dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver: self]; + [super dealloc]; +} + +- (void) audioSessionChangedInterruptionType: (NSNotification*) notification +{ + NSUInteger value; + + if (juce::getNotificationValueForKey (notification, AVAudioSessionInterruptionTypeKey, value)) + { + switch ((AVAudioSessionInterruptionType) value) + { + case AVAudioSessionInterruptionTypeBegan: + audioSessionHolder->handleStatusChange (false, "AVAudioSessionInterruptionTypeBegan"); + break; + + case AVAudioSessionInterruptionTypeEnded: + audioSessionHolder->handleStatusChange (true, "AVAudioSessionInterruptionTypeEnded"); + break; + + // No default so the code doesn't compile if this enum is extended. + } + } +} + +- (void) handleMediaServicesReset +{ + audioSessionHolder->handleStatusChange (true, "AVAudioSessionMediaServicesWereResetNotification"); +} + +- (void) handleMediaServicesLost +{ + audioSessionHolder->handleStatusChange (false, "AVAudioSessionMediaServicesWereLostNotification"); +} + +- (void) handleRouteChange: (NSNotification*) notification +{ + NSUInteger value; + + if (juce::getNotificationValueForKey (notification, AVAudioSessionRouteChangeReasonKey, value)) + audioSessionHolder->handleRouteChange (juce::getRoutingChangeReason ((AVAudioSessionRouteChangeReason) value)); +} + +@end + +//============================================================================== +#if JUCE_MODULE_AVAILABLE_juce_graphics + #include +#endif + +namespace juce { + +#ifndef JUCE_IOS_AUDIO_LOGGING + #define JUCE_IOS_AUDIO_LOGGING 0 +#endif + +#if JUCE_IOS_AUDIO_LOGGING + #define JUCE_IOS_AUDIO_LOG(x) DBG(x) +#else + #define JUCE_IOS_AUDIO_LOG(x) +#endif + +static void logNSError (NSError* e) +{ + if (e != nil) + { + JUCE_IOS_AUDIO_LOG ("iOS Audio error: " << [e.localizedDescription UTF8String]); + jassertfalse; + } +} + +#define JUCE_NSERROR_CHECK(X) { NSError* error = nil; X; logNSError (error); } + +//============================================================================== +struct iOSAudioIODevice::Pimpl : public AudioPlayHead, + public AsyncUpdater +{ + Pimpl (iOSAudioIODevice& ioDevice) + : owner (ioDevice) + { + sessionHolder->activeDevices.add (&owner); + + updateSampleRateAndAudioInput(); + } + + ~Pimpl() + { + sessionHolder->activeDevices.removeFirstMatchingValue (&owner); + + close(); + } + + static void setAudioSessionActive (bool enabled) + { + JUCE_NSERROR_CHECK ([[AVAudioSession sharedInstance] setActive: enabled + error: &error]); + } + + static double trySampleRate (double rate) + { + auto session = [AVAudioSession sharedInstance]; + JUCE_NSERROR_CHECK ([session setPreferredSampleRate: rate + error: &error]); + return session.sampleRate; + } + + Array getAvailableSampleRates() + { + const ScopedLock sl (callbackLock); + + Array rates; + + // Important: the supported audio sample rates change on the iPhone 6S + // depending on whether the headphones are plugged in or not! + setAudioSessionActive (true); + + AudioUnitRemovePropertyListenerWithUserData (audioUnit, + kAudioUnitProperty_StreamFormat, + dispatchAudioUnitPropertyChange, + this); + + const double lowestRate = trySampleRate (4000); + const double highestRate = trySampleRate (192000); + + for (double rate = lowestRate; rate <= highestRate; rate += 1000) + { + const double supportedRate = trySampleRate (rate); + + if (rates.addIfNotAlreadyThere (supportedRate)) + JUCE_IOS_AUDIO_LOG ("available rate = " + String (supportedRate, 0) + "Hz"); + + rate = jmax (rate, supportedRate); + } + + trySampleRate (sampleRate); + updateCurrentBufferSize(); + + AudioUnitAddPropertyListener (audioUnit, + kAudioUnitProperty_StreamFormat, + dispatchAudioUnitPropertyChange, + this); + + return rates; + } + + Array getAvailableBufferSizes() + { + Array r; + + for (int i = 6; i < 13; ++i) + r.add (1 << i); + + return r; + } + + void updateSampleRateAndAudioInput() + { + auto session = [AVAudioSession sharedInstance]; + sampleRate = session.sampleRate; + audioInputIsAvailable = session.isInputAvailable; + actualBufferSize = roundToInt (sampleRate * session.IOBufferDuration); + + JUCE_IOS_AUDIO_LOG ("AVAudioSession: sampleRate: " << sampleRate + << " Hz, audioInputAvailable: " << (int) audioInputIsAvailable + << ", buffer size: " << actualBufferSize); + } + + String open (const BigInteger& inputChannelsWanted, + const BigInteger& outputChannelsWanted, + double targetSampleRate, int bufferSize) + { + close(); + + firstHostTime = true; + lastNumFrames = 0; + xrun = 0; + lastError.clear(); + preferredBufferSize = bufferSize <= 0 ? defaultBufferSize : bufferSize; + + // xxx set up channel mapping + + activeOutputChans = outputChannelsWanted; + activeOutputChans.setRange (2, activeOutputChans.getHighestBit(), false); + numOutputChannels = activeOutputChans.countNumberOfSetBits(); + monoOutputChannelNumber = activeOutputChans.findNextSetBit (0); + + activeInputChans = inputChannelsWanted; + activeInputChans.setRange (2, activeInputChans.getHighestBit(), false); + numInputChannels = activeInputChans.countNumberOfSetBits(); + monoInputChannelNumber = activeInputChans.findNextSetBit (0); + + setAudioSessionActive (true); + + // Set the session category & options: + auto session = [AVAudioSession sharedInstance]; + + const bool useInputs = (numInputChannels > 0 && audioInputIsAvailable); + + NSString* category = (useInputs ? AVAudioSessionCategoryPlayAndRecord : AVAudioSessionCategoryPlayback); + + NSUInteger options = AVAudioSessionCategoryOptionMixWithOthers; // Alternatively AVAudioSessionCategoryOptionDuckOthers + if (useInputs) // These options are only valid for category = PlayAndRecord + options |= (AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth); + + JUCE_NSERROR_CHECK ([session setCategory: category + withOptions: options + error: &error]); + + fixAudioRouteIfSetToReceiver(); + + // Set the sample rate + trySampleRate (targetSampleRate); + updateSampleRateAndAudioInput(); + updateCurrentBufferSize(); + + prepareFloatBuffers (actualBufferSize); + + isRunning = true; + handleRouteChange ("Started AudioUnit"); + + lastError = (audioUnit != 0 ? "" : "Couldn't open the device"); + + setAudioSessionActive (true); + + return lastError; + } + + void close() + { + if (isRunning) + { + isRunning = false; + + if (audioUnit != 0) + { + AudioOutputUnitStart (audioUnit); + AudioComponentInstanceDispose (audioUnit); + audioUnit = 0; + } + + setAudioSessionActive (false); + } + } + + void start (AudioIODeviceCallback* newCallback) + { + if (isRunning && callback != newCallback) + { + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (&owner); + + const ScopedLock sl (callbackLock); + callback = newCallback; + } + } + + void stop() + { + if (isRunning) + { + AudioIODeviceCallback* lastCallback; + + { + const ScopedLock sl (callbackLock); + lastCallback = callback; + callback = nullptr; + } + + if (lastCallback != nullptr) + lastCallback->audioDeviceStopped(); + } + } + + bool setAudioPreprocessingEnabled (bool enable) + { + auto session = [AVAudioSession sharedInstance]; + + NSString* mode = (enable ? AVAudioSessionModeMeasurement + : AVAudioSessionModeDefault); + + JUCE_NSERROR_CHECK ([session setMode: mode + error: &error]); + + return session.mode == mode; + } + + //============================================================================== + bool canControlTransport() override { return interAppAudioConnected; } + + void transportPlay (bool shouldSartPlaying) override + { + if (! canControlTransport()) + return; + + HostCallbackInfo callbackInfo; + fillHostCallbackInfo (callbackInfo); + + Boolean hostIsPlaying = NO; + OSStatus err = callbackInfo.transportStateProc2 (callbackInfo.hostUserData, + &hostIsPlaying, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + + ignoreUnused (err); + jassert (err == noErr); + + if (hostIsPlaying != shouldSartPlaying) + handleAudioTransportEvent (kAudioUnitRemoteControlEvent_TogglePlayPause); + } + + void transportRecord (bool shouldStartRecording) override + { + if (! canControlTransport()) + return; + + HostCallbackInfo callbackInfo; + fillHostCallbackInfo (callbackInfo); + + Boolean hostIsRecording = NO; + OSStatus err = callbackInfo.transportStateProc2 (callbackInfo.hostUserData, + nullptr, + &hostIsRecording, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + ignoreUnused (err); + jassert (err == noErr); + + if (hostIsRecording != shouldStartRecording) + handleAudioTransportEvent (kAudioUnitRemoteControlEvent_ToggleRecord); + } + + void transportRewind() override + { + if (canControlTransport()) + handleAudioTransportEvent (kAudioUnitRemoteControlEvent_Rewind); + } + + bool getCurrentPosition (CurrentPositionInfo& result) override + { + if (! canControlTransport()) + return false; + + zerostruct (result); + + HostCallbackInfo callbackInfo; + fillHostCallbackInfo (callbackInfo); + + if (callbackInfo.hostUserData == nullptr) + return false; + + Boolean hostIsPlaying = NO; + Boolean hostIsRecording = NO; + Float64 hostCurrentSampleInTimeLine = 0; + Boolean hostIsCycling = NO; + Float64 hostCycleStartBeat = 0; + Float64 hostCycleEndBeat = 0; + OSStatus err = callbackInfo.transportStateProc2 (callbackInfo.hostUserData, + &hostIsPlaying, + &hostIsRecording, + nullptr, + &hostCurrentSampleInTimeLine, + &hostIsCycling, + &hostCycleStartBeat, + &hostCycleEndBeat); + if (err == kAUGraphErr_CannotDoInCurrentContext) + return false; + + jassert (err == noErr); + + result.timeInSamples = (int64) hostCurrentSampleInTimeLine; + result.isPlaying = hostIsPlaying; + result.isRecording = hostIsRecording; + result.isLooping = hostIsCycling; + result.ppqLoopStart = hostCycleStartBeat; + result.ppqLoopEnd = hostCycleEndBeat; + + result.timeInSeconds = result.timeInSamples / sampleRate; + + Float64 hostBeat = 0; + Float64 hostTempo = 0; + err = callbackInfo.beatAndTempoProc (callbackInfo.hostUserData, + &hostBeat, + &hostTempo); + jassert (err == noErr); + + result.ppqPosition = hostBeat; + result.bpm = hostTempo; + + Float32 hostTimeSigNumerator = 0; + UInt32 hostTimeSigDenominator = 0; + Float64 hostCurrentMeasureDownBeat = 0; + err = callbackInfo.musicalTimeLocationProc (callbackInfo.hostUserData, + nullptr, + &hostTimeSigNumerator, + &hostTimeSigDenominator, + &hostCurrentMeasureDownBeat); + jassert (err == noErr); + + result.ppqPositionOfLastBarStart = hostCurrentMeasureDownBeat; + result.timeSigNumerator = (int) hostTimeSigNumerator; + result.timeSigDenominator = (int) hostTimeSigDenominator; + + result.frameRate = AudioPlayHead::fpsUnknown; + + return true; + } + + //============================================================================== + #if JUCE_MODULE_AVAILABLE_juce_graphics + Image getIcon (int size) + { + if (interAppAudioConnected) + { + UIImage* hostUIImage = AudioOutputUnitGetHostIcon (audioUnit, size); + if (hostUIImage != nullptr) + return juce_createImageFromUIImage (hostUIImage); + } + return Image(); + } + #endif + + void switchApplication() + { + if (! interAppAudioConnected) + return; + + CFURLRef hostUrl; + UInt32 dataSize = sizeof (hostUrl); + OSStatus err = AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_PeerURL, + kAudioUnitScope_Global, + 0, + &hostUrl, + &dataSize); + if (err == noErr) + { + #if (! defined __IPHONE_OS_VERSION_MIN_REQUIRED) || (! defined __IPHONE_10_0) || (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0) + [[UIApplication sharedApplication] openURL: (NSURL*)hostUrl]; + #else + [[UIApplication sharedApplication] openURL: (NSURL*)hostUrl options: @{} completionHandler: nil]; + #endif + } + } + + //============================================================================== + void invokeAudioDeviceErrorCallback (const String& reason) + { + const ScopedLock sl (callbackLock); + + if (callback != nullptr) + callback->audioDeviceError (reason); + } + + void handleStatusChange (bool enabled, const char* reason) + { + const ScopedLock myScopedLock (callbackLock); + + JUCE_IOS_AUDIO_LOG ("handleStatusChange: enabled: " << (int) enabled << ", reason: " << reason); + + isRunning = enabled; + setAudioSessionActive (enabled); + + if (enabled) + AudioOutputUnitStart (audioUnit); + else + AudioOutputUnitStop (audioUnit); + + if (! enabled) + invokeAudioDeviceErrorCallback (reason); + } + + void handleRouteChange (const char* reason) + { + const ScopedLock myScopedLock (callbackLock); + + JUCE_IOS_AUDIO_LOG ("handleRouteChange: reason: " << reason); + + fixAudioRouteIfSetToReceiver(); + + if (isRunning) + invokeAudioDeviceErrorCallback (reason); + + restart(); + } + + void handleAudioUnitPropertyChange (AudioUnit, + AudioUnitPropertyID propertyID, + AudioUnitScope scope, + AudioUnitElement element) + { + JUCE_IOS_AUDIO_LOG ("handleAudioUnitPropertyChange: propertyID: " << String (propertyID) + << " scope: " << String (scope) + << " element: " << String (element)); + + switch (propertyID) + { + case kAudioUnitProperty_IsInterAppConnected: + handleInterAppAudioConnectionChange(); + return; + case kAudioUnitProperty_StreamFormat: + if (scope == kAudioUnitScope_Output && element == 0) + handleStreamFormatChange(); + + return; + default: + jassertfalse; + return; + } + } + + void handleInterAppAudioConnectionChange() + { + UInt32 connected; + UInt32 dataSize = sizeof (connected); + OSStatus err = AudioUnitGetProperty (audioUnit, kAudioUnitProperty_IsInterAppConnected, + kAudioUnitScope_Global, 0, &connected, &dataSize); + ignoreUnused (err); + jassert (err == noErr); + + JUCE_IOS_AUDIO_LOG ("handleInterAppAudioConnectionChange: " << (connected ? "connected" + : "disconnected")); + + if (connected != interAppAudioConnected) + { + const ScopedLock myScopedLock (callbackLock); + + interAppAudioConnected = connected; + + UIApplicationState appstate = [UIApplication sharedApplication].applicationState; + bool inForeground = (appstate != UIApplicationStateBackground); + + if (interAppAudioConnected || inForeground) + { + setAudioSessionActive (true); + AudioOutputUnitStart (audioUnit); + + if (callback != nullptr) + callback->audioDeviceAboutToStart (&owner); + } + else if (! inForeground) + { + AudioOutputUnitStop (audioUnit); + setAudioSessionActive (false); + } + } + } + + //============================================================================== + void prepareFloatBuffers (int bufferSize) + { + if (numInputChannels + numOutputChannels > 0) + { + floatData.setSize (numInputChannels + numOutputChannels, bufferSize); + zeromem (inputChannels, sizeof (inputChannels)); + zeromem (outputChannels, sizeof (outputChannels)); + + for (int i = 0; i < numInputChannels; ++i) + inputChannels[i] = floatData.getWritePointer (i); + + for (int i = 0; i < numOutputChannels; ++i) + outputChannels[i] = floatData.getWritePointer (i + numInputChannels); + } + } + + //============================================================================== + OSStatus process (AudioUnitRenderActionFlags* flags, const AudioTimeStamp* time, + const UInt32 numFrames, AudioBufferList* data) + { + OSStatus err = noErr; + + recordXruns (time, numFrames); + + if (audioInputIsAvailable && numInputChannels > 0) + err = AudioUnitRender (audioUnit, flags, time, 1, numFrames, data); + + const ScopedTryLock stl (callbackLock); + + if (stl.isLocked() && callback != nullptr) + { + if ((int) numFrames > floatData.getNumSamples()) + prepareFloatBuffers ((int) numFrames); + + if (audioInputIsAvailable && numInputChannels > 0) + { + short* shortData = (short*) data->mBuffers[0].mData; + + if (numInputChannels >= 2) + { + for (UInt32 i = 0; i < numFrames; ++i) + { + inputChannels[0][i] = *shortData++ * (1.0f / 32768.0f); + inputChannels[1][i] = *shortData++ * (1.0f / 32768.0f); + } + } + else + { + if (monoInputChannelNumber > 0) + ++shortData; + + for (UInt32 i = 0; i < numFrames; ++i) + { + inputChannels[0][i] = *shortData++ * (1.0f / 32768.0f); + ++shortData; + } + } + } + else + { + for (int i = numInputChannels; --i >= 0;) + zeromem (inputChannels[i], sizeof (float) * numFrames); + } + + callback->audioDeviceIOCallback ((const float**) inputChannels, numInputChannels, + outputChannels, numOutputChannels, (int) numFrames); + + short* const shortData = (short*) data->mBuffers[0].mData; + int n = 0; + + if (numOutputChannels >= 2) + { + for (UInt32 i = 0; i < numFrames; ++i) + { + shortData [n++] = (short) (outputChannels[0][i] * 32767.0f); + shortData [n++] = (short) (outputChannels[1][i] * 32767.0f); + } + } + else if (numOutputChannels == 1) + { + for (UInt32 i = 0; i < numFrames; ++i) + { + const short s = (short) (outputChannels[monoOutputChannelNumber][i] * 32767.0f); + shortData [n++] = s; + shortData [n++] = s; + } + } + else + { + zeromem (data->mBuffers[0].mData, 2 * sizeof (short) * numFrames); + } + } + else + { + zeromem (data->mBuffers[0].mData, 2 * sizeof (short) * numFrames); + } + + return err; + } + + void updateCurrentBufferSize() + { + NSTimeInterval bufferDuration = sampleRate > 0 ? (NSTimeInterval) ((preferredBufferSize + 1) / sampleRate) : 0.0; + + JUCE_NSERROR_CHECK ([[AVAudioSession sharedInstance] setPreferredIOBufferDuration: bufferDuration + error: &error]); + updateSampleRateAndAudioInput(); + } + + void recordXruns (const AudioTimeStamp* time, UInt32 numFrames) + { + if (time != nullptr && (time->mFlags & kAudioTimeStampSampleTimeValid) != 0) + { + if (! firstHostTime) + { + if ((time->mSampleTime - lastSampleTime) != lastNumFrames) + xrun++; + } + else + firstHostTime = false; + + lastSampleTime = time->mSampleTime; + } + else + firstHostTime = true; + + lastNumFrames = numFrames; + } + + //============================================================================== + static OSStatus processStatic (void* client, AudioUnitRenderActionFlags* flags, const AudioTimeStamp* time, + UInt32 /*busNumber*/, UInt32 numFrames, AudioBufferList* data) + { + + return static_cast (client)->process (flags, time, numFrames, data); + } + + //============================================================================== + void resetFormat (const int numChannels) noexcept + { + zerostruct (format); + format.mFormatID = kAudioFormatLinearPCM; + format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked | kAudioFormatFlagsNativeEndian; + format.mBitsPerChannel = 8 * sizeof (short); + format.mChannelsPerFrame = (UInt32) numChannels; + format.mFramesPerPacket = 1; + format.mBytesPerFrame = format.mBytesPerPacket = (UInt32) numChannels * sizeof (short); + } + + bool createAudioUnit() + { + if (audioUnit != 0) + { + AudioComponentInstanceDispose (audioUnit); + audioUnit = 0; + } + + resetFormat (2); + + AudioComponentDescription desc; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_RemoteIO; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + AudioComponent comp = AudioComponentFindNext (0, &desc); + AudioComponentInstanceNew (comp, &audioUnit); + + if (audioUnit == 0) + return false; + + #if JucePlugin_Enable_IAA + AudioComponentDescription appDesc; + appDesc.componentType = JucePlugin_IAAType; + appDesc.componentSubType = JucePlugin_IAASubType; + appDesc.componentManufacturer = JucePlugin_ManufacturerCode; + appDesc.componentFlags = 0; + appDesc.componentFlagsMask = 0; + OSStatus err = AudioOutputUnitPublish (&appDesc, + CFSTR(JucePlugin_IAAName), + JucePlugin_VersionCode, + audioUnit); + + // This assert will be hit if the Inter-App Audio entitlement has not + // been enabled, or the description being published with + // AudioOutputUnitPublish is different from any in the AudioComponents + // array in this application's .plist file. + jassert (err == noErr); + + err = AudioUnitAddPropertyListener (audioUnit, + kAudioUnitProperty_IsInterAppConnected, + dispatchAudioUnitPropertyChange, + this); + jassert (err == noErr); + #endif + + if (numInputChannels > 0) + { + const UInt32 one = 1; + AudioUnitSetProperty (audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, sizeof (one)); + } + + { + AudioChannelLayout layout; + layout.mChannelBitmap = 0; + layout.mNumberChannelDescriptions = 0; + layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo; + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Input, 0, &layout, sizeof (layout)); + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output, 0, &layout, sizeof (layout)); + } + + { + AURenderCallbackStruct inputProc; + inputProc.inputProc = processStatic; + inputProc.inputProcRefCon = this; + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &inputProc, sizeof (inputProc)); + } + + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, sizeof (format)); + AudioUnitSetProperty (audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &format, sizeof (format)); + + UInt32 framesPerSlice; + UInt32 dataSize = sizeof (framesPerSlice); + + AudioUnitInitialize (audioUnit); + + updateCurrentBufferSize(); + + if (AudioUnitGetProperty (audioUnit, kAudioUnitProperty_MaximumFramesPerSlice, + kAudioUnitScope_Global, 0, &framesPerSlice, &dataSize) == noErr + && dataSize == sizeof (framesPerSlice) && static_cast (framesPerSlice) != actualBufferSize) + { + prepareFloatBuffers (static_cast (framesPerSlice)); + } + + AudioUnitAddPropertyListener (audioUnit, kAudioUnitProperty_StreamFormat, dispatchAudioUnitPropertyChange, this); + + return true; + } + + void fillHostCallbackInfo (HostCallbackInfo& callbackInfo) + { + zerostruct (callbackInfo); + UInt32 dataSize = sizeof (HostCallbackInfo); + OSStatus err = AudioUnitGetProperty (audioUnit, + kAudioUnitProperty_HostCallbacks, + kAudioUnitScope_Global, + 0, + &callbackInfo, + &dataSize); + ignoreUnused (err); + jassert (err == noErr); + } + + void handleAudioTransportEvent (AudioUnitRemoteControlEvent event) + { + OSStatus err = AudioUnitSetProperty (audioUnit, kAudioOutputUnitProperty_RemoteControlToHost, + kAudioUnitScope_Global, 0, &event, sizeof (event)); + ignoreUnused (err); + jassert (err == noErr); + } + + // If the routing is set to go through the receiver (i.e. the speaker, but quiet), this re-routes it + // to make it loud. Needed because by default when using an input + output, the output is kept quiet. + static void fixAudioRouteIfSetToReceiver() + { + auto session = [AVAudioSession sharedInstance]; + auto route = session.currentRoute; + + for (AVAudioSessionPortDescription* port in route.inputs) + { + ignoreUnused (port); + JUCE_IOS_AUDIO_LOG ("AVAudioSession: input: " << [port.description UTF8String]); + } + + for (AVAudioSessionPortDescription* port in route.outputs) + { + JUCE_IOS_AUDIO_LOG ("AVAudioSession: output: " << [port.description UTF8String]); + + if ([port.portName isEqualToString: @"Receiver"]) + { + JUCE_NSERROR_CHECK ([session overrideOutputAudioPort: AVAudioSessionPortOverrideSpeaker + error: &error]); + setAudioSessionActive (true); + } + } + } + + void restart() + { + if (isRunning) + { + updateSampleRateAndAudioInput(); + updateCurrentBufferSize(); + createAudioUnit(); + + setAudioSessionActive (true); + + if (audioUnit != 0) + { + UInt32 formatSize = sizeof (format); + AudioUnitGetProperty (audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &format, &formatSize); + AudioOutputUnitStart (audioUnit); + } + + if (callback != nullptr) + { + callback->audioDeviceStopped(); + callback->audioDeviceAboutToStart (&owner); + } + } + } + + void handleAsyncUpdate() override + { + restart(); + } + + void handleStreamFormatChange() + { + AudioStreamBasicDescription desc; + zerostruct (desc); + UInt32 dataSize = sizeof (desc); + AudioUnitGetProperty(audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + 0, + &desc, + &dataSize); + + if (desc.mSampleRate != sampleRate) + { + JUCE_IOS_AUDIO_LOG ("handleStreamFormatChange: sample rate " << desc.mSampleRate); + triggerAsyncUpdate(); + } + } + + static void dispatchAudioUnitPropertyChange (void* data, AudioUnit unit, AudioUnitPropertyID propertyID, + AudioUnitScope scope, AudioUnitElement element) + { + static_cast (data)->handleAudioUnitPropertyChange (unit, propertyID, scope, element); + } + + void handleMidiMessage (MidiMessage msg) + { + if (messageCollector != nullptr) + messageCollector->addMessageToQueue (msg); + } + + static void midiEventCallback (void *client, UInt32 status, UInt32 data1, UInt32 data2, UInt32) + { + return static_cast (client)->handleMidiMessage (MidiMessage ((int) status, + (int) data1, + (int) data2, + Time::getMillisecondCounter() / 1000.0)); + } + + bool isRunning = false; + AudioIODeviceCallback* callback = nullptr; + + String lastError; + + bool audioInputIsAvailable = false; + + const int defaultBufferSize = + #if TARGET_IPHONE_SIMULATOR + 512; + #else + 256; + #endif + double sampleRate = 0; + int numInputChannels = 2, numOutputChannels = 2; + int preferredBufferSize = 0, actualBufferSize = 0; + + bool interAppAudioConnected = false; + + BigInteger activeOutputChans, activeInputChans; + + MidiMessageCollector* messageCollector = nullptr; + + iOSAudioIODevice& owner; + SharedResourcePointer sessionHolder; + CriticalSection callbackLock; + + AudioStreamBasicDescription format; + AudioUnit audioUnit {}; + + AudioSampleBuffer floatData; + float* inputChannels[3]; + float* outputChannels[3]; + bool monoInputChannelNumber, monoOutputChannelNumber; + + bool firstHostTime; + Float64 lastSampleTime; + unsigned int lastNumFrames; + int xrun; + + JUCE_DECLARE_NON_COPYABLE (Pimpl) +}; + + +//============================================================================== +iOSAudioIODevice::iOSAudioIODevice (const String& deviceName) + : AudioIODevice (deviceName, iOSAudioDeviceName), + pimpl (new Pimpl (*this)) +{} + +//============================================================================== +String iOSAudioIODevice::open (const BigInteger& inChans, const BigInteger& outChans, + double requestedSampleRate, int requestedBufferSize) +{ + return pimpl->open (inChans, outChans, requestedSampleRate, requestedBufferSize); +} +void iOSAudioIODevice::close() { pimpl->close(); } + +void iOSAudioIODevice::start (AudioIODeviceCallback* callbackToUse) { pimpl->start (callbackToUse); } +void iOSAudioIODevice::stop() { pimpl->stop(); } + +Array iOSAudioIODevice::getAvailableSampleRates() { return pimpl->getAvailableSampleRates(); } +Array iOSAudioIODevice::getAvailableBufferSizes() { return pimpl->getAvailableBufferSizes(); } + +bool iOSAudioIODevice::setAudioPreprocessingEnabled (bool enabled) { return pimpl->setAudioPreprocessingEnabled (enabled); } + +bool iOSAudioIODevice::isPlaying() { return pimpl->isRunning && pimpl->callback != nullptr; } +bool iOSAudioIODevice::isOpen() { return pimpl->isRunning; } +String iOSAudioIODevice::getLastError() { return pimpl->lastError; } + +StringArray iOSAudioIODevice::getOutputChannelNames() { return { "Left", "Right" }; } +StringArray iOSAudioIODevice::getInputChannelNames() { return pimpl->audioInputIsAvailable ? getOutputChannelNames() : StringArray(); } + +int iOSAudioIODevice::getDefaultBufferSize() { return pimpl->defaultBufferSize; } +int iOSAudioIODevice::getCurrentBufferSizeSamples() { return pimpl->actualBufferSize; } + +double iOSAudioIODevice::getCurrentSampleRate() { return pimpl->sampleRate; } + +int iOSAudioIODevice::getCurrentBitDepth() { return 16; } + +BigInteger iOSAudioIODevice::getActiveOutputChannels() const { return pimpl->activeOutputChans; } +BigInteger iOSAudioIODevice::getActiveInputChannels() const { return pimpl->activeInputChans; } + +int iOSAudioIODevice::getOutputLatencyInSamples() { return roundToInt (pimpl->sampleRate * [AVAudioSession sharedInstance].outputLatency); } +int iOSAudioIODevice::getInputLatencyInSamples() { return roundToInt (pimpl->sampleRate * [AVAudioSession sharedInstance].inputLatency); } +int iOSAudioIODevice::getXRunCount() const noexcept { return pimpl->xrun; } + +void iOSAudioIODevice::setMidiMessageCollector (MidiMessageCollector* collector) { pimpl->messageCollector = collector; } +AudioPlayHead* iOSAudioIODevice::getAudioPlayHead() const { return pimpl; } + +bool iOSAudioIODevice::isInterAppAudioConnected() const { return pimpl->interAppAudioConnected; } +#if JUCE_MODULE_AVAILABLE_juce_graphics +Image iOSAudioIODevice::getIcon (int size) { return pimpl->getIcon (size); } +#endif +void iOSAudioIODevice::switchApplication() { return pimpl->switchApplication(); } + +//============================================================================== +struct iOSAudioIODeviceType : public AudioIODeviceType +{ + iOSAudioIODeviceType() : AudioIODeviceType (iOSAudioDeviceName) {} + + void scanForDevices() {} + StringArray getDeviceNames (bool /*wantInputNames*/) const { return StringArray (iOSAudioDeviceName); } + int getDefaultDeviceIndex (bool /*forInput*/) const { return 0; } + int getIndexOfDevice (AudioIODevice* d, bool /*asInput*/) const { return d != nullptr ? 0 : -1; } + bool hasSeparateInputsAndOutputs() const { return false; } + + AudioIODevice* createDevice (const String& outputDeviceName, const String& inputDeviceName) + { + if (outputDeviceName.isNotEmpty() || inputDeviceName.isNotEmpty()) + return new iOSAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName); + + return nullptr; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (iOSAudioIODeviceType) +}; + +//============================================================================== +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_iOSAudio() +{ + return new iOSAudioIODeviceType(); +} + +//============================================================================== +AudioSessionHolder::AudioSessionHolder() { nativeSession = [[iOSAudioSessionNative alloc] init: this]; } +AudioSessionHolder::~AudioSessionHolder() { [nativeSession release]; } + +void AudioSessionHolder::handleAsyncUpdate() +{ + const ScopedLock sl (routeChangeLock); + for (auto device: activeDevices) + device->pimpl->handleRouteChange (lastRouteChangeReason.toRawUTF8()); +} + +void AudioSessionHolder::handleStatusChange (bool enabled, const char* reason) const +{ + for (auto device: activeDevices) + device->pimpl->handleStatusChange (enabled, reason); +} + +void AudioSessionHolder::handleRouteChange (const char* reason) +{ + const ScopedLock sl (routeChangeLock); + lastRouteChangeReason = reason; + triggerAsyncUpdate(); +} + +#undef JUCE_NSERROR_CHECK + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_ios_Audio.h b/source/modules/juce_audio_devices/native/juce_ios_Audio.h new file mode 100644 index 000000000..b06be4d97 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_ios_Audio.h @@ -0,0 +1,93 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +struct iOSAudioIODeviceType; + +class iOSAudioIODevice : public AudioIODevice +{ +public: + //============================================================================== + String open (const BigInteger&, const BigInteger&, double, int) override; + void close() override; + + void start (AudioIODeviceCallback*) override; + void stop() override; + + Array getAvailableSampleRates() override; + Array getAvailableBufferSizes() override; + + bool setAudioPreprocessingEnabled (bool) override; + + //============================================================================== + bool isPlaying() override; + bool isOpen() override; + String getLastError() override; + + //============================================================================== + StringArray getOutputChannelNames() override; + StringArray getInputChannelNames() override; + + int getDefaultBufferSize() override; + int getCurrentBufferSizeSamples() override; + + double getCurrentSampleRate() override; + + int getCurrentBitDepth() override; + + BigInteger getActiveOutputChannels() const override; + BigInteger getActiveInputChannels() const override; + + int getOutputLatencyInSamples() override; + int getInputLatencyInSamples() override; + + int getXRunCount() const noexcept override; + + //============================================================================== + void setMidiMessageCollector (MidiMessageCollector*); + AudioPlayHead* getAudioPlayHead() const; + + //============================================================================== + bool isInterAppAudioConnected() const; + #if JUCE_MODULE_AVAILABLE_juce_graphics + Image getIcon (int size); + #endif + void switchApplication(); + +private: + //============================================================================== + iOSAudioIODevice (const String&); + + //============================================================================== + friend struct iOSAudioIODeviceType; + friend struct AudioSessionHolder; + + struct Pimpl; + friend struct Pimpl; + ScopedPointer pimpl; + + JUCE_DECLARE_NON_COPYABLE (iOSAudioIODevice) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_linux_ALSA.cpp b/source/modules/juce_audio_devices/native/juce_linux_ALSA.cpp new file mode 100644 index 000000000..7a521f468 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_linux_ALSA.cpp @@ -0,0 +1,1314 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +namespace +{ + +#ifndef JUCE_ALSA_LOGGING + #define JUCE_ALSA_LOGGING 0 +#endif + +#if JUCE_ALSA_LOGGING + #define JUCE_ALSA_LOG(dbgtext) { juce::String tempDbgBuf ("ALSA: "); tempDbgBuf << dbgtext; Logger::writeToLog (tempDbgBuf); DBG (tempDbgBuf); } + #define JUCE_CHECKED_RESULT(x) (logErrorMessage (x, __LINE__)) + + static int logErrorMessage (int err, int lineNum) + { + if (err < 0) + JUCE_ALSA_LOG ("Error: line " << lineNum << ": code " << err << " (" << snd_strerror (err) << ")"); + + return err; + } +#else + #define JUCE_ALSA_LOG(x) {} + #define JUCE_CHECKED_RESULT(x) (x) +#endif + +#define JUCE_ALSA_FAILED(x) failed (x) + +static void getDeviceSampleRates (snd_pcm_t* handle, Array& rates) +{ + const int ratesToTry[] = { 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 0 }; + + snd_pcm_hw_params_t* hwParams; + snd_pcm_hw_params_alloca (&hwParams); + + for (int i = 0; ratesToTry[i] != 0; ++i) + { + if (snd_pcm_hw_params_any (handle, hwParams) >= 0 + && snd_pcm_hw_params_test_rate (handle, hwParams, (unsigned int) ratesToTry[i], 0) == 0) + { + rates.addIfNotAlreadyThere ((double) ratesToTry[i]); + } + } +} + +static void getDeviceNumChannels (snd_pcm_t* handle, unsigned int* minChans, unsigned int* maxChans) +{ + snd_pcm_hw_params_t *params; + snd_pcm_hw_params_alloca (¶ms); + + if (snd_pcm_hw_params_any (handle, params) >= 0) + { + snd_pcm_hw_params_get_channels_min (params, minChans); + snd_pcm_hw_params_get_channels_max (params, maxChans); + + JUCE_ALSA_LOG ("getDeviceNumChannels: " << (int) *minChans << " " << (int) *maxChans); + + // some virtual devices (dmix for example) report 10000 channels , we have to clamp these values + *maxChans = jmin (*maxChans, 256u); + *minChans = jmin (*minChans, *maxChans); + } + else + { + JUCE_ALSA_LOG ("getDeviceNumChannels failed"); + } +} + +static void getDeviceProperties (const String& deviceID, + unsigned int& minChansOut, + unsigned int& maxChansOut, + unsigned int& minChansIn, + unsigned int& maxChansIn, + Array& rates, + bool testOutput, + bool testInput) +{ + minChansOut = maxChansOut = minChansIn = maxChansIn = 0; + + if (deviceID.isEmpty()) + return; + + JUCE_ALSA_LOG ("getDeviceProperties(" << deviceID.toUTF8().getAddress() << ")"); + + snd_pcm_info_t* info; + snd_pcm_info_alloca (&info); + + if (testOutput) + { + snd_pcm_t* pcmHandle; + + if (JUCE_CHECKED_RESULT (snd_pcm_open (&pcmHandle, deviceID.toUTF8().getAddress(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) >= 0) + { + getDeviceNumChannels (pcmHandle, &minChansOut, &maxChansOut); + getDeviceSampleRates (pcmHandle, rates); + + snd_pcm_close (pcmHandle); + } + } + + if (testInput) + { + snd_pcm_t* pcmHandle; + + if (JUCE_CHECKED_RESULT (snd_pcm_open (&pcmHandle, deviceID.toUTF8(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK) >= 0)) + { + getDeviceNumChannels (pcmHandle, &minChansIn, &maxChansIn); + + if (rates.size() == 0) + getDeviceSampleRates (pcmHandle, rates); + + snd_pcm_close (pcmHandle); + } + } +} + +static void ensureMinimumNumBitsSet (BigInteger& chans, int minNumChans) +{ + int i = 0; + + while (chans.countNumberOfSetBits() < minNumChans) + chans.setBit (i++); +} + +static void silentErrorHandler (const char*, int, const char*, int, const char*,...) {} + +//============================================================================== +class ALSADevice +{ +public: + ALSADevice (const String& devID, bool forInput) + : handle (nullptr), + bitDepth (16), + numChannelsRunning (0), + latency (0), + deviceID (devID), + isInput (forInput), + isInterleaved (true) + { + JUCE_ALSA_LOG ("snd_pcm_open (" << deviceID.toUTF8().getAddress() << ", forInput=" << (int) forInput << ")"); + + int err = snd_pcm_open (&handle, deviceID.toUTF8(), + forInput ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK, + SND_PCM_ASYNC); + if (err < 0) + { + if (-err == EBUSY) + error << "The device \"" << deviceID << "\" is busy (another application is using it)."; + else if (-err == ENOENT) + error << "The device \"" << deviceID << "\" is not available."; + else + error << "Could not open " << (forInput ? "input" : "output") << " device \"" << deviceID + << "\": " << snd_strerror(err) << " (" << err << ")"; + + JUCE_ALSA_LOG ("snd_pcm_open failed; " << error); + } + } + + ~ALSADevice() + { + closeNow(); + } + + void closeNow() + { + if (handle != nullptr) + { + snd_pcm_close (handle); + handle = nullptr; + } + } + + bool setParameters (unsigned int sampleRate, int numChannels, int bufferSize) + { + if (handle == nullptr) + return false; + + JUCE_ALSA_LOG ("ALSADevice::setParameters(" << deviceID << ", " + << (int) sampleRate << ", " << numChannels << ", " << bufferSize << ")"); + + snd_pcm_hw_params_t* hwParams; + snd_pcm_hw_params_alloca (&hwParams); + + if (snd_pcm_hw_params_any (handle, hwParams) < 0) + { + // this is the error message that aplay returns when an error happens here, + // it is a bit more explicit that "Invalid parameter" + error = "Broken configuration for this PCM: no configurations available"; + return false; + } + + if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED) >= 0) // works better for plughw.. + isInterleaved = true; + else if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_NONINTERLEAVED) >= 0) + isInterleaved = false; + else + { + jassertfalse; + return false; + } + + enum { isFloatBit = 1 << 16, isLittleEndianBit = 1 << 17, onlyUseLower24Bits = 1 << 18 }; + + const int formatsToTry[] = { SND_PCM_FORMAT_FLOAT_LE, 32 | isFloatBit | isLittleEndianBit, + SND_PCM_FORMAT_FLOAT_BE, 32 | isFloatBit, + SND_PCM_FORMAT_S32_LE, 32 | isLittleEndianBit, + SND_PCM_FORMAT_S32_BE, 32, + SND_PCM_FORMAT_S24_3LE, 24 | isLittleEndianBit, + SND_PCM_FORMAT_S24_3BE, 24, + SND_PCM_FORMAT_S24_LE, 32 | isLittleEndianBit | onlyUseLower24Bits, + SND_PCM_FORMAT_S16_LE, 16 | isLittleEndianBit, + SND_PCM_FORMAT_S16_BE, 16 }; + bitDepth = 0; + + for (int i = 0; i < numElementsInArray (formatsToTry); i += 2) + { + if (snd_pcm_hw_params_set_format (handle, hwParams, (_snd_pcm_format) formatsToTry [i]) >= 0) + { + const int type = formatsToTry [i + 1]; + bitDepth = type & 255; + + converter = createConverter (isInput, bitDepth, + (type & isFloatBit) != 0, + (type & isLittleEndianBit) != 0, + (type & onlyUseLower24Bits) != 0, + numChannels, + isInterleaved); + break; + } + } + + if (bitDepth == 0) + { + error = "device doesn't support a compatible PCM format"; + JUCE_ALSA_LOG ("Error: " + error); + return false; + } + + int dir = 0; + unsigned int periods = 4; + snd_pcm_uframes_t samplesPerPeriod = (snd_pcm_uframes_t) bufferSize; + + if (JUCE_ALSA_FAILED (snd_pcm_hw_params_set_rate_near (handle, hwParams, &sampleRate, 0)) + || JUCE_ALSA_FAILED (snd_pcm_hw_params_set_channels (handle, hwParams, (unsigned int ) numChannels)) + || JUCE_ALSA_FAILED (snd_pcm_hw_params_set_periods_near (handle, hwParams, &periods, &dir)) + || JUCE_ALSA_FAILED (snd_pcm_hw_params_set_period_size_near (handle, hwParams, &samplesPerPeriod, &dir)) + || JUCE_ALSA_FAILED (snd_pcm_hw_params (handle, hwParams))) + { + return false; + } + + snd_pcm_uframes_t frames = 0; + + if (JUCE_ALSA_FAILED (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir)) + || JUCE_ALSA_FAILED (snd_pcm_hw_params_get_periods (hwParams, &periods, &dir))) + latency = 0; + else + latency = (int) frames * ((int) periods - 1); // (this is the method JACK uses to guess the latency..) + + JUCE_ALSA_LOG ("frames: " << (int) frames << ", periods: " << (int) periods + << ", samplesPerPeriod: " << (int) samplesPerPeriod); + + snd_pcm_sw_params_t* swParams; + snd_pcm_sw_params_alloca (&swParams); + snd_pcm_uframes_t boundary; + + if (JUCE_ALSA_FAILED (snd_pcm_sw_params_current (handle, swParams)) + || JUCE_ALSA_FAILED (snd_pcm_sw_params_get_boundary (swParams, &boundary)) + || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_silence_threshold (handle, swParams, 0)) + || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_silence_size (handle, swParams, boundary)) + || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_start_threshold (handle, swParams, samplesPerPeriod)) + || JUCE_ALSA_FAILED (snd_pcm_sw_params_set_stop_threshold (handle, swParams, boundary)) + || JUCE_ALSA_FAILED (snd_pcm_sw_params (handle, swParams))) + { + return false; + } + + #if JUCE_ALSA_LOGGING + // enable this to dump the config of the devices that get opened + snd_output_t* out; + snd_output_stdio_attach (&out, stderr, 0); + snd_pcm_hw_params_dump (hwParams, out); + snd_pcm_sw_params_dump (swParams, out); + #endif + + numChannelsRunning = numChannels; + + return true; + } + + //============================================================================== + bool writeToOutputDevice (AudioSampleBuffer& outputChannelBuffer, const int numSamples) + { + jassert (numChannelsRunning <= outputChannelBuffer.getNumChannels()); + float* const* const data = outputChannelBuffer.getArrayOfWritePointers(); + snd_pcm_sframes_t numDone = 0; + + if (isInterleaved) + { + scratch.ensureSize ((size_t) ((int) sizeof (float) * numSamples * numChannelsRunning), false); + + for (int i = 0; i < numChannelsRunning; ++i) + converter->convertSamples (scratch.getData(), i, data[i], 0, numSamples); + + numDone = snd_pcm_writei (handle, scratch.getData(), (snd_pcm_uframes_t) numSamples); + } + else + { + for (int i = 0; i < numChannelsRunning; ++i) + converter->convertSamples (data[i], data[i], numSamples); + + numDone = snd_pcm_writen (handle, (void**) data, (snd_pcm_uframes_t) numSamples); + } + + if (numDone < 0) + { + if (numDone == -(EPIPE)) + underrunCount++; + + if (JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) numDone, 1 /* silent */))) + return false; + } + + if (numDone < numSamples) + JUCE_ALSA_LOG ("Did not write all samples: numDone: " << numDone << ", numSamples: " << numSamples); + + return true; + } + + bool readFromInputDevice (AudioSampleBuffer& inputChannelBuffer, const int numSamples) + { + jassert (numChannelsRunning <= inputChannelBuffer.getNumChannels()); + float* const* const data = inputChannelBuffer.getArrayOfWritePointers(); + + if (isInterleaved) + { + scratch.ensureSize ((size_t) ((int) sizeof (float) * numSamples * numChannelsRunning), false); + scratch.fillWith (0); // (not clearing this data causes warnings in valgrind) + + snd_pcm_sframes_t num = snd_pcm_readi (handle, scratch.getData(), (snd_pcm_uframes_t) numSamples); + + if (num < 0) + { + if (num == -(EPIPE)) + overrunCount++; + + if (JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) num, 1 /* silent */))) + return false; + } + + + if (num < numSamples) + JUCE_ALSA_LOG ("Did not read all samples: num: " << num << ", numSamples: " << numSamples); + + for (int i = 0; i < numChannelsRunning; ++i) + converter->convertSamples (data[i], 0, scratch.getData(), i, numSamples); + } + else + { + snd_pcm_sframes_t num = snd_pcm_readn (handle, (void**) data, (snd_pcm_uframes_t) numSamples); + + if (num < 0) + { + if (num == -(EPIPE)) + overrunCount++; + + if (JUCE_ALSA_FAILED (snd_pcm_recover (handle, (int) num, 1 /* silent */))) + return false; + } + + if (num < numSamples) + JUCE_ALSA_LOG ("Did not read all samples: num: " << num << ", numSamples: " << numSamples); + + for (int i = 0; i < numChannelsRunning; ++i) + converter->convertSamples (data[i], data[i], numSamples); + } + + return true; + } + + //============================================================================== + snd_pcm_t* handle; + String error; + int bitDepth, numChannelsRunning, latency; + int underrunCount = 0, overrunCount = 0; + +private: + //============================================================================== + String deviceID; + const bool isInput; + bool isInterleaved; + MemoryBlock scratch; + ScopedPointer converter; + + //============================================================================== + template + struct ConverterHelper + { + static AudioData::Converter* createConverter (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels, bool interleaved) + { + if (interleaved) + return create (forInput, isLittleEndian, numInterleavedChannels); + + return create (forInput, isLittleEndian, numInterleavedChannels); + } + + private: + template + static AudioData::Converter* create (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels) + { + if (forInput) + { + typedef AudioData::Pointer DestType; + + if (isLittleEndian) + return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); + + return new AudioData::ConverterInstance , DestType> (numInterleavedChannels, 1); + } + + typedef AudioData::Pointer SourceType; + + if (isLittleEndian) + return new AudioData::ConverterInstance > (1, numInterleavedChannels); + + return new AudioData::ConverterInstance > (1, numInterleavedChannels); + } + }; + + static AudioData::Converter* createConverter (bool forInput, int bitDepth, + bool isFloat, bool isLittleEndian, bool useOnlyLower24Bits, + int numInterleavedChannels, + bool interleaved) + { + JUCE_ALSA_LOG ("format: bitDepth=" << bitDepth << ", isFloat=" << (int) isFloat + << ", isLittleEndian=" << (int) isLittleEndian << ", numChannels=" << numInterleavedChannels); + + if (isFloat) return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved); + if (bitDepth == 16) return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved); + if (bitDepth == 24) return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved); + + jassert (bitDepth == 32); + + if (useOnlyLower24Bits) + return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved); + + return ConverterHelper ::createConverter (forInput, isLittleEndian, numInterleavedChannels, interleaved); + } + + //============================================================================== + bool failed (const int errorNum) + { + if (errorNum >= 0) + return false; + + error = snd_strerror (errorNum); + JUCE_ALSA_LOG ("ALSA error: " << error); + return true; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSADevice) +}; + +//============================================================================== +class ALSAThread : public Thread +{ +public: + ALSAThread (const String& inputDeviceID, const String& outputDeviceID) + : Thread ("Juce ALSA"), + sampleRate (0), + bufferSize (0), + outputLatency (0), + inputLatency (0), + callback (0), + inputId (inputDeviceID), + outputId (outputDeviceID), + numCallbacks (0), + audioIoInProgress (false), + inputChannelBuffer (1, 1), + outputChannelBuffer (1, 1) + { + initialiseRatesAndChannels(); + } + + ~ALSAThread() + { + close(); + } + + void open (BigInteger inputChannels, + BigInteger outputChannels, + const double newSampleRate, + const int newBufferSize) + { + close(); + + error.clear(); + sampleRate = newSampleRate; + bufferSize = newBufferSize; + + int maxInputsRequested = inputChannels.getHighestBit() + 1; + maxInputsRequested = jmax ((int) minChansIn, jmin ((int) maxChansIn, maxInputsRequested)); + + inputChannelBuffer.setSize (maxInputsRequested, bufferSize); + inputChannelBuffer.clear(); + inputChannelDataForCallback.clear(); + currentInputChans.clear(); + + if (inputChannels.getHighestBit() >= 0) + { + for (int i = 0; i < maxInputsRequested; ++i) + { + if (inputChannels[i]) + { + inputChannelDataForCallback.add (inputChannelBuffer.getReadPointer (i)); + currentInputChans.setBit (i); + } + } + } + + ensureMinimumNumBitsSet (outputChannels, (int) minChansOut); + + int maxOutputsRequested = outputChannels.getHighestBit() + 1; + maxOutputsRequested = jmax ((int) minChansOut, jmin ((int) maxChansOut, maxOutputsRequested)); + + outputChannelBuffer.setSize (maxOutputsRequested, bufferSize); + outputChannelBuffer.clear(); + outputChannelDataForCallback.clear(); + currentOutputChans.clear(); + + if (outputChannels.getHighestBit() >= 0) + { + for (int i = 0; i < maxOutputsRequested; ++i) + { + if (outputChannels[i]) + { + outputChannelDataForCallback.add (outputChannelBuffer.getWritePointer (i)); + currentOutputChans.setBit (i); + } + } + } + + if (outputChannelDataForCallback.size() > 0 && outputId.isNotEmpty()) + { + outputDevice = new ALSADevice (outputId, false); + + if (outputDevice->error.isNotEmpty()) + { + error = outputDevice->error; + outputDevice = nullptr; + return; + } + + if (! outputDevice->setParameters ((unsigned int) sampleRate, + jlimit ((int) minChansOut, (int) maxChansOut, + currentOutputChans.getHighestBit() + 1), + bufferSize)) + { + error = outputDevice->error; + outputDevice = nullptr; + return; + } + + outputLatency = outputDevice->latency; + } + + if (inputChannelDataForCallback.size() > 0 && inputId.isNotEmpty()) + { + inputDevice = new ALSADevice (inputId, true); + + if (inputDevice->error.isNotEmpty()) + { + error = inputDevice->error; + inputDevice = nullptr; + return; + } + + ensureMinimumNumBitsSet (currentInputChans, (int) minChansIn); + + if (! inputDevice->setParameters ((unsigned int) sampleRate, + jlimit ((int) minChansIn, (int) maxChansIn, currentInputChans.getHighestBit() + 1), + bufferSize)) + { + error = inputDevice->error; + inputDevice = nullptr; + return; + } + + inputLatency = inputDevice->latency; + } + + if (outputDevice == nullptr && inputDevice == nullptr) + { + error = "no channels"; + return; + } + + if (outputDevice != nullptr && inputDevice != nullptr) + snd_pcm_link (outputDevice->handle, inputDevice->handle); + + if (inputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (inputDevice->handle))) + return; + + if (outputDevice != nullptr && JUCE_ALSA_FAILED (snd_pcm_prepare (outputDevice->handle))) + return; + + startThread (9); + + int count = 1000; + + while (numCallbacks == 0) + { + sleep (5); + + if (--count < 0 || ! isThreadRunning()) + { + error = "device didn't start"; + break; + } + } + } + + void close() + { + if (isThreadRunning()) + { + // problem: when pulseaudio is suspended (with pasuspend) , the ALSAThread::run is just stuck in + // snd_pcm_writei -- no error, no nothing it just stays stuck. So the only way I found to exit "nicely" + // (that is without the "killing thread by force" of stopThread) , is to just call snd_pcm_close from + // here which will cause the thread to resume, and exit + signalThreadShouldExit(); + + const int callbacksToStop = numCallbacks; + + if ((! waitForThreadToExit (400)) && audioIoInProgress && numCallbacks == callbacksToStop) + { + JUCE_ALSA_LOG ("Thread is stuck in i/o.. Is pulseaudio suspended?"); + + if (outputDevice != nullptr) outputDevice->closeNow(); + if (inputDevice != nullptr) inputDevice->closeNow(); + } + } + + stopThread (6000); + + inputDevice = nullptr; + outputDevice = nullptr; + + inputChannelBuffer.setSize (1, 1); + outputChannelBuffer.setSize (1, 1); + + numCallbacks = 0; + } + + void setCallback (AudioIODeviceCallback* const newCallback) noexcept + { + const ScopedLock sl (callbackLock); + callback = newCallback; + } + + void run() override + { + while (! threadShouldExit()) + { + if (inputDevice != nullptr && inputDevice->handle != nullptr) + { + if (outputDevice == nullptr || outputDevice->handle == nullptr) + { + JUCE_ALSA_FAILED (snd_pcm_wait (inputDevice->handle, 2000)); + + if (threadShouldExit()) + break; + + snd_pcm_sframes_t avail = snd_pcm_avail_update (inputDevice->handle); + + if (avail < 0) + JUCE_ALSA_FAILED (snd_pcm_recover (inputDevice->handle, (int) avail, 0)); + } + + audioIoInProgress = true; + + if (! inputDevice->readFromInputDevice (inputChannelBuffer, bufferSize)) + { + JUCE_ALSA_LOG ("Read failure"); + break; + } + + audioIoInProgress = false; + } + + if (threadShouldExit()) + break; + + { + const ScopedLock sl (callbackLock); + ++numCallbacks; + + if (callback != nullptr) + { + callback->audioDeviceIOCallback (inputChannelDataForCallback.getRawDataPointer(), + inputChannelDataForCallback.size(), + outputChannelDataForCallback.getRawDataPointer(), + outputChannelDataForCallback.size(), + bufferSize); + } + else + { + for (int i = 0; i < outputChannelDataForCallback.size(); ++i) + zeromem (outputChannelDataForCallback[i], sizeof (float) * (size_t) bufferSize); + } + } + + if (outputDevice != nullptr && outputDevice->handle != nullptr) + { + JUCE_ALSA_FAILED (snd_pcm_wait (outputDevice->handle, 2000)); + + if (threadShouldExit()) + break; + + snd_pcm_sframes_t avail = snd_pcm_avail_update (outputDevice->handle); + + if (avail < 0) + JUCE_ALSA_FAILED (snd_pcm_recover (outputDevice->handle, (int) avail, 0)); + + audioIoInProgress = true; + + if (! outputDevice->writeToOutputDevice (outputChannelBuffer, bufferSize)) + { + JUCE_ALSA_LOG ("write failure"); + break; + } + + audioIoInProgress = false; + } + } + + audioIoInProgress = false; + } + + int getBitDepth() const noexcept + { + if (outputDevice != nullptr) + return outputDevice->bitDepth; + + if (inputDevice != nullptr) + return inputDevice->bitDepth; + + return 16; + } + + int getXRunCount() const noexcept + { + int result = 0; + + if (outputDevice != nullptr) + result += outputDevice->underrunCount; + + if (inputDevice != nullptr) + result += inputDevice->overrunCount; + + return result; + } + + //============================================================================== + String error; + double sampleRate; + int bufferSize, outputLatency, inputLatency; + BigInteger currentInputChans, currentOutputChans; + + Array sampleRates; + StringArray channelNamesOut, channelNamesIn; + AudioIODeviceCallback* callback; + +private: + //============================================================================== + const String inputId, outputId; + ScopedPointer outputDevice, inputDevice; + int numCallbacks; + bool audioIoInProgress; + + CriticalSection callbackLock; + + AudioSampleBuffer inputChannelBuffer, outputChannelBuffer; + Array inputChannelDataForCallback; + Array outputChannelDataForCallback; + + unsigned int minChansOut, maxChansOut; + unsigned int minChansIn, maxChansIn; + + bool failed (const int errorNum) + { + if (errorNum >= 0) + return false; + + error = snd_strerror (errorNum); + JUCE_ALSA_LOG ("ALSA error: " << error); + return true; + } + + void initialiseRatesAndChannels() + { + sampleRates.clear(); + channelNamesOut.clear(); + channelNamesIn.clear(); + minChansOut = 0; + maxChansOut = 0; + minChansIn = 0; + maxChansIn = 0; + unsigned int dummy = 0; + + getDeviceProperties (inputId, dummy, dummy, minChansIn, maxChansIn, sampleRates, false, true); + getDeviceProperties (outputId, minChansOut, maxChansOut, dummy, dummy, sampleRates, true, false); + + for (unsigned int i = 0; i < maxChansOut; ++i) + channelNamesOut.add ("channel " + String ((int) i + 1)); + + for (unsigned int i = 0; i < maxChansIn; ++i) + channelNamesIn.add ("channel " + String ((int) i + 1)); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAThread) +}; + + +//============================================================================== +class ALSAAudioIODevice : public AudioIODevice +{ +public: + ALSAAudioIODevice (const String& deviceName, + const String& deviceTypeName, + const String& inputDeviceID, + const String& outputDeviceID) + : AudioIODevice (deviceName, deviceTypeName), + inputId (inputDeviceID), + outputId (outputDeviceID), + isOpen_ (false), + isStarted (false), + internal (inputDeviceID, outputDeviceID) + { + } + + ~ALSAAudioIODevice() + { + close(); + } + + StringArray getOutputChannelNames() override { return internal.channelNamesOut; } + StringArray getInputChannelNames() override { return internal.channelNamesIn; } + + Array getAvailableSampleRates() override { return internal.sampleRates; } + + Array getAvailableBufferSizes() override + { + Array r; + int n = 16; + + for (int i = 0; i < 50; ++i) + { + r.add (n); + n += n < 64 ? 16 + : (n < 512 ? 32 + : (n < 1024 ? 64 + : (n < 2048 ? 128 : 256))); + } + + return r; + } + + int getDefaultBufferSize() override { return 512; } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate, + int bufferSizeSamples) override + { + close(); + + if (bufferSizeSamples <= 0) + bufferSizeSamples = getDefaultBufferSize(); + + if (sampleRate <= 0) + { + for (int i = 0; i < internal.sampleRates.size(); ++i) + { + double rate = internal.sampleRates[i]; + + if (rate >= 44100) + { + sampleRate = rate; + break; + } + } + } + + internal.open (inputChannels, outputChannels, + sampleRate, bufferSizeSamples); + + isOpen_ = internal.error.isEmpty(); + return internal.error; + } + + void close() override + { + stop(); + internal.close(); + isOpen_ = false; + } + + bool isOpen() override { return isOpen_; } + bool isPlaying() override { return isStarted && internal.error.isEmpty(); } + String getLastError() override { return internal.error; } + + int getCurrentBufferSizeSamples() override { return internal.bufferSize; } + double getCurrentSampleRate() override { return internal.sampleRate; } + int getCurrentBitDepth() override { return internal.getBitDepth(); } + + BigInteger getActiveOutputChannels() const override { return internal.currentOutputChans; } + BigInteger getActiveInputChannels() const override { return internal.currentInputChans; } + + int getOutputLatencyInSamples() override { return internal.outputLatency; } + int getInputLatencyInSamples() override { return internal.inputLatency; } + + int getXRunCount() const noexcept override { return internal.getXRunCount(); } + + void start (AudioIODeviceCallback* callback) override + { + if (! isOpen_) + callback = nullptr; + + if (callback != nullptr) + callback->audioDeviceAboutToStart (this); + + internal.setCallback (callback); + + isStarted = (callback != nullptr); + } + + void stop() override + { + AudioIODeviceCallback* const oldCallback = internal.callback; + + start (nullptr); + + if (oldCallback != nullptr) + oldCallback->audioDeviceStopped(); + } + + String inputId, outputId; + +private: + bool isOpen_, isStarted; + ALSAThread internal; +}; + + +//============================================================================== +class ALSAAudioIODeviceType : public AudioIODeviceType +{ +public: + ALSAAudioIODeviceType (bool onlySoundcards, const String &deviceTypeName) + : AudioIODeviceType (deviceTypeName), + hasScanned (false), + listOnlySoundcards (onlySoundcards) + { + #if ! JUCE_ALSA_LOGGING + snd_lib_error_set_handler (&silentErrorHandler); + #endif + } + + ~ALSAAudioIODeviceType() + { + #if ! JUCE_ALSA_LOGGING + snd_lib_error_set_handler (nullptr); + #endif + + snd_config_update_free_global(); // prevent valgrind from screaming about alsa leaks + } + + //============================================================================== + void scanForDevices() + { + if (hasScanned) + return; + + hasScanned = true; + inputNames.clear(); + inputIds.clear(); + outputNames.clear(); + outputIds.clear(); + + JUCE_ALSA_LOG ("scanForDevices()"); + + if (listOnlySoundcards) + enumerateAlsaSoundcards(); + else + enumerateAlsaPCMDevices(); + + inputNames.appendNumbersToDuplicates (false, true); + outputNames.appendNumbersToDuplicates (false, true); + } + + StringArray getDeviceNames (bool wantInputNames) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + return wantInputNames ? inputNames : outputNames; + } + + int getDefaultDeviceIndex (bool forInput) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + const int idx = (forInput ? inputIds : outputIds).indexOf ("default"); + return idx >= 0 ? idx : 0; + } + + bool hasSeparateInputsAndOutputs() const { return true; } + + int getIndexOfDevice (AudioIODevice* device, bool asInput) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + if (ALSAAudioIODevice* d = dynamic_cast (device)) + return asInput ? inputIds.indexOf (d->inputId) + : outputIds.indexOf (d->outputId); + + return -1; + } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + const int inputIndex = inputNames.indexOf (inputDeviceName); + const int outputIndex = outputNames.indexOf (outputDeviceName); + + String deviceName (outputIndex >= 0 ? outputDeviceName + : inputDeviceName); + + if (inputIndex >= 0 || outputIndex >= 0) + return new ALSAAudioIODevice (deviceName, getTypeName(), + inputIds [inputIndex], + outputIds [outputIndex]); + + return nullptr; + } + +private: + //============================================================================== + StringArray inputNames, outputNames, inputIds, outputIds; + bool hasScanned, listOnlySoundcards; + + bool testDevice (const String &id, const String &outputName, const String &inputName) + { + unsigned int minChansOut = 0, maxChansOut = 0; + unsigned int minChansIn = 0, maxChansIn = 0; + Array rates; + + bool isInput = inputName.isNotEmpty(), isOutput = outputName.isNotEmpty(); + getDeviceProperties (id, minChansOut, maxChansOut, minChansIn, maxChansIn, rates, isOutput, isInput); + + isInput = maxChansIn > 0; + isOutput = maxChansOut > 0; + + if ((isInput || isOutput) && rates.size() > 0) + { + JUCE_ALSA_LOG ("testDevice: '" << id.toUTF8().getAddress() << "' -> isInput: " + << (int) isInput << ", isOutput: " << (int) isOutput); + + if (isInput) + { + inputNames.add (inputName); + inputIds.add (id); + } + + if (isOutput) + { + outputNames.add (outputName); + outputIds.add (id); + } + + return isInput || isOutput; + } + + return false; + } + + void enumerateAlsaSoundcards() + { + snd_ctl_t* handle = nullptr; + snd_ctl_card_info_t* info = nullptr; + snd_ctl_card_info_alloca (&info); + + int cardNum = -1; + + while (outputIds.size() + inputIds.size() <= 64) + { + snd_card_next (&cardNum); + + if (cardNum < 0) + break; + + if (JUCE_CHECKED_RESULT (snd_ctl_open (&handle, ("hw:" + String (cardNum)).toUTF8(), SND_CTL_NONBLOCK)) >= 0) + { + if (JUCE_CHECKED_RESULT (snd_ctl_card_info (handle, info)) >= 0) + { + String cardId (snd_ctl_card_info_get_id (info)); + + if (cardId.removeCharacters ("0123456789").isEmpty()) + cardId = String (cardNum); + + String cardName = snd_ctl_card_info_get_name (info); + + if (cardName.isEmpty()) + cardName = cardId; + + int device = -1; + + snd_pcm_info_t* pcmInfo; + snd_pcm_info_alloca (&pcmInfo); + + for (;;) + { + if (snd_ctl_pcm_next_device (handle, &device) < 0 || device < 0) + break; + + snd_pcm_info_set_device (pcmInfo, (unsigned int) device); + + for (unsigned int subDevice = 0, nbSubDevice = 1; subDevice < nbSubDevice; ++subDevice) + { + snd_pcm_info_set_subdevice (pcmInfo, subDevice); + snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_CAPTURE); + const bool isInput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0); + + snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_PLAYBACK); + const bool isOutput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0); + + if (! (isInput || isOutput)) + continue; + + if (nbSubDevice == 1) + nbSubDevice = snd_pcm_info_get_subdevices_count (pcmInfo); + + String id, name; + + if (nbSubDevice == 1) + { + id << "hw:" << cardId << "," << device; + name << cardName << ", " << snd_pcm_info_get_name (pcmInfo); + } + else + { + id << "hw:" << cardId << "," << device << "," << (int) subDevice; + name << cardName << ", " << snd_pcm_info_get_name (pcmInfo) + << " {" << snd_pcm_info_get_subdevice_name (pcmInfo) << "}"; + } + + JUCE_ALSA_LOG ("Soundcard ID: " << id << ", name: '" << name + << ", isInput:" << (int) isInput + << ", isOutput:" << (int) isOutput << "\n"); + + if (isInput) + { + inputNames.add (name); + inputIds.add (id); + } + + if (isOutput) + { + outputNames.add (name); + outputIds.add (id); + } + } + } + } + + JUCE_CHECKED_RESULT (snd_ctl_close (handle)); + } + } + } + + /* Enumerates all ALSA output devices (as output by the command aplay -L) + Does not try to open the devices (with "testDevice" for example), + so that it also finds devices that are busy and not yet available. + */ + void enumerateAlsaPCMDevices() + { + void** hints = nullptr; + + if (JUCE_CHECKED_RESULT (snd_device_name_hint (-1, "pcm", &hints)) == 0) + { + for (char** h = (char**) hints; *h; ++h) + { + const String id (hintToString (*h, "NAME")); + const String description (hintToString (*h, "DESC")); + const String ioid (hintToString (*h, "IOID")); + + JUCE_ALSA_LOG ("ID: " << id << "; desc: " << description << "; ioid: " << ioid); + + String ss = id.fromFirstOccurrenceOf ("=", false, false) + .upToFirstOccurrenceOf (",", false, false); + + if (id.isEmpty() + || id.startsWith ("default:") || id.startsWith ("sysdefault:") + || id.startsWith ("plughw:") || id == "null") + continue; + + String name (description.replace ("\n", "; ")); + + if (name.isEmpty()) + name = id; + + bool isOutput = (ioid != "Input"); + bool isInput = (ioid != "Output"); + + // alsa is stupid here, it advertises dmix and dsnoop as input/output devices, but + // opening dmix as input, or dsnoop as output will trigger errors.. + isInput = isInput && ! id.startsWith ("dmix"); + isOutput = isOutput && ! id.startsWith ("dsnoop"); + + if (isInput) + { + inputNames.add (name); + inputIds.add (id); + } + + if (isOutput) + { + outputNames.add (name); + outputIds.add (id); + } + } + + snd_device_name_free_hint (hints); + } + + // sometimes the "default" device is not listed, but it is nice to see it explicitely in the list + if (! outputIds.contains ("default")) + testDevice ("default", "Default ALSA Output", "Default ALSA Input"); + + // same for the pulseaudio plugin + if (! outputIds.contains ("pulse")) + testDevice ("pulse", "Pulseaudio output", "Pulseaudio input"); + + // make sure the default device is listed first, and followed by the pulse device (if present) + int idx = outputIds.indexOf ("pulse"); + outputIds.move (idx, 0); + outputNames.move (idx, 0); + + idx = inputIds.indexOf ("pulse"); + inputIds.move (idx, 0); + inputNames.move (idx, 0); + + idx = outputIds.indexOf ("default"); + outputIds.move (idx, 0); + outputNames.move (idx, 0); + + idx = inputIds.indexOf ("default"); + inputIds.move (idx, 0); + inputNames.move (idx, 0); + } + + static String hintToString (const void* hints, const char* type) + { + char* const hint = snd_device_name_get_hint (hints, type); + const String s (String::fromUTF8 (hint)); + ::free (hint); + return s; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAAudioIODeviceType) +}; + +} + +//============================================================================== +AudioIODeviceType* createAudioIODeviceType_ALSA_Soundcards() +{ + return new ALSAAudioIODeviceType (true, "ALSA HW"); +} + +AudioIODeviceType* createAudioIODeviceType_ALSA_PCMDevices() +{ + return new ALSAAudioIODeviceType (false, "ALSA"); +} + +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_ALSA() +{ + return createAudioIODeviceType_ALSA_PCMDevices(); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_linux_JackAudio.cpp b/source/modules/juce_audio_devices/native/juce_linux_JackAudio.cpp new file mode 100644 index 000000000..35302a316 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_linux_JackAudio.cpp @@ -0,0 +1,624 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +static void* juce_libjackHandle = nullptr; + +static void* juce_loadJackFunction (const char* const name) +{ + if (juce_libjackHandle == nullptr) + return nullptr; + + return dlsym (juce_libjackHandle, name); +} + +#define JUCE_DECL_JACK_FUNCTION(return_type, fn_name, argument_types, arguments) \ + return_type fn_name argument_types \ + { \ + typedef return_type (*fn_type) argument_types; \ + static fn_type fn = (fn_type) juce_loadJackFunction (#fn_name); \ + return (fn != nullptr) ? ((*fn) arguments) : (return_type) 0; \ + } + +#define JUCE_DECL_VOID_JACK_FUNCTION(fn_name, argument_types, arguments) \ + void fn_name argument_types \ + { \ + typedef void (*fn_type) argument_types; \ + static fn_type fn = (fn_type) juce_loadJackFunction (#fn_name); \ + if (fn != nullptr) (*fn) arguments; \ + } + +//============================================================================== +JUCE_DECL_JACK_FUNCTION (jack_client_t*, jack_client_open, (const char* client_name, jack_options_t options, jack_status_t* status, ...), (client_name, options, status)); +JUCE_DECL_JACK_FUNCTION (int, jack_client_close, (jack_client_t *client), (client)); +JUCE_DECL_JACK_FUNCTION (int, jack_activate, (jack_client_t* client), (client)); +JUCE_DECL_JACK_FUNCTION (int, jack_deactivate, (jack_client_t* client), (client)); +JUCE_DECL_JACK_FUNCTION (jack_nframes_t, jack_get_buffer_size, (jack_client_t* client), (client)); +JUCE_DECL_JACK_FUNCTION (jack_nframes_t, jack_get_sample_rate, (jack_client_t* client), (client)); +JUCE_DECL_VOID_JACK_FUNCTION (jack_on_shutdown, (jack_client_t* client, void (*function)(void* arg), void* arg), (client, function, arg)); +JUCE_DECL_JACK_FUNCTION (void* , jack_port_get_buffer, (jack_port_t* port, jack_nframes_t nframes), (port, nframes)); +JUCE_DECL_JACK_FUNCTION (jack_nframes_t, jack_port_get_total_latency, (jack_client_t* client, jack_port_t* port), (client, port)); +JUCE_DECL_JACK_FUNCTION (jack_port_t* , jack_port_register, (jack_client_t* client, const char* port_name, const char* port_type, unsigned long flags, unsigned long buffer_size), (client, port_name, port_type, flags, buffer_size)); +JUCE_DECL_VOID_JACK_FUNCTION (jack_set_error_function, (void (*func)(const char*)), (func)); +JUCE_DECL_JACK_FUNCTION (int, jack_set_process_callback, (jack_client_t* client, JackProcessCallback process_callback, void* arg), (client, process_callback, arg)); +JUCE_DECL_JACK_FUNCTION (const char**, jack_get_ports, (jack_client_t* client, const char* port_name_pattern, const char* type_name_pattern, unsigned long flags), (client, port_name_pattern, type_name_pattern, flags)); +JUCE_DECL_JACK_FUNCTION (int, jack_connect, (jack_client_t* client, const char* source_port, const char* destination_port), (client, source_port, destination_port)); +JUCE_DECL_JACK_FUNCTION (const char*, jack_port_name, (const jack_port_t* port), (port)); +JUCE_DECL_JACK_FUNCTION (void*, jack_set_port_connect_callback, (jack_client_t* client, JackPortConnectCallback connect_callback, void* arg), (client, connect_callback, arg)); +JUCE_DECL_JACK_FUNCTION (jack_port_t* , jack_port_by_id, (jack_client_t* client, jack_port_id_t port_id), (client, port_id)); +JUCE_DECL_JACK_FUNCTION (int, jack_port_connected, (const jack_port_t* port), (port)); +JUCE_DECL_JACK_FUNCTION (int, jack_port_connected_to, (const jack_port_t* port, const char* port_name), (port, port_name)); +JUCE_DECL_JACK_FUNCTION (int, jack_set_xrun_callback, (jack_client_t* client, JackXRunCallback xrun_callback, void* arg), (client, xrun_callback, arg)); + +#if JUCE_DEBUG + #define JACK_LOGGING_ENABLED 1 +#endif + +#if JACK_LOGGING_ENABLED +namespace +{ + void jack_Log (const String& s) + { + std::cerr << s << std::endl; + } + + const char* getJackErrorMessage (const jack_status_t status) + { + if (status & JackServerFailed + || status & JackServerError) return "Unable to connect to JACK server"; + if (status & JackVersionError) return "Client's protocol version does not match"; + if (status & JackInvalidOption) return "The operation contained an invalid or unsupported option"; + if (status & JackNameNotUnique) return "The desired client name was not unique"; + if (status & JackNoSuchClient) return "Requested client does not exist"; + if (status & JackInitFailure) return "Unable to initialize client"; + return nullptr; + } +} + #define JUCE_JACK_LOG_STATUS(x) { if (const char* m = getJackErrorMessage (x)) jack_Log (m); } + #define JUCE_JACK_LOG(x) jack_Log(x) +#else + #define JUCE_JACK_LOG_STATUS(x) {} + #define JUCE_JACK_LOG(x) {} +#endif + + +//============================================================================== +#ifndef JUCE_JACK_CLIENT_NAME + #define JUCE_JACK_CLIENT_NAME "JUCEJack" +#endif + +struct JackPortIterator +{ + JackPortIterator (jack_client_t* const client, const bool forInput) + : ports (nullptr), index (-1) + { + if (client != nullptr) + ports = juce::jack_get_ports (client, nullptr, nullptr, + forInput ? JackPortIsOutput : JackPortIsInput); + // (NB: This looks like it's the wrong way round, but it is correct!) + } + + ~JackPortIterator() + { + ::free (ports); + } + + bool next() + { + if (ports == nullptr || ports [index + 1] == nullptr) + return false; + + name = CharPointer_UTF8 (ports[++index]); + clientName = name.upToFirstOccurrenceOf (":", false, false); + return true; + } + + const char** ports; + int index; + String name; + String clientName; +}; + +class JackAudioIODeviceType; +static Array activeDeviceTypes; + +//============================================================================== +class JackAudioIODevice : public AudioIODevice +{ +public: + JackAudioIODevice (const String& deviceName, + const String& inId, + const String& outId) + : AudioIODevice (deviceName, "JACK"), + inputId (inId), + outputId (outId), + deviceIsOpen (false), + callback (nullptr), + totalNumberOfInputChannels (0), + totalNumberOfOutputChannels (0) + { + jassert (deviceName.isNotEmpty()); + + jack_status_t status; + client = juce::jack_client_open (JUCE_JACK_CLIENT_NAME, JackNoStartServer, &status); + + if (client == nullptr) + { + JUCE_JACK_LOG_STATUS (status); + } + else + { + juce::jack_set_error_function (errorCallback); + + // open input ports + const StringArray inputChannels (getInputChannelNames()); + for (int i = 0; i < inputChannels.size(); ++i) + { + String inputName; + inputName << "in_" << ++totalNumberOfInputChannels; + + inputPorts.add (juce::jack_port_register (client, inputName.toUTF8(), + JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0)); + } + + // open output ports + const StringArray outputChannels (getOutputChannelNames()); + for (int i = 0; i < outputChannels.size(); ++i) + { + String outputName; + outputName << "out_" << ++totalNumberOfOutputChannels; + + outputPorts.add (juce::jack_port_register (client, outputName.toUTF8(), + JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0)); + } + + inChans.calloc (totalNumberOfInputChannels + 2); + outChans.calloc (totalNumberOfOutputChannels + 2); + } + } + + ~JackAudioIODevice() + { + close(); + if (client != nullptr) + { + juce::jack_client_close (client); + client = nullptr; + } + } + + StringArray getChannelNames (bool forInput) const + { + StringArray names; + + for (JackPortIterator i (client, forInput); i.next();) + if (i.clientName == getName()) + names.add (i.name.fromFirstOccurrenceOf (":", false, false)); + + return names; + } + + StringArray getOutputChannelNames() override { return getChannelNames (false); } + StringArray getInputChannelNames() override { return getChannelNames (true); } + + Array getAvailableSampleRates() override + { + Array rates; + + if (client != nullptr) + rates.add (juce::jack_get_sample_rate (client)); + + return rates; + } + + Array getAvailableBufferSizes() override + { + Array sizes; + + if (client != nullptr) + sizes.add (juce::jack_get_buffer_size (client)); + + return sizes; + } + + int getDefaultBufferSize() override { return getCurrentBufferSizeSamples(); } + int getCurrentBufferSizeSamples() override { return client != nullptr ? juce::jack_get_buffer_size (client) : 0; } + double getCurrentSampleRate() override { return client != nullptr ? juce::jack_get_sample_rate (client) : 0; } + + + String open (const BigInteger& inputChannels, const BigInteger& outputChannels, + double /* sampleRate */, int /* bufferSizeSamples */) override + { + if (client == nullptr) + { + lastError = "No JACK client running"; + return lastError; + } + + lastError.clear(); + close(); + + xruns = 0; + juce::jack_set_process_callback (client, processCallback, this); + juce::jack_set_port_connect_callback (client, portConnectCallback, this); + juce::jack_on_shutdown (client, shutdownCallback, this); + juce::jack_set_xrun_callback (client, xrunCallback, this); + juce::jack_activate (client); + deviceIsOpen = true; + + if (! inputChannels.isZero()) + { + for (JackPortIterator i (client, true); i.next();) + { + if (inputChannels [i.index] && i.clientName == getName()) + { + int error = juce::jack_connect (client, i.ports[i.index], juce::jack_port_name ((jack_port_t*) inputPorts[i.index])); + if (error != 0) + JUCE_JACK_LOG ("Cannot connect input port " + String (i.index) + " (" + i.name + "), error " + String (error)); + } + } + } + + if (! outputChannels.isZero()) + { + for (JackPortIterator i (client, false); i.next();) + { + if (outputChannels [i.index] && i.clientName == getName()) + { + int error = juce::jack_connect (client, juce::jack_port_name ((jack_port_t*) outputPorts[i.index]), i.ports[i.index]); + if (error != 0) + JUCE_JACK_LOG ("Cannot connect output port " + String (i.index) + " (" + i.name + "), error " + String (error)); + } + } + } + + updateActivePorts(); + + return lastError; + } + + void close() override + { + stop(); + + if (client != nullptr) + { + juce::jack_deactivate (client); + + juce::jack_set_xrun_callback (client, xrunCallback, nullptr); + juce::jack_set_process_callback (client, processCallback, nullptr); + juce::jack_set_port_connect_callback (client, portConnectCallback, nullptr); + juce::jack_on_shutdown (client, shutdownCallback, nullptr); + } + + deviceIsOpen = false; + } + + void start (AudioIODeviceCallback* newCallback) override + { + if (deviceIsOpen && newCallback != callback) + { + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (this); + + AudioIODeviceCallback* const oldCallback = callback; + + { + const ScopedLock sl (callbackLock); + callback = newCallback; + } + + if (oldCallback != nullptr) + oldCallback->audioDeviceStopped(); + } + } + + void stop() override + { + start (nullptr); + } + + bool isOpen() override { return deviceIsOpen; } + bool isPlaying() override { return callback != nullptr; } + int getCurrentBitDepth() override { return 32; } + String getLastError() override { return lastError; } + int getXRunCount() const noexcept override { return xruns; } + + BigInteger getActiveOutputChannels() const override { return activeOutputChannels; } + BigInteger getActiveInputChannels() const override { return activeInputChannels; } + + int getOutputLatencyInSamples() override + { + int latency = 0; + + for (int i = 0; i < outputPorts.size(); i++) + latency = jmax (latency, (int) juce::jack_port_get_total_latency (client, (jack_port_t*) outputPorts [i])); + + return latency; + } + + int getInputLatencyInSamples() override + { + int latency = 0; + + for (int i = 0; i < inputPorts.size(); i++) + latency = jmax (latency, (int) juce::jack_port_get_total_latency (client, (jack_port_t*) inputPorts [i])); + + return latency; + } + + String inputId, outputId; + +private: + void process (const int numSamples) + { + int numActiveInChans = 0, numActiveOutChans = 0; + + for (int i = 0; i < totalNumberOfInputChannels; ++i) + { + if (activeInputChannels[i]) + if (jack_default_audio_sample_t* in + = (jack_default_audio_sample_t*) juce::jack_port_get_buffer ((jack_port_t*) inputPorts.getUnchecked(i), numSamples)) + inChans [numActiveInChans++] = (float*) in; + } + + for (int i = 0; i < totalNumberOfOutputChannels; ++i) + { + if (activeOutputChannels[i]) + if (jack_default_audio_sample_t* out + = (jack_default_audio_sample_t*) juce::jack_port_get_buffer ((jack_port_t*) outputPorts.getUnchecked(i), numSamples)) + outChans [numActiveOutChans++] = (float*) out; + } + + const ScopedLock sl (callbackLock); + + if (callback != nullptr) + { + if ((numActiveInChans + numActiveOutChans) > 0) + callback->audioDeviceIOCallback (const_cast (inChans.getData()), numActiveInChans, + outChans, numActiveOutChans, numSamples); + } + else + { + for (int i = 0; i < numActiveOutChans; ++i) + zeromem (outChans[i], sizeof (float) * numSamples); + } + } + + static int processCallback (jack_nframes_t nframes, void* callbackArgument) + { + if (callbackArgument != nullptr) + ((JackAudioIODevice*) callbackArgument)->process (nframes); + + return 0; + } + + static int xrunCallback (void* callbackArgument) + { + if (callbackArgument != nullptr) + ((JackAudioIODevice*) callbackArgument)->xruns++; + + return 0; + } + + void updateActivePorts() + { + BigInteger newOutputChannels, newInputChannels; + + for (int i = 0; i < outputPorts.size(); ++i) + if (juce::jack_port_connected ((jack_port_t*) outputPorts.getUnchecked(i))) + newOutputChannels.setBit (i); + + for (int i = 0; i < inputPorts.size(); ++i) + if (juce::jack_port_connected ((jack_port_t*) inputPorts.getUnchecked(i))) + newInputChannels.setBit (i); + + if (newOutputChannels != activeOutputChannels + || newInputChannels != activeInputChannels) + { + AudioIODeviceCallback* const oldCallback = callback; + + stop(); + + activeOutputChannels = newOutputChannels; + activeInputChannels = newInputChannels; + + if (oldCallback != nullptr) + start (oldCallback); + + sendDeviceChangedCallback(); + } + } + + static void portConnectCallback (jack_port_id_t, jack_port_id_t, int, void* arg) + { + if (JackAudioIODevice* device = static_cast (arg)) + device->updateActivePorts(); + } + + static void threadInitCallback (void* /* callbackArgument */) + { + JUCE_JACK_LOG ("JackAudioIODevice::initialise"); + } + + static void shutdownCallback (void* callbackArgument) + { + JUCE_JACK_LOG ("JackAudioIODevice::shutdown"); + + if (JackAudioIODevice* device = (JackAudioIODevice*) callbackArgument) + { + device->client = nullptr; + device->close(); + } + } + + static void errorCallback (const char* msg) + { + JUCE_JACK_LOG ("JackAudioIODevice::errorCallback " + String (msg)); + } + + static void sendDeviceChangedCallback(); + + bool deviceIsOpen; + jack_client_t* client; + String lastError; + AudioIODeviceCallback* callback; + CriticalSection callbackLock; + + HeapBlock inChans, outChans; + int totalNumberOfInputChannels; + int totalNumberOfOutputChannels; + Array inputPorts, outputPorts; + BigInteger activeInputChannels, activeOutputChannels; + + int xruns; +}; + + +//============================================================================== +class JackAudioIODeviceType : public AudioIODeviceType +{ +public: + JackAudioIODeviceType() + : AudioIODeviceType ("JACK"), + hasScanned (false) + { + activeDeviceTypes.add (this); + } + + ~JackAudioIODeviceType() + { + activeDeviceTypes.removeFirstMatchingValue (this); + } + + void scanForDevices() + { + hasScanned = true; + inputNames.clear(); + inputIds.clear(); + outputNames.clear(); + outputIds.clear(); + + if (juce_libjackHandle == nullptr) juce_libjackHandle = dlopen ("libjack.so.0", RTLD_LAZY); + if (juce_libjackHandle == nullptr) juce_libjackHandle = dlopen ("libjack.so", RTLD_LAZY); + if (juce_libjackHandle == nullptr) return; + + jack_status_t status; + + // open a dummy client + if (jack_client_t* const client = juce::jack_client_open ("JuceJackDummy", JackNoStartServer, &status)) + { + // scan for output devices + for (JackPortIterator i (client, false); i.next();) + { + if (i.clientName != (JUCE_JACK_CLIENT_NAME) && ! inputNames.contains (i.clientName)) + { + inputNames.add (i.clientName); + inputIds.add (i.ports [i.index]); + } + } + + // scan for input devices + for (JackPortIterator i (client, true); i.next();) + { + if (i.clientName != (JUCE_JACK_CLIENT_NAME) && ! outputNames.contains (i.clientName)) + { + outputNames.add (i.clientName); + outputIds.add (i.ports [i.index]); + } + } + + juce::jack_client_close (client); + } + else + { + JUCE_JACK_LOG_STATUS (status); + } + } + + StringArray getDeviceNames (bool wantInputNames) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + return wantInputNames ? inputNames : outputNames; + } + + int getDefaultDeviceIndex (bool /* forInput */) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + return 0; + } + + bool hasSeparateInputsAndOutputs() const { return true; } + + int getIndexOfDevice (AudioIODevice* device, bool asInput) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + if (JackAudioIODevice* d = dynamic_cast (device)) + return asInput ? inputIds.indexOf (d->inputId) + : outputIds.indexOf (d->outputId); + + return -1; + } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + const int inputIndex = inputNames.indexOf (inputDeviceName); + const int outputIndex = outputNames.indexOf (outputDeviceName); + + if (inputIndex >= 0 || outputIndex >= 0) + return new JackAudioIODevice (outputIndex >= 0 ? outputDeviceName + : inputDeviceName, + inputIds [inputIndex], + outputIds [outputIndex]); + + return nullptr; + } + + void portConnectionChange() { callDeviceChangeListeners(); } + +private: + StringArray inputNames, outputNames, inputIds, outputIds; + bool hasScanned; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JackAudioIODeviceType) +}; + +void JackAudioIODevice::sendDeviceChangedCallback() +{ + for (int i = activeDeviceTypes.size(); --i >= 0;) + if (JackAudioIODeviceType* d = activeDeviceTypes[i]) + d->portConnectionChange(); +} + +//============================================================================== +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_JACK() +{ + return new JackAudioIODeviceType(); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_linux_Midi.cpp b/source/modules/juce_audio_devices/native/juce_linux_Midi.cpp new file mode 100644 index 000000000..ef4dfade8 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_linux_Midi.cpp @@ -0,0 +1,617 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#if JUCE_ALSA + +// You can define these strings in your app if you want to override the default names: +#ifndef JUCE_ALSA_MIDI_NAME + #define JUCE_ALSA_MIDI_NAME JUCEApplicationBase::getInstance()->getApplicationName().toUTF8() +#endif + +//============================================================================== +namespace +{ + +//============================================================================== +class AlsaClient : public ReferenceCountedObject +{ +public: + typedef ReferenceCountedObjectPtr Ptr; + + //============================================================================== + // represents an input or output port of the supplied AlsaClient + class Port + { + public: + Port (AlsaClient& c, bool forInput) noexcept + : portId (-1), + callbackEnabled (false), + client (c), + isInput (forInput), + callback (nullptr), + maxEventSize (4 * 1024), + midiInput (nullptr) + {} + + ~Port() + { + if (isValid()) + { + if (isInput) + enableCallback (false); + else + snd_midi_event_free (midiParser); + + snd_seq_delete_simple_port (client.get(), portId); + } + } + + void connectWith (int sourceClient, int sourcePort) const noexcept + { + if (isInput) + snd_seq_connect_from (client.get(), portId, sourceClient, sourcePort); + else + snd_seq_connect_to (client.get(), portId, sourceClient, sourcePort); + } + + bool isValid() const noexcept + { + return client.get() != nullptr && portId >= 0; + } + + void setupInput(MidiInput* input, MidiInputCallback* cb) + { + jassert (cb && input); + + callback = cb; + midiInput = input; + } + + void setupOutput() + { + jassert (! isInput); + + snd_midi_event_new ((size_t) maxEventSize, &midiParser); + } + + void enableCallback (bool enable) + { + if (callbackEnabled != enable) + { + callbackEnabled = enable; + + if (enable) + client.registerCallback(); + else + client.unregisterCallback(); + } + } + + bool sendMessageNow (const MidiMessage& message) + { + if (message.getRawDataSize() > maxEventSize) + { + maxEventSize = message.getRawDataSize(); + snd_midi_event_free (midiParser); + snd_midi_event_new ((size_t) maxEventSize, &midiParser); + } + + snd_seq_event_t event; + snd_seq_ev_clear (&event); + + long numBytes = (long) message.getRawDataSize(); + const uint8* data = message.getRawData(); + + snd_seq_t* seqHandle = client.get(); + bool success = true; + + while (numBytes > 0) + { + const long numSent = snd_midi_event_encode (midiParser, data, numBytes, &event); + + if (numSent <= 0) + { + success = numSent == 0; + break; + } + + numBytes -= numSent; + data += numSent; + + snd_seq_ev_set_source (&event, (unsigned char) portId); + snd_seq_ev_set_subs (&event); + snd_seq_ev_set_direct (&event); + + if (snd_seq_event_output_direct (seqHandle, &event) < 0) + { + success = false; + break; + } + } + + snd_midi_event_reset_encode (midiParser); + return success; + } + + + bool operator== (const Port& lhs) const noexcept + { + return portId != -1 && portId == lhs.portId; + } + + int portId; + bool callbackEnabled; + + private: + friend class AlsaClient; + + AlsaClient& client; + bool isInput; + MidiInputCallback* callback; + snd_midi_event_t* midiParser; + int maxEventSize; + MidiInput* midiInput; + + void createPort (const String& name, bool enableSubscription) + { + if (snd_seq_t* seqHandle = client.get()) + { + const unsigned int caps = + isInput + ? (SND_SEQ_PORT_CAP_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_WRITE : 0)) + : (SND_SEQ_PORT_CAP_WRITE | (enableSubscription ? SND_SEQ_PORT_CAP_SUBS_READ : 0)); + portId = snd_seq_create_simple_port (seqHandle, name.toUTF8(), caps, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + } + } + + void handleIncomingMidiMessage (const MidiMessage& message) const + { + callback->handleIncomingMidiMessage (midiInput, message); + } + + void handlePartialSysexMessage (const uint8* messageData, int numBytesSoFar, double timeStamp) + { + callback->handlePartialSysexMessage (midiInput, messageData, numBytesSoFar, timeStamp); + } + }; + + static Ptr getInstance() + { + if (instance == nullptr) + instance = new AlsaClient(); + + return instance; + } + + void registerCallback() + { + if (inputThread == nullptr) + inputThread = new MidiInputThread (*this); + + if (++activeCallbacks - 1 == 0) + inputThread->startThread(); + } + + void unregisterCallback() + { + jassert (activeCallbacks.get() > 0); + if (--activeCallbacks == 0 && inputThread->isThreadRunning()) + inputThread->signalThreadShouldExit(); + } + + void handleIncomingMidiMessage (snd_seq_event* event, const MidiMessage& message) + { + if (event->dest.port < ports.size() + && ports[event->dest.port]->callbackEnabled) + ports[event->dest.port]->handleIncomingMidiMessage (message); + } + + void handlePartialSysexMessage (snd_seq_event* event, const uint8* messageData, int numBytesSoFar, double timeStamp) + { + if (event->dest.port < ports.size() + && ports[event->dest.port]->callbackEnabled) + ports[event->dest.port]->handlePartialSysexMessage (messageData, numBytesSoFar, timeStamp); + } + + snd_seq_t* get() const noexcept { return handle; } + int getId() const noexcept { return clientId; } + + Port* createPort (const String& name, bool forInput, bool enableSubscription) + { + Port* port = new Port (*this, forInput); + port->createPort (name, enableSubscription); + ports.set (port->portId, port); + incReferenceCount(); + return port; + } + + void deletePort (Port* port) + { + ports.remove (port->portId); + decReferenceCount(); + } + +private: + snd_seq_t* handle; + int clientId; + OwnedArray ports; + Atomic activeCallbacks; + CriticalSection callbackLock; + + static AlsaClient* instance; + + //============================================================================== + friend class ReferenceCountedObjectPtr; + friend struct ContainerDeletePolicy; + + AlsaClient() + : handle (nullptr), + inputThread (nullptr) + { + jassert (instance == nullptr); + + snd_seq_open (&handle, "default", SND_SEQ_OPEN_DUPLEX, 0); + snd_seq_nonblock (handle, SND_SEQ_NONBLOCK); + snd_seq_set_client_name (handle, JUCE_ALSA_MIDI_NAME); + clientId = snd_seq_client_id(handle); + + // It's good idea to pre-allocate a good number of elements + ports.ensureStorageAllocated (32); + } + + ~AlsaClient() + { + jassert (instance != nullptr); + + instance = nullptr; + + if (handle != nullptr) + snd_seq_close (handle); + + jassert (activeCallbacks.get() == 0); + + if (inputThread) + inputThread->stopThread (3000); + } + + //============================================================================== + class MidiInputThread : public Thread + { + public: + MidiInputThread (AlsaClient& c) + : Thread ("Juce MIDI Input"), client (c), concatenator (2048) + { + jassert (client.get() != nullptr); + } + + void run() override + { + const int maxEventSize = 16 * 1024; + snd_midi_event_t* midiParser; + snd_seq_t* seqHandle = client.get(); + + if (snd_midi_event_new (maxEventSize, &midiParser) >= 0) + { + const int numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN); + HeapBlock pfd ((size_t) numPfds); + snd_seq_poll_descriptors (seqHandle, pfd, (unsigned int) numPfds, POLLIN); + + HeapBlock buffer (maxEventSize); + + while (! threadShouldExit()) + { + if (poll (pfd, (nfds_t) numPfds, 100) > 0) // there was a "500" here which is a bit long when we exit the program and have to wait for a timeout on this poll call + { + if (threadShouldExit()) + break; + + do + { + snd_seq_event_t* inputEvent = nullptr; + + if (snd_seq_event_input (seqHandle, &inputEvent) >= 0) + { + // xxx what about SYSEXes that are too big for the buffer? + const long numBytes = snd_midi_event_decode (midiParser, buffer, + maxEventSize, inputEvent); + + snd_midi_event_reset_decode (midiParser); + + concatenator.pushMidiData (buffer, (int) numBytes, + Time::getMillisecondCounter() * 0.001, + inputEvent, client); + + snd_seq_free_event (inputEvent); + } + } + while (snd_seq_event_input_pending (seqHandle, 0) > 0); + } + } + + snd_midi_event_free (midiParser); + } + } + + private: + AlsaClient& client; + MidiDataConcatenator concatenator; + }; + + ScopedPointer inputThread; +}; + +AlsaClient* AlsaClient::instance = nullptr; + +//============================================================================== +static AlsaClient::Port* iterateMidiClient (const AlsaClient::Ptr& client, + snd_seq_client_info_t* clientInfo, + const bool forInput, + StringArray& deviceNamesFound, + const int deviceIndexToOpen) +{ + AlsaClient::Port* port = nullptr; + + snd_seq_t* seqHandle = client->get(); + snd_seq_port_info_t* portInfo = nullptr; + + snd_seq_port_info_alloca (&portInfo); + jassert (portInfo); + int numPorts = snd_seq_client_info_get_num_ports (clientInfo); + const int sourceClient = snd_seq_client_info_get_client (clientInfo); + + snd_seq_port_info_set_client (portInfo, sourceClient); + snd_seq_port_info_set_port (portInfo, -1); + + while (--numPorts >= 0) + { + if (snd_seq_query_next_port (seqHandle, portInfo) == 0 + && (snd_seq_port_info_get_capability (portInfo) + & (forInput ? SND_SEQ_PORT_CAP_SUBS_WRITE : SND_SEQ_PORT_CAP_SUBS_READ)) != 0) + { + const String portName = snd_seq_port_info_get_name(portInfo); + + deviceNamesFound.add (portName); + + if (deviceNamesFound.size() == deviceIndexToOpen + 1) + { + const int sourcePort = snd_seq_port_info_get_port (portInfo); + if (sourcePort != -1) + { + port = client->createPort (portName, forInput, false); + jassert (port->isValid()); + port->connectWith (sourceClient, sourcePort); + break; + } + } + } + } + + return port; +} + +static AlsaClient::Port* iterateMidiDevices (const bool forInput, + StringArray& deviceNamesFound, + const int deviceIndexToOpen) +{ + AlsaClient::Port* port = nullptr; + const AlsaClient::Ptr client (AlsaClient::getInstance()); + + if (snd_seq_t* const seqHandle = client->get()) + { + snd_seq_system_info_t* systemInfo = nullptr; + snd_seq_client_info_t* clientInfo = nullptr; + + snd_seq_system_info_alloca (&systemInfo); + jassert(systemInfo); + if (snd_seq_system_info (seqHandle, systemInfo) == 0) + { + snd_seq_client_info_alloca (&clientInfo); + jassert(clientInfo); + int numClients = snd_seq_system_info_get_cur_clients (systemInfo); + + while (--numClients >= 0) + { + if (snd_seq_query_next_client (seqHandle, clientInfo) == 0) + { + const int sourceClient = snd_seq_client_info_get_client (clientInfo); + if (sourceClient != client->getId() + && sourceClient != SND_SEQ_CLIENT_SYSTEM) + { + port = iterateMidiClient (client, clientInfo, forInput, + deviceNamesFound, deviceIndexToOpen); + if (port) + break; + } + } + } + } + } + + deviceNamesFound.appendNumbersToDuplicates (true, true); + + return port; +} + +} // namespace + +StringArray MidiOutput::getDevices() +{ + StringArray devices; + iterateMidiDevices (false, devices, -1); + return devices; +} + +int MidiOutput::getDefaultDeviceIndex() +{ + return 0; +} + +MidiOutput* MidiOutput::openDevice (int deviceIndex) +{ + MidiOutput* newDevice = nullptr; + + StringArray devices; + AlsaClient::Port* port = iterateMidiDevices (false, devices, deviceIndex); + + if (port == nullptr) + return nullptr; + + jassert (port->isValid()); + + newDevice = new MidiOutput (devices [deviceIndex]); + port->setupOutput(); + newDevice->internal = port; + + return newDevice; +} + +MidiOutput* MidiOutput::createNewDevice (const String& deviceName) +{ + MidiOutput* newDevice = nullptr; + + const AlsaClient::Ptr client (AlsaClient::getInstance()); + + AlsaClient::Port* port = client->createPort (deviceName, false, true); + + jassert (port->isValid()); + + newDevice = new MidiOutput (deviceName); + port->setupOutput(); + newDevice->internal = port; + + return newDevice; +} + +MidiOutput::~MidiOutput() +{ + stopBackgroundThread(); + + AlsaClient::Ptr client (AlsaClient::getInstance()); + client->deletePort (static_cast (internal)); +} + +void MidiOutput::sendMessageNow (const MidiMessage& message) +{ + static_cast (internal)->sendMessageNow (message); +} + +//============================================================================== +MidiInput::MidiInput (const String& nm) + : name (nm), internal (nullptr) +{ +} + +MidiInput::~MidiInput() +{ + stop(); + AlsaClient::Ptr client (AlsaClient::getInstance()); + client->deletePort (static_cast (internal)); +} + +void MidiInput::start() +{ + static_cast (internal)->enableCallback (true); +} + +void MidiInput::stop() +{ + static_cast (internal)->enableCallback (false); +} + +int MidiInput::getDefaultDeviceIndex() +{ + return 0; +} + +StringArray MidiInput::getDevices() +{ + StringArray devices; + iterateMidiDevices (true, devices, -1); + return devices; +} + +MidiInput* MidiInput::openDevice (int deviceIndex, MidiInputCallback* callback) +{ + MidiInput* newDevice = nullptr; + + StringArray devices; + AlsaClient::Port* port = iterateMidiDevices (true, devices, deviceIndex); + + if (port == nullptr) + return nullptr; + + jassert (port->isValid()); + + newDevice = new MidiInput (devices [deviceIndex]); + port->setupInput (newDevice, callback); + newDevice->internal = port; + + return newDevice; +} + +MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) +{ + MidiInput* newDevice = nullptr; + + AlsaClient::Ptr client (AlsaClient::getInstance()); + + AlsaClient::Port* port = client->createPort (deviceName, true, true); + + jassert (port->isValid()); + + newDevice = new MidiInput (deviceName); + port->setupInput (newDevice, callback); + newDevice->internal = port; + + return newDevice; +} + + +//============================================================================== +#else + +// (These are just stub functions if ALSA is unavailable...) + +StringArray MidiOutput::getDevices() { return {}; } +int MidiOutput::getDefaultDeviceIndex() { return 0; } +MidiOutput* MidiOutput::openDevice (int) { return nullptr; } +MidiOutput* MidiOutput::createNewDevice (const String&) { return nullptr; } +MidiOutput::~MidiOutput() {} +void MidiOutput::sendMessageNow (const MidiMessage&) {} + +MidiInput::MidiInput (const String& nm) : name (nm), internal (nullptr) {} +MidiInput::~MidiInput() {} +void MidiInput::start() {} +void MidiInput::stop() {} +int MidiInput::getDefaultDeviceIndex() { return 0; } +StringArray MidiInput::getDevices() { return {}; } +MidiInput* MidiInput::openDevice (int, MidiInputCallback*) { return nullptr; } +MidiInput* MidiInput::createNewDevice (const String&, MidiInputCallback*) { return nullptr; } + +#endif + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_mac_CoreAudio.cpp b/source/modules/juce_audio_devices/native/juce_mac_CoreAudio.cpp new file mode 100644 index 000000000..0bd8f3fbc --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_mac_CoreAudio.cpp @@ -0,0 +1,2073 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#if JUCE_COREAUDIO_LOGGING_ENABLED + #define JUCE_COREAUDIOLOG(a) { String camsg ("CoreAudio: "); camsg << a; Logger::writeToLog (camsg); } +#else + #define JUCE_COREAUDIOLOG(a) +#endif + +#ifdef __clang__ + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wnonnull" // aovid some spurious 10.11 SDK warnings + + // The AudioHardwareService stuff was deprecated in 10.11 but there's no replacement yet, + // so we'll have to silence the warnings here and revisit it in a future OS version.. + #if ((defined (MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_12) \ + || (defined (MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_11)) + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + #endif +#endif + +//============================================================================== +struct SystemVol +{ + SystemVol (AudioObjectPropertySelector selector) noexcept + : outputDeviceID (kAudioObjectUnknown) + { + addr.mScope = kAudioObjectPropertyScopeGlobal; + addr.mElement = kAudioObjectPropertyElementMaster; + addr.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + + if (AudioHardwareServiceHasProperty (kAudioObjectSystemObject, &addr)) + { + UInt32 deviceIDSize = sizeof (outputDeviceID); + OSStatus status = AudioHardwareServiceGetPropertyData (kAudioObjectSystemObject, &addr, 0, + nullptr, &deviceIDSize, &outputDeviceID); + + if (status == noErr) + { + addr.mElement = kAudioObjectPropertyElementMaster; + addr.mSelector = selector; + addr.mScope = kAudioDevicePropertyScopeOutput; + + if (! AudioHardwareServiceHasProperty (outputDeviceID, &addr)) + outputDeviceID = kAudioObjectUnknown; + } + } + } + + float getGain() const noexcept + { + Float32 gain = 0; + + if (outputDeviceID != kAudioObjectUnknown) + { + UInt32 size = sizeof (gain); + AudioHardwareServiceGetPropertyData (outputDeviceID, &addr, + 0, nullptr, &size, &gain); + } + + return (float) gain; + } + + bool setGain (float gain) const noexcept + { + if (outputDeviceID != kAudioObjectUnknown && canSetVolume()) + { + Float32 newVolume = gain; + UInt32 size = sizeof (newVolume); + + return AudioHardwareServiceSetPropertyData (outputDeviceID, &addr, 0, nullptr, + size, &newVolume) == noErr; + } + + return false; + } + + bool isMuted() const noexcept + { + UInt32 muted = 0; + + if (outputDeviceID != kAudioObjectUnknown) + { + UInt32 size = sizeof (muted); + AudioHardwareServiceGetPropertyData (outputDeviceID, &addr, + 0, nullptr, &size, &muted); + } + + return muted != 0; + } + + bool setMuted (bool mute) const noexcept + { + if (outputDeviceID != kAudioObjectUnknown && canSetVolume()) + { + UInt32 newMute = mute ? 1 : 0; + UInt32 size = sizeof (newMute); + + return AudioHardwareServiceSetPropertyData (outputDeviceID, &addr, 0, nullptr, + size, &newMute) == noErr; + } + + return false; + } + +private: + AudioDeviceID outputDeviceID; + AudioObjectPropertyAddress addr; + + bool canSetVolume() const noexcept + { + Boolean isSettable = NO; + return AudioHardwareServiceIsPropertySettable (outputDeviceID, &addr, &isSettable) == noErr + && isSettable; + } +}; + +#ifdef __clang__ + #pragma clang diagnostic pop +#endif + +#define JUCE_SYSTEMAUDIOVOL_IMPLEMENTED 1 +float JUCE_CALLTYPE SystemAudioVolume::getGain() { return SystemVol (kAudioHardwareServiceDeviceProperty_VirtualMasterVolume).getGain(); } +bool JUCE_CALLTYPE SystemAudioVolume::setGain (float gain) { return SystemVol (kAudioHardwareServiceDeviceProperty_VirtualMasterVolume).setGain (gain); } +bool JUCE_CALLTYPE SystemAudioVolume::isMuted() { return SystemVol (kAudioDevicePropertyMute).isMuted(); } +bool JUCE_CALLTYPE SystemAudioVolume::setMuted (bool mute) { return SystemVol (kAudioDevicePropertyMute).setMuted (mute); } + +//============================================================================== +struct CoreAudioClasses +{ + +class CoreAudioIODeviceType; +class CoreAudioIODevice; + +//============================================================================== +class CoreAudioInternal : private Timer +{ +public: + CoreAudioInternal (CoreAudioIODevice& d, AudioDeviceID id, bool input, bool output) + : owner (d), + deviceID (id), + isInputDevice (input), + isOutputDevice (output) + { + jassert (deviceID != 0); + + updateDetailsFromDevice(); + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioObjectPropertySelectorWildcard; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementWildcard; + + AudioObjectAddPropertyListener (deviceID, &pa, deviceListenerProc, this); + } + + ~CoreAudioInternal() + { + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioObjectPropertySelectorWildcard; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementWildcard; + + AudioObjectRemovePropertyListener (deviceID, &pa, deviceListenerProc, this); + + stop (false); + } + + void allocateTempBuffers() + { + const int tempBufSize = bufferSize + 4; + audioBuffer.calloc ((size_t) ((numInputChans + numOutputChans) * tempBufSize)); + + tempInputBuffers.calloc ((size_t) numInputChans + 2); + tempOutputBuffers.calloc ((size_t) numOutputChans + 2); + + int count = 0; + for (int i = 0; i < numInputChans; ++i) tempInputBuffers[i] = audioBuffer + count++ * tempBufSize; + for (int i = 0; i < numOutputChans; ++i) tempOutputBuffers[i] = audioBuffer + count++ * tempBufSize; + } + + struct CallbackDetailsForChannel + { + int streamNum; + int dataOffsetSamples; + int dataStrideSamples; + }; + + // returns the number of actual available channels + StringArray getChannelInfo (const bool input, Array& newChannelInfo) const + { + StringArray newNames; + int chanNum = 0; + UInt32 size; + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyStreamConfiguration; + pa.mScope = input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + pa.mElement = kAudioObjectPropertyElementMaster; + + if (OK (AudioObjectGetPropertyDataSize (deviceID, &pa, 0, nullptr, &size))) + { + HeapBlock bufList; + bufList.calloc (size, 1); + + if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, bufList))) + { + const int numStreams = (int) bufList->mNumberBuffers; + + for (int i = 0; i < numStreams; ++i) + { + auto& b = bufList->mBuffers[i]; + + for (unsigned int j = 0; j < b.mNumberChannels; ++j) + { + String name; + NSString* nameNSString = nil; + size = sizeof (nameNSString); + + pa.mSelector = kAudioObjectPropertyElementName; + pa.mElement = (AudioObjectPropertyElement) chanNum + 1; + + if (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &nameNSString) == noErr) + { + name = nsStringToJuce (nameNSString); + [nameNSString release]; + } + + if ((input ? activeInputChans : activeOutputChans) [chanNum]) + { + CallbackDetailsForChannel info = { i, (int) j, (int) b.mNumberChannels }; + newChannelInfo.add (info); + } + + if (name.isEmpty()) + name << (input ? "Input " : "Output ") << (chanNum + 1); + + newNames.add (name); + ++chanNum; + } + } + } + } + + return newNames; + } + + Array getSampleRatesFromDevice() const + { + Array newSampleRates; + + AudioObjectPropertyAddress pa; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + pa.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + UInt32 size = 0; + + if (OK (AudioObjectGetPropertyDataSize (deviceID, &pa, 0, nullptr, &size))) + { + HeapBlock ranges; + ranges.calloc (size, 1); + + if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, ranges))) + { + static const double possibleRates[] = { 44100.0, 48000.0, 88200.0, 96000.0, 176400.0, 192000.0, 384000.0 }; + + for (int i = 0; i < numElementsInArray (possibleRates); ++i) + { + for (int j = size / (int) sizeof (AudioValueRange); --j >= 0;) + { + if (possibleRates[i] >= ranges[j].mMinimum - 2 && possibleRates[i] <= ranges[j].mMaximum + 2) + { + newSampleRates.add (possibleRates[i]); + break; + } + } + } + } + } + + if (newSampleRates.size() == 0 && sampleRate > 0) + newSampleRates.add (sampleRate); + + return newSampleRates; + } + + Array getBufferSizesFromDevice() const + { + Array newBufferSizes; + + AudioObjectPropertyAddress pa; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + pa.mSelector = kAudioDevicePropertyBufferFrameSizeRange; + UInt32 size = 0; + + if (OK (AudioObjectGetPropertyDataSize (deviceID, &pa, 0, nullptr, &size))) + { + HeapBlock ranges; + ranges.calloc (size, 1); + + if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, ranges))) + { + newBufferSizes.add ((int) (ranges[0].mMinimum + 15) & ~15); + + for (int i = 32; i <= 2048; i += 32) + { + for (int j = size / (int) sizeof (AudioValueRange); --j >= 0;) + { + if (i >= ranges[j].mMinimum && i <= ranges[j].mMaximum) + { + newBufferSizes.addIfNotAlreadyThere (i); + break; + } + } + } + + if (bufferSize > 0) + newBufferSizes.addIfNotAlreadyThere (bufferSize); + } + } + + if (newBufferSizes.size() == 0 && bufferSize > 0) + newBufferSizes.add (bufferSize); + + return newBufferSizes; + } + + int getLatencyFromDevice (AudioObjectPropertyScope scope) const + { + UInt32 latency = 0; + UInt32 size = sizeof (latency); + AudioObjectPropertyAddress pa; + pa.mElement = kAudioObjectPropertyElementMaster; + pa.mSelector = kAudioDevicePropertyLatency; + pa.mScope = scope; + AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &latency); + + UInt32 safetyOffset = 0; + size = sizeof (safetyOffset); + pa.mSelector = kAudioDevicePropertySafetyOffset; + AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &safetyOffset); + + return (int) (latency + safetyOffset); + } + + int getBitDepthFromDevice (AudioObjectPropertyScope scope) const + { + AudioObjectPropertyAddress pa; + pa.mElement = kAudioObjectPropertyElementMaster; + pa.mSelector = kAudioStreamPropertyPhysicalFormat; + pa.mScope = scope; + + AudioStreamBasicDescription asbd; + UInt32 size = sizeof (asbd); + + if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &asbd))) + return (int) asbd.mBitsPerChannel; + + return 0; + } + + int getFrameSizeFromDevice() const + { + AudioObjectPropertyAddress pa; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + pa.mSelector = kAudioDevicePropertyBufferFrameSize; + + UInt32 framesPerBuf = (UInt32) bufferSize; + UInt32 size = sizeof (framesPerBuf); + AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &framesPerBuf); + return (int) framesPerBuf; + } + + bool isDeviceAlive() const + { + AudioObjectPropertyAddress pa; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + pa.mSelector = kAudioDevicePropertyDeviceIsAlive; + + UInt32 isAlive = 0; + UInt32 size = sizeof (isAlive); + return deviceID != 0 + && OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &isAlive)) + && isAlive != 0; + } + + bool updateDetailsFromDevice() + { + stopTimer(); + + if (! isDeviceAlive()) + return false; + + // this collects all the new details from the device without any locking, then + // locks + swaps them afterwards. + + const double newSampleRate = getNominalSampleRate(); + const int newBufferSize = getFrameSizeFromDevice(); + + Array newBufferSizes = getBufferSizesFromDevice(); + Array newSampleRates = getSampleRatesFromDevice(); + + const int newInputLatency = getLatencyFromDevice (kAudioDevicePropertyScopeInput); + const int newOutputLatency = getLatencyFromDevice (kAudioDevicePropertyScopeOutput); + + Array newInChans, newOutChans; + auto newInNames = isInputDevice ? getChannelInfo (true, newInChans) : StringArray(); + auto newOutNames = isOutputDevice ? getChannelInfo (false, newOutChans) : StringArray(); + + const int newBitDepth = jmax (getBitDepthFromDevice (kAudioDevicePropertyScopeInput), + getBitDepthFromDevice (kAudioDevicePropertyScopeOutput)); + + { + const ScopedLock sl (callbackLock); + + bitDepth = newBitDepth > 0 ? newBitDepth : 32; + + if (newSampleRate > 0) + sampleRate = newSampleRate; + + inputLatency = newInputLatency; + outputLatency = newOutputLatency; + bufferSize = newBufferSize; + + sampleRates.swapWith (newSampleRates); + bufferSizes.swapWith (newBufferSizes); + + inChanNames.swapWith (newInNames); + outChanNames.swapWith (newOutNames); + + inputChannelInfo.swapWith (newInChans); + outputChannelInfo.swapWith (newOutChans); + + allocateTempBuffers(); + } + + return true; + } + + //============================================================================== + StringArray getSources (bool input) + { + StringArray s; + HeapBlock types; + const int num = getAllDataSourcesForDevice (deviceID, types); + + for (int i = 0; i < num; ++i) + { + AudioValueTranslation avt; + char buffer[256]; + + avt.mInputData = &(types[i]); + avt.mInputDataSize = sizeof (UInt32); + avt.mOutputData = buffer; + avt.mOutputDataSize = 256; + + UInt32 transSize = sizeof (avt); + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyDataSourceNameForID; + pa.mScope = input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + pa.mElement = kAudioObjectPropertyElementMaster; + + if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &transSize, &avt))) + s.add (buffer); + } + + return s; + } + + int getCurrentSourceIndex (bool input) const + { + OSType currentSourceID = 0; + UInt32 size = sizeof (currentSourceID); + int result = -1; + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyDataSource; + pa.mScope = input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + pa.mElement = kAudioObjectPropertyElementMaster; + + if (deviceID != 0) + { + if (OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, ¤tSourceID))) + { + HeapBlock types; + const int num = getAllDataSourcesForDevice (deviceID, types); + + for (int i = 0; i < num; ++i) + { + if (types[num] == currentSourceID) + { + result = i; + break; + } + } + } + } + + return result; + } + + void setCurrentSourceIndex (int index, bool input) + { + if (deviceID != 0) + { + HeapBlock types; + const int num = getAllDataSourcesForDevice (deviceID, types); + + if (isPositiveAndBelow (index, num)) + { + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyDataSource; + pa.mScope = input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + pa.mElement = kAudioObjectPropertyElementMaster; + + OSType typeId = types[index]; + + OK (AudioObjectSetPropertyData (deviceID, &pa, 0, 0, sizeof (typeId), &typeId)); + } + } + } + + double getNominalSampleRate() const + { + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyNominalSampleRate; + pa.mScope = kAudioObjectPropertyScopeGlobal; + pa.mElement = kAudioObjectPropertyElementMaster; + Float64 sr = 0; + UInt32 size = (UInt32) sizeof (sr); + return OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &sr)) ? (double) sr : 0.0; + } + + bool setNominalSampleRate (double newSampleRate) const + { + if (std::abs (getNominalSampleRate() - newSampleRate) < 1.0) + return true; + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyNominalSampleRate; + pa.mScope = kAudioObjectPropertyScopeGlobal; + pa.mElement = kAudioObjectPropertyElementMaster; + Float64 sr = newSampleRate; + return OK (AudioObjectSetPropertyData (deviceID, &pa, 0, 0, sizeof (sr), &sr)); + } + + //============================================================================== + String reopen (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double newSampleRate, int bufferSizeSamples) + { + String error; + callbacksAllowed = false; + stopTimer(); + + stop (false); + + updateDetailsFromDevice(); + + activeInputChans = inputChannels; + activeInputChans.setRange (inChanNames.size(), + activeInputChans.getHighestBit() + 1 - inChanNames.size(), + false); + + activeOutputChans = outputChannels; + activeOutputChans.setRange (outChanNames.size(), + activeOutputChans.getHighestBit() + 1 - outChanNames.size(), + false); + + numInputChans = activeInputChans.countNumberOfSetBits(); + numOutputChans = activeOutputChans.countNumberOfSetBits(); + + if (! setNominalSampleRate (newSampleRate)) + { + updateDetailsFromDevice(); + error = "Couldn't change sample rate"; + } + else + { + // change buffer size + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyBufferFrameSize; + pa.mScope = kAudioObjectPropertyScopeGlobal; + pa.mElement = kAudioObjectPropertyElementMaster; + UInt32 framesPerBuf = (UInt32) bufferSizeSamples; + + if (! OK (AudioObjectSetPropertyData (deviceID, &pa, 0, 0, sizeof (framesPerBuf), &framesPerBuf))) + { + updateDetailsFromDevice(); + error = "Couldn't change buffer size"; + } + else + { + // Annoyingly, after changing the rate and buffer size, some devices fail to + // correctly report their new settings until some random time in the future, so + // after calling updateDetailsFromDevice, we need to manually bodge these values + // to make sure we're using the correct numbers.. + updateDetailsFromDevice(); + sampleRate = newSampleRate; + bufferSize = bufferSizeSamples; + + if (sampleRates.size() == 0) + error = "Device has no available sample-rates"; + else if (bufferSizes.size() == 0) + error = "Device has no available buffer-sizes"; + } + } + + callbacksAllowed = true; + return error; + } + + bool start() + { + if (! started) + { + callback = nullptr; + + if (deviceID != 0) + { + if (OK (AudioDeviceCreateIOProcID (deviceID, audioIOProc, this, &audioProcID))) + { + if (OK (AudioDeviceStart (deviceID, audioIOProc))) + { + started = true; + } + else + { + OK (AudioDeviceDestroyIOProcID (deviceID, audioProcID)); + audioProcID = 0; + } + } + } + } + + return started; + } + + void setCallback (AudioIODeviceCallback* cb) + { + const ScopedLock sl (callbackLock); + callback = cb; + } + + void stop (bool leaveInterruptRunning) + { + { + const ScopedLock sl (callbackLock); + callback = nullptr; + } + + if (started + && (deviceID != 0) + && ! leaveInterruptRunning) + { + OK (AudioDeviceStop (deviceID, audioIOProc)); + OK (AudioDeviceDestroyIOProcID (deviceID, audioProcID)); + audioProcID = 0; + + started = false; + + { const ScopedLock sl (callbackLock); } + + // wait until it's definitely stopped calling back.. + for (int i = 40; --i >= 0;) + { + Thread::sleep (50); + + UInt32 running = 0; + UInt32 size = sizeof (running); + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyDeviceIsRunning; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + + OK (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, &running)); + + if (running == 0) + break; + } + + const ScopedLock sl (callbackLock); + } + } + + double getSampleRate() const { return sampleRate; } + int getBufferSize() const { return bufferSize; } + + void audioCallback (const AudioBufferList* inInputData, + AudioBufferList* outOutputData) + { + const ScopedLock sl (callbackLock); + + if (callback != nullptr) + { + for (int i = numInputChans; --i >= 0;) + { + const CallbackDetailsForChannel& info = inputChannelInfo.getReference(i); + float* dest = tempInputBuffers [i]; + const float* src = ((const float*) inInputData->mBuffers[info.streamNum].mData) + + info.dataOffsetSamples; + const int stride = info.dataStrideSamples; + + if (stride != 0) // if this is zero, info is invalid + { + for (int j = bufferSize; --j >= 0;) + { + *dest++ = *src; + src += stride; + } + } + } + + callback->audioDeviceIOCallback (const_cast (tempInputBuffers.get()), + numInputChans, + tempOutputBuffers, + numOutputChans, + bufferSize); + + for (int i = numOutputChans; --i >= 0;) + { + const CallbackDetailsForChannel& info = outputChannelInfo.getReference(i); + const float* src = tempOutputBuffers [i]; + float* dest = ((float*) outOutputData->mBuffers[info.streamNum].mData) + + info.dataOffsetSamples; + const int stride = info.dataStrideSamples; + + if (stride != 0) // if this is zero, info is invalid + { + for (int j = bufferSize; --j >= 0;) + { + *dest = *src++; + dest += stride; + } + } + } + } + else + { + for (UInt32 i = 0; i < outOutputData->mNumberBuffers; ++i) + zeromem (outOutputData->mBuffers[i].mData, + outOutputData->mBuffers[i].mDataByteSize); + } + } + + // called by callbacks + void deviceDetailsChanged() + { + if (callbacksAllowed) + startTimer (100); + } + + void timerCallback() override + { + JUCE_COREAUDIOLOG ("Device changed"); + + stopTimer(); + const double oldSampleRate = sampleRate; + const int oldBufferSize = bufferSize; + + if (! updateDetailsFromDevice()) + owner.stop(); + else if (oldBufferSize != bufferSize || oldSampleRate != sampleRate) + owner.restart(); + } + + //============================================================================== + CoreAudioIODevice& owner; + int inputLatency = 0; + int outputLatency = 0; + int bitDepth = 32; + int xruns = 0; + BigInteger activeInputChans, activeOutputChans; + StringArray inChanNames, outChanNames; + Array sampleRates; + Array bufferSizes; + AudioIODeviceCallback* callback = nullptr; + AudioDeviceIOProcID audioProcID = 0; + +private: + CriticalSection callbackLock; + AudioDeviceID deviceID; + bool started = false; + double sampleRate = 0; + int bufferSize = 512; + HeapBlock audioBuffer; + int numInputChans = 0; + int numOutputChans = 0; + bool callbacksAllowed = true; + const bool isInputDevice, isOutputDevice; + + Array inputChannelInfo, outputChannelInfo; + HeapBlock tempInputBuffers, tempOutputBuffers; + + //============================================================================== + static OSStatus audioIOProc (AudioDeviceID /*inDevice*/, + const AudioTimeStamp* /*inNow*/, + const AudioBufferList* inInputData, + const AudioTimeStamp* /*inInputTime*/, + AudioBufferList* outOutputData, + const AudioTimeStamp* /*inOutputTime*/, + void* device) + { + static_cast (device)->audioCallback (inInputData, outOutputData); + return noErr; + } + + static OSStatus deviceListenerProc (AudioDeviceID /*inDevice*/, UInt32 /*inLine*/, const AudioObjectPropertyAddress* pa, void* inClientData) + { + CoreAudioInternal* const intern = static_cast (inClientData); + + switch (pa->mSelector) + { + case kAudioDeviceProcessorOverload: + intern->xruns++; + break; + case kAudioDevicePropertyBufferSize: + case kAudioDevicePropertyBufferFrameSize: + case kAudioDevicePropertyNominalSampleRate: + case kAudioDevicePropertyStreamFormat: + case kAudioDevicePropertyDeviceIsAlive: + case kAudioStreamPropertyPhysicalFormat: + intern->deviceDetailsChanged(); + break; + + case kAudioObjectPropertyOwnedObjects: + intern->stop (false); + intern->owner.deviceType.triggerAsyncAudioDeviceListChange(); + break; + + case kAudioDevicePropertyBufferSizeRange: + case kAudioDevicePropertyVolumeScalar: + case kAudioDevicePropertyMute: + case kAudioDevicePropertyPlayThru: + case kAudioDevicePropertyDataSource: + case kAudioDevicePropertyDeviceIsRunning: + break; + } + + return noErr; + } + + //============================================================================== + static int getAllDataSourcesForDevice (AudioDeviceID deviceID, HeapBlock& types) + { + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyDataSources; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + UInt32 size = 0; + + if (deviceID != 0 + && AudioObjectGetPropertyDataSize (deviceID, &pa, 0, nullptr, &size) == noErr) + { + types.calloc (size, 1); + + if (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, types) == noErr) + return size / (int) sizeof (OSType); + } + + return 0; + } + + bool OK (const OSStatus errorCode) const + { + if (errorCode == noErr) + return true; + + const String errorMessage ("CoreAudio error: " + String::toHexString ((int) errorCode)); + JUCE_COREAUDIOLOG (errorMessage); + + if (callback != nullptr) + callback->audioDeviceError (errorMessage); + + return false; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioInternal) +}; + + +//============================================================================== +class CoreAudioIODevice : public AudioIODevice +{ +public: + CoreAudioIODevice (CoreAudioIODeviceType& dt, + const String& deviceName, + AudioDeviceID inputDeviceId, const int inputIndex_, + AudioDeviceID outputDeviceId, const int outputIndex_) + : AudioIODevice (deviceName, "CoreAudio"), + deviceType (dt), + inputIndex (inputIndex_), + outputIndex (outputIndex_), + isOpen_ (false), + isStarted (false) + { + CoreAudioInternal* device = nullptr; + if (outputDeviceId == 0 || outputDeviceId == inputDeviceId) + { + jassert (inputDeviceId != 0); + device = new CoreAudioInternal (*this, inputDeviceId, true, outputDeviceId != 0); + } + else + { + device = new CoreAudioInternal (*this, outputDeviceId, false, true); + } + jassert (device != nullptr); + + internal = device; + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioObjectPropertySelectorWildcard; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementWildcard; + + AudioObjectAddPropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, internal); + } + + ~CoreAudioIODevice() + { + close(); + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioObjectPropertySelectorWildcard; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementWildcard; + + AudioObjectRemovePropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, internal); + } + + StringArray getOutputChannelNames() override { return internal->outChanNames; } + StringArray getInputChannelNames() override { return internal->inChanNames; } + + bool isOpen() override { return isOpen_; } + + Array getAvailableSampleRates() override { return internal->sampleRates; } + Array getAvailableBufferSizes() override { return internal->bufferSizes; } + + double getCurrentSampleRate() override { return internal->getSampleRate(); } + int getCurrentBitDepth() override { return internal->bitDepth; } + int getCurrentBufferSizeSamples() override { return internal->getBufferSize(); } + int getXRunCount() const noexcept override { return internal->xruns; } + + int getDefaultBufferSize() override + { + int best = 0; + + for (int i = 0; best < 512 && i < internal->bufferSizes.size(); ++i) + best = internal->bufferSizes.getUnchecked(i); + + if (best == 0) + best = 512; + + return best; + } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate, int bufferSizeSamples) override + { + isOpen_ = true; + + internal->xruns = 0; + if (bufferSizeSamples <= 0) + bufferSizeSamples = getDefaultBufferSize(); + + lastError = internal->reopen (inputChannels, outputChannels, sampleRate, bufferSizeSamples); + + JUCE_COREAUDIOLOG ("Opened: " << getName()); + JUCE_COREAUDIOLOG ("Latencies: " << getInputLatencyInSamples() << ' ' << getOutputLatencyInSamples()); + + isOpen_ = lastError.isEmpty(); + return lastError; + } + + void close() override + { + isOpen_ = false; + internal->stop (false); + } + + BigInteger getActiveOutputChannels() const override { return internal->activeOutputChans; } + BigInteger getActiveInputChannels() const override { return internal->activeInputChans; } + + int getOutputLatencyInSamples() override + { + // this seems like a good guess at getting the latency right - comparing + // this with a round-trip measurement, it gets it to within a few millisecs + // for the built-in mac soundcard + return internal->outputLatency; + } + + int getInputLatencyInSamples() override + { + return internal->inputLatency; + } + + void start (AudioIODeviceCallback* callback) override + { + if (! isStarted) + { + if (callback != nullptr) + callback->audioDeviceAboutToStart (this); + + isStarted = internal->start(); + + if (isStarted) + internal->setCallback (callback); + } + } + + void stop() override + { + if (isStarted) + { + AudioIODeviceCallback* const lastCallback = internal->callback; + + isStarted = false; + internal->stop (true); + + if (lastCallback != nullptr) + lastCallback->audioDeviceStopped(); + } + } + + bool isPlaying() override + { + if (internal->callback == nullptr) + isStarted = false; + + return isStarted; + } + + String getLastError() override + { + return lastError; + } + + void audioDeviceListChanged() + { + deviceType.audioDeviceListChanged(); + } + + void restart() + { + JUCE_COREAUDIOLOG ("Restarting"); + AudioIODeviceCallback* oldCallback = internal->callback; + stop(); + start (oldCallback); + } + + bool setCurrentSampleRate (double newSampleRate) + { + return internal->setNominalSampleRate (newSampleRate); + } + + CoreAudioIODeviceType& deviceType; + int inputIndex, outputIndex; + +private: + ScopedPointer internal; + bool isOpen_, isStarted; + String lastError; + + static OSStatus hardwareListenerProc (AudioDeviceID /*inDevice*/, UInt32 /*inLine*/, const AudioObjectPropertyAddress* pa, void* inClientData) + { + switch (pa->mSelector) + { + case kAudioHardwarePropertyDevices: + static_cast (inClientData)->deviceDetailsChanged(); + break; + + case kAudioHardwarePropertyDefaultOutputDevice: + case kAudioHardwarePropertyDefaultInputDevice: + case kAudioHardwarePropertyDefaultSystemOutputDevice: + break; + } + + return noErr; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioIODevice) +}; + +//============================================================================== +class AudioIODeviceCombiner : public AudioIODevice, + private Thread +{ +public: + AudioIODeviceCombiner (const String& deviceName, CoreAudioIODeviceType& deviceType) + : AudioIODevice (deviceName, "CoreAudio"), + Thread (deviceName), + owner (deviceType) + { + } + + ~AudioIODeviceCombiner() + { + close(); + devices.clear(); + } + + void addDevice (CoreAudioIODevice* device, bool useInputs, bool useOutputs) + { + jassert (device != nullptr); + jassert (! isOpen()); + jassert (! device->isOpen()); + devices.add (new DeviceWrapper (*this, device, useInputs, useOutputs)); + + if (currentSampleRate == 0) + currentSampleRate = device->getCurrentSampleRate(); + + if (currentBufferSize == 0) + currentBufferSize = device->getCurrentBufferSizeSamples(); + } + + Array getDevices() const + { + Array devs; + + for (int i = 0; i < devices.size(); ++i) + devs.add (devices.getUnchecked(i)->device); + + return devs; + } + + StringArray getOutputChannelNames() override + { + StringArray names; + + for (int i = 0; i < devices.size(); ++i) + names.addArray (devices.getUnchecked(i)->getOutputChannelNames()); + + names.appendNumbersToDuplicates (false, true); + return names; + } + + StringArray getInputChannelNames() override + { + StringArray names; + + for (int i = 0; i < devices.size(); ++i) + names.addArray (devices.getUnchecked(i)->getInputChannelNames()); + + names.appendNumbersToDuplicates (false, true); + return names; + } + + Array getAvailableSampleRates() override + { + Array commonRates; + + for (int i = 0; i < devices.size(); ++i) + { + Array rates (devices.getUnchecked(i)->device->getAvailableSampleRates()); + + if (i == 0) + commonRates = rates; + else + commonRates.removeValuesNotIn (rates); + } + + return commonRates; + } + + Array getAvailableBufferSizes() override + { + Array commonSizes; + + for (int i = 0; i < devices.size(); ++i) + { + Array sizes (devices.getUnchecked(i)->device->getAvailableBufferSizes()); + + if (i == 0) + commonSizes = sizes; + else + commonSizes.removeValuesNotIn (sizes); + } + + return commonSizes; + } + + bool isOpen() override { return active; } + bool isPlaying() override { return callback != nullptr; } + double getCurrentSampleRate() override { return currentSampleRate; } + int getCurrentBufferSizeSamples() override { return currentBufferSize; } + + int getCurrentBitDepth() override + { + int depth = 32; + + for (int i = 0; i < devices.size(); ++i) + depth = jmin (depth, devices.getUnchecked(i)->device->getCurrentBitDepth()); + + return depth; + } + + int getDefaultBufferSize() override + { + int size = 0; + + for (int i = 0; i < devices.size(); ++i) + size = jmax (size, devices.getUnchecked(i)->device->getDefaultBufferSize()); + + return size; + } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate, int bufferSize) override + { + close(); + active = true; + + if (bufferSize <= 0) + bufferSize = getDefaultBufferSize(); + + if (sampleRate <= 0) + { + Array rates (getAvailableSampleRates()); + + for (int i = 0; i < rates.size() && sampleRate < 44100.0; ++i) + sampleRate = rates.getUnchecked(i); + } + + currentSampleRate = sampleRate; + currentBufferSize = bufferSize; + + const int fifoSize = bufferSize * 3 + 1; + int totalInputChanIndex = 0, totalOutputChanIndex = 0; + int chanIndex = 0; + + for (int i = 0; i < devices.size(); ++i) + { + DeviceWrapper& d = *devices.getUnchecked(i); + + BigInteger ins (inputChannels >> totalInputChanIndex); + BigInteger outs (outputChannels >> totalOutputChanIndex); + + int numIns = d.getInputChannelNames().size(); + int numOuts = d.getOutputChannelNames().size(); + + totalInputChanIndex += numIns; + totalOutputChanIndex += numOuts; + + String err = d.open (ins, outs, sampleRate, bufferSize, + chanIndex, fifoSize); + + if (err.isNotEmpty()) + { + close(); + lastError = err; + return err; + } + + chanIndex += d.numInputChans + d.numOutputChans; + } + + fifos.setSize (chanIndex, fifoSize); + fifos.clear(); + startThread (9); + + return {}; + } + + void close() override + { + stop(); + stopThread (10000); + fifos.clear(); + active = false; + + for (int i = 0; i < devices.size(); ++i) + devices.getUnchecked(i)->close(); + } + + BigInteger getActiveOutputChannels() const override + { + BigInteger chans; + int start = 0; + + for (int i = 0; i < devices.size(); ++i) + { + const int numChans = devices.getUnchecked(i)->getOutputChannelNames().size(); + + if (numChans > 0) + { + chans |= (devices.getUnchecked(i)->device->getActiveOutputChannels() << start); + start += numChans; + } + } + + return chans; + } + + BigInteger getActiveInputChannels() const override + { + BigInteger chans; + int start = 0; + + for (int i = 0; i < devices.size(); ++i) + { + const int numChans = devices.getUnchecked(i)->getInputChannelNames().size(); + + if (numChans > 0) + { + chans |= (devices.getUnchecked(i)->device->getActiveInputChannels() << start); + start += numChans; + } + } + + return chans; + } + + int getOutputLatencyInSamples() override + { + int lat = 0; + + for (int i = 0; i < devices.size(); ++i) + lat = jmax (lat, devices.getUnchecked(i)->device->getOutputLatencyInSamples()); + + return lat + currentBufferSize * 2; + } + + int getInputLatencyInSamples() override + { + int lat = 0; + + for (int i = 0; i < devices.size(); ++i) + lat = jmax (lat, devices.getUnchecked(i)->device->getInputLatencyInSamples()); + + return lat + currentBufferSize * 2; + } + + void start (AudioIODeviceCallback* newCallback) override + { + if (callback != newCallback) + { + stop(); + fifos.clear(); + + for (int i = 0; i < devices.size(); ++i) + devices.getUnchecked(i)->start(); + + if (newCallback != nullptr) + newCallback->audioDeviceAboutToStart (this); + + const ScopedLock sl (callbackLock); + callback = newCallback; + } + } + + void stop() override { shutdown ({}); } + + String getLastError() override + { + return lastError; + } + +private: + CoreAudioIODeviceType& owner; + CriticalSection callbackLock; + AudioIODeviceCallback* callback = nullptr; + double currentSampleRate = 0; + int currentBufferSize = 0; + bool active = false; + String lastError; + + AudioSampleBuffer fifos; + + void run() override + { + const int numSamples = currentBufferSize; + + AudioSampleBuffer buffer (fifos.getNumChannels(), numSamples); + buffer.clear(); + + Array inputChans; + Array outputChans; + + for (int i = 0; i < devices.size(); ++i) + { + DeviceWrapper& d = *devices.getUnchecked(i); + + for (int j = 0; j < d.numInputChans; ++j) inputChans.add (buffer.getReadPointer (d.inputIndex + j)); + for (int j = 0; j < d.numOutputChans; ++j) outputChans.add (buffer.getWritePointer (d.outputIndex + j)); + } + + const int numInputChans = inputChans.size(); + const int numOutputChans = outputChans.size(); + + inputChans.add (nullptr); + outputChans.add (nullptr); + + const int blockSizeMs = jmax (1, (int) (1000 * numSamples / currentSampleRate)); + + jassert (numInputChans + numOutputChans == buffer.getNumChannels()); + + while (! threadShouldExit()) + { + readInput (buffer, numSamples, blockSizeMs); + + bool didCallback = true; + + { + const ScopedLock sl (callbackLock); + + if (callback != nullptr) + callback->audioDeviceIOCallback ((const float**) inputChans.getRawDataPointer(), numInputChans, + outputChans.getRawDataPointer(), numOutputChans, numSamples); + else + didCallback = false; + } + + if (didCallback) + { + pushOutputData (buffer, numSamples, blockSizeMs); + } + else + { + for (int i = 0; i < numOutputChans; ++i) + FloatVectorOperations::clear (outputChans[i], numSamples); + + reset(); + } + } + } + + void shutdown (const String& error) + { + AudioIODeviceCallback* lastCallback = nullptr; + + { + const ScopedLock sl (callbackLock); + std::swap (callback, lastCallback); + } + + for (int i = 0; i < devices.size(); ++i) + devices.getUnchecked(i)->device->stop(); + + if (lastCallback != nullptr) + { + if (error.isNotEmpty()) + lastCallback->audioDeviceError (error); + else + lastCallback->audioDeviceStopped(); + } + } + + void reset() + { + for (int i = 0; i < devices.size(); ++i) + devices.getUnchecked(i)->reset(); + } + + void underrun() + { + } + + void readInput (AudioSampleBuffer& buffer, const int numSamples, const int blockSizeMs) + { + for (int i = 0; i < devices.size(); ++i) + { + DeviceWrapper& d = *devices.getUnchecked(i); + d.done = (d.numInputChans == 0); + } + + for (int tries = 5;;) + { + bool anyRemaining = false; + + for (int i = 0; i < devices.size(); ++i) + { + DeviceWrapper& d = *devices.getUnchecked(i); + + if (! d.done) + { + if (d.isInputReady (numSamples)) + { + d.readInput (buffer, numSamples); + d.done = true; + } + else + anyRemaining = true; + } + } + + if (! anyRemaining) + return; + + if (--tries == 0) + break; + + wait (blockSizeMs); + } + + for (int j = 0; j < devices.size(); ++j) + { + DeviceWrapper& d = *devices.getUnchecked(j); + + if (! d.done) + for (int i = 0; i < d.numInputChans; ++i) + buffer.clear (d.inputIndex + i, 0, numSamples); + } + } + + void pushOutputData (AudioSampleBuffer& buffer, const int numSamples, const int blockSizeMs) + { + for (int i = 0; i < devices.size(); ++i) + { + DeviceWrapper& d = *devices.getUnchecked(i); + d.done = (d.numOutputChans == 0); + } + + for (int tries = 5;;) + { + bool anyRemaining = false; + + for (int i = 0; i < devices.size(); ++i) + { + DeviceWrapper& d = *devices.getUnchecked(i); + + if (! d.done) + { + if (d.isOutputReady (numSamples)) + { + d.pushOutputData (buffer, numSamples); + d.done = true; + } + else + anyRemaining = true; + } + } + + if ((! anyRemaining) || --tries == 0) + return; + + wait (blockSizeMs); + } + } + + void handleAudioDeviceAboutToStart (AudioIODevice* device) + { + const ScopedLock sl (callbackLock); + + auto newSampleRate = device->getCurrentSampleRate(); + auto commonRates = getAvailableSampleRates(); + if (! commonRates.contains (newSampleRate)) + { + commonRates.sort(); + if (newSampleRate < commonRates.getFirst() || newSampleRate > commonRates.getLast()) + newSampleRate = jlimit (commonRates.getFirst(), commonRates.getLast(), newSampleRate); + else + for (auto it = commonRates.begin(); it < commonRates.end() - 1; ++it) + if (it[0] < newSampleRate && it[1] > newSampleRate) + { + newSampleRate = newSampleRate - it[0] < it[1] - newSampleRate ? it[0] : it[1]; + break; + } + } + currentSampleRate = newSampleRate; + + bool anySampleRateChanges = false; + for (int i = 0; i < devices.size(); ++i) + if (devices.getUnchecked(i)->getCurrentSampleRate() != currentSampleRate) + { + devices.getUnchecked(i)->setCurrentSampleRate (currentSampleRate); + anySampleRateChanges = true; + } + + if (anySampleRateChanges) + owner.audioDeviceListChanged(); + + if (callback != nullptr) + callback->audioDeviceAboutToStart (device); + } + + void handleAudioDeviceStopped() { shutdown ({}); } + void handleAudioDeviceError (const String& errorMessage) { shutdown (errorMessage.isNotEmpty() ? errorMessage : String ("unknown")); } + + //============================================================================== + struct DeviceWrapper : private AudioIODeviceCallback + { + DeviceWrapper (AudioIODeviceCombiner& cd, CoreAudioIODevice* d, bool useIns, bool useOuts) + : owner (cd), device (d), inputIndex (0), outputIndex (0), + useInputs (useIns), useOutputs (useOuts), + inputFifo (32), outputFifo (32), done (false) + { + } + + ~DeviceWrapper() + { + close(); + } + + String open (const BigInteger& inputChannels, const BigInteger& outputChannels, + double sampleRate, int bufferSize, + int channelIndex, + int fifoSize) + { + inputFifo.setTotalSize (fifoSize); + outputFifo.setTotalSize (fifoSize); + inputFifo.reset(); + outputFifo.reset(); + + String err (device->open (useInputs ? inputChannels : BigInteger(), + useOutputs ? outputChannels : BigInteger(), + sampleRate, bufferSize)); + + numInputChans = useInputs ? device->getActiveInputChannels().countNumberOfSetBits() : 0; + numOutputChans = useOutputs ? device->getActiveOutputChannels().countNumberOfSetBits() : 0; + + inputIndex = channelIndex; + outputIndex = channelIndex + numInputChans; + + return err; + } + + void close() + { + device->close(); + } + + void start() + { + reset(); + device->start (this); + } + + void reset() + { + inputFifo.reset(); + outputFifo.reset(); + } + + StringArray getOutputChannelNames() const { return useOutputs ? device->getOutputChannelNames() : StringArray(); } + StringArray getInputChannelNames() const { return useInputs ? device->getInputChannelNames() : StringArray(); } + + bool isInputReady (int numSamples) const noexcept + { + return numInputChans == 0 || inputFifo.getNumReady() >= numSamples; + } + + void readInput (AudioSampleBuffer& destBuffer, int numSamples) + { + if (numInputChans == 0) + return; + + int start1, size1, start2, size2; + inputFifo.prepareToRead (numSamples, start1, size1, start2, size2); + + for (int i = 0; i < numInputChans; ++i) + { + const int index = inputIndex + i; + float* const dest = destBuffer.getWritePointer (index); + const float* const src = owner.fifos.getReadPointer (index); + + if (size1 > 0) FloatVectorOperations::copy (dest, src + start1, size1); + if (size2 > 0) FloatVectorOperations::copy (dest + size1, src + start2, size2); + } + + inputFifo.finishedRead (size1 + size2); + } + + bool isOutputReady (int numSamples) const noexcept + { + return numOutputChans == 0 || outputFifo.getFreeSpace() >= numSamples; + } + + void pushOutputData (AudioSampleBuffer& srcBuffer, int numSamples) + { + if (numOutputChans == 0) + return; + + int start1, size1, start2, size2; + outputFifo.prepareToWrite (numSamples, start1, size1, start2, size2); + + for (int i = 0; i < numOutputChans; ++i) + { + const int index = outputIndex + i; + float* const dest = owner.fifos.getWritePointer (index); + const float* const src = srcBuffer.getReadPointer (index); + + if (size1 > 0) FloatVectorOperations::copy (dest + start1, src, size1); + if (size2 > 0) FloatVectorOperations::copy (dest + start2, src + size1, size2); + } + + outputFifo.finishedWrite (size1 + size2); + } + + void audioDeviceIOCallback (const float** inputChannelData, int numInputChannels, + float** outputChannelData, int numOutputChannels, + int numSamples) override + { + AudioSampleBuffer& buf = owner.fifos; + + if (numInputChannels > 0) + { + int start1, size1, start2, size2; + inputFifo.prepareToWrite (numSamples, start1, size1, start2, size2); + + if (size1 + size2 < numSamples) + { + inputFifo.reset(); + inputFifo.prepareToWrite (numSamples, start1, size1, start2, size2); + } + + for (int i = 0; i < numInputChannels; ++i) + { + float* const dest = buf.getWritePointer (inputIndex + i); + const float* const src = inputChannelData[i]; + + if (size1 > 0) FloatVectorOperations::copy (dest + start1, src, size1); + if (size2 > 0) FloatVectorOperations::copy (dest + start2, src + size1, size2); + } + + inputFifo.finishedWrite (size1 + size2); + + if (numSamples > size1 + size2) + { + for (int i = 0; i < numInputChans; ++i) + buf.clear (inputIndex + i, size1 + size2, numSamples - (size1 + size2)); + + owner.underrun(); + } + } + + if (numOutputChannels > 0) + { + int start1, size1, start2, size2; + outputFifo.prepareToRead (numSamples, start1, size1, start2, size2); + + if (size1 + size2 < numSamples) + { + Thread::sleep (1); + outputFifo.prepareToRead (numSamples, start1, size1, start2, size2); + } + + for (int i = 0; i < numOutputChannels; ++i) + { + float* const dest = outputChannelData[i]; + const float* const src = buf.getReadPointer (outputIndex + i); + + if (size1 > 0) FloatVectorOperations::copy (dest, src + start1, size1); + if (size2 > 0) FloatVectorOperations::copy (dest + size1, src + start2, size2); + } + + outputFifo.finishedRead (size1 + size2); + + if (numSamples > size1 + size2) + { + for (int i = 0; i < numOutputChannels; ++i) + FloatVectorOperations::clear (outputChannelData[i] + (size1 + size2), numSamples - (size1 + size2)); + + owner.underrun(); + } + } + + owner.notify(); + } + + double getCurrentSampleRate() { return device->getCurrentSampleRate(); } + bool setCurrentSampleRate (double newSampleRate) { return device->setCurrentSampleRate (newSampleRate); } + + void audioDeviceAboutToStart (AudioIODevice* d) override { owner.handleAudioDeviceAboutToStart (d); } + void audioDeviceStopped() override { owner.handleAudioDeviceStopped(); } + void audioDeviceError (const String& errorMessage) override { owner.handleAudioDeviceError (errorMessage); } + + AudioIODeviceCombiner& owner; + ScopedPointer device; + int inputIndex, numInputChans, outputIndex, numOutputChans; + bool useInputs, useOutputs; + AbstractFifo inputFifo, outputFifo; + bool done; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DeviceWrapper) + }; + + OwnedArray devices; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioIODeviceCombiner) +}; + + +//============================================================================== +class CoreAudioIODeviceType : public AudioIODeviceType, + private AsyncUpdater +{ +public: + CoreAudioIODeviceType() + : AudioIODeviceType ("CoreAudio"), + hasScanned (false) + { + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioHardwarePropertyDevices; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementWildcard; + + AudioObjectAddPropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, this); + } + + ~CoreAudioIODeviceType() + { + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioHardwarePropertyDevices; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementWildcard; + + AudioObjectRemovePropertyListener (kAudioObjectSystemObject, &pa, hardwareListenerProc, this); + } + + //============================================================================== + void scanForDevices() override + { + hasScanned = true; + + inputDeviceNames.clear(); + outputDeviceNames.clear(); + inputIds.clear(); + outputIds.clear(); + + UInt32 size; + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioHardwarePropertyDevices; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + + if (AudioObjectGetPropertyDataSize (kAudioObjectSystemObject, &pa, 0, nullptr, &size) == noErr) + { + HeapBlock devs; + devs.calloc (size, 1); + + if (AudioObjectGetPropertyData (kAudioObjectSystemObject, &pa, 0, nullptr, &size, devs) == noErr) + { + const int num = size / (int) sizeof (AudioDeviceID); + for (int i = 0; i < num; ++i) + { + char name [1024]; + size = sizeof (name); + pa.mSelector = kAudioDevicePropertyDeviceName; + + if (AudioObjectGetPropertyData (devs[i], &pa, 0, nullptr, &size, name) == noErr) + { + const String nameString (String::fromUTF8 (name, (int) strlen (name))); + const int numIns = getNumChannels (devs[i], true); + const int numOuts = getNumChannels (devs[i], false); + + if (numIns > 0) + { + inputDeviceNames.add (nameString); + inputIds.add (devs[i]); + } + + if (numOuts > 0) + { + outputDeviceNames.add (nameString); + outputIds.add (devs[i]); + } + } + } + } + } + + inputDeviceNames.appendNumbersToDuplicates (false, true); + outputDeviceNames.appendNumbersToDuplicates (false, true); + } + + StringArray getDeviceNames (bool wantInputNames) const override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + return wantInputNames ? inputDeviceNames + : outputDeviceNames; + } + + int getDefaultDeviceIndex (bool forInput) const override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + AudioDeviceID deviceID; + UInt32 size = sizeof (deviceID); + + // if they're asking for any input channels at all, use the default input, so we + // get the built-in mic rather than the built-in output with no inputs.. + + AudioObjectPropertyAddress pa; + pa.mSelector = forInput ? kAudioHardwarePropertyDefaultInputDevice + : kAudioHardwarePropertyDefaultOutputDevice; + pa.mScope = kAudioObjectPropertyScopeWildcard; + pa.mElement = kAudioObjectPropertyElementMaster; + + if (AudioObjectGetPropertyData (kAudioObjectSystemObject, &pa, 0, nullptr, &size, &deviceID) == noErr) + { + if (forInput) + { + for (int i = inputIds.size(); --i >= 0;) + if (inputIds[i] == deviceID) + return i; + } + else + { + for (int i = outputIds.size(); --i >= 0;) + if (outputIds[i] == deviceID) + return i; + } + } + + return 0; + } + + int getIndexOfDevice (AudioIODevice* device, bool asInput) const override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + if (CoreAudioIODevice* const d = dynamic_cast (device)) + return asInput ? d->inputIndex + : d->outputIndex; + + if (AudioIODeviceCombiner* const d = dynamic_cast (device)) + { + const Array devs (d->getDevices()); + + for (int i = 0; i < devs.size(); ++i) + { + const int index = getIndexOfDevice (devs.getUnchecked(i), asInput); + + if (index >= 0) + return index; + } + } + + return -1; + } + + bool hasSeparateInputsAndOutputs() const override { return true; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) override + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + const int inputIndex = inputDeviceNames.indexOf (inputDeviceName); + const int outputIndex = outputDeviceNames.indexOf (outputDeviceName); + + AudioDeviceID inputDeviceID = inputIds [inputIndex]; + AudioDeviceID outputDeviceID = outputIds [outputIndex]; + + if (inputDeviceID == 0 && outputDeviceID == 0) + return nullptr; + + String combinedName (outputDeviceName.isEmpty() ? inputDeviceName : outputDeviceName); + + if (inputDeviceID == outputDeviceID) + return new CoreAudioIODevice (*this, combinedName, inputDeviceID, inputIndex, outputDeviceID, outputIndex); + + ScopedPointer in, out; + + if (inputDeviceID != 0) + in = new CoreAudioIODevice (*this, inputDeviceName, inputDeviceID, inputIndex, 0, -1); + + if (outputDeviceID != 0) + out = new CoreAudioIODevice (*this, outputDeviceName, 0, -1, outputDeviceID, outputIndex); + + if (in == nullptr) return out.release(); + if (out == nullptr) return in.release(); + + ScopedPointer combo (new AudioIODeviceCombiner (combinedName, *this)); + combo->addDevice (in.release(), true, false); + combo->addDevice (out.release(), false, true); + return combo.release(); + } + + void audioDeviceListChanged() + { + scanForDevices(); + callDeviceChangeListeners(); + } + + void triggerAsyncAudioDeviceListChange() + { + triggerAsyncUpdate(); + } + + //============================================================================== +private: + StringArray inputDeviceNames, outputDeviceNames; + Array inputIds, outputIds; + + bool hasScanned; + + static int getNumChannels (AudioDeviceID deviceID, bool input) + { + int total = 0; + UInt32 size; + + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioDevicePropertyStreamConfiguration; + pa.mScope = input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; + pa.mElement = kAudioObjectPropertyElementMaster; + + if (AudioObjectGetPropertyDataSize (deviceID, &pa, 0, nullptr, &size) == noErr) + { + HeapBlock bufList; + bufList.calloc (size, 1); + + if (AudioObjectGetPropertyData (deviceID, &pa, 0, nullptr, &size, bufList) == noErr) + { + const int numStreams = (int) bufList->mNumberBuffers; + + for (int i = 0; i < numStreams; ++i) + { + const ::AudioBuffer& b = bufList->mBuffers[i]; + total += b.mNumberChannels; + } + } + } + + return total; + } + + static OSStatus hardwareListenerProc (AudioDeviceID, UInt32, const AudioObjectPropertyAddress*, void* clientData) + { + static_cast (clientData)->triggerAsyncAudioDeviceListChange(); + return noErr; + } + + void handleAsyncUpdate() override + { + audioDeviceListChanged(); + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreAudioIODeviceType) +}; + +}; + +//============================================================================== +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_CoreAudio() +{ + return new CoreAudioClasses::CoreAudioIODeviceType(); +} + +#undef JUCE_COREAUDIOLOG + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp b/source/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp new file mode 100644 index 000000000..9637792a7 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp @@ -0,0 +1,562 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#ifndef JUCE_LOG_COREMIDI_ERRORS + #define JUCE_LOG_COREMIDI_ERRORS 1 +#endif + +namespace CoreMidiHelpers +{ + static bool checkError (const OSStatus err, const int lineNum) + { + if (err == noErr) + return true; + + #if JUCE_LOG_COREMIDI_ERRORS + Logger::writeToLog ("CoreMIDI error: " + String (lineNum) + " - " + String::toHexString ((int) err)); + #endif + + ignoreUnused (lineNum); + return false; + } + + #undef CHECK_ERROR + #define CHECK_ERROR(a) CoreMidiHelpers::checkError (a, __LINE__) + + //============================================================================== + struct ScopedCFString + { + ScopedCFString() noexcept : cfString (nullptr) {} + ~ScopedCFString() noexcept { if (cfString != nullptr) CFRelease (cfString); } + + CFStringRef cfString; + }; + + static String getMidiObjectName (MIDIObjectRef entity) + { + String result; + CFStringRef str = nullptr; + MIDIObjectGetStringProperty (entity, kMIDIPropertyName, &str); + + if (str != nullptr) + { + result = String::fromCFString (str); + CFRelease (str); + } + + return result; + } + + void enableSimulatorMidiSession() + { + #if TARGET_OS_SIMULATOR + static bool hasEnabledNetworkSession = false; + + if (! hasEnabledNetworkSession) + { + MIDINetworkSession* session = [MIDINetworkSession defaultSession]; + session.enabled = YES; + session.connectionPolicy = MIDINetworkConnectionPolicy_Anyone; + + hasEnabledNetworkSession = true; + } + #endif + } + + static String getEndpointName (MIDIEndpointRef endpoint, bool isExternal) + { + String result (getMidiObjectName (endpoint)); + + MIDIEntityRef entity = 0; // NB: don't attempt to use nullptr for refs - it fails in some types of build. + MIDIEndpointGetEntity (endpoint, &entity); + + if (entity == 0) + return result; // probably virtual + + if (result.isEmpty()) + result = getMidiObjectName (entity); // endpoint name is empty - try the entity + + // now consider the device's name + MIDIDeviceRef device = 0; + MIDIEntityGetDevice (entity, &device); + + if (device != 0) + { + const String deviceName (getMidiObjectName (device)); + + if (deviceName.isNotEmpty()) + { + // if an external device has only one entity, throw away + // the endpoint name and just use the device name + if (isExternal && MIDIDeviceGetNumberOfEntities (device) < 2) + { + result = deviceName; + } + else if (! result.startsWithIgnoreCase (deviceName)) + { + // prepend the device name to the entity name + result = (deviceName + " " + result).trimEnd(); + } + } + } + + return result; + } + + static String getConnectedEndpointName (MIDIEndpointRef endpoint) + { + String result; + + // Does the endpoint have connections? + CFDataRef connections = nullptr; + int numConnections = 0; + + MIDIObjectGetDataProperty (endpoint, kMIDIPropertyConnectionUniqueID, &connections); + + if (connections != nullptr) + { + numConnections = ((int) CFDataGetLength (connections)) / (int) sizeof (MIDIUniqueID); + + if (numConnections > 0) + { + const SInt32* pid = reinterpret_cast (CFDataGetBytePtr (connections)); + + for (int i = 0; i < numConnections; ++i, ++pid) + { + MIDIUniqueID uid = (MIDIUniqueID) ByteOrder::swapIfLittleEndian ((uint32) *pid); + MIDIObjectRef connObject; + MIDIObjectType connObjectType; + OSStatus err = MIDIObjectFindByUniqueID (uid, &connObject, &connObjectType); + + if (err == noErr) + { + String s; + + if (connObjectType == kMIDIObjectType_ExternalSource + || connObjectType == kMIDIObjectType_ExternalDestination) + { + // Connected to an external device's endpoint (10.3 and later). + s = getEndpointName (static_cast (connObject), true); + } + else + { + // Connected to an external device (10.2) (or something else, catch-all) + s = getMidiObjectName (connObject); + } + + if (s.isNotEmpty()) + { + if (result.isNotEmpty()) + result += ", "; + + result += s; + } + } + } + } + + CFRelease (connections); + } + + if (result.isEmpty()) // Here, either the endpoint had no connections, or we failed to obtain names for them. + result = getEndpointName (endpoint, false); + + return result; + } + + static StringArray findDevices (const bool forInput) + { + // It seems that OSX can be a bit picky about the thread that's first used to + // search for devices. It's safest to use the message thread for calling this. + jassert (MessageManager::getInstance()->isThisTheMessageThread()); + + enableSimulatorMidiSession(); + + const ItemCount num = forInput ? MIDIGetNumberOfSources() + : MIDIGetNumberOfDestinations(); + StringArray s; + + for (ItemCount i = 0; i < num; ++i) + { + MIDIEndpointRef dest = forInput ? MIDIGetSource (i) + : MIDIGetDestination (i); + String name; + + if (dest != 0) + name = getConnectedEndpointName (dest); + + if (name.isEmpty()) + name = ""; + + s.add (name); + } + + return s; + } + + static void globalSystemChangeCallback (const MIDINotification*, void*) + { + // TODO.. Should pass-on this notification.. + } + + static String getGlobalMidiClientName() + { + if (JUCEApplicationBase* const app = JUCEApplicationBase::getInstance()) + return app->getApplicationName(); + + return "JUCE"; + } + + static MIDIClientRef getGlobalMidiClient() + { + static MIDIClientRef globalMidiClient = 0; + + if (globalMidiClient == 0) + { + // Since OSX 10.6, the MIDIClientCreate function will only work + // correctly when called from the message thread! + jassert (MessageManager::getInstance()->isThisTheMessageThread()); + + enableSimulatorMidiSession(); + + CoreMidiHelpers::ScopedCFString name; + name.cfString = getGlobalMidiClientName().toCFString(); + CHECK_ERROR (MIDIClientCreate (name.cfString, &globalSystemChangeCallback, nullptr, &globalMidiClient)); + } + + return globalMidiClient; + } + + //============================================================================== + class MidiPortAndEndpoint + { + public: + MidiPortAndEndpoint (MIDIPortRef p, MIDIEndpointRef ep) noexcept + : port (p), endPoint (ep) + { + } + + ~MidiPortAndEndpoint() noexcept + { + if (port != 0) + MIDIPortDispose (port); + + if (port == 0 && endPoint != 0) // if port == nullptr, it means we created the endpoint, so it's safe to delete it + MIDIEndpointDispose (endPoint); + } + + void send (const MIDIPacketList* const packets) noexcept + { + if (port != 0) + MIDISend (port, endPoint, packets); + else + MIDIReceived (endPoint, packets); + } + + MIDIPortRef port; + MIDIEndpointRef endPoint; + }; + + //============================================================================== + class MidiPortAndCallback; + CriticalSection callbackLock; + Array activeCallbacks; + + class MidiPortAndCallback + { + public: + MidiPortAndCallback (MidiInputCallback& cb) + : input (nullptr), active (false), callback (cb), concatenator (2048) + { + } + + ~MidiPortAndCallback() + { + active = false; + + { + const ScopedLock sl (callbackLock); + activeCallbacks.removeFirstMatchingValue (this); + } + + if (portAndEndpoint != 0 && portAndEndpoint->port != 0) + CHECK_ERROR (MIDIPortDisconnectSource (portAndEndpoint->port, portAndEndpoint->endPoint)); + } + + void handlePackets (const MIDIPacketList* const pktlist) + { + const double time = Time::getMillisecondCounterHiRes() * 0.001; + + const ScopedLock sl (callbackLock); + if (activeCallbacks.contains (this) && active) + { + const MIDIPacket* packet = &pktlist->packet[0]; + + for (unsigned int i = 0; i < pktlist->numPackets; ++i) + { + concatenator.pushMidiData (packet->data, (int) packet->length, time, + input, callback); + + packet = MIDIPacketNext (packet); + } + } + } + + MidiInput* input; + ScopedPointer portAndEndpoint; + volatile bool active; + + private: + MidiInputCallback& callback; + MidiDataConcatenator concatenator; + }; + + static void midiInputProc (const MIDIPacketList* pktlist, void* readProcRefCon, void* /*srcConnRefCon*/) + { + static_cast (readProcRefCon)->handlePackets (pktlist); + } +} + +//============================================================================== +StringArray MidiOutput::getDevices() { return CoreMidiHelpers::findDevices (false); } +int MidiOutput::getDefaultDeviceIndex() { return 0; } + +MidiOutput* MidiOutput::openDevice (int index) +{ + MidiOutput* mo = nullptr; + + if (isPositiveAndBelow (index, MIDIGetNumberOfDestinations())) + { + MIDIEndpointRef endPoint = MIDIGetDestination ((ItemCount) index); + + CoreMidiHelpers::ScopedCFString pname; + + if (CHECK_ERROR (MIDIObjectGetStringProperty (endPoint, kMIDIPropertyName, &pname.cfString))) + { + MIDIClientRef client = CoreMidiHelpers::getGlobalMidiClient(); + MIDIPortRef port; + String deviceName = CoreMidiHelpers::getConnectedEndpointName (endPoint); + + if (client != 0 && CHECK_ERROR (MIDIOutputPortCreate (client, pname.cfString, &port))) + { + mo = new MidiOutput (deviceName); + mo->internal = new CoreMidiHelpers::MidiPortAndEndpoint (port, endPoint); + } + } + } + + return mo; +} + +MidiOutput* MidiOutput::createNewDevice (const String& deviceName) +{ + MIDIClientRef client = CoreMidiHelpers::getGlobalMidiClient(); + MIDIEndpointRef endPoint; + + CoreMidiHelpers::ScopedCFString name; + name.cfString = deviceName.toCFString(); + + if (client != 0 && CHECK_ERROR (MIDISourceCreate (client, name.cfString, &endPoint))) + { + MidiOutput* mo = new MidiOutput (deviceName); + mo->internal = new CoreMidiHelpers::MidiPortAndEndpoint (0, endPoint); + return mo; + } + + return nullptr; +} + +MidiOutput::~MidiOutput() +{ + stopBackgroundThread(); + + delete static_cast (internal); +} + +void MidiOutput::sendMessageNow (const MidiMessage& message) +{ + #if JUCE_IOS + const MIDITimeStamp timeStamp = mach_absolute_time(); + #else + const MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + #endif + + HeapBlock allocatedPackets; + MIDIPacketList stackPacket; + MIDIPacketList* packetToSend = &stackPacket; + const size_t dataSize = (size_t) message.getRawDataSize(); + + if (message.isSysEx()) + { + const int maxPacketSize = 256; + int pos = 0, bytesLeft = (int) dataSize; + const int numPackets = (bytesLeft + maxPacketSize - 1) / maxPacketSize; + allocatedPackets.malloc ((size_t) (32 * (size_t) numPackets + dataSize), 1); + packetToSend = allocatedPackets; + packetToSend->numPackets = (UInt32) numPackets; + + MIDIPacket* p = packetToSend->packet; + + for (int i = 0; i < numPackets; ++i) + { + p->timeStamp = timeStamp; + p->length = (UInt16) jmin (maxPacketSize, bytesLeft); + memcpy (p->data, message.getRawData() + pos, p->length); + pos += p->length; + bytesLeft -= p->length; + p = MIDIPacketNext (p); + } + } + else if (dataSize < 65536) // max packet size + { + const size_t stackCapacity = sizeof (stackPacket.packet->data); + + if (dataSize > stackCapacity) + { + allocatedPackets.malloc ((sizeof (MIDIPacketList) - stackCapacity) + dataSize, 1); + packetToSend = allocatedPackets; + } + + packetToSend->numPackets = 1; + MIDIPacket& p = *(packetToSend->packet); + p.timeStamp = timeStamp; + p.length = (UInt16) dataSize; + memcpy (p.data, message.getRawData(), dataSize); + } + else + { + jassertfalse; // packet too large to send! + return; + } + + static_cast (internal)->send (packetToSend); +} + +//============================================================================== +StringArray MidiInput::getDevices() { return CoreMidiHelpers::findDevices (true); } +int MidiInput::getDefaultDeviceIndex() { return 0; } + +MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) +{ + jassert (callback != nullptr); + + using namespace CoreMidiHelpers; + MidiInput* newInput = nullptr; + + if (isPositiveAndBelow (index, MIDIGetNumberOfSources())) + { + if (MIDIEndpointRef endPoint = MIDIGetSource ((ItemCount) index)) + { + ScopedCFString name; + + if (CHECK_ERROR (MIDIObjectGetStringProperty (endPoint, kMIDIPropertyName, &name.cfString))) + { + if (MIDIClientRef client = getGlobalMidiClient()) + { + MIDIPortRef port; + ScopedPointer mpc (new MidiPortAndCallback (*callback)); + + if (CHECK_ERROR (MIDIInputPortCreate (client, name.cfString, midiInputProc, mpc, &port))) + { + if (CHECK_ERROR (MIDIPortConnectSource (port, endPoint, nullptr))) + { + mpc->portAndEndpoint = new MidiPortAndEndpoint (port, endPoint); + + newInput = new MidiInput (getDevices() [index]); + mpc->input = newInput; + newInput->internal = mpc; + + const ScopedLock sl (callbackLock); + activeCallbacks.add (mpc.release()); + } + else + { + CHECK_ERROR (MIDIPortDispose (port)); + } + } + } + } + } + } + + return newInput; +} + +MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback) +{ + jassert (callback != nullptr); + + using namespace CoreMidiHelpers; + MidiInput* mi = nullptr; + + if (MIDIClientRef client = getGlobalMidiClient()) + { + ScopedPointer mpc (new MidiPortAndCallback (*callback)); + mpc->active = false; + + MIDIEndpointRef endPoint; + ScopedCFString name; + name.cfString = deviceName.toCFString(); + + if (CHECK_ERROR (MIDIDestinationCreate (client, name.cfString, midiInputProc, mpc, &endPoint))) + { + mpc->portAndEndpoint = new MidiPortAndEndpoint (0, endPoint); + + mi = new MidiInput (deviceName); + mpc->input = mi; + mi->internal = mpc; + + const ScopedLock sl (callbackLock); + activeCallbacks.add (mpc.release()); + } + } + + return mi; +} + +MidiInput::MidiInput (const String& nm) : name (nm) +{ +} + +MidiInput::~MidiInput() +{ + delete static_cast (internal); +} + +void MidiInput::start() +{ + const ScopedLock sl (CoreMidiHelpers::callbackLock); + static_cast (internal)->active = true; +} + +void MidiInput::stop() +{ + const ScopedLock sl (CoreMidiHelpers::callbackLock); + static_cast (internal)->active = false; +} + +#undef CHECK_ERROR + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_win32_ASIO.cpp b/source/modules/juce_audio_devices/native/juce_win32_ASIO.cpp new file mode 100644 index 000000000..3a0c6409d --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_win32_ASIO.cpp @@ -0,0 +1,1649 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#undef WINDOWS + +/* The ASIO SDK *should* declare its callback functions as being __cdecl, but different versions seem + to be pretty random about whether or not they do this. If you hit an error using these functions + it'll be because you're trying to build using __stdcall, in which case you'd need to either get hold of + an ASIO SDK which correctly specifies __cdecl, or add the __cdecl keyword to its functions yourself. +*/ +#define JUCE_ASIOCALLBACK __cdecl + +//============================================================================== +namespace ASIODebugging +{ + #if JUCE_ASIO_DEBUGGING + #define JUCE_ASIO_LOG(msg) ASIODebugging::logMessage (msg) + #define JUCE_ASIO_LOG_ERROR(msg, errNum) ASIODebugging::logError ((msg), (errNum)) + + static void logMessage (String message) + { + message = "ASIO: " + message; + DBG (message); + Logger::writeToLog (message); + } + + static void logError (const String& context, long error) + { + const char* err = "Unknown error"; + + switch (error) + { + case ASE_OK: return; + case ASE_NotPresent: err = "Not Present"; break; + case ASE_HWMalfunction: err = "Hardware Malfunction"; break; + case ASE_InvalidParameter: err = "Invalid Parameter"; break; + case ASE_InvalidMode: err = "Invalid Mode"; break; + case ASE_SPNotAdvancing: err = "Sample position not advancing"; break; + case ASE_NoClock: err = "No Clock"; break; + case ASE_NoMemory: err = "Out of memory"; break; + default: break; + } + + logMessage ("error: " + context + " - " + err); + } + #else + static void dummyLog() {} + #define JUCE_ASIO_LOG(msg) ASIODebugging::dummyLog() + #define JUCE_ASIO_LOG_ERROR(msg, errNum) ignoreUnused (errNum); ASIODebugging::dummyLog() + #endif +} + +//============================================================================== +struct ASIOSampleFormat +{ + ASIOSampleFormat() noexcept {} + + ASIOSampleFormat (const long type) noexcept + : bitDepth (24), + littleEndian (true), + formatIsFloat (false), + byteStride (4) + { + switch (type) + { + case ASIOSTInt16MSB: byteStride = 2; littleEndian = false; bitDepth = 16; break; + case ASIOSTInt24MSB: byteStride = 3; littleEndian = false; break; + case ASIOSTInt32MSB: bitDepth = 32; littleEndian = false; break; + case ASIOSTFloat32MSB: bitDepth = 32; littleEndian = false; formatIsFloat = true; break; + case ASIOSTFloat64MSB: bitDepth = 64; byteStride = 8; littleEndian = false; break; + case ASIOSTInt32MSB16: bitDepth = 16; littleEndian = false; break; + case ASIOSTInt32MSB18: littleEndian = false; break; + case ASIOSTInt32MSB20: littleEndian = false; break; + case ASIOSTInt32MSB24: littleEndian = false; break; + case ASIOSTInt16LSB: byteStride = 2; bitDepth = 16; break; + case ASIOSTInt24LSB: byteStride = 3; break; + case ASIOSTInt32LSB: bitDepth = 32; break; + case ASIOSTFloat32LSB: bitDepth = 32; formatIsFloat = true; break; + case ASIOSTFloat64LSB: bitDepth = 64; byteStride = 8; break; + case ASIOSTInt32LSB16: bitDepth = 16; break; + case ASIOSTInt32LSB18: break; // (unhandled) + case ASIOSTInt32LSB20: break; // (unhandled) + case ASIOSTInt32LSB24: break; + + case ASIOSTDSDInt8LSB1: break; // (unhandled) + case ASIOSTDSDInt8MSB1: break; // (unhandled) + case ASIOSTDSDInt8NER8: break; // (unhandled) + + default: + jassertfalse; // (not a valid format code..) + break; + } + } + + void convertToFloat (const void* const src, float* const dst, const int samps) const noexcept + { + if (formatIsFloat) + { + memcpy (dst, src, samps * sizeof (float)); + } + else + { + switch (bitDepth) + { + case 16: convertInt16ToFloat (static_cast (src), dst, byteStride, samps, littleEndian); break; + case 24: convertInt24ToFloat (static_cast (src), dst, byteStride, samps, littleEndian); break; + case 32: convertInt32ToFloat (static_cast (src), dst, byteStride, samps, littleEndian); break; + default: jassertfalse; break; + } + } + } + + void convertFromFloat (const float* const src, void* const dst, const int samps) const noexcept + { + if (formatIsFloat) + { + memcpy (dst, src, samps * sizeof (float)); + } + else + { + switch (bitDepth) + { + case 16: convertFloatToInt16 (src, static_cast (dst), byteStride, samps, littleEndian); break; + case 24: convertFloatToInt24 (src, static_cast (dst), byteStride, samps, littleEndian); break; + case 32: convertFloatToInt32 (src, static_cast (dst), byteStride, samps, littleEndian); break; + default: jassertfalse; break; + } + } + } + + void clear (void* dst, const int numSamps) noexcept + { + if (dst != nullptr) + zeromem (dst, numSamps * byteStride); + } + + int bitDepth, byteStride; + bool formatIsFloat, littleEndian; + +private: + static void convertInt16ToFloat (const char* src, float* dest, const int srcStrideBytes, + int numSamples, const bool littleEndian) noexcept + { + const double g = 1.0 / 32768.0; + + if (littleEndian) + { + while (--numSamples >= 0) + { + *dest++ = (float) (g * (short) ByteOrder::littleEndianShort (src)); + src += srcStrideBytes; + } + } + else + { + while (--numSamples >= 0) + { + *dest++ = (float) (g * (short) ByteOrder::bigEndianShort (src)); + src += srcStrideBytes; + } + } + } + + static void convertFloatToInt16 (const float* src, char* dest, const int dstStrideBytes, + int numSamples, const bool littleEndian) noexcept + { + const double maxVal = (double) 0x7fff; + + if (littleEndian) + { + while (--numSamples >= 0) + { + *(uint16*) dest = ByteOrder::swapIfBigEndian ((uint16) (short) roundToInt (jlimit (-maxVal, maxVal, maxVal * *src++))); + dest += dstStrideBytes; + } + } + else + { + while (--numSamples >= 0) + { + *(uint16*) dest = ByteOrder::swapIfLittleEndian ((uint16) (short) roundToInt (jlimit (-maxVal, maxVal, maxVal * *src++))); + dest += dstStrideBytes; + } + } + } + + static void convertInt24ToFloat (const char* src, float* dest, const int srcStrideBytes, + int numSamples, const bool littleEndian) noexcept + { + const double g = 1.0 / 0x7fffff; + + if (littleEndian) + { + while (--numSamples >= 0) + { + *dest++ = (float) (g * ByteOrder::littleEndian24Bit (src)); + src += srcStrideBytes; + } + } + else + { + while (--numSamples >= 0) + { + *dest++ = (float) (g * ByteOrder::bigEndian24Bit (src)); + src += srcStrideBytes; + } + } + } + + static void convertFloatToInt24 (const float* src, char* dest, const int dstStrideBytes, + int numSamples, const bool littleEndian) noexcept + { + const double maxVal = (double) 0x7fffff; + + if (littleEndian) + { + while (--numSamples >= 0) + { + ByteOrder::littleEndian24BitToChars ((uint32) roundToInt (jlimit (-maxVal, maxVal, maxVal * *src++)), dest); + dest += dstStrideBytes; + } + } + else + { + while (--numSamples >= 0) + { + ByteOrder::bigEndian24BitToChars ((uint32) roundToInt (jlimit (-maxVal, maxVal, maxVal * *src++)), dest); + dest += dstStrideBytes; + } + } + } + + static void convertInt32ToFloat (const char* src, float* dest, const int srcStrideBytes, + int numSamples, const bool littleEndian) noexcept + { + const double g = 1.0 / 0x7fffffff; + + if (littleEndian) + { + while (--numSamples >= 0) + { + *dest++ = (float) (g * (int) ByteOrder::littleEndianInt (src)); + src += srcStrideBytes; + } + } + else + { + while (--numSamples >= 0) + { + *dest++ = (float) (g * (int) ByteOrder::bigEndianInt (src)); + src += srcStrideBytes; + } + } + } + + static void convertFloatToInt32 (const float* src, char* dest, const int dstStrideBytes, + int numSamples, const bool littleEndian) noexcept + { + const double maxVal = (double) 0x7fffffff; + + if (littleEndian) + { + while (--numSamples >= 0) + { + *(uint32*) dest = ByteOrder::swapIfBigEndian ((uint32) roundToInt (jlimit (-maxVal, maxVal, maxVal * *src++))); + dest += dstStrideBytes; + } + } + else + { + while (--numSamples >= 0) + { + *(uint32*) dest = ByteOrder::swapIfLittleEndian ((uint32) roundToInt (jlimit (-maxVal, maxVal, maxVal * *src++))); + dest += dstStrideBytes; + } + } + } +}; + +//============================================================================== +class ASIOAudioIODevice; +static ASIOAudioIODevice* volatile currentASIODev[16] = { 0 }; + +extern HWND juce_messageWindowHandle; + +class ASIOAudioIODeviceType; +static void sendASIODeviceChangeToListeners (ASIOAudioIODeviceType*); + +//============================================================================== +class ASIOAudioIODevice : public AudioIODevice, + private Timer +{ +public: + ASIOAudioIODevice (ASIOAudioIODeviceType* ownerType, const String& devName, + const CLSID clsID, const int slotNumber) + : AudioIODevice (devName, "ASIO"), + owner (ownerType), + asioObject (nullptr), + classId (clsID), + inputLatency (0), + outputLatency (0), + minBufferSize (0), maxBufferSize (0), + preferredBufferSize (0), + bufferGranularity (0), + numClockSources (0), + currentBlockSizeSamples (0), + currentBitDepth (16), + currentSampleRate (0), + currentCallback (nullptr), + bufferIndex (0), + numActiveInputChans (0), + numActiveOutputChans (0), + deviceIsOpen (false), + isStarted (false), + buffersCreated (false), + calledback (false), + littleEndian (false), + postOutput (true), + needToReset (false), + insideControlPanelModalLoop (false), + shouldUsePreferredSize (false) + { + ::CoInitialize (nullptr); + + name = devName; + inBuffers.calloc (4); + outBuffers.calloc (4); + + jassert (currentASIODev [slotNumber] == nullptr); + currentASIODev [slotNumber] = this; + + openDevice(); + } + + ~ASIOAudioIODevice() + { + for (int i = 0; i < numElementsInArray (currentASIODev); ++i) + if (currentASIODev[i] == this) + currentASIODev[i] = nullptr; + + close(); + JUCE_ASIO_LOG ("closed"); + removeCurrentDriver(); + } + + void updateSampleRates() + { + // find a list of sample rates.. + Array newRates; + + if (asioObject != nullptr) + { + const int possibleSampleRates[] = { 32000, 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000 }; + + for (int index = 0; index < numElementsInArray (possibleSampleRates); ++index) + if (asioObject->canSampleRate ((double) possibleSampleRates[index]) == 0) + newRates.add ((double) possibleSampleRates[index]); + } + + if (newRates.isEmpty()) + { + double cr = getSampleRate(); + JUCE_ASIO_LOG ("No sample rates supported - current rate: " + String ((int) cr)); + + if (cr > 0) + newRates.add ((int) cr); + } + + if (sampleRates != newRates) + { + sampleRates.swapWith (newRates); + + #if JUCE_ASIO_DEBUGGING + StringArray s; + for (int i = 0; i < sampleRates.size(); ++i) + s.add (String (sampleRates.getUnchecked(i))); + + JUCE_ASIO_LOG ("Rates: " + s.joinIntoString (" ")); + #endif + } + } + + StringArray getOutputChannelNames() override { return outputChannelNames; } + StringArray getInputChannelNames() override { return inputChannelNames; } + + Array getAvailableSampleRates() override { return sampleRates; } + Array getAvailableBufferSizes() override { return bufferSizes; } + int getDefaultBufferSize() override { return preferredBufferSize; } + + int getXRunCount() const noexcept override { return xruns; } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sr, int bufferSizeSamples) override + { + if (isOpen()) + close(); + + jassert (currentCallback == nullptr); + + if (bufferSizeSamples < 8 || bufferSizeSamples > 16384) + shouldUsePreferredSize = true; + + if (asioObject == nullptr) + { + const String openingError (openDevice()); + + if (asioObject == nullptr) + return openingError; + } + + isStarted = false; + bufferIndex = -1; + + long err = asioObject->getChannels (&totalNumInputChans, &totalNumOutputChans); + jassert (err == ASE_OK); + + bufferSizeSamples = readBufferSizes (bufferSizeSamples); + + double sampleRate = sr; + currentSampleRate = sampleRate; + currentBlockSizeSamples = bufferSizeSamples; + currentChansOut.clear(); + currentChansIn.clear(); + + updateSampleRates(); + + if (sampleRate == 0 || (sampleRates.size() > 0 && ! sampleRates.contains (sampleRate))) + sampleRate = sampleRates[0]; + + jassert (sampleRate != 0); + if (sampleRate == 0) + sampleRate = 44100.0; + + updateClockSources(); + currentSampleRate = getSampleRate(); + + error.clear(); + buffersCreated = false; + + setSampleRate (sampleRate); + + // (need to get this again in case a sample rate change affected the channel count) + err = asioObject->getChannels (&totalNumInputChans, &totalNumOutputChans); + jassert (err == ASE_OK); + + if (asioObject->future (kAsioCanReportOverload, nullptr) != ASE_OK) + xruns = -1; + + inBuffers.calloc (totalNumInputChans + 8); + outBuffers.calloc (totalNumOutputChans + 8); + + if (needToReset) + { + JUCE_ASIO_LOG (" Resetting"); + removeCurrentDriver(); + + loadDriver(); + String initError = initDriver(); + + if (initError.isNotEmpty()) + JUCE_ASIO_LOG ("ASIOInit: " + initError); + + needToReset = false; + } + + const int totalBuffers = resetBuffers (inputChannels, outputChannels); + + setCallbackFunctions(); + + JUCE_ASIO_LOG ("disposing buffers"); + err = asioObject->disposeBuffers(); + + JUCE_ASIO_LOG ("creating buffers: " + String (totalBuffers) + ", " + String (currentBlockSizeSamples)); + err = asioObject->createBuffers (bufferInfos, totalBuffers, currentBlockSizeSamples, &callbacks); + + if (err != ASE_OK) + { + currentBlockSizeSamples = preferredBufferSize; + JUCE_ASIO_LOG_ERROR ("create buffers 2", err); + + asioObject->disposeBuffers(); + err = asioObject->createBuffers (bufferInfos, totalBuffers, currentBlockSizeSamples, &callbacks); + } + + if (err == ASE_OK) + { + buffersCreated = true; + + tempBuffer.calloc (totalBuffers * currentBlockSizeSamples + 32); + + int n = 0; + Array types; + currentBitDepth = 16; + + for (int i = 0; i < (int) totalNumInputChans; ++i) + { + if (inputChannels[i]) + { + inBuffers[n] = tempBuffer + (currentBlockSizeSamples * n); + + ASIOChannelInfo channelInfo = { 0 }; + channelInfo.channel = i; + channelInfo.isInput = 1; + asioObject->getChannelInfo (&channelInfo); + + types.addIfNotAlreadyThere (channelInfo.type); + inputFormat[n] = ASIOSampleFormat (channelInfo.type); + + currentBitDepth = jmax (currentBitDepth, inputFormat[n].bitDepth); + ++n; + } + } + + jassert (numActiveInputChans == n); + n = 0; + + for (int i = 0; i < (int) totalNumOutputChans; ++i) + { + if (outputChannels[i]) + { + outBuffers[n] = tempBuffer + (currentBlockSizeSamples * (numActiveInputChans + n)); + + ASIOChannelInfo channelInfo = { 0 }; + channelInfo.channel = i; + channelInfo.isInput = 0; + asioObject->getChannelInfo (&channelInfo); + + types.addIfNotAlreadyThere (channelInfo.type); + outputFormat[n] = ASIOSampleFormat (channelInfo.type); + + currentBitDepth = jmax (currentBitDepth, outputFormat[n].bitDepth); + ++n; + } + } + + jassert (numActiveOutputChans == n); + + for (int i = types.size(); --i >= 0;) + JUCE_ASIO_LOG ("channel format: " + String (types[i])); + + jassert (n <= totalBuffers); + + for (int i = 0; i < numActiveOutputChans; ++i) + { + outputFormat[i].clear (bufferInfos [numActiveInputChans + i].buffers[0], currentBlockSizeSamples); + outputFormat[i].clear (bufferInfos [numActiveInputChans + i].buffers[1], currentBlockSizeSamples); + } + + readLatencies(); + refreshBufferSizes(); + deviceIsOpen = true; + + JUCE_ASIO_LOG ("starting"); + calledback = false; + err = asioObject->start(); + + if (err != 0) + { + deviceIsOpen = false; + JUCE_ASIO_LOG ("stop on failure"); + Thread::sleep (10); + asioObject->stop(); + error = "Can't start device"; + Thread::sleep (10); + } + else + { + int count = 300; + while (--count > 0 && ! calledback) + Thread::sleep (10); + + isStarted = true; + + if (! calledback) + { + error = "Device didn't start correctly"; + JUCE_ASIO_LOG ("no callbacks - stopping.."); + asioObject->stop(); + } + } + } + else + { + error = "Can't create i/o buffers"; + } + + if (error.isNotEmpty()) + { + JUCE_ASIO_LOG_ERROR (error, err); + disposeBuffers(); + + Thread::sleep (20); + isStarted = false; + deviceIsOpen = false; + + const String errorCopy (error); + close(); // (this resets the error string) + error = errorCopy; + } + + needToReset = false; + return error; + } + + void close() override + { + error.clear(); + stopTimer(); + stop(); + + if (asioObject != nullptr && deviceIsOpen) + { + const ScopedLock sl (callbackLock); + + deviceIsOpen = false; + isStarted = false; + needToReset = false; + + JUCE_ASIO_LOG ("stopping"); + + if (asioObject != nullptr) + { + Thread::sleep (20); + asioObject->stop(); + Thread::sleep (10); + disposeBuffers(); + } + + Thread::sleep (10); + } + } + + bool isOpen() override { return deviceIsOpen || insideControlPanelModalLoop; } + bool isPlaying() override { return asioObject != nullptr && currentCallback != nullptr; } + + int getCurrentBufferSizeSamples() override { return currentBlockSizeSamples; } + double getCurrentSampleRate() override { return currentSampleRate; } + int getCurrentBitDepth() override { return currentBitDepth; } + + BigInteger getActiveOutputChannels() const override { return currentChansOut; } + BigInteger getActiveInputChannels() const override { return currentChansIn; } + + int getOutputLatencyInSamples() override { return outputLatency + currentBlockSizeSamples / 4; } + int getInputLatencyInSamples() override { return inputLatency + currentBlockSizeSamples / 4; } + + void start (AudioIODeviceCallback* callback) override + { + if (callback != nullptr) + { + callback->audioDeviceAboutToStart (this); + + const ScopedLock sl (callbackLock); + currentCallback = callback; + } + } + + void stop() override + { + AudioIODeviceCallback* const lastCallback = currentCallback; + + { + const ScopedLock sl (callbackLock); + currentCallback = nullptr; + } + + if (lastCallback != nullptr) + lastCallback->audioDeviceStopped(); + } + + String getLastError() { return error; } + bool hasControlPanel() const { return true; } + + bool showControlPanel() + { + JUCE_ASIO_LOG ("showing control panel"); + + bool done = false; + insideControlPanelModalLoop = true; + + const uint32 started = Time::getMillisecondCounter(); + + if (asioObject != nullptr) + { + asioObject->controlPanel(); + + const int spent = (int) Time::getMillisecondCounter() - (int) started; + + JUCE_ASIO_LOG ("spent: " + String (spent)); + + if (spent > 300) + { + shouldUsePreferredSize = true; + done = true; + } + } + + insideControlPanelModalLoop = false; + return done; + } + + void resetRequest() noexcept + { + startTimer (500); + } + + void timerCallback() override + { + if (! insideControlPanelModalLoop) + { + stopTimer(); + + JUCE_ASIO_LOG ("restart request!"); + + AudioIODeviceCallback* const oldCallback = currentCallback; + + close(); + + needToReset = true; + open (BigInteger (currentChansIn), BigInteger (currentChansOut), + currentSampleRate, currentBlockSizeSamples); + + reloadChannelNames(); + + if (oldCallback != nullptr) + start (oldCallback); + + sendASIODeviceChangeToListeners (owner); + } + else + { + startTimer (100); + } + } + +private: + //============================================================================== + WeakReference owner; + IASIO* volatile asioObject; + ASIOCallbacks callbacks; + + CLSID classId; + String error; + + long totalNumInputChans, totalNumOutputChans; + StringArray inputChannelNames, outputChannelNames; + + Array sampleRates; + Array bufferSizes; + long inputLatency, outputLatency; + long minBufferSize, maxBufferSize, preferredBufferSize, bufferGranularity; + ASIOClockSource clocks[32]; + int numClockSources; + + int volatile currentBlockSizeSamples; + int volatile currentBitDepth; + double volatile currentSampleRate; + BigInteger currentChansOut, currentChansIn; + AudioIODeviceCallback* volatile currentCallback; + CriticalSection callbackLock; + + HeapBlock bufferInfos; + HeapBlock inBuffers, outBuffers; + HeapBlock inputFormat, outputFormat; + + WaitableEvent event1; + HeapBlock tempBuffer; + int volatile bufferIndex, numActiveInputChans, numActiveOutputChans; + + bool deviceIsOpen, isStarted, buffersCreated; + bool volatile calledback; + bool volatile littleEndian, postOutput, needToReset; + bool volatile insideControlPanelModalLoop; + bool volatile shouldUsePreferredSize; + int xruns = 0; + + //============================================================================== + static String convertASIOString (char* const text, int length) + { + if (CharPointer_UTF8::isValidString (text, length)) + return String::fromUTF8 (text, length); + + WCHAR wideVersion [64] = { 0 }; + MultiByteToWideChar (CP_ACP, 0, text, length, wideVersion, numElementsInArray (wideVersion)); + return wideVersion; + } + + String getChannelName (int index, bool isInput) const + { + ASIOChannelInfo channelInfo = { 0 }; + channelInfo.channel = index; + channelInfo.isInput = isInput ? 1 : 0; + asioObject->getChannelInfo (&channelInfo); + + return convertASIOString (channelInfo.name, sizeof (channelInfo.name)); + } + + void reloadChannelNames() + { + if (asioObject != nullptr + && asioObject->getChannels (&totalNumInputChans, &totalNumOutputChans) == ASE_OK) + { + inputChannelNames.clear(); + outputChannelNames.clear(); + + for (int i = 0; i < totalNumInputChans; ++i) + inputChannelNames.add (getChannelName (i, true)); + + for (int i = 0; i < totalNumOutputChans; ++i) + outputChannelNames.add (getChannelName (i, false)); + + outputChannelNames.trim(); + inputChannelNames.trim(); + outputChannelNames.appendNumbersToDuplicates (false, true); + inputChannelNames.appendNumbersToDuplicates (false, true); + } + } + + long refreshBufferSizes() + { + return asioObject->getBufferSize (&minBufferSize, &maxBufferSize, &preferredBufferSize, &bufferGranularity); + } + + int readBufferSizes (int bufferSizeSamples) + { + minBufferSize = 0; + maxBufferSize = 0; + bufferGranularity = 0; + long newPreferredSize = 0; + + if (asioObject->getBufferSize (&minBufferSize, &maxBufferSize, &newPreferredSize, &bufferGranularity) == ASE_OK) + { + if (preferredBufferSize != 0 && newPreferredSize != 0 && newPreferredSize != preferredBufferSize) + shouldUsePreferredSize = true; + + if (bufferSizeSamples < minBufferSize || bufferSizeSamples > maxBufferSize) + shouldUsePreferredSize = true; + + preferredBufferSize = newPreferredSize; + } + + // unfortunate workaround for certain drivers which crash if you make + // dynamic changes to the buffer size... + shouldUsePreferredSize = shouldUsePreferredSize || getName().containsIgnoreCase ("Digidesign"); + + if (shouldUsePreferredSize) + { + JUCE_ASIO_LOG ("Using preferred size for buffer.."); + long err = refreshBufferSizes(); + + if (err == ASE_OK) + { + bufferSizeSamples = (int) preferredBufferSize; + } + else + { + bufferSizeSamples = 1024; + JUCE_ASIO_LOG_ERROR ("getBufferSize1", err); + } + + shouldUsePreferredSize = false; + } + + return bufferSizeSamples; + } + + int resetBuffers (const BigInteger& inputChannels, + const BigInteger& outputChannels) + { + numActiveInputChans = 0; + numActiveOutputChans = 0; + + ASIOBufferInfo* info = bufferInfos; + for (int i = 0; i < totalNumInputChans; ++i) + { + if (inputChannels[i]) + { + currentChansIn.setBit (i); + info->isInput = 1; + info->channelNum = i; + info->buffers[0] = info->buffers[1] = nullptr; + ++info; + ++numActiveInputChans; + } + } + + for (int i = 0; i < totalNumOutputChans; ++i) + { + if (outputChannels[i]) + { + currentChansOut.setBit (i); + info->isInput = 0; + info->channelNum = i; + info->buffers[0] = info->buffers[1] = nullptr; + ++info; + ++numActiveOutputChans; + } + } + + return numActiveInputChans + numActiveOutputChans; + } + + void addBufferSizes (long minSize, long maxSize, long preferredSize, long granularity) + { + // find a list of buffer sizes.. + JUCE_ASIO_LOG (String ((int) minSize) + "->" + String ((int) maxSize) + ", " + + String ((int) preferredSize) + ", " + String ((int) granularity)); + + if (granularity >= 0) + { + granularity = jmax (16, (int) granularity); + + for (int i = jmax ((int) (minSize + 15) & ~15, (int) granularity); i < jmin (6400, (int) maxSize); i += granularity) + bufferSizes.addIfNotAlreadyThere (granularity * (i / granularity)); + } + else if (granularity < 0) + { + for (int i = 0; i < 18; ++i) + { + const int s = (1 << i); + + if (s >= minSize && s <= maxSize) + bufferSizes.add (s); + } + } + + bufferSizes.addIfNotAlreadyThere (preferredSize); + bufferSizes.sort(); + } + + double getSampleRate() const + { + double cr = 0; + long err = asioObject->getSampleRate (&cr); + JUCE_ASIO_LOG_ERROR ("getSampleRate", err); + return cr; + } + + void setSampleRate (double newRate) + { + if (currentSampleRate != newRate) + { + JUCE_ASIO_LOG ("rate change: " + String (currentSampleRate) + " to " + String (newRate)); + long err = asioObject->setSampleRate (newRate); + + if (err == ASE_NoClock && numClockSources > 0) + { + JUCE_ASIO_LOG ("trying to set a clock source.."); + Thread::sleep (10); + err = asioObject->setClockSource (clocks[0].index); + JUCE_ASIO_LOG_ERROR ("setClockSource2", err); + + Thread::sleep (10); + err = asioObject->setSampleRate (newRate); + } + + if (err == 0) + currentSampleRate = newRate; + + // on fail, ignore the attempt to change rate, and run with the current one.. + } + } + + void updateClockSources() + { + zeromem (clocks, sizeof (clocks)); + long numSources = numElementsInArray (clocks); + asioObject->getClockSources (clocks, &numSources); + numClockSources = (int) numSources; + + bool isSourceSet = false; + + // careful not to remove this loop because it does more than just logging! + for (int i = 0; i < numClockSources; ++i) + { + String s ("clock: "); + s += clocks[i].name; + + if (clocks[i].isCurrentSource) + { + isSourceSet = true; + s << " (cur)"; + } + + JUCE_ASIO_LOG (s); + } + + if (numClockSources > 1 && ! isSourceSet) + { + JUCE_ASIO_LOG ("setting clock source"); + long err = asioObject->setClockSource (clocks[0].index); + JUCE_ASIO_LOG_ERROR ("setClockSource1", err); + Thread::sleep (20); + } + else + { + if (numClockSources == 0) + JUCE_ASIO_LOG ("no clock sources!"); + } + } + + void readLatencies() + { + inputLatency = outputLatency = 0; + + if (asioObject->getLatencies (&inputLatency, &outputLatency) != 0) + JUCE_ASIO_LOG ("getLatencies() failed"); + else + JUCE_ASIO_LOG ("Latencies: in = " + String ((int) inputLatency) + ", out = " + String ((int) outputLatency)); + } + + void createDummyBuffers (long preferredSize) + { + numActiveInputChans = 0; + numActiveOutputChans = 0; + + ASIOBufferInfo* info = bufferInfos; + int numChans = 0; + + for (int i = 0; i < jmin (2, (int) totalNumInputChans); ++i) + { + info->isInput = 1; + info->channelNum = i; + info->buffers[0] = info->buffers[1] = nullptr; + ++info; + ++numChans; + } + + const int outputBufferIndex = numChans; + + for (int i = 0; i < jmin (2, (int) totalNumOutputChans); ++i) + { + info->isInput = 0; + info->channelNum = i; + info->buffers[0] = info->buffers[1] = nullptr; + ++info; + ++numChans; + } + + setCallbackFunctions(); + + JUCE_ASIO_LOG ("creating buffers (dummy): " + String (numChans) + ", " + String ((int) preferredSize)); + + if (preferredSize > 0) + { + long err = asioObject->createBuffers (bufferInfos, numChans, preferredSize, &callbacks); + JUCE_ASIO_LOG_ERROR ("dummy buffers", err); + } + + long newInps = 0, newOuts = 0; + asioObject->getChannels (&newInps, &newOuts); + + if (totalNumInputChans != newInps || totalNumOutputChans != newOuts) + { + totalNumInputChans = newInps; + totalNumOutputChans = newOuts; + + JUCE_ASIO_LOG (String ((int) totalNumInputChans) + " in; " + String ((int) totalNumOutputChans) + " out"); + } + + updateSampleRates(); + reloadChannelNames(); + + for (int i = 0; i < totalNumOutputChans; ++i) + { + ASIOChannelInfo channelInfo = { 0 }; + channelInfo.channel = i; + channelInfo.isInput = 0; + asioObject->getChannelInfo (&channelInfo); + + outputFormat[i] = ASIOSampleFormat (channelInfo.type); + + if (i < 2) + { + // clear the channels that are used with the dummy stuff + outputFormat[i].clear (bufferInfos [outputBufferIndex + i].buffers[0], preferredBufferSize); + outputFormat[i].clear (bufferInfos [outputBufferIndex + i].buffers[1], preferredBufferSize); + } + } + } + + void removeCurrentDriver() + { + if (asioObject != nullptr) + { + asioObject->Release(); + asioObject = nullptr; + } + } + + bool loadDriver() + { + removeCurrentDriver(); + + bool crashed = false; + bool ok = tryCreatingDriver (crashed); + + if (crashed) + JUCE_ASIO_LOG ("** Driver crashed while being opened"); + + return ok; + } + + bool tryCreatingDriver (bool& crashed) + { + #if ! JUCE_MINGW + __try + #endif + { + return CoCreateInstance (classId, 0, CLSCTX_INPROC_SERVER, + classId, (void**) &asioObject) == S_OK; + } + #if ! JUCE_MINGW + __except (EXCEPTION_EXECUTE_HANDLER) { crashed = true; } + return false; + #endif + } + + String getLastDriverError() const + { + jassert (asioObject != nullptr); + char buffer [512] = { 0 }; + asioObject->getErrorMessage (buffer); + return String (buffer, sizeof (buffer) - 1); + } + + String initDriver() + { + if (asioObject == nullptr) + return "No Driver"; + + const bool initOk = !! asioObject->init (juce_messageWindowHandle); + String driverError; + + // Get error message if init() failed, or if it's a buggy Denon driver, + // which returns true from init() even when it fails. + if ((! initOk) || getName().containsIgnoreCase ("denon dj")) + driverError = getLastDriverError(); + + if ((! initOk) && driverError.isEmpty()) + driverError = "Driver failed to initialise"; + + if (driverError.isEmpty()) + { + char buffer [512]; + asioObject->getDriverName (buffer); // just in case any flimsy drivers expect this to be called.. + } + + return driverError; + } + + String openDevice() + { + // open the device and get its info.. + JUCE_ASIO_LOG ("opening device: " + getName()); + + needToReset = false; + outputChannelNames.clear(); + inputChannelNames.clear(); + bufferSizes.clear(); + sampleRates.clear(); + deviceIsOpen = false; + totalNumInputChans = 0; + totalNumOutputChans = 0; + numActiveInputChans = 0; + numActiveOutputChans = 0; + xruns = 0; + currentCallback = nullptr; + + error.clear(); + + if (getName().isEmpty()) + return error; + + long err = 0; + + if (loadDriver()) + { + if ((error = initDriver()).isEmpty()) + { + numActiveInputChans = 0; + numActiveOutputChans = 0; + totalNumInputChans = 0; + totalNumOutputChans = 0; + + if (asioObject != nullptr + && (err = asioObject->getChannels (&totalNumInputChans, &totalNumOutputChans)) == 0) + { + JUCE_ASIO_LOG (String ((int) totalNumInputChans) + " in, " + String ((int) totalNumOutputChans) + " out"); + + const int chansToAllocate = totalNumInputChans + totalNumOutputChans + 4; + bufferInfos.calloc (chansToAllocate); + inBuffers.calloc (chansToAllocate); + outBuffers.calloc (chansToAllocate); + inputFormat.calloc (chansToAllocate); + outputFormat.calloc (chansToAllocate); + + if ((err = refreshBufferSizes()) == 0) + { + addBufferSizes (minBufferSize, maxBufferSize, preferredBufferSize, bufferGranularity); + + double currentRate = getSampleRate(); + + if (currentRate < 1.0 || currentRate > 192001.0) + { + JUCE_ASIO_LOG ("setting default sample rate"); + err = asioObject->setSampleRate (44100.0); + JUCE_ASIO_LOG_ERROR ("setting sample rate", err); + + currentRate = getSampleRate(); + } + + currentSampleRate = currentRate; + + postOutput = (asioObject->outputReady() == 0); + if (postOutput) + JUCE_ASIO_LOG ("outputReady true"); + + updateSampleRates(); + + readLatencies(); // ..doing these steps because cubase does so at this stage + createDummyBuffers (preferredBufferSize); // in initialisation, and some devices fail if we don't. + readLatencies(); + + // start and stop because cubase does it.. + err = asioObject->start(); + // ignore an error here, as it might start later after setting other stuff up + JUCE_ASIO_LOG_ERROR ("start", err); + + Thread::sleep (80); + asioObject->stop(); + } + else + { + error = "Can't detect buffer sizes"; + } + } + else + { + error = "Can't detect asio channels"; + } + } + } + else + { + error = "No such device"; + } + + if (error.isNotEmpty()) + { + JUCE_ASIO_LOG_ERROR (error, err); + disposeBuffers(); + removeCurrentDriver(); + } + else + { + JUCE_ASIO_LOG ("device open"); + } + + deviceIsOpen = false; + needToReset = false; + stopTimer(); + return error; + } + + void disposeBuffers() + { + if (asioObject != nullptr && buffersCreated) + { + buffersCreated = false; + asioObject->disposeBuffers(); + } + } + + //============================================================================== + void JUCE_ASIOCALLBACK callback (const long index) + { + if (isStarted) + { + bufferIndex = index; + processBuffer(); + } + else + { + if (postOutput && (asioObject != nullptr)) + asioObject->outputReady(); + } + + calledback = true; + } + + void processBuffer() + { + const ASIOBufferInfo* const infos = bufferInfos; + const int bi = bufferIndex; + + const ScopedLock sl (callbackLock); + + if (bi >= 0) + { + const int samps = currentBlockSizeSamples; + + if (currentCallback != nullptr) + { + for (int i = 0; i < numActiveInputChans; ++i) + { + jassert (inBuffers[i] != nullptr); + inputFormat[i].convertToFloat (infos[i].buffers[bi], inBuffers[i], samps); + } + + currentCallback->audioDeviceIOCallback (const_cast (inBuffers.getData()), numActiveInputChans, + outBuffers, numActiveOutputChans, samps); + + for (int i = 0; i < numActiveOutputChans; ++i) + { + jassert (outBuffers[i] != nullptr); + outputFormat[i].convertFromFloat (outBuffers[i], infos [numActiveInputChans + i].buffers[bi], samps); + } + } + else + { + for (int i = 0; i < numActiveOutputChans; ++i) + outputFormat[i].clear (infos[numActiveInputChans + i].buffers[bi], samps); + } + } + + if (postOutput) + asioObject->outputReady(); + } + + long asioMessagesCallback (long selector, long value) + { + switch (selector) + { + case kAsioSelectorSupported: + if (value == kAsioResetRequest || value == kAsioEngineVersion || value == kAsioResyncRequest + || value == kAsioLatenciesChanged || value == kAsioSupportsInputMonitor || value == kAsioOverload) + return 1; + break; + + case kAsioBufferSizeChange: JUCE_ASIO_LOG ("kAsioBufferSizeChange"); resetRequest(); return 1; + case kAsioResetRequest: JUCE_ASIO_LOG ("kAsioResetRequest"); resetRequest(); return 1; + case kAsioResyncRequest: JUCE_ASIO_LOG ("kAsioResyncRequest"); resetRequest(); return 1; + case kAsioLatenciesChanged: JUCE_ASIO_LOG ("kAsioLatenciesChanged"); return 1; + case kAsioEngineVersion: return 2; + + case kAsioSupportsTimeInfo: + case kAsioSupportsTimeCode: return 0; + case kAsioOverload: xruns++; return 1; + } + + return 0; + } + + //============================================================================== + template + struct ASIOCallbackFunctions + { + static ASIOTime* JUCE_ASIOCALLBACK bufferSwitchTimeInfoCallback (ASIOTime*, long index, long) + { + if (currentASIODev[deviceIndex] != nullptr) + currentASIODev[deviceIndex]->callback (index); + + return nullptr; + } + + static void JUCE_ASIOCALLBACK bufferSwitchCallback (long index, long) + { + if (currentASIODev[deviceIndex] != nullptr) + currentASIODev[deviceIndex]->callback (index); + } + + static long JUCE_ASIOCALLBACK asioMessagesCallback (long selector, long value, void*, double*) + { + return currentASIODev[deviceIndex] != nullptr + ? currentASIODev[deviceIndex]->asioMessagesCallback (selector, value) + : 0; + } + + static void JUCE_ASIOCALLBACK sampleRateChangedCallback (ASIOSampleRate) + { + if (currentASIODev[deviceIndex] != nullptr) + currentASIODev[deviceIndex]->resetRequest(); + } + + static void setCallbacks (ASIOCallbacks& callbacks) noexcept + { + callbacks.bufferSwitch = &bufferSwitchCallback; + callbacks.asioMessage = &asioMessagesCallback; + callbacks.bufferSwitchTimeInfo = &bufferSwitchTimeInfoCallback; + callbacks.sampleRateDidChange = &sampleRateChangedCallback; + } + + static void setCallbacksForDevice (ASIOCallbacks& callbacks, ASIOAudioIODevice* device) noexcept + { + if (currentASIODev[deviceIndex] == device) + setCallbacks (callbacks); + else + ASIOCallbackFunctions::setCallbacksForDevice (callbacks, device); + } + }; + + void setCallbackFunctions() noexcept + { + /**/ if (currentASIODev[0] == this) ASIOCallbackFunctions<0>::setCallbacks (callbacks); + else if (currentASIODev[1] == this) ASIOCallbackFunctions<1>::setCallbacks (callbacks); + else if (currentASIODev[2] == this) ASIOCallbackFunctions<2>::setCallbacks (callbacks); + else if (currentASIODev[3] == this) ASIOCallbackFunctions<3>::setCallbacks (callbacks); + else jassertfalse; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ASIOAudioIODevice) +}; + +template <> +struct ASIOAudioIODevice::ASIOCallbackFunctions +{ + static void setCallbacksForDevice (ASIOCallbacks&, ASIOAudioIODevice*) noexcept {} +}; + +//============================================================================== +class ASIOAudioIODeviceType : public AudioIODeviceType +{ +public: + ASIOAudioIODeviceType() : AudioIODeviceType ("ASIO") {} + + //============================================================================== + void scanForDevices() + { + hasScanned = true; + + deviceNames.clear(); + classIds.clear(); + + HKEY hk = 0; + int index = 0; + + if (RegOpenKey (HKEY_LOCAL_MACHINE, _T("software\\asio"), &hk) == ERROR_SUCCESS) + { + TCHAR name [256]; + + while (RegEnumKey (hk, index++, name, numElementsInArray (name)) == ERROR_SUCCESS) + addDriverInfo (name, hk); + + RegCloseKey (hk); + } + } + + StringArray getDeviceNames (bool /*wantInputNames*/) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + return deviceNames; + } + + int getDefaultDeviceIndex (bool) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + for (int i = deviceNames.size(); --i >= 0;) + if (deviceNames[i].containsIgnoreCase ("asio4all")) + return i; // asio4all is a safe choice for a default.. + + #if JUCE_DEBUG + if (deviceNames.size() > 1 && deviceNames[0].containsIgnoreCase ("digidesign")) + return 1; // (the digi m-box driver crashes the app when you run + // it in the debugger, which can be a bit annoying) + #endif + + return 0; + } + + static int findFreeSlot() + { + for (int i = 0; i < numElementsInArray (currentASIODev); ++i) + if (currentASIODev[i] == 0) + return i; + + jassertfalse; // unfortunately you can only have a finite number + // of ASIO devices open at the same time.. + return -1; + } + + int getIndexOfDevice (AudioIODevice* d, bool /*asInput*/) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + return d == nullptr ? -1 : deviceNames.indexOf (d->getName()); + } + + bool hasSeparateInputsAndOutputs() const { return false; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + // ASIO can't open two different devices for input and output - they must be the same one. + jassert (inputDeviceName == outputDeviceName || outputDeviceName.isEmpty() || inputDeviceName.isEmpty()); + jassert (hasScanned); // need to call scanForDevices() before doing this + + const String deviceName (outputDeviceName.isNotEmpty() ? outputDeviceName + : inputDeviceName); + const int index = deviceNames.indexOf (deviceName); + + if (index >= 0) + { + const int freeSlot = findFreeSlot(); + + if (freeSlot >= 0) + return new ASIOAudioIODevice (this, deviceName, + classIds.getReference (index), freeSlot); + } + + return nullptr; + } + + void sendDeviceChangeToListeners() + { + callDeviceChangeListeners(); + } + + JUCE_DECLARE_WEAK_REFERENCEABLE (ASIOAudioIODeviceType) + +private: + StringArray deviceNames; + Array classIds; + + bool hasScanned = false; + + //============================================================================== + static bool checkClassIsOk (const String& classId) + { + HKEY hk = 0; + bool ok = false; + + if (RegOpenKey (HKEY_CLASSES_ROOT, _T("clsid"), &hk) == ERROR_SUCCESS) + { + int index = 0; + TCHAR name [512]; + + while (RegEnumKey (hk, index++, name, numElementsInArray (name)) == ERROR_SUCCESS) + { + if (classId.equalsIgnoreCase (name)) + { + HKEY subKey, pathKey; + + if (RegOpenKeyEx (hk, name, 0, KEY_READ, &subKey) == ERROR_SUCCESS) + { + if (RegOpenKeyEx (subKey, _T("InprocServer32"), 0, KEY_READ, &pathKey) == ERROR_SUCCESS) + { + TCHAR pathName [1024] = { 0 }; + DWORD dtype = REG_SZ; + DWORD dsize = sizeof (pathName); + + if (RegQueryValueEx (pathKey, 0, 0, &dtype, (LPBYTE) pathName, &dsize) == ERROR_SUCCESS) + // In older code, this used to check for the existence of the file, but there are situations + // where our process doesn't have access to it, but where the driver still loads ok.. + ok = (pathName[0] != 0); + + RegCloseKey (pathKey); + } + + RegCloseKey (subKey); + } + + break; + } + } + + RegCloseKey (hk); + } + + return ok; + } + + //============================================================================== + void addDriverInfo (const String& keyName, HKEY hk) + { + HKEY subKey; + + if (RegOpenKeyEx (hk, keyName.toWideCharPointer(), 0, KEY_READ, &subKey) == ERROR_SUCCESS) + { + TCHAR buf [256] = { 0 }; + DWORD dtype = REG_SZ; + DWORD dsize = sizeof (buf); + + if (RegQueryValueEx (subKey, _T("clsid"), 0, &dtype, (LPBYTE) buf, &dsize) == ERROR_SUCCESS) + { + if (dsize > 0 && checkClassIsOk (buf)) + { + CLSID classId; + if (CLSIDFromString ((LPOLESTR) buf, &classId) == S_OK) + { + dtype = REG_SZ; + dsize = sizeof (buf); + String deviceName; + + if (RegQueryValueEx (subKey, _T("description"), 0, &dtype, (LPBYTE) buf, &dsize) == ERROR_SUCCESS) + deviceName = buf; + else + deviceName = keyName; + + JUCE_ASIO_LOG ("found " + deviceName); + deviceNames.add (deviceName); + classIds.add (classId); + } + } + + RegCloseKey (subKey); + } + } + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ASIOAudioIODeviceType) +}; + +void sendASIODeviceChangeToListeners (ASIOAudioIODeviceType* type) +{ + if (type != nullptr) + type->sendDeviceChangeToListeners(); +} + +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_ASIO() +{ + return new ASIOAudioIODeviceType(); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp b/source/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp new file mode 100644 index 000000000..10ea41aba --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp @@ -0,0 +1,1301 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +extern "C" +{ + // Declare just the minimum number of interfaces for the DSound objects that we need.. + typedef struct typeDSBUFFERDESC + { + DWORD dwSize; + DWORD dwFlags; + DWORD dwBufferBytes; + DWORD dwReserved; + LPWAVEFORMATEX lpwfxFormat; + GUID guid3DAlgorithm; + } DSBUFFERDESC; + + struct IDirectSoundBuffer; + + #undef INTERFACE + #define INTERFACE IDirectSound + DECLARE_INTERFACE_(IDirectSound, IUnknown) + { + STDMETHOD(QueryInterface) (THIS_ REFIID, LPVOID*) PURE; + STDMETHOD_(ULONG,AddRef) (THIS) PURE; + STDMETHOD_(ULONG,Release) (THIS) PURE; + STDMETHOD(CreateSoundBuffer) (THIS_ DSBUFFERDESC*, IDirectSoundBuffer**, LPUNKNOWN) PURE; + STDMETHOD(GetCaps) (THIS_ void*) PURE; + STDMETHOD(DuplicateSoundBuffer) (THIS_ IDirectSoundBuffer*, IDirectSoundBuffer**) PURE; + STDMETHOD(SetCooperativeLevel) (THIS_ HWND, DWORD) PURE; + STDMETHOD(Compact) (THIS) PURE; + STDMETHOD(GetSpeakerConfig) (THIS_ LPDWORD) PURE; + STDMETHOD(SetSpeakerConfig) (THIS_ DWORD) PURE; + STDMETHOD(Initialize) (THIS_ const GUID*) PURE; + }; + + #undef INTERFACE + #define INTERFACE IDirectSoundBuffer + DECLARE_INTERFACE_(IDirectSoundBuffer, IUnknown) + { + STDMETHOD(QueryInterface) (THIS_ REFIID, LPVOID*) PURE; + STDMETHOD_(ULONG,AddRef) (THIS) PURE; + STDMETHOD_(ULONG,Release) (THIS) PURE; + STDMETHOD(GetCaps) (THIS_ void*) PURE; + STDMETHOD(GetCurrentPosition) (THIS_ LPDWORD, LPDWORD) PURE; + STDMETHOD(GetFormat) (THIS_ LPWAVEFORMATEX, DWORD, LPDWORD) PURE; + STDMETHOD(GetVolume) (THIS_ LPLONG) PURE; + STDMETHOD(GetPan) (THIS_ LPLONG) PURE; + STDMETHOD(GetFrequency) (THIS_ LPDWORD) PURE; + STDMETHOD(GetStatus) (THIS_ LPDWORD) PURE; + STDMETHOD(Initialize) (THIS_ IDirectSound*, DSBUFFERDESC*) PURE; + STDMETHOD(Lock) (THIS_ DWORD, DWORD, LPVOID*, LPDWORD, LPVOID*, LPDWORD, DWORD) PURE; + STDMETHOD(Play) (THIS_ DWORD, DWORD, DWORD) PURE; + STDMETHOD(SetCurrentPosition) (THIS_ DWORD) PURE; + STDMETHOD(SetFormat) (THIS_ const WAVEFORMATEX*) PURE; + STDMETHOD(SetVolume) (THIS_ LONG) PURE; + STDMETHOD(SetPan) (THIS_ LONG) PURE; + STDMETHOD(SetFrequency) (THIS_ DWORD) PURE; + STDMETHOD(Stop) (THIS) PURE; + STDMETHOD(Unlock) (THIS_ LPVOID, DWORD, LPVOID, DWORD) PURE; + STDMETHOD(Restore) (THIS) PURE; + }; + + //============================================================================== + typedef struct typeDSCBUFFERDESC + { + DWORD dwSize; + DWORD dwFlags; + DWORD dwBufferBytes; + DWORD dwReserved; + LPWAVEFORMATEX lpwfxFormat; + } DSCBUFFERDESC; + + struct IDirectSoundCaptureBuffer; + + #undef INTERFACE + #define INTERFACE IDirectSoundCapture + DECLARE_INTERFACE_(IDirectSoundCapture, IUnknown) + { + STDMETHOD(QueryInterface) (THIS_ REFIID, LPVOID*) PURE; + STDMETHOD_(ULONG,AddRef) (THIS) PURE; + STDMETHOD_(ULONG,Release) (THIS) PURE; + STDMETHOD(CreateCaptureBuffer) (THIS_ DSCBUFFERDESC*, IDirectSoundCaptureBuffer**, LPUNKNOWN) PURE; + STDMETHOD(GetCaps) (THIS_ void*) PURE; + STDMETHOD(Initialize) (THIS_ const GUID*) PURE; + }; + + #undef INTERFACE + #define INTERFACE IDirectSoundCaptureBuffer + DECLARE_INTERFACE_(IDirectSoundCaptureBuffer, IUnknown) + { + STDMETHOD(QueryInterface) (THIS_ REFIID, LPVOID*) PURE; + STDMETHOD_(ULONG,AddRef) (THIS) PURE; + STDMETHOD_(ULONG,Release) (THIS) PURE; + STDMETHOD(GetCaps) (THIS_ void*) PURE; + STDMETHOD(GetCurrentPosition) (THIS_ LPDWORD, LPDWORD) PURE; + STDMETHOD(GetFormat) (THIS_ LPWAVEFORMATEX, DWORD, LPDWORD) PURE; + STDMETHOD(GetStatus) (THIS_ LPDWORD) PURE; + STDMETHOD(Initialize) (THIS_ IDirectSoundCapture*, DSCBUFFERDESC*) PURE; + STDMETHOD(Lock) (THIS_ DWORD, DWORD, LPVOID*, LPDWORD, LPVOID*, LPDWORD, DWORD) PURE; + STDMETHOD(Start) (THIS_ DWORD) PURE; + STDMETHOD(Stop) (THIS) PURE; + STDMETHOD(Unlock) (THIS_ LPVOID, DWORD, LPVOID, DWORD) PURE; + }; + + #undef INTERFACE +} + +namespace juce +{ + +//============================================================================== +namespace DSoundLogging +{ + String getErrorMessage (HRESULT hr) + { + const char* result = nullptr; + + switch (hr) + { + case MAKE_HRESULT(1, 0x878, 10): result = "Device already allocated"; break; + case MAKE_HRESULT(1, 0x878, 30): result = "Control unavailable"; break; + case E_INVALIDARG: result = "Invalid parameter"; break; + case MAKE_HRESULT(1, 0x878, 50): result = "Invalid call"; break; + case E_FAIL: result = "Generic error"; break; + case MAKE_HRESULT(1, 0x878, 70): result = "Priority level error"; break; + case E_OUTOFMEMORY: result = "Out of memory"; break; + case MAKE_HRESULT(1, 0x878, 100): result = "Bad format"; break; + case E_NOTIMPL: result = "Unsupported function"; break; + case MAKE_HRESULT(1, 0x878, 120): result = "No driver"; break; + case MAKE_HRESULT(1, 0x878, 130): result = "Already initialised"; break; + case CLASS_E_NOAGGREGATION: result = "No aggregation"; break; + case MAKE_HRESULT(1, 0x878, 150): result = "Buffer lost"; break; + case MAKE_HRESULT(1, 0x878, 160): result = "Another app has priority"; break; + case MAKE_HRESULT(1, 0x878, 170): result = "Uninitialised"; break; + case E_NOINTERFACE: result = "No interface"; break; + case S_OK: result = "No error"; break; + default: return "Unknown error: " + String ((int) hr); + } + + return result; + } + + //============================================================================== + #if JUCE_DIRECTSOUND_LOGGING + static void logMessage (String message) + { + message = "DSOUND: " + message; + DBG (message); + Logger::writeToLog (message); + } + + static void logError (HRESULT hr, int lineNum) + { + if (FAILED (hr)) + { + String error ("Error at line "); + error << lineNum << ": " << getErrorMessage (hr); + logMessage (error); + } + } + + #define JUCE_DS_LOG(a) DSoundLogging::logMessage(a); + #define JUCE_DS_LOG_ERROR(a) DSoundLogging::logError(a, __LINE__); + #else + #define JUCE_DS_LOG(a) + #define JUCE_DS_LOG_ERROR(a) + #endif +} + +//============================================================================== +namespace +{ + #define DSOUND_FUNCTION(functionName, params) \ + typedef HRESULT (WINAPI *type##functionName) params; \ + static type##functionName ds##functionName = nullptr; + + #define DSOUND_FUNCTION_LOAD(functionName) \ + ds##functionName = (type##functionName) GetProcAddress (h, #functionName); \ + jassert (ds##functionName != nullptr); + + typedef BOOL (CALLBACK *LPDSENUMCALLBACKW) (LPGUID, LPCWSTR, LPCWSTR, LPVOID); + typedef BOOL (CALLBACK *LPDSENUMCALLBACKA) (LPGUID, LPCSTR, LPCSTR, LPVOID); + + DSOUND_FUNCTION (DirectSoundCreate, (const GUID*, IDirectSound**, LPUNKNOWN)) + DSOUND_FUNCTION (DirectSoundCaptureCreate, (const GUID*, IDirectSoundCapture**, LPUNKNOWN)) + DSOUND_FUNCTION (DirectSoundEnumerateW, (LPDSENUMCALLBACKW, LPVOID)) + DSOUND_FUNCTION (DirectSoundCaptureEnumerateW, (LPDSENUMCALLBACKW, LPVOID)) + + void initialiseDSoundFunctions() + { + if (dsDirectSoundCreate == nullptr) + { + HMODULE h = LoadLibraryA ("dsound.dll"); + + DSOUND_FUNCTION_LOAD (DirectSoundCreate) + DSOUND_FUNCTION_LOAD (DirectSoundCaptureCreate) + DSOUND_FUNCTION_LOAD (DirectSoundEnumerateW) + DSOUND_FUNCTION_LOAD (DirectSoundCaptureEnumerateW) + } + } + + // the overall size of buffer used is this value x the block size + enum { blocksPerOverallBuffer = 16 }; +} + +//============================================================================== +class DSoundInternalOutChannel +{ +public: + DSoundInternalOutChannel (const String& name_, const GUID& guid_, int rate, + int bufferSize, float* left, float* right) + : bitDepth (16), name (name_), guid (guid_), sampleRate (rate), + bufferSizeSamples (bufferSize), leftBuffer (left), rightBuffer (right), + pDirectSound (nullptr), pOutputBuffer (nullptr) + { + } + + ~DSoundInternalOutChannel() + { + close(); + } + + void close() + { + if (pOutputBuffer != nullptr) + { + JUCE_DS_LOG ("closing output: " + name); + HRESULT hr = pOutputBuffer->Stop(); + JUCE_DS_LOG_ERROR (hr); ignoreUnused (hr); + + pOutputBuffer->Release(); + pOutputBuffer = nullptr; + } + + if (pDirectSound != nullptr) + { + pDirectSound->Release(); + pDirectSound = nullptr; + } + } + + String open() + { + JUCE_DS_LOG ("opening output: " + name + " rate=" + String (sampleRate) + + " bits=" + String (bitDepth) + " buf=" + String (bufferSizeSamples)); + + pDirectSound = nullptr; + pOutputBuffer = nullptr; + writeOffset = 0; + xruns = 0; + + firstPlayTime = true; + lastPlayTime = 0; + + String error; + HRESULT hr = E_NOINTERFACE; + + if (dsDirectSoundCreate != nullptr) + hr = dsDirectSoundCreate (&guid, &pDirectSound, nullptr); + + if (SUCCEEDED (hr)) + { + bytesPerBuffer = (bufferSizeSamples * (bitDepth >> 2)) & ~15; + ticksPerBuffer = bytesPerBuffer * Time::getHighResolutionTicksPerSecond() / (sampleRate * (bitDepth >> 2)); + totalBytesPerBuffer = (blocksPerOverallBuffer * bytesPerBuffer) & ~15; + const int numChannels = 2; + + hr = pDirectSound->SetCooperativeLevel (GetDesktopWindow(), 2 /* DSSCL_PRIORITY */); + JUCE_DS_LOG_ERROR (hr); + + if (SUCCEEDED (hr)) + { + IDirectSoundBuffer* pPrimaryBuffer; + + DSBUFFERDESC primaryDesc = { 0 }; + primaryDesc.dwSize = sizeof (DSBUFFERDESC); + primaryDesc.dwFlags = 1 /* DSBCAPS_PRIMARYBUFFER */; + primaryDesc.dwBufferBytes = 0; + primaryDesc.lpwfxFormat = 0; + + JUCE_DS_LOG ("co-op level set"); + hr = pDirectSound->CreateSoundBuffer (&primaryDesc, &pPrimaryBuffer, 0); + JUCE_DS_LOG_ERROR (hr); + + if (SUCCEEDED (hr)) + { + WAVEFORMATEX wfFormat; + wfFormat.wFormatTag = WAVE_FORMAT_PCM; + wfFormat.nChannels = (unsigned short) numChannels; + wfFormat.nSamplesPerSec = (DWORD) sampleRate; + wfFormat.wBitsPerSample = (unsigned short) bitDepth; + wfFormat.nBlockAlign = (unsigned short) (wfFormat.nChannels * wfFormat.wBitsPerSample / 8); + wfFormat.nAvgBytesPerSec = wfFormat.nSamplesPerSec * wfFormat.nBlockAlign; + wfFormat.cbSize = 0; + + hr = pPrimaryBuffer->SetFormat (&wfFormat); + JUCE_DS_LOG_ERROR (hr); + + if (SUCCEEDED (hr)) + { + DSBUFFERDESC secondaryDesc = { 0 }; + secondaryDesc.dwSize = sizeof (DSBUFFERDESC); + secondaryDesc.dwFlags = 0x8000 /* DSBCAPS_GLOBALFOCUS */ + | 0x10000 /* DSBCAPS_GETCURRENTPOSITION2 */; + secondaryDesc.dwBufferBytes = (DWORD) totalBytesPerBuffer; + secondaryDesc.lpwfxFormat = &wfFormat; + + hr = pDirectSound->CreateSoundBuffer (&secondaryDesc, &pOutputBuffer, 0); + JUCE_DS_LOG_ERROR (hr); + + if (SUCCEEDED (hr)) + { + JUCE_DS_LOG ("buffer created"); + + DWORD dwDataLen; + unsigned char* pDSBuffData; + + hr = pOutputBuffer->Lock (0, (DWORD) totalBytesPerBuffer, + (LPVOID*) &pDSBuffData, &dwDataLen, 0, 0, 0); + JUCE_DS_LOG_ERROR (hr); + + if (SUCCEEDED (hr)) + { + zeromem (pDSBuffData, dwDataLen); + + hr = pOutputBuffer->Unlock (pDSBuffData, dwDataLen, 0, 0); + + if (SUCCEEDED (hr)) + { + hr = pOutputBuffer->SetCurrentPosition (0); + + if (SUCCEEDED (hr)) + { + hr = pOutputBuffer->Play (0, 0, 1 /* DSBPLAY_LOOPING */); + + if (SUCCEEDED (hr)) + return {}; + } + } + } + } + } + } + } + } + + error = DSoundLogging::getErrorMessage (hr); + close(); + return error; + } + + void synchronisePosition() + { + if (pOutputBuffer != nullptr) + { + DWORD playCursor; + pOutputBuffer->GetCurrentPosition (&playCursor, &writeOffset); + } + } + + bool service() + { + if (pOutputBuffer == 0) + return true; + + DWORD playCursor, writeCursor; + + for (;;) + { + HRESULT hr = pOutputBuffer->GetCurrentPosition (&playCursor, &writeCursor); + + if (hr == MAKE_HRESULT (1, 0x878, 150)) // DSERR_BUFFERLOST + { + pOutputBuffer->Restore(); + continue; + } + + if (SUCCEEDED (hr)) + break; + + JUCE_DS_LOG_ERROR (hr); + jassertfalse; + return true; + } + + auto currentPlayTime = Time::getHighResolutionTicks (); + if (! firstPlayTime) + { + auto expectedBuffers = (currentPlayTime - lastPlayTime) / ticksPerBuffer; + + playCursor += static_cast (expectedBuffers * bytesPerBuffer); + } + else + firstPlayTime = false; + + lastPlayTime = currentPlayTime; + + int playWriteGap = (int) (writeCursor - playCursor); + if (playWriteGap < 0) + playWriteGap += totalBytesPerBuffer; + + int bytesEmpty = (int) (playCursor - writeOffset); + if (bytesEmpty < 0) + bytesEmpty += totalBytesPerBuffer; + + if (bytesEmpty > (totalBytesPerBuffer - playWriteGap)) + { + writeOffset = writeCursor; + bytesEmpty = totalBytesPerBuffer - playWriteGap; + + // buffer underflow + xruns++; + } + + if (bytesEmpty >= bytesPerBuffer) + { + int* buf1 = nullptr; + int* buf2 = nullptr; + DWORD dwSize1 = 0; + DWORD dwSize2 = 0; + + HRESULT hr = pOutputBuffer->Lock (writeOffset, (DWORD) bytesPerBuffer, + (void**) &buf1, &dwSize1, + (void**) &buf2, &dwSize2, 0); + + if (hr == MAKE_HRESULT (1, 0x878, 150)) // DSERR_BUFFERLOST + { + pOutputBuffer->Restore(); + + hr = pOutputBuffer->Lock (writeOffset, (DWORD) bytesPerBuffer, + (void**) &buf1, &dwSize1, + (void**) &buf2, &dwSize2, 0); + } + + if (SUCCEEDED (hr)) + { + if (bitDepth == 16) + { + const float* left = leftBuffer; + const float* right = rightBuffer; + int samples1 = (int) (dwSize1 >> 2); + int samples2 = (int) (dwSize2 >> 2); + + if (left == nullptr) + { + for (int* dest = buf1; --samples1 >= 0;) *dest++ = convertInputValues (0, *right++); + for (int* dest = buf2; --samples2 >= 0;) *dest++ = convertInputValues (0, *right++); + } + else if (right == nullptr) + { + for (int* dest = buf1; --samples1 >= 0;) *dest++ = convertInputValues (*left++, 0); + for (int* dest = buf2; --samples2 >= 0;) *dest++ = convertInputValues (*left++, 0); + } + else + { + for (int* dest = buf1; --samples1 >= 0;) *dest++ = convertInputValues (*left++, *right++); + for (int* dest = buf2; --samples2 >= 0;) *dest++ = convertInputValues (*left++, *right++); + } + } + else + { + jassertfalse; + } + + writeOffset = (writeOffset + dwSize1 + dwSize2) % totalBytesPerBuffer; + + pOutputBuffer->Unlock (buf1, dwSize1, buf2, dwSize2); + } + else + { + jassertfalse; + JUCE_DS_LOG_ERROR (hr); + } + + bytesEmpty -= bytesPerBuffer; + return true; + } + else + { + return false; + } + } + + int bitDepth, xruns; + bool doneFlag; + +private: + String name; + GUID guid; + int sampleRate, bufferSizeSamples; + float* leftBuffer; + float* rightBuffer; + + IDirectSound* pDirectSound; + IDirectSoundBuffer* pOutputBuffer; + DWORD writeOffset; + int totalBytesPerBuffer, bytesPerBuffer; + unsigned int lastPlayCursor; + + bool firstPlayTime; + int64 lastPlayTime, ticksPerBuffer; + + static inline int convertInputValues (const float l, const float r) noexcept + { + return jlimit (-32768, 32767, roundToInt (32767.0f * r)) << 16 + | (0xffff & jlimit (-32768, 32767, roundToInt (32767.0f * l))); + } + + JUCE_DECLARE_NON_COPYABLE (DSoundInternalOutChannel) +}; + +//============================================================================== +struct DSoundInternalInChannel +{ +public: + DSoundInternalInChannel (const String& name_, const GUID& guid_, int rate, + int bufferSize, float* left, float* right) + : bitDepth (16), name (name_), guid (guid_), sampleRate (rate), + bufferSizeSamples (bufferSize), leftBuffer (left), rightBuffer (right), + pDirectSound (nullptr), pDirectSoundCapture (nullptr), pInputBuffer (nullptr) + { + } + + ~DSoundInternalInChannel() + { + close(); + } + + void close() + { + if (pInputBuffer != nullptr) + { + JUCE_DS_LOG ("closing input: " + name); + HRESULT hr = pInputBuffer->Stop(); + JUCE_DS_LOG_ERROR (hr); ignoreUnused (hr); + + pInputBuffer->Release(); + pInputBuffer = nullptr; + } + + if (pDirectSoundCapture != nullptr) + { + pDirectSoundCapture->Release(); + pDirectSoundCapture = nullptr; + } + + if (pDirectSound != nullptr) + { + pDirectSound->Release(); + pDirectSound = nullptr; + } + } + + String open() + { + JUCE_DS_LOG ("opening input: " + name + + " rate=" + String (sampleRate) + " bits=" + String (bitDepth) + " buf=" + String (bufferSizeSamples)); + + pDirectSound = nullptr; + pDirectSoundCapture = nullptr; + pInputBuffer = nullptr; + readOffset = 0; + totalBytesPerBuffer = 0; + + HRESULT hr = dsDirectSoundCaptureCreate != nullptr + ? dsDirectSoundCaptureCreate (&guid, &pDirectSoundCapture, nullptr) + : E_NOINTERFACE; + + if (SUCCEEDED (hr)) + { + const int numChannels = 2; + bytesPerBuffer = (bufferSizeSamples * (bitDepth >> 2)) & ~15; + totalBytesPerBuffer = (blocksPerOverallBuffer * bytesPerBuffer) & ~15; + + WAVEFORMATEX wfFormat; + wfFormat.wFormatTag = WAVE_FORMAT_PCM; + wfFormat.nChannels = (unsigned short)numChannels; + wfFormat.nSamplesPerSec = (DWORD) sampleRate; + wfFormat.wBitsPerSample = (unsigned short) bitDepth; + wfFormat.nBlockAlign = (unsigned short) (wfFormat.nChannels * (wfFormat.wBitsPerSample / 8)); + wfFormat.nAvgBytesPerSec = wfFormat.nSamplesPerSec * wfFormat.nBlockAlign; + wfFormat.cbSize = 0; + + DSCBUFFERDESC captureDesc = { 0 }; + captureDesc.dwSize = sizeof (DSCBUFFERDESC); + captureDesc.dwFlags = 0; + captureDesc.dwBufferBytes = (DWORD) totalBytesPerBuffer; + captureDesc.lpwfxFormat = &wfFormat; + + JUCE_DS_LOG ("object created"); + hr = pDirectSoundCapture->CreateCaptureBuffer (&captureDesc, &pInputBuffer, 0); + + if (SUCCEEDED (hr)) + { + hr = pInputBuffer->Start (1 /* DSCBSTART_LOOPING */); + + if (SUCCEEDED (hr)) + return {}; + } + } + + JUCE_DS_LOG_ERROR (hr); + const String error (DSoundLogging::getErrorMessage (hr)); + close(); + + return error; + } + + void synchronisePosition() + { + if (pInputBuffer != nullptr) + { + DWORD capturePos; + pInputBuffer->GetCurrentPosition (&capturePos, (DWORD*) &readOffset); + } + } + + bool service() + { + if (pInputBuffer == 0) + return true; + + DWORD capturePos, readPos; + HRESULT hr = pInputBuffer->GetCurrentPosition (&capturePos, &readPos); + JUCE_DS_LOG_ERROR (hr); + + if (FAILED (hr)) + return true; + + int bytesFilled = (int) (readPos - readOffset); + if (bytesFilled < 0) + bytesFilled += totalBytesPerBuffer; + + if (bytesFilled >= bytesPerBuffer) + { + short* buf1 = nullptr; + short* buf2 = nullptr; + DWORD dwsize1 = 0; + DWORD dwsize2 = 0; + + hr = pInputBuffer->Lock ((DWORD) readOffset, (DWORD) bytesPerBuffer, + (void**) &buf1, &dwsize1, + (void**) &buf2, &dwsize2, 0); + + if (SUCCEEDED (hr)) + { + if (bitDepth == 16) + { + const float g = 1.0f / 32768.0f; + + float* destL = leftBuffer; + float* destR = rightBuffer; + int samples1 = (int) (dwsize1 >> 2); + int samples2 = (int) (dwsize2 >> 2); + + if (destL == nullptr) + { + for (const short* src = buf1; --samples1 >= 0;) { ++src; *destR++ = *src++ * g; } + for (const short* src = buf2; --samples2 >= 0;) { ++src; *destR++ = *src++ * g; } + } + else if (destR == nullptr) + { + for (const short* src = buf1; --samples1 >= 0;) { *destL++ = *src++ * g; ++src; } + for (const short* src = buf2; --samples2 >= 0;) { *destL++ = *src++ * g; ++src; } + } + else + { + for (const short* src = buf1; --samples1 >= 0;) { *destL++ = *src++ * g; *destR++ = *src++ * g; } + for (const short* src = buf2; --samples2 >= 0;) { *destL++ = *src++ * g; *destR++ = *src++ * g; } + } + } + else + { + jassertfalse; + } + + readOffset = (readOffset + dwsize1 + dwsize2) % totalBytesPerBuffer; + + pInputBuffer->Unlock (buf1, dwsize1, buf2, dwsize2); + } + else + { + JUCE_DS_LOG_ERROR (hr); + jassertfalse; + } + + bytesFilled -= bytesPerBuffer; + + return true; + } + else + { + return false; + } + } + + unsigned int readOffset; + int bytesPerBuffer, totalBytesPerBuffer; + int bitDepth; + bool doneFlag; + +private: + String name; + GUID guid; + int sampleRate, bufferSizeSamples; + float* leftBuffer; + float* rightBuffer; + + IDirectSound* pDirectSound; + IDirectSoundCapture* pDirectSoundCapture; + IDirectSoundCaptureBuffer* pInputBuffer; + + JUCE_DECLARE_NON_COPYABLE (DSoundInternalInChannel) +}; + +//============================================================================== +class DSoundAudioIODevice : public AudioIODevice, + public Thread +{ +public: + DSoundAudioIODevice (const String& deviceName, + const int outputDeviceIndex_, + const int inputDeviceIndex_) + : AudioIODevice (deviceName, "DirectSound"), + Thread ("Juce DSound"), + outputDeviceIndex (outputDeviceIndex_), + inputDeviceIndex (inputDeviceIndex_), + isOpen_ (false), + isStarted (false), + bufferSizeSamples (0), + sampleRate (0.0), + callback (nullptr) + { + if (outputDeviceIndex_ >= 0) + { + outChannels.add (TRANS("Left")); + outChannels.add (TRANS("Right")); + } + + if (inputDeviceIndex_ >= 0) + { + inChannels.add (TRANS("Left")); + inChannels.add (TRANS("Right")); + } + } + + ~DSoundAudioIODevice() + { + close(); + } + + String open (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double newSampleRate, int newBufferSize) override + { + lastError = openDevice (inputChannels, outputChannels, newSampleRate, newBufferSize); + isOpen_ = lastError.isEmpty(); + + return lastError; + } + + void close() override + { + stop(); + + if (isOpen_) + { + closeDevice(); + isOpen_ = false; + } + } + + bool isOpen() override { return isOpen_ && isThreadRunning(); } + int getCurrentBufferSizeSamples() override { return bufferSizeSamples; } + double getCurrentSampleRate() override { return sampleRate; } + BigInteger getActiveOutputChannels() const override { return enabledOutputs; } + BigInteger getActiveInputChannels() const override { return enabledInputs; } + int getOutputLatencyInSamples() override { return (int) (getCurrentBufferSizeSamples() * 1.5); } + int getInputLatencyInSamples() override { return getOutputLatencyInSamples(); } + StringArray getOutputChannelNames() override { return outChannels; } + StringArray getInputChannelNames() override { return inChannels; } + + Array getAvailableSampleRates() override + { + static const double rates[] = { 44100.0, 48000.0, 88200.0, 96000.0 }; + return Array (rates, numElementsInArray (rates)); + } + + Array getAvailableBufferSizes() override + { + Array r; + int n = 64; + + for (int i = 0; i < 50; ++i) + { + r.add (n); + n += (n < 512) ? 32 + : ((n < 1024) ? 64 + : ((n < 2048) ? 128 : 256)); + } + + return r; + } + + int getDefaultBufferSize() override { return 2560; } + + int getCurrentBitDepth() override + { + int bits = 256; + + for (int i = inChans.size(); --i >= 0;) + bits = jmin (bits, inChans[i]->bitDepth); + + for (int i = outChans.size(); --i >= 0;) + bits = jmin (bits, outChans[i]->bitDepth); + + if (bits > 32) + bits = 16; + + return bits; + } + + void start (AudioIODeviceCallback* call) override + { + if (isOpen_ && call != nullptr && ! isStarted) + { + if (! isThreadRunning()) + { + // something gone wrong and the thread's stopped.. + isOpen_ = false; + return; + } + + call->audioDeviceAboutToStart (this); + + const ScopedLock sl (startStopLock); + callback = call; + isStarted = true; + } + } + + void stop() override + { + if (isStarted) + { + AudioIODeviceCallback* const callbackLocal = callback; + + { + const ScopedLock sl (startStopLock); + isStarted = false; + } + + if (callbackLocal != nullptr) + callbackLocal->audioDeviceStopped(); + } + } + + bool isPlaying() override { return isStarted && isOpen_ && isThreadRunning(); } + String getLastError() override { return lastError; } + + int getXRunCount () const noexcept override + { + return (outChans[0] != nullptr ? outChans[0]->xruns : -1); + } + + //============================================================================== + StringArray inChannels, outChannels; + int outputDeviceIndex, inputDeviceIndex; + +private: + bool isOpen_; + bool isStarted; + String lastError; + + OwnedArray inChans; + OwnedArray outChans; + WaitableEvent startEvent; + + int bufferSizeSamples; + double sampleRate; + BigInteger enabledInputs, enabledOutputs; + AudioSampleBuffer inputBuffers, outputBuffers; + + AudioIODeviceCallback* callback; + CriticalSection startStopLock; + + String openDevice (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate_, int bufferSizeSamples_); + + void closeDevice() + { + isStarted = false; + stopThread (5000); + + inChans.clear(); + outChans.clear(); + inputBuffers.setSize (1, 1); + outputBuffers.setSize (1, 1); + } + + void resync() + { + if (! threadShouldExit()) + { + sleep (5); + + for (int i = 0; i < outChans.size(); ++i) + outChans.getUnchecked(i)->synchronisePosition(); + + for (int i = 0; i < inChans.size(); ++i) + inChans.getUnchecked(i)->synchronisePosition(); + } + } + +public: + void run() override + { + while (! threadShouldExit()) + { + if (wait (100)) + break; + } + + const int latencyMs = (int) (bufferSizeSamples * 1000.0 / sampleRate); + const int maxTimeMS = jmax (5, 3 * latencyMs); + + while (! threadShouldExit()) + { + int numToDo = 0; + uint32 startTime = Time::getMillisecondCounter(); + + for (int i = inChans.size(); --i >= 0;) + { + inChans.getUnchecked(i)->doneFlag = false; + ++numToDo; + } + + for (int i = outChans.size(); --i >= 0;) + { + outChans.getUnchecked(i)->doneFlag = false; + ++numToDo; + } + + if (numToDo > 0) + { + const int maxCount = 3; + int count = maxCount; + + for (;;) + { + for (int i = inChans.size(); --i >= 0;) + { + DSoundInternalInChannel* const in = inChans.getUnchecked(i); + + if ((! in->doneFlag) && in->service()) + { + in->doneFlag = true; + --numToDo; + } + } + + for (int i = outChans.size(); --i >= 0;) + { + DSoundInternalOutChannel* const out = outChans.getUnchecked(i); + + if ((! out->doneFlag) && out->service()) + { + out->doneFlag = true; + --numToDo; + } + } + + if (numToDo <= 0) + break; + + if (Time::getMillisecondCounter() > startTime + maxTimeMS) + { + resync(); + break; + } + + if (--count <= 0) + { + Sleep (1); + count = maxCount; + } + + if (threadShouldExit()) + return; + } + } + else + { + sleep (1); + } + + const ScopedLock sl (startStopLock); + + if (isStarted) + { + callback->audioDeviceIOCallback (inputBuffers.getArrayOfReadPointers(), inputBuffers.getNumChannels(), + outputBuffers.getArrayOfWritePointers(), outputBuffers.getNumChannels(), + bufferSizeSamples); + } + else + { + outputBuffers.clear(); + sleep (1); + } + } + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSoundAudioIODevice) +}; + +//============================================================================== +struct DSoundDeviceList +{ + StringArray outputDeviceNames, inputDeviceNames; + Array outputGuids, inputGuids; + + void scan() + { + outputDeviceNames.clear(); + inputDeviceNames.clear(); + outputGuids.clear(); + inputGuids.clear(); + + if (dsDirectSoundEnumerateW != 0) + { + dsDirectSoundEnumerateW (outputEnumProcW, this); + dsDirectSoundCaptureEnumerateW (inputEnumProcW, this); + } + } + + bool operator!= (const DSoundDeviceList& other) const noexcept + { + return outputDeviceNames != other.outputDeviceNames + || inputDeviceNames != other.inputDeviceNames + || outputGuids != other.outputGuids + || inputGuids != other.inputGuids; + } + +private: + static BOOL enumProc (LPGUID lpGUID, String desc, StringArray& names, Array& guids) + { + desc = desc.trim(); + + if (desc.isNotEmpty()) + { + const String origDesc (desc); + + int n = 2; + while (names.contains (desc)) + desc = origDesc + " (" + String (n++) + ")"; + + names.add (desc); + guids.add (lpGUID != nullptr ? *lpGUID : GUID()); + } + + return TRUE; + } + + BOOL outputEnumProc (LPGUID guid, LPCWSTR desc) { return enumProc (guid, desc, outputDeviceNames, outputGuids); } + BOOL inputEnumProc (LPGUID guid, LPCWSTR desc) { return enumProc (guid, desc, inputDeviceNames, inputGuids); } + + static BOOL CALLBACK outputEnumProcW (LPGUID lpGUID, LPCWSTR description, LPCWSTR, LPVOID object) + { + return static_cast (object)->outputEnumProc (lpGUID, description); + } + + static BOOL CALLBACK inputEnumProcW (LPGUID lpGUID, LPCWSTR description, LPCWSTR, LPVOID object) + { + return static_cast (object)->inputEnumProc (lpGUID, description); + } +}; + +//============================================================================== +String DSoundAudioIODevice::openDevice (const BigInteger& inputChannels, + const BigInteger& outputChannels, + double sampleRate_, int bufferSizeSamples_) +{ + closeDevice(); + + sampleRate = sampleRate_; + + if (bufferSizeSamples_ <= 0) + bufferSizeSamples_ = 960; // use as a default size if none is set. + + bufferSizeSamples = bufferSizeSamples_ & ~7; + + DSoundDeviceList dlh; + dlh.scan(); + + enabledInputs = inputChannels; + enabledInputs.setRange (inChannels.size(), + enabledInputs.getHighestBit() + 1 - inChannels.size(), + false); + + inputBuffers.setSize (jmax (1, enabledInputs.countNumberOfSetBits()), bufferSizeSamples); + inputBuffers.clear(); + int numIns = 0; + + for (int i = 0; i <= enabledInputs.getHighestBit(); i += 2) + { + float* left = enabledInputs[i] ? inputBuffers.getWritePointer (numIns++) : nullptr; + float* right = enabledInputs[i + 1] ? inputBuffers.getWritePointer (numIns++) : nullptr; + + if (left != nullptr || right != nullptr) + inChans.add (new DSoundInternalInChannel (dlh.inputDeviceNames [inputDeviceIndex], + dlh.inputGuids [inputDeviceIndex], + (int) sampleRate, bufferSizeSamples, + left, right)); + } + + enabledOutputs = outputChannels; + enabledOutputs.setRange (outChannels.size(), + enabledOutputs.getHighestBit() + 1 - outChannels.size(), + false); + + outputBuffers.setSize (jmax (1, enabledOutputs.countNumberOfSetBits()), bufferSizeSamples); + outputBuffers.clear(); + int numOuts = 0; + + for (int i = 0; i <= enabledOutputs.getHighestBit(); i += 2) + { + float* left = enabledOutputs[i] ? outputBuffers.getWritePointer (numOuts++) : nullptr; + float* right = enabledOutputs[i + 1] ? outputBuffers.getWritePointer (numOuts++) : nullptr; + + if (left != nullptr || right != nullptr) + outChans.add (new DSoundInternalOutChannel (dlh.outputDeviceNames[outputDeviceIndex], + dlh.outputGuids [outputDeviceIndex], + (int) sampleRate, bufferSizeSamples, + left, right)); + } + + String error; + + // boost our priority while opening the devices to try to get better sync between them + const int oldThreadPri = GetThreadPriority (GetCurrentThread()); + const DWORD oldProcPri = GetPriorityClass (GetCurrentProcess()); + SetThreadPriority (GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); + SetPriorityClass (GetCurrentProcess(), REALTIME_PRIORITY_CLASS); + + for (int i = 0; i < outChans.size(); ++i) + { + error = outChans[i]->open(); + + if (error.isNotEmpty()) + { + error = "Error opening " + dlh.outputDeviceNames[i] + ": \"" + error + "\""; + break; + } + } + + if (error.isEmpty()) + { + for (int i = 0; i < inChans.size(); ++i) + { + error = inChans[i]->open(); + + if (error.isNotEmpty()) + { + error = "Error opening " + dlh.inputDeviceNames[i] + ": \"" + error + "\""; + break; + } + } + } + + if (error.isEmpty()) + { + for (int i = 0; i < outChans.size(); ++i) + outChans.getUnchecked(i)->synchronisePosition(); + + for (int i = 0; i < inChans.size(); ++i) + inChans.getUnchecked(i)->synchronisePosition(); + + startThread (9); + sleep (10); + + notify(); + } + else + { + JUCE_DS_LOG ("Opening failed: " + error); + } + + SetThreadPriority (GetCurrentThread(), oldThreadPri); + SetPriorityClass (GetCurrentProcess(), oldProcPri); + + return error; +} + +//============================================================================== +class DSoundAudioIODeviceType : public AudioIODeviceType, + private DeviceChangeDetector +{ +public: + DSoundAudioIODeviceType() + : AudioIODeviceType ("DirectSound"), + DeviceChangeDetector (L"DirectSound"), + hasScanned (false) + { + initialiseDSoundFunctions(); + } + + void scanForDevices() + { + hasScanned = true; + deviceList.scan(); + } + + StringArray getDeviceNames (bool wantInputNames) const + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + return wantInputNames ? deviceList.inputDeviceNames + : deviceList.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 + + if (DSoundAudioIODevice* const d = dynamic_cast (device)) + return asInput ? d->inputDeviceIndex + : d->outputDeviceIndex; + + return -1; + } + + bool hasSeparateInputsAndOutputs() const { return true; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + const int outputIndex = deviceList.outputDeviceNames.indexOf (outputDeviceName); + const int inputIndex = deviceList.inputDeviceNames.indexOf (inputDeviceName); + + if (outputIndex >= 0 || inputIndex >= 0) + return new DSoundAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName + : inputDeviceName, + outputIndex, inputIndex); + + return nullptr; + } + +private: + DSoundDeviceList deviceList; + bool hasScanned; + + void systemDeviceChanged() override + { + DSoundDeviceList newList; + newList.scan(); + + if (newList != deviceList) + { + deviceList = newList; + callDeviceChangeListeners(); + } + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSoundAudioIODeviceType) +}; + +//============================================================================== +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_DirectSound() +{ + return new DSoundAudioIODeviceType(); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_win32_Midi.cpp b/source/modules/juce_audio_devices/native/juce_win32_Midi.cpp new file mode 100644 index 000000000..c45ab2dc5 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_win32_Midi.cpp @@ -0,0 +1,1232 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +struct MidiServiceType +{ + struct InputWrapper + { + virtual ~InputWrapper() {} + + virtual String getDeviceName() = 0; + virtual void start() = 0; + virtual void stop() = 0; + }; + + struct OutputWrapper + { + virtual ~OutputWrapper() {} + + virtual String getDeviceName() = 0; + virtual void sendMessageNow (const MidiMessage&) = 0; + }; + + MidiServiceType() {} + virtual ~MidiServiceType() {} + + virtual StringArray getDevices (bool) = 0; + virtual int getDefaultDeviceIndex (bool) = 0; + + virtual InputWrapper* createInputWrapper (MidiInput*, int, MidiInputCallback*) = 0; + virtual OutputWrapper* createOutputWrapper (int) = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiServiceType) +}; + +//============================================================================== +class WindowsMidiService : public MidiServiceType +{ +private: + struct WindowsInputWrapper : public InputWrapper + { + struct MidiInCollector + { + MidiInCollector (WindowsMidiService& s, + MidiInput* const inputDevice, + MidiInputCallback& cb) + : midiService (s), + input (inputDevice), + callback (cb) + { + } + + ~MidiInCollector() + { + stop(); + + if (deviceHandle != 0) + { + for (int count = 5; --count >= 0;) + { + if (midiInClose (deviceHandle) == MMSYSERR_NOERROR) + break; + + Sleep (20); + } + } + } + + void handleMessage (const uint8* bytes, const uint32 timeStamp) + { + if (bytes[0] >= 0x80 && isStarted) + { + concatenator.pushMidiData (bytes, + MidiMessage::getMessageLengthFromFirstByte (bytes[0]), + convertTimeStamp (timeStamp), + input, + callback); + writeFinishedBlocks(); + } + } + + void handleSysEx (MIDIHDR* const hdr, const uint32 timeStamp) + { + if (isStarted && hdr->dwBytesRecorded > 0) + { + concatenator.pushMidiData (hdr->lpData, (int) hdr->dwBytesRecorded, + convertTimeStamp (timeStamp), input, callback); + writeFinishedBlocks(); + } + } + + void start() + { + if (deviceHandle != 0 && ! isStarted) + { + midiService.activeMidiCollectors.addIfNotAlreadyThere (this); + + for (int i = 0; i < (int) numHeaders; ++i) + { + headers[i].prepare (deviceHandle); + headers[i].write (deviceHandle); + } + + startTime = Time::getMillisecondCounterHiRes(); + MMRESULT res = midiInStart (deviceHandle); + + if (res == MMSYSERR_NOERROR) + { + concatenator.reset(); + isStarted = true; + } + else + { + unprepareAllHeaders(); + } + } + } + + void stop() + { + if (isStarted) + { + isStarted = false; + midiInReset (deviceHandle); + midiInStop (deviceHandle); + midiService.activeMidiCollectors.removeFirstMatchingValue (this); + unprepareAllHeaders(); + concatenator.reset(); + } + } + + static void CALLBACK midiInCallback (HMIDIIN, UINT uMsg, DWORD_PTR dwInstance, + DWORD_PTR midiMessage, DWORD_PTR timeStamp) + { + auto* collector = reinterpret_cast (dwInstance); + + if (collector->midiService.activeMidiCollectors.contains (collector)) + { + if (uMsg == MIM_DATA) + collector->handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp); + else if (uMsg == MIM_LONGDATA) + collector->handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp); + } + } + + HMIDIIN deviceHandle = 0; + + private: + WindowsMidiService& midiService; + MidiInput* input; + MidiInputCallback& callback; + MidiDataConcatenator concatenator { 4096 }; + bool volatile isStarted = false; + double startTime = 0; + + struct MidiHeader + { + MidiHeader() {} + + void prepare (HMIDIIN device) + { + zerostruct (hdr); + hdr.lpData = data; + hdr.dwBufferLength = (DWORD) numElementsInArray (data); + + midiInPrepareHeader (device, &hdr, sizeof (hdr)); + } + + void unprepare (HMIDIIN device) + { + if ((hdr.dwFlags & WHDR_DONE) != 0) + { + int c = 10; + while (--c >= 0 && midiInUnprepareHeader (device, &hdr, sizeof (hdr)) == MIDIERR_STILLPLAYING) + Thread::sleep (20); + + jassert (c >= 0); + } + } + + void write (HMIDIIN device) + { + hdr.dwBytesRecorded = 0; + midiInAddBuffer (device, &hdr, sizeof (hdr)); + } + + void writeIfFinished (HMIDIIN device) + { + if ((hdr.dwFlags & WHDR_DONE) != 0) + write (device); + } + + MIDIHDR hdr; + char data [256]; + + JUCE_DECLARE_NON_COPYABLE (MidiHeader) + }; + + enum { numHeaders = 32 }; + MidiHeader headers [numHeaders]; + + void writeFinishedBlocks() + { + for (int i = 0; i < (int) numHeaders; ++i) + headers[i].writeIfFinished (deviceHandle); + } + + void unprepareAllHeaders() + { + for (int i = 0; i < (int) numHeaders; ++i) + headers[i].unprepare (deviceHandle); + } + + double convertTimeStamp (uint32 timeStamp) + { + auto t = startTime + timeStamp; + auto now = Time::getMillisecondCounterHiRes(); + + if (t > now) + { + if (t > now + 2.0) + startTime -= 1.0; + + t = now; + } + + return t * 0.001; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInCollector) + }; + + //============================================================================== + WindowsInputWrapper (WindowsMidiService& parentService, + MidiInput* const input, + const int index, + MidiInputCallback* const callback) + { + auto names = getDevices(); + UINT deviceId = MIDI_MAPPER; + + if (isPositiveAndBelow (index, names.size())) + { + deviceName = names[index]; + deviceId = index; + } + + collector = new MidiInCollector (parentService, input, *callback); + + HMIDIIN h; + MMRESULT err = midiInOpen (&h, deviceId, + (DWORD_PTR) &MidiInCollector::midiInCallback, + (DWORD_PTR) (MidiInCollector*) collector.get(), + CALLBACK_FUNCTION); + + if (err != MMSYSERR_NOERROR) + throw std::runtime_error ("Failed to create Windows input device wrapper"); + + collector->deviceHandle = h; + } + + ~WindowsInputWrapper() {} + + static StringArray getDevices() + { + StringArray s; + const UINT num = midiInGetNumDevs(); + + for (UINT i = 0; i < num; ++i) + { + MIDIINCAPS mc = { 0 }; + + if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) + s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname))); + } + + s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); + return s; + } + + static int getDefaultDeviceIndex() + { + return 0; + } + + void start() override { collector->start(); } + void stop() override { collector->stop(); } + + String getDeviceName() override + { + return deviceName; + } + + String deviceName; + ScopedPointer collector; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsInputWrapper) + }; + + //============================================================================== + struct WindowsOutputWrapper : public OutputWrapper + { + struct MidiOutHandle + { + int refCount; + UINT deviceId; + HMIDIOUT handle; + + JUCE_LEAK_DETECTOR (MidiOutHandle) + }; + + WindowsOutputWrapper (WindowsMidiService& p, int index) : parent (p) + { + auto names = getDevices(); + UINT deviceId = MIDI_MAPPER; + + if (isPositiveAndBelow (index, names.size())) + { + deviceName = names[index]; + deviceId = index; + } + + if (deviceId == MIDI_MAPPER) + { + // use the microsoft sw synth as a default - best not to allow deviceId + // to be MIDI_MAPPER, or else device sharing breaks + for (int i = 0; i < names.size(); ++i) + if (names[i].containsIgnoreCase ("microsoft")) + deviceId = (UINT) i; + } + + for (int i = parent.activeOutputHandles.size(); --i >= 0;) + { + auto* activeHandle = parent.activeOutputHandles.getUnchecked (i); + + if (activeHandle->deviceId == deviceId) + { + activeHandle->refCount++; + han = activeHandle; + return; + } + } + + for (int i = 4; --i >= 0;) + { + HMIDIOUT h = 0; + MMRESULT res = midiOutOpen (&h, deviceId, 0, 0, CALLBACK_NULL); + + if (res == MMSYSERR_NOERROR) + { + han = new MidiOutHandle(); + han->deviceId = deviceId; + han->refCount = 1; + han->handle = h; + parent.activeOutputHandles.add (han); + return; + } + + if (res == MMSYSERR_ALLOCATED) + Sleep (100); + else + break; + } + + throw std::runtime_error ("Failed to create Windows output device wrapper"); + } + + ~WindowsOutputWrapper() + { + if (parent.activeOutputHandles.contains (han.get()) && --(han->refCount) == 0) + { + midiOutClose (han->handle); + parent.activeOutputHandles.removeFirstMatchingValue (han.get()); + } + } + + void sendMessageNow (const MidiMessage& message) override + { + if (message.getRawDataSize() > 3 || message.isSysEx()) + { + MIDIHDR h = { 0 }; + + h.lpData = (char*) message.getRawData(); + h.dwBytesRecorded = h.dwBufferLength = (DWORD) message.getRawDataSize(); + + if (midiOutPrepareHeader (han->handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR) + { + MMRESULT res = midiOutLongMsg (han->handle, &h, sizeof (MIDIHDR)); + + if (res == MMSYSERR_NOERROR) + { + while ((h.dwFlags & MHDR_DONE) == 0) + Sleep (1); + + int count = 500; // 1 sec timeout + + while (--count >= 0) + { + res = midiOutUnprepareHeader (han->handle, &h, sizeof (MIDIHDR)); + + if (res == MIDIERR_STILLPLAYING) + Sleep (2); + else + break; + } + } + } + } + else + { + for (int i = 0; i < 50; ++i) + { + if (midiOutShortMsg (han->handle, *(unsigned int*) message.getRawData()) != MIDIERR_NOTREADY) + break; + + Sleep (1); + } + } + } + + static Array getDeviceCaps() + { + Array devices; + const UINT num = midiOutGetNumDevs(); + + for (UINT i = 0; i < num; ++i) + { + MIDIOUTCAPS mc = { 0 }; + + if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) + devices.add (mc); + } + + return devices; + } + + static StringArray getDevices() + { + StringArray s; + + for (auto& mc : getDeviceCaps()) + s.add (String (mc.szPname, (size_t) numElementsInArray (mc.szPname))); + + s.appendNumbersToDuplicates (false, false, CharPointer_UTF8 ("-"), CharPointer_UTF8 ("")); + return s; + } + + static int getDefaultDeviceIndex() + { + int n = 0; + + for (auto& mc : getDeviceCaps()) + { + if ((mc.wTechnology & MOD_MAPPER) != 0) + return n; + + ++n; + } + + return 0; + } + + String getDeviceName() override + { + return deviceName; + } + + WindowsMidiService& parent; + String deviceName; + + ScopedPointer han; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsOutputWrapper) + }; + +public: + WindowsMidiService() {} + + StringArray getDevices (bool isInput) override + { + return isInput ? WindowsInputWrapper::getDevices() + : WindowsOutputWrapper::getDevices(); + } + + int getDefaultDeviceIndex (bool isInput) override + { + return isInput ? WindowsInputWrapper::getDefaultDeviceIndex() + : WindowsOutputWrapper::getDefaultDeviceIndex(); + } + + InputWrapper* createInputWrapper (MidiInput* input, int index, MidiInputCallback* callback) override + { + return new WindowsInputWrapper (*this, input, index, callback); + } + + OutputWrapper* createOutputWrapper (int index) override + { + return new WindowsOutputWrapper (*this, index); + } + +private: + Array activeMidiCollectors; + Array activeOutputHandles; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsMidiService) +}; + +//============================================================================== +#if JUCE_USE_WINRT_MIDI + +using namespace Microsoft::WRL; + +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Devices::Midi; +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Storage::Streams; + +class WinRTMidiService : public MidiServiceType +{ +private: + template + struct MidiIODeviceWatcher + { + struct DeviceInfo + { + String name; + String id; + bool isDefault = false; + }; + + MidiIODeviceWatcher (ComSmartPtr& comFactory) + : factory (comFactory) + { + } + + ~MidiIODeviceWatcher() + { + stop(); + } + + bool start() + { + HSTRING deviceSelector; + HRESULT hr = factory->GetDeviceSelector (&deviceSelector); + if (FAILED (hr)) + return false; + + auto deviceInformationFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Devices_Enumeration_DeviceInformation[0]); + if (deviceInformationFactory == nullptr) + return false; + + hr = deviceInformationFactory->CreateWatcherAqsFilter (deviceSelector, watcher.resetAndGetPointerAddress()); + if (FAILED (hr)) + return false; + + class DeviceEnumerationThread : public Thread + { + public: + DeviceEnumerationThread (String threadName, MidiIODeviceWatcher& p) + : Thread (threadName), parent (p) + {} + + void run() override + { + auto parentPtr = &parent; + + parent.watcher->add_Added ( + Callback> ( + [parentPtr](IDeviceWatcher*, IDeviceInformation* info) { return parentPtr->addDevice (info); } + ).Get(), + &parent.deviceAddedToken); + + parent.watcher->add_Removed ( + Callback> ( + [parentPtr](IDeviceWatcher*, IDeviceInformationUpdate* info) { return parentPtr->removeDevice (info); } + ).Get(), + &parent.deviceRemovedToken); + + EventRegistrationToken deviceEnumerationCompletedToken { 0 }; + parent.watcher->add_EnumerationCompleted ( + Callback> ( + [this](IDeviceWatcher*, IInspectable*) { enumerationCompleted.signal(); return S_OK; } + ).Get(), + &deviceEnumerationCompletedToken); + + parent.watcher->Start(); + enumerationCompleted.wait(); + + if (deviceEnumerationCompletedToken.value != 0) + parent.watcher->remove_EnumerationCompleted (deviceEnumerationCompletedToken); + } + + private: + MidiIODeviceWatcher& parent; + WaitableEvent enumerationCompleted; + }; + + DeviceEnumerationThread enumerationThread ("WinRT Device Enumeration Thread", *this); + enumerationThread.startThread(); + enumerationThread.waitForThreadToExit (4000); + + return true; + } + + bool stop() + { + if (watcher == nullptr) + return true; + + if (deviceAddedToken.value != 0) + { + HRESULT hr = watcher->remove_Added (deviceAddedToken); + if (FAILED (hr)) + return false; + + deviceAddedToken.value = 0; + } + + if (deviceRemovedToken.value != 0) + { + HRESULT hr = watcher->remove_Removed (deviceRemovedToken); + if (FAILED (hr)) + return false; + + deviceRemovedToken.value = 0; + } + + HRESULT hr = watcher->Stop(); + if (FAILED (hr)) + return false; + + watcher = nullptr; + return true; + } + + HRESULT addDevice (IDeviceInformation* addedDeviceInfo) + { + boolean isEnabled; + HRESULT hr = addedDeviceInfo->get_IsEnabled (&isEnabled); + if (FAILED (hr)) + return S_OK; + + if (! isEnabled) + return S_OK; + + const ScopedLock lock (deviceChanges); + + DeviceInfo info; + + HSTRING name; + hr = addedDeviceInfo->get_Name (&name); + if (FAILED (hr)) + return S_OK; + + info.name = WinRTWrapper::getInstance()->hStringToString (name); + + HSTRING id; + hr = addedDeviceInfo->get_Id (&id); + if (FAILED (hr)) + return S_OK; + + info.id = WinRTWrapper::getInstance()->hStringToString (id); + + boolean isDefault; + hr = addedDeviceInfo->get_IsDefault (&isDefault); + if (FAILED (hr)) + return S_OK; + + info.isDefault = isDefault != 0; + + connectedDevices.add (info); + + return S_OK; + } + + HRESULT removeDevice (IDeviceInformationUpdate* removedDeviceInfo) + { + const ScopedLock lock (deviceChanges); + + HSTRING removedDeviceIdHstr; + removedDeviceInfo->get_Id (&removedDeviceIdHstr); + String removedDeviceId = WinRTWrapper::getInstance()->hStringToString (removedDeviceIdHstr); + + for (int i = 0; i < connectedDevices.size(); ++i) + { + if (connectedDevices[i].id == removedDeviceId) + { + connectedDevices.remove (i); + break; + } + } + + return S_OK; + } + + StringArray getDevices() + { + { + const ScopedLock lock (deviceChanges); + lastQueriedConnectedDevices = connectedDevices; + } + + StringArray result; + for (auto info : lastQueriedConnectedDevices.get()) + result.add (info.name); + + return result; + } + + int getDefaultDeviceIndex() + { + auto& lastDevices = lastQueriedConnectedDevices.get(); + for (int i = 0; i < lastDevices.size(); ++i) + if (lastDevices[i].isDefault) + return i; + + return 0; + } + + String getDeviceNameFromIndex (const int index) + { + if (isPositiveAndBelow (index, lastQueriedConnectedDevices.get().size())) + return lastQueriedConnectedDevices.get()[index].name; + + return {}; + } + + String getDeviceID (const String name) + { + const ScopedLock lock (deviceChanges); + + for (auto info : connectedDevices) + if (info.name == name) + return info.id; + + return {}; + } + + ComSmartPtr& factory; + + EventRegistrationToken deviceAddedToken { 0 }, + deviceRemovedToken { 0 }; + + ComSmartPtr watcher; + + Array connectedDevices; + CriticalSection deviceChanges; + ThreadLocalValue> lastQueriedConnectedDevices; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiIODeviceWatcher); + }; + + template + class OpenMidiPortThread : public Thread + { + public: + OpenMidiPortThread (String threadName, + String midiDeviceId, + ComSmartPtr& comFactory, + ComSmartPtr& comPort) + : Thread (threadName), + deviceId (midiDeviceId), + factory (comFactory), + port (comPort) + { + } + + ~OpenMidiPortThread() + { + } + + void run() override + { + WinRTWrapper::ScopedHString hDeviceId (deviceId); + ComSmartPtr> asyncOp; + HRESULT hr = factory->FromIdAsync (hDeviceId.get(), asyncOp.resetAndGetPointerAddress()); + if (FAILED (hr)) + return; + + hr = asyncOp->put_Completed (Callback> ( + [this] (IAsyncOperation* asyncOpPtr, AsyncStatus) + { + if (asyncOpPtr == nullptr) + return E_ABORT; + + HRESULT hr = asyncOpPtr->GetResults (port.resetAndGetPointerAddress()); + if (FAILED (hr)) + return hr; + + portOpened.signal(); + return S_OK; + } + ).Get()); + + // When using Bluetooth the asynchronous port opening operation will occasionally + // hang, so we use a timeout. We will be able to remove this when Microsoft + // improves the Bluetooth MIDI stack. + portOpened.wait (2000); + } + + const String deviceId; + ComSmartPtr& factory; + ComSmartPtr& port; + + WaitableEvent portOpened { true }; + }; + + struct WinRTInputWrapper : public InputWrapper + { + WinRTInputWrapper (WinRTMidiService& service, + MidiInput* const input, + const int index, + MidiInputCallback& cb) + : inputDevice (input), + callback (cb), + concatenator (4096) + { + const ScopedLock lock (service.inputDeviceWatcher->deviceChanges); + + deviceName = service.inputDeviceWatcher->getDeviceNameFromIndex (index); + if (deviceName.isEmpty()) + throw std::runtime_error ("Invalid device index"); + + const auto deviceID = service.inputDeviceWatcher->getDeviceID (deviceName); + if (deviceID.isEmpty()) + throw std::runtime_error ("Device unavailable"); + + OpenMidiPortThread portThread ("Open WinRT MIDI input port", + deviceID, + service.midiInFactory, + midiInPort); + portThread.startThread(); + portThread.waitForThreadToExit (-1); + if (midiInPort == nullptr) + throw std::runtime_error ("Timed out waiting for midi input port creation"); + + startTime = Time::getMillisecondCounterHiRes(); + + HRESULT hr = midiInPort->add_MessageReceived ( + Callback> ( + [this] (IMidiInPort*, IMidiMessageReceivedEventArgs* args) { return midiInMessageReceived (args); } + ).Get(), + &midiInMessageToken); + if (FAILED (hr)) + throw std::runtime_error ("Failed to set midi input callback"); + } + + ~WinRTInputWrapper() + { + if (midiInMessageToken.value != 0) + midiInPort->remove_MessageReceived (midiInMessageToken); + + midiInPort = nullptr; + } + + void start() override + { + if (!isStarted) + { + concatenator.reset(); + isStarted = true; + } + } + + void stop() override + { + if (isStarted) + { + isStarted = false; + concatenator.reset(); + } + } + + String getDeviceName() override + { + return deviceName; + } + + HRESULT midiInMessageReceived (IMidiMessageReceivedEventArgs* args) + { + if (! isStarted) + return S_OK; + + ComSmartPtr message; + HRESULT hr = args->get_Message (message.resetAndGetPointerAddress()); + if (FAILED (hr)) + return hr; + + ComSmartPtr buffer; + hr = message->get_RawData (buffer.resetAndGetPointerAddress()); + if (FAILED (hr)) + return hr; + + ComSmartPtr bufferByteAccess; + hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress()); + if (FAILED (hr)) + return hr; + + uint8_t* bufferData = nullptr; + hr = bufferByteAccess->Buffer (&bufferData); + if (FAILED (hr)) + return hr; + + uint32_t numBytes = 0; + hr = buffer->get_Length (&numBytes); + if (FAILED (hr)) + return hr; + + ABI::Windows::Foundation::TimeSpan timespan; + hr = message->get_Timestamp (×pan); + if (FAILED (hr)) + return hr; + + concatenator.pushMidiData (bufferData, + numBytes, + convertTimeStamp (timespan.Duration), + inputDevice, + callback); + + return S_OK; + } + + double convertTimeStamp (int64 timestamp) + { + const auto millisecondsSinceStart = static_cast (timestamp) / 10000.0; + double t = startTime + millisecondsSinceStart; + + const double now = Time::getMillisecondCounterHiRes(); + if (t > now) + { + if (t > now + 2.0) + startTime -= 1.0; + + t = now; + } + + return t * 0.001; + } + + MidiInput* inputDevice; + MidiInputCallback& callback; + String deviceName; + MidiDataConcatenator concatenator; + ComSmartPtr midiInPort; + EventRegistrationToken midiInMessageToken { 0 }; + + double startTime = 0; + bool isStarted = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTInputWrapper); + }; + + struct WinRTOutputWrapper : public OutputWrapper + { + WinRTOutputWrapper (WinRTMidiService& service, const int index) + { + const ScopedLock lock (service.outputDeviceWatcher->deviceChanges); + + deviceName = service.outputDeviceWatcher->getDeviceNameFromIndex (index); + if (deviceName.isEmpty()) + throw std::runtime_error ("Invalid device index"); + + const auto deviceID = service.outputDeviceWatcher->getDeviceID (deviceName); + if (deviceID.isEmpty()) + throw std::runtime_error ("Device unavailable"); + + OpenMidiPortThread portThread ("Open WinRT MIDI output port", + deviceID, + service.midiOutFactory, + midiOutPort); + portThread.startThread(); + portThread.waitForThreadToExit (-1); + if (midiOutPort == nullptr) + throw std::runtime_error ("Timed out waiting for midi output port creation"); + + auto bufferFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Storage_Streams_Buffer[0]); + if (bufferFactory == nullptr) + throw std::runtime_error ("Failed to create output buffer factory"); + + HRESULT hr = bufferFactory->Create (static_cast (65536), buffer.resetAndGetPointerAddress()); + if (FAILED (hr)) + throw std::runtime_error ("Failed to create output buffer"); + + hr = buffer->QueryInterface (bufferByteAccess.resetAndGetPointerAddress()); + if (FAILED (hr)) + throw std::runtime_error ("Failed to get buffer byte access"); + + hr = bufferByteAccess->Buffer (&bufferData); + if (FAILED (hr)) + throw std::runtime_error ("Failed to get buffer data pointer"); + } + + ~WinRTOutputWrapper() {} + + void sendMessageNow (const MidiMessage& message) override + { + const UINT32 numBytes = message.getRawDataSize(); + HRESULT hr = buffer->put_Length (numBytes); + if (FAILED (hr)) + jassertfalse; + + memcpy_s (bufferData, numBytes, message.getRawData(), numBytes); + + midiOutPort->SendBuffer (buffer); + } + + String getDeviceName() override + { + return deviceName; + } + + String deviceName; + ComSmartPtr midiOutPort; + ComSmartPtr buffer; + ComSmartPtr bufferByteAccess; + uint8_t* bufferData = nullptr; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTOutputWrapper); + }; + +public: + WinRTMidiService() + { + if (! WinRTWrapper::getInstance()->isInitialised()) + throw std::runtime_error ("Failed to initialise the WinRT wrapper"); + + midiInFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Devices_Midi_MidiInPort[0]); + if (midiInFactory == nullptr) + throw std::runtime_error ("Failed to create midi in factory"); + + midiOutFactory = WinRTWrapper::getInstance()->getWRLFactory (&RuntimeClass_Windows_Devices_Midi_MidiOutPort[0]); + if (midiOutFactory == nullptr) + throw std::runtime_error ("Failed to create midi out factory"); + + inputDeviceWatcher = new MidiIODeviceWatcher (midiInFactory); + if (! inputDeviceWatcher->start()) + throw std::runtime_error ("Failed to start midi input device watcher"); + + outputDeviceWatcher = new MidiIODeviceWatcher (midiOutFactory); + if (! outputDeviceWatcher->start()) + throw std::runtime_error ("Failed to start midi output device watcher"); + } + + ~WinRTMidiService() + { + } + + StringArray getDevices (bool isInput) override + { + return isInput ? inputDeviceWatcher ->getDevices() + : outputDeviceWatcher->getDevices(); + } + + int getDefaultDeviceIndex (bool isInput) override + { + return isInput ? inputDeviceWatcher ->getDefaultDeviceIndex() + : outputDeviceWatcher->getDefaultDeviceIndex(); + } + + InputWrapper* createInputWrapper (MidiInput* input, int index, MidiInputCallback* callback) override + { + return new WinRTInputWrapper (*this, input, index, *callback); + } + + OutputWrapper* createOutputWrapper (int index) override + { + return new WinRTOutputWrapper (*this, index); + } + + ComSmartPtr midiInFactory; + ComSmartPtr midiOutFactory; + + ScopedPointer> inputDeviceWatcher; + ScopedPointer> outputDeviceWatcher; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WinRTMidiService) +}; + +#endif // JUCE_USE_WINRT_MIDI + +//============================================================================== +class MidiService : public DeletedAtShutdown +{ +public: + ~MidiService(); + + MidiServiceType* getService(); + + juce_DeclareSingleton (MidiService, false) + +private: + MidiService(); + + ScopedPointer internal; +}; + +juce_ImplementSingleton (MidiService) + +MidiService::~MidiService() +{ + clearSingletonInstance(); +} + +MidiServiceType* MidiService::getService() +{ + return internal.get(); +} + +MidiService::MidiService() +{ + #if JUCE_USE_WINRT_MIDI + try + { + internal = new WinRTMidiService(); + return; + } + catch (std::runtime_error&) + { + } + #endif + + internal = new WindowsMidiService(); +} + +//============================================================================== +StringArray MidiInput::getDevices() +{ + return MidiService::getInstance()->getService()->getDevices (true); +} + +int MidiInput::getDefaultDeviceIndex() +{ + return MidiService::getInstance()->getService()->getDefaultDeviceIndex (true); +} + +MidiInput::MidiInput (const String& deviceName) + : name (deviceName) +{ +} + +MidiInput* MidiInput::openDevice (const int index, MidiInputCallback* const callback) +{ + if (callback == nullptr) + return nullptr; + + ScopedPointer in (new MidiInput ({})); + ScopedPointer wrapper; + + try + { + wrapper = MidiService::getInstance()->getService()->createInputWrapper (in, index, callback); + } + catch (std::runtime_error&) + { + return nullptr; + } + + in->setName (wrapper->getDeviceName()); + in->internal = wrapper.release(); + return in.release(); +} + +MidiInput::~MidiInput() +{ + delete static_cast (internal); +} + +void MidiInput::start() { static_cast (internal)->start(); } +void MidiInput::stop() { static_cast (internal)->stop(); } + +//============================================================================== +StringArray MidiOutput::getDevices() +{ + return MidiService::getInstance()->getService()->getDevices (false); +} + +int MidiOutput::getDefaultDeviceIndex() +{ + return MidiService::getInstance()->getService()->getDefaultDeviceIndex (false); +} + +MidiOutput* MidiOutput::openDevice (const int index) +{ + ScopedPointer wrapper; + + try + { + wrapper = MidiService::getInstance()->getService()->createOutputWrapper (index); + } + catch (std::runtime_error&) + { + return nullptr; + } + + ScopedPointer out (new MidiOutput (wrapper->getDeviceName())); + out->internal = wrapper.release(); + return out.release(); +} + +MidiOutput::~MidiOutput() +{ + stopBackgroundThread(); + delete static_cast (internal); +} + +void MidiOutput::sendMessageNow (const MidiMessage& message) +{ + static_cast (internal)->sendMessageNow (message); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/native/juce_win32_WASAPI.cpp b/source/modules/juce_audio_devices/native/juce_win32_WASAPI.cpp new file mode 100644 index 000000000..d0380d789 --- /dev/null +++ b/source/modules/juce_audio_devices/native/juce_win32_WASAPI.cpp @@ -0,0 +1,1731 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#ifndef JUCE_WASAPI_LOGGING + #define JUCE_WASAPI_LOGGING 0 +#endif + +//============================================================================== +namespace WasapiClasses +{ + +void logFailure (HRESULT hr) +{ + ignoreUnused (hr); + jassert (hr != (HRESULT) 0x800401f0); // If you hit this, it means you're trying to call from + // a thread which hasn't been initialised with CoInitialize(). + + #if JUCE_WASAPI_LOGGING + if (FAILED (hr)) + { + const char* m = nullptr; + + switch (hr) + { + case E_POINTER: m = "E_POINTER"; break; + case E_INVALIDARG: m = "E_INVALIDARG"; break; + case E_NOINTERFACE: m = "E_NOINTERFACE"; break; + + #define JUCE_WASAPI_ERR(desc, n) \ + case MAKE_HRESULT(1, 0x889, n): m = #desc; break; + + JUCE_WASAPI_ERR (AUDCLNT_E_NOT_INITIALIZED, 0x001) + JUCE_WASAPI_ERR (AUDCLNT_E_ALREADY_INITIALIZED, 0x002) + JUCE_WASAPI_ERR (AUDCLNT_E_WRONG_ENDPOINT_TYPE, 0x003) + JUCE_WASAPI_ERR (AUDCLNT_E_DEVICE_INVALIDATED, 0x004) + JUCE_WASAPI_ERR (AUDCLNT_E_NOT_STOPPED, 0x005) + JUCE_WASAPI_ERR (AUDCLNT_E_BUFFER_TOO_LARGE, 0x006) + JUCE_WASAPI_ERR (AUDCLNT_E_OUT_OF_ORDER, 0x007) + JUCE_WASAPI_ERR (AUDCLNT_E_UNSUPPORTED_FORMAT, 0x008) + JUCE_WASAPI_ERR (AUDCLNT_E_INVALID_SIZE, 0x009) + JUCE_WASAPI_ERR (AUDCLNT_E_DEVICE_IN_USE, 0x00a) + JUCE_WASAPI_ERR (AUDCLNT_E_BUFFER_OPERATION_PENDING, 0x00b) + JUCE_WASAPI_ERR (AUDCLNT_E_THREAD_NOT_REGISTERED, 0x00c) + JUCE_WASAPI_ERR (AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED, 0x00e) + JUCE_WASAPI_ERR (AUDCLNT_E_ENDPOINT_CREATE_FAILED, 0x00f) + JUCE_WASAPI_ERR (AUDCLNT_E_SERVICE_NOT_RUNNING, 0x010) + JUCE_WASAPI_ERR (AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED, 0x011) + JUCE_WASAPI_ERR (AUDCLNT_E_EXCLUSIVE_MODE_ONLY, 0x012) + JUCE_WASAPI_ERR (AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL, 0x013) + JUCE_WASAPI_ERR (AUDCLNT_E_EVENTHANDLE_NOT_SET, 0x014) + JUCE_WASAPI_ERR (AUDCLNT_E_INCORRECT_BUFFER_SIZE, 0x015) + JUCE_WASAPI_ERR (AUDCLNT_E_BUFFER_SIZE_ERROR, 0x016) + JUCE_WASAPI_ERR (AUDCLNT_E_CPUUSAGE_EXCEEDED, 0x017) + JUCE_WASAPI_ERR (AUDCLNT_E_BUFFER_ERROR, 0x018) + JUCE_WASAPI_ERR (AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED, 0x019) + JUCE_WASAPI_ERR (AUDCLNT_E_INVALID_DEVICE_PERIOD, 0x020) + default: break; + } + + Logger::writeToLog ("WASAPI error: " + (m != nullptr ? String (m) + : String::toHexString ((int) hr))); + } + #endif +} + +#undef check + +bool check (HRESULT hr) +{ + logFailure (hr); + return SUCCEEDED (hr); +} + +//============================================================================== +} + +#if JUCE_MINGW + struct PROPERTYKEY + { + GUID fmtid; + DWORD pid; + }; + + WINOLEAPI PropVariantClear (PROPVARIANT*); +#endif + +#if JUCE_MINGW && defined (KSDATAFORMAT_SUBTYPE_PCM) + #undef KSDATAFORMAT_SUBTYPE_PCM + #undef KSDATAFORMAT_SUBTYPE_IEEE_FLOAT +#endif + +#ifndef KSDATAFORMAT_SUBTYPE_PCM + #define KSDATAFORMAT_SUBTYPE_PCM uuidFromString ("00000001-0000-0010-8000-00aa00389b71") + #define KSDATAFORMAT_SUBTYPE_IEEE_FLOAT uuidFromString ("00000003-0000-0010-8000-00aa00389b71") +#endif + +#define JUCE_IUNKNOWNCLASS(name, guid) JUCE_COMCLASS(name, guid) : public IUnknown +#define JUCE_COMCALL virtual HRESULT STDMETHODCALLTYPE + +enum EDataFlow +{ + eRender = 0, + eCapture = (eRender + 1), + eAll = (eCapture + 1) +}; + +enum +{ + DEVICE_STATE_ACTIVE = 1 +}; + +enum +{ + AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY = 1, + AUDCLNT_BUFFERFLAGS_SILENT = 2 +}; + +JUCE_IUNKNOWNCLASS (IPropertyStore, "886d8eeb-8cf2-4446-8d02-cdba1dbdcf99") +{ + JUCE_COMCALL GetCount (DWORD*) = 0; + JUCE_COMCALL GetAt (DWORD, PROPERTYKEY*) = 0; + JUCE_COMCALL GetValue (const PROPERTYKEY&, PROPVARIANT*) = 0; + JUCE_COMCALL SetValue (const PROPERTYKEY&, const PROPVARIANT&) = 0; + JUCE_COMCALL Commit() = 0; +}; + +JUCE_IUNKNOWNCLASS (IMMDevice, "D666063F-1587-4E43-81F1-B948E807363F") +{ + JUCE_COMCALL Activate (REFIID, DWORD, PROPVARIANT*, void**) = 0; + JUCE_COMCALL OpenPropertyStore (DWORD, IPropertyStore**) = 0; + JUCE_COMCALL GetId (LPWSTR*) = 0; + JUCE_COMCALL GetState (DWORD*) = 0; +}; + +JUCE_IUNKNOWNCLASS (IMMEndpoint, "1BE09788-6894-4089-8586-9A2A6C265AC5") +{ + JUCE_COMCALL GetDataFlow (EDataFlow*) = 0; +}; + +struct IMMDeviceCollection : public IUnknown +{ + JUCE_COMCALL GetCount (UINT*) = 0; + JUCE_COMCALL Item (UINT, IMMDevice**) = 0; +}; + +enum ERole +{ + eConsole = 0, + eMultimedia = (eConsole + 1), + eCommunications = (eMultimedia + 1) +}; + +JUCE_IUNKNOWNCLASS (IMMNotificationClient, "7991EEC9-7E89-4D85-8390-6C703CEC60C0") +{ + JUCE_COMCALL OnDeviceStateChanged (LPCWSTR, DWORD) = 0; + JUCE_COMCALL OnDeviceAdded (LPCWSTR) = 0; + JUCE_COMCALL OnDeviceRemoved (LPCWSTR) = 0; + JUCE_COMCALL OnDefaultDeviceChanged (EDataFlow, ERole, LPCWSTR) = 0; + JUCE_COMCALL OnPropertyValueChanged (LPCWSTR, const PROPERTYKEY) = 0; +}; + +JUCE_IUNKNOWNCLASS (IMMDeviceEnumerator, "A95664D2-9614-4F35-A746-DE8DB63617E6") +{ + JUCE_COMCALL EnumAudioEndpoints (EDataFlow, DWORD, IMMDeviceCollection**) = 0; + JUCE_COMCALL GetDefaultAudioEndpoint (EDataFlow, ERole, IMMDevice**) = 0; + JUCE_COMCALL GetDevice (LPCWSTR, IMMDevice**) = 0; + JUCE_COMCALL RegisterEndpointNotificationCallback (IMMNotificationClient*) = 0; + JUCE_COMCALL UnregisterEndpointNotificationCallback (IMMNotificationClient*) = 0; +}; + +JUCE_COMCLASS (MMDeviceEnumerator, "BCDE0395-E52F-467C-8E3D-C4579291692E"); + +typedef LONGLONG REFERENCE_TIME; + +enum AVRT_PRIORITY +{ + AVRT_PRIORITY_LOW = -1, + AVRT_PRIORITY_NORMAL, + AVRT_PRIORITY_HIGH, + AVRT_PRIORITY_CRITICAL +}; + +enum AUDCLNT_SHAREMODE +{ + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_SHAREMODE_EXCLUSIVE +}; + +JUCE_IUNKNOWNCLASS (IAudioClient, "1CB9AD4C-DBFA-4c32-B178-C2F568A703B2") +{ + JUCE_COMCALL Initialize (AUDCLNT_SHAREMODE, DWORD, REFERENCE_TIME, REFERENCE_TIME, const WAVEFORMATEX*, LPCGUID) = 0; + JUCE_COMCALL GetBufferSize (UINT32*) = 0; + JUCE_COMCALL GetStreamLatency (REFERENCE_TIME*) = 0; + JUCE_COMCALL GetCurrentPadding (UINT32*) = 0; + JUCE_COMCALL IsFormatSupported (AUDCLNT_SHAREMODE, const WAVEFORMATEX*, WAVEFORMATEX**) = 0; + JUCE_COMCALL GetMixFormat (WAVEFORMATEX**) = 0; + JUCE_COMCALL GetDevicePeriod (REFERENCE_TIME*, REFERENCE_TIME*) = 0; + JUCE_COMCALL Start() = 0; + JUCE_COMCALL Stop() = 0; + JUCE_COMCALL Reset() = 0; + JUCE_COMCALL SetEventHandle (HANDLE) = 0; + JUCE_COMCALL GetService (REFIID, void**) = 0; +}; + +JUCE_IUNKNOWNCLASS (IAudioCaptureClient, "C8ADBD64-E71E-48a0-A4DE-185C395CD317") +{ + JUCE_COMCALL GetBuffer (BYTE**, UINT32*, DWORD*, UINT64*, UINT64*) = 0; + JUCE_COMCALL ReleaseBuffer (UINT32) = 0; + JUCE_COMCALL GetNextPacketSize (UINT32*) = 0; +}; + +JUCE_IUNKNOWNCLASS (IAudioRenderClient, "F294ACFC-3146-4483-A7BF-ADDCA7C260E2") +{ + JUCE_COMCALL GetBuffer (UINT32, BYTE**) = 0; + JUCE_COMCALL ReleaseBuffer (UINT32, DWORD) = 0; +}; + +JUCE_IUNKNOWNCLASS (IAudioEndpointVolume, "5CDF2C82-841E-4546-9722-0CF74078229A") +{ + JUCE_COMCALL RegisterControlChangeNotify (void*) = 0; + JUCE_COMCALL UnregisterControlChangeNotify (void*) = 0; + JUCE_COMCALL GetChannelCount (UINT*) = 0; + JUCE_COMCALL SetMasterVolumeLevel (float, LPCGUID) = 0; + JUCE_COMCALL SetMasterVolumeLevelScalar (float, LPCGUID) = 0; + JUCE_COMCALL GetMasterVolumeLevel (float*) = 0; + JUCE_COMCALL GetMasterVolumeLevelScalar (float*) = 0; + JUCE_COMCALL SetChannelVolumeLevel (UINT, float, LPCGUID) = 0; + JUCE_COMCALL SetChannelVolumeLevelScalar (UINT, float, LPCGUID) = 0; + JUCE_COMCALL GetChannelVolumeLevel (UINT, float*) = 0; + JUCE_COMCALL GetChannelVolumeLevelScalar (UINT, float*) = 0; + JUCE_COMCALL SetMute (BOOL, LPCGUID) = 0; + JUCE_COMCALL GetMute (BOOL*) = 0; + JUCE_COMCALL GetVolumeStepInfo (UINT*, UINT*) = 0; + JUCE_COMCALL VolumeStepUp (LPCGUID) = 0; + JUCE_COMCALL VolumeStepDown (LPCGUID) = 0; + JUCE_COMCALL QueryHardwareSupport (DWORD*) = 0; + JUCE_COMCALL GetVolumeRange (float*, float*, float*) = 0; +}; + +enum AudioSessionDisconnectReason +{ + DisconnectReasonDeviceRemoval = 0, + DisconnectReasonServerShutdown = 1, + DisconnectReasonFormatChanged = 2, + DisconnectReasonSessionLogoff = 3, + DisconnectReasonSessionDisconnected = 4, + DisconnectReasonExclusiveModeOverride = 5 +}; + +enum AudioSessionState +{ + AudioSessionStateInactive = 0, + AudioSessionStateActive = 1, + AudioSessionStateExpired = 2 +}; + +JUCE_IUNKNOWNCLASS (IAudioSessionEvents, "24918ACC-64B3-37C1-8CA9-74A66E9957A8") +{ + JUCE_COMCALL OnDisplayNameChanged (LPCWSTR, LPCGUID) = 0; + JUCE_COMCALL OnIconPathChanged (LPCWSTR, LPCGUID) = 0; + JUCE_COMCALL OnSimpleVolumeChanged (float, BOOL, LPCGUID) = 0; + JUCE_COMCALL OnChannelVolumeChanged (DWORD, float*, DWORD, LPCGUID) = 0; + JUCE_COMCALL OnGroupingParamChanged (LPCGUID, LPCGUID) = 0; + JUCE_COMCALL OnStateChanged (AudioSessionState) = 0; + JUCE_COMCALL OnSessionDisconnected (AudioSessionDisconnectReason) = 0; +}; + +JUCE_IUNKNOWNCLASS (IAudioSessionControl, "F4B1A599-7266-4319-A8CA-E70ACB11E8CD") +{ + JUCE_COMCALL GetState (AudioSessionState*) = 0; + JUCE_COMCALL GetDisplayName (LPWSTR*) = 0; + JUCE_COMCALL SetDisplayName (LPCWSTR, LPCGUID) = 0; + JUCE_COMCALL GetIconPath (LPWSTR*) = 0; + JUCE_COMCALL SetIconPath (LPCWSTR, LPCGUID) = 0; + JUCE_COMCALL GetGroupingParam (GUID*) = 0; + JUCE_COMCALL SetGroupingParam (LPCGUID, LPCGUID) = 0; + JUCE_COMCALL RegisterAudioSessionNotification (IAudioSessionEvents*) = 0; + JUCE_COMCALL UnregisterAudioSessionNotification (IAudioSessionEvents*) = 0; +}; + +#undef JUCE_COMCALL +#undef JUCE_COMCLASS +#undef JUCE_IUNKNOWNCLASS + +//============================================================================== +namespace WasapiClasses +{ + +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 roundToInt (sampleRate * ((double) t) * 0.0000001); +} + +REFERENCE_TIME samplesToRefTime (const int numSamples, const double sampleRate) noexcept +{ + return (REFERENCE_TIME) ((numSamples * 10000.0 * 1000.0 / sampleRate) + 0.5); +} + +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& d, const bool exclusiveMode) + : device (d), + sampleRate (0), + defaultSampleRate (0), + numChannels (0), + actualNumChannels (0), + minBufferSize (0), + defaultBufferSize (0), + latencySamples (0), + useExclusiveMode (exclusiveMode), + actualBufferSize (0), + bytesPerSample (0), + bytesPerFrame (0), + sampleRateHasChanged (false), + shouldClose (false) + { + clientEvent = CreateEvent (nullptr, false, false, nullptr); + + 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); + mixFormatChannelMask = format.dwChannelMask; + + rates.addUsingDefaultSort (defaultSampleRate); + + if (useExclusiveMode + && findSupportedFormat (tempClient, defaultSampleRate, format.dwChannelMask, format)) + { + // Got a format that is supported by the device so we can ask what sample rates are supported (in whatever format) + } + + static const int ratesToTest[] = { 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000 }; + + for (int i = 0; i < numElementsInArray (ratesToTest); ++i) + { + if (rates.contains (ratesToTest[i])) + continue; + + format.Format.nSamplesPerSec = (DWORD) ratesToTest[i]; + format.Format.nAvgBytesPerSec = (DWORD) (format.Format.nSamplesPerSec * format.Format.nChannels * format.Format.wBitsPerSample / 8); + + if (SUCCEEDED (tempClient->IsFormatSupported (useExclusiveMode ? AUDCLNT_SHAREMODE_EXCLUSIVE + : AUDCLNT_SHAREMODE_SHARED, + (WAVEFORMATEX*) &format, 0))) + if (! rates.contains (ratesToTest[i])) + rates.addUsingDefaultSort (ratesToTest[i]); + } + } + + virtual ~WASAPIDeviceBase() + { + device = nullptr; + CloseHandle (clientEvent); + } + + bool isOk() const noexcept { return defaultBufferSize > 0 && defaultSampleRate > 0; } + + bool openClient (const double newSampleRate, const BigInteger& newChannels, const int bufferSizeSamples) + { + 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 + && tryInitialisingWithBufferSize (bufferSizeSamples)) + { + 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; + } + + void deviceBecameInactive() + { + shouldClose = true; + } + + //============================================================================== + ComSmartPtr device; + ComSmartPtr client; + double sampleRate, defaultSampleRate; + int numChannels, actualNumChannels; + int minBufferSize, defaultBufferSize, latencySamples; + DWORD mixFormatChannelMask; + const bool useExclusiveMode; + Array rates; + HANDLE clientEvent; + BigInteger channels; + Array channelMaps; + UINT32 actualBufferSize; + int bytesPerSample, bytesPerFrame; + bool sampleRateHasChanged, shouldClose; + + virtual void updateFormat (bool isFloat) = 0; + +private: + //============================================================================== + class SessionEventCallback : public ComBaseClassHelper + { + public: + SessionEventCallback (WASAPIDeviceBase& d) : owner (d) {} + + 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 state) + { + if (state == AudioSessionStateInactive || state == AudioSessionStateExpired) + owner.deviceBecameInactive(); + + return S_OK; + } + + JUCE_COMRESULT OnSessionDisconnected (AudioSessionDisconnectReason reason) + { + Logger::writeToLog("OnSessionDisconnected"); + 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; + } + + //============================================================================== + ComSmartPtr createClient() + { + ComSmartPtr newClient; + + if (device != nullptr) + logFailure (device->Activate (__uuidof (IAudioClient), CLSCTX_INPROC_SERVER, + nullptr, (void**) newClient.resetAndGetPointerAddress())); + + return newClient; + } + + struct AudioSampleFormat + { + bool useFloat; + int bitsPerSampleToTry; + int bytesPerSampleContainer; + }; + + bool tryFormat (const AudioSampleFormat sampleFormat, IAudioClient* clientToUse, double newSampleRate, + DWORD newMixFormatChannelMask, WAVEFORMATEXTENSIBLE& format) const + { + zerostruct (format); + + if (numChannels <= 2 && sampleFormat.bitsPerSampleToTry <= 16) + { + format.Format.wFormatTag = WAVE_FORMAT_PCM; + } + else + { + format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + format.Format.cbSize = sizeof (WAVEFORMATEXTENSIBLE) - sizeof (WAVEFORMATEX); + } + + format.Format.nSamplesPerSec = (DWORD) newSampleRate; + format.Format.nChannels = (WORD) numChannels; + format.Format.wBitsPerSample = (WORD) (8 * sampleFormat.bytesPerSampleContainer); + format.Samples.wValidBitsPerSample = (WORD) (sampleFormat.bitsPerSampleToTry); + format.Format.nBlockAlign = (WORD) (format.Format.nChannels * format.Format.wBitsPerSample / 8); + format.Format.nAvgBytesPerSec = (DWORD) (format.Format.nSamplesPerSec * format.Format.nBlockAlign); + format.SubFormat = sampleFormat.useFloat ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM; + format.dwChannelMask = newMixFormatChannelMask; + + WAVEFORMATEXTENSIBLE* nearestFormat = nullptr; + + HRESULT hr = clientToUse->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, (const WAVEFORMATEX*) nearestFormat); + hr = S_OK; + } + + CoTaskMemFree (nearestFormat); + return check (hr); + } + + bool findSupportedFormat (IAudioClient* clientToUse, double newSampleRate, + DWORD newMixFormatChannelMask, WAVEFORMATEXTENSIBLE& format) const + { + static const AudioSampleFormat formats[] = + { + { true, 32, 4 }, + { false, 32, 4 }, + { false, 24, 4 }, + { false, 24, 3 }, + { false, 20, 4 }, + { false, 20, 3 }, + { false, 16, 2 } + }; + + for (int i = 0; i < numElementsInArray (formats); ++i) + if (tryFormat (formats[i], clientToUse, newSampleRate, newMixFormatChannelMask, format)) + return true; + + return false; + } + + bool tryInitialisingWithBufferSize (const int bufferSizeSamples) + { + WAVEFORMATEXTENSIBLE format; + + if (findSupportedFormat (client, sampleRate, mixFormatChannelMask, format)) + { + REFERENCE_TIME defaultPeriod = 0, minPeriod = 0; + + check (client->GetDevicePeriod (&defaultPeriod, &minPeriod)); + + if (useExclusiveMode && bufferSizeSamples > 0) + defaultPeriod = jmax (minPeriod, samplesToRefTime (bufferSizeSamples, format.Format.nSamplesPerSec)); + + for (;;) + { + GUID session; + HRESULT hr = client->Initialize (useExclusiveMode ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED, + 0x40000 /*AUDCLNT_STREAMFLAGS_EVENTCALLBACK*/, + defaultPeriod, useExclusiveMode ? defaultPeriod : 0, (WAVEFORMATEX*) &format, &session); + + if (check (hr)) + { + actualNumChannels = format.Format.nChannels; + const bool isFloat = format.Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE && format.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + bytesPerSample = format.Format.wBitsPerSample / 8; + bytesPerFrame = format.Format.nBlockAlign; + + updateFormat (isFloat); + return true; + } + + // Handle the "alignment dance" : http://msdn.microsoft.com/en-us/library/windows/desktop/dd370875(v=vs.85).aspx (see Remarks) + if (hr != MAKE_HRESULT (1, 0x889, 0x19)) // AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED + break; + + UINT32 numFrames = 0; + if (! check (client->GetBufferSize (&numFrames))) + break; + + // Recreate client + client = nullptr; + client = createClient(); + + defaultPeriod = samplesToRefTime (numFrames, format.Format.nSamplesPerSec); + } + } + + return false; + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIDeviceBase) +}; + +//============================================================================== +class WASAPIInputDevice : public WASAPIDeviceBase +{ +public: + WASAPIInputDevice (const ComSmartPtr& d, const bool exclusiveMode) + : WASAPIDeviceBase (d, exclusiveMode), + reservoir (1, 1) + { + } + + ~WASAPIInputDevice() + { + close(); + } + + bool open (const double newSampleRate, const BigInteger& newChannels, int bufferSizeSamples) + { + return openClient (newSampleRate, newChannels, bufferSizeSamples) + && (numChannels == 0 || check (client->GetService (__uuidof (IAudioCaptureClient), + (void**) captureClient.resetAndGetPointerAddress()))); + } + + void close() + { + closeClient(); + captureClient = nullptr; + reservoir.reset(); + reservoirReadPos = reservoirWritePos = 0; + } + + template + void updateFormatWithType (SourceType*) noexcept + { + typedef AudioData::Pointer NativeType; + converter = new AudioData::ConverterInstance, NativeType> (actualNumChannels, 1); + } + + void updateFormat (bool isFloat) override + { + if (isFloat) updateFormatWithType ((AudioData::Float32*) nullptr); + else if (bytesPerSample == 4) updateFormatWithType ((AudioData::Int32*) nullptr); + else if (bytesPerSample == 3) updateFormatWithType ((AudioData::Int24*) nullptr); + else updateFormatWithType ((AudioData::Int16*) nullptr); + } + + bool start (const int userBufferSize) + { + reservoirSize = actualBufferSize + userBufferSize; + reservoirMask = nextPowerOfTwo (reservoirSize) - 1; + reservoir.setSize ((reservoirMask + 1) * bytesPerFrame, true); + reservoirReadPos = reservoirWritePos = 0; + xruns = 0; + + if (! check (client->Start())) + return false; + + purgeInputBuffers(); + return true; + } + + void purgeInputBuffers() + { + uint8* inputData; + UINT32 numSamplesAvailable; + DWORD flags; + + while (captureClient->GetBuffer (&inputData, &numSamplesAvailable, &flags, nullptr, nullptr) + != MAKE_HRESULT (0, 0x889, 0x1) /* AUDCLNT_S_BUFFER_EMPTY */) + captureClient->ReleaseBuffer (numSamplesAvailable); + } + + int getNumSamplesInReservoir() const noexcept { return reservoirWritePos - reservoirReadPos; } + + void handleDeviceBuffer() + { + if (numChannels <= 0) + return; + + uint8* inputData; + UINT32 numSamplesAvailable; + DWORD flags; + + while (check (captureClient->GetBuffer (&inputData, &numSamplesAvailable, &flags, nullptr, nullptr)) && numSamplesAvailable > 0) + { + if ((flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) + xruns++; + + int samplesLeft = (int) numSamplesAvailable; + + while (samplesLeft > 0) + { + const int localWrite = reservoirWritePos & reservoirMask; + const int samplesToDo = jmin (samplesLeft, reservoirMask + 1 - localWrite); + const int samplesToDoBytes = samplesToDo * bytesPerFrame; + + void* reservoirPtr = addBytesToPointer (reservoir.getData(), localWrite * bytesPerFrame); + + if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0) + zeromem (reservoirPtr, samplesToDoBytes); + else + memcpy (reservoirPtr, inputData, samplesToDoBytes); + + reservoirWritePos += samplesToDo; + inputData += samplesToDoBytes; + samplesLeft -= samplesToDo; + } + + if (getNumSamplesInReservoir() > reservoirSize) + reservoirReadPos = reservoirWritePos - reservoirSize; + + captureClient->ReleaseBuffer (numSamplesAvailable); + } + } + + void copyBuffersFromReservoir (float** destBuffers, int numDestBuffers, int bufferSize) + { + if ((numChannels <= 0 && bufferSize == 0) || reservoir.getSize() == 0) + return; + + int offset = jmax (0, bufferSize - getNumSamplesInReservoir()); + + if (offset > 0) + { + for (int i = 0; i < numDestBuffers; ++i) + zeromem (destBuffers[i], offset * sizeof (float)); + + bufferSize -= offset; + reservoirReadPos -= offset / 2; + } + + while (bufferSize > 0) + { + const int localRead = reservoirReadPos & reservoirMask; + + const int samplesToDo = jmin (bufferSize, getNumSamplesInReservoir(), reservoirMask + 1 - localRead); + if (samplesToDo <= 0) + break; + + const int reservoirOffset = localRead * bytesPerFrame; + + for (int i = 0; i < numDestBuffers; ++i) + converter->convertSamples (destBuffers[i] + offset, 0, addBytesToPointer (reservoir.getData(), reservoirOffset), channelMaps.getUnchecked(i), samplesToDo); + + bufferSize -= samplesToDo; + offset += samplesToDo; + reservoirReadPos += samplesToDo; + } + } + + ComSmartPtr captureClient; + MemoryBlock reservoir; + int reservoirSize, reservoirMask, xruns; + volatile int reservoirReadPos, reservoirWritePos; + + ScopedPointer converter; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIInputDevice) +}; + +//============================================================================== +class WASAPIOutputDevice : public WASAPIDeviceBase +{ +public: + WASAPIOutputDevice (const ComSmartPtr& d, const bool exclusiveMode) + : WASAPIDeviceBase (d, exclusiveMode) + { + } + + ~WASAPIOutputDevice() + { + close(); + } + + bool open (const double newSampleRate, const BigInteger& newChannels, int bufferSizeSamples) + { + return openClient (newSampleRate, newChannels, bufferSizeSamples) + && (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) override + { + if (isFloat) updateFormatWithType ((AudioData::Float32*) nullptr); + else if (bytesPerSample == 4) updateFormatWithType ((AudioData::Int32*) nullptr); + else if (bytesPerSample == 3) updateFormatWithType ((AudioData::Int24*) nullptr); + else updateFormatWithType ((AudioData::Int16*) nullptr); + } + + bool start() + { + int samplesToDo = getNumSamplesAvailableToCopy(); + uint8* outputData; + + if (check (renderClient->GetBuffer (samplesToDo, &outputData))) + renderClient->ReleaseBuffer (samplesToDo, AUDCLNT_BUFFERFLAGS_SILENT); + + return check (client->Start()); + } + + int getNumSamplesAvailableToCopy() const + { + if (numChannels <= 0) + return 0; + + if (! useExclusiveMode) + { + UINT32 padding = 0; + if (check (client->GetCurrentPadding (&padding))) + return actualBufferSize - (int) padding; + } + + return actualBufferSize; + } + + void copyBuffers (const float** const srcBuffers, const int numSrcBuffers, int bufferSize, + WASAPIInputDevice* inputDevice, Thread& thread) + { + if (numChannels <= 0) + return; + + int offset = 0; + + while (bufferSize > 0) + { + // This is needed in order not to drop any input data if the output device endpoint buffer was full + if ((! useExclusiveMode) && inputDevice != nullptr + && WaitForSingleObject (inputDevice->clientEvent, 0) == WAIT_OBJECT_0) + inputDevice->handleDeviceBuffer(); + + int samplesToDo = jmin (getNumSamplesAvailableToCopy(), bufferSize); + + if (samplesToDo == 0) + { + // This can ONLY occur in non-exclusive mode + if (! thread.threadShouldExit() && WaitForSingleObject (clientEvent, 1000) == WAIT_OBJECT_0) + continue; + + break; + } + + if (useExclusiveMode && WaitForSingleObject (clientEvent, 1000) == WAIT_TIMEOUT) + break; + + 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); + } + + bufferSize -= samplesToDo; + offset += samplesToDo; + } + } + + ComSmartPtr renderClient; + ScopedPointer converter; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIOutputDevice) +}; + +//============================================================================== +class WASAPIAudioIODevice : public AudioIODevice, + public Thread, + private AsyncUpdater +{ +public: + WASAPIAudioIODevice (const String& deviceName, + const String& typeName, + const String& outputDeviceID, + const String& inputDeviceID, + const bool exclusiveMode) + : AudioIODevice (deviceName, typeName), + Thread ("Juce WASAPI"), + outputDeviceId (outputDeviceID), + inputDeviceId (inputDeviceID), + useExclusiveMode (exclusiveMode), + isOpen_ (false), + isStarted (false), + currentBufferSizeSamples (0), + currentSampleRate (0), + callback (nullptr), + deviceBecameInactive (false) + { + } + + ~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() override + { + StringArray outChannels; + + if (outputDevice != nullptr) + for (int i = 1; i <= outputDevice->actualNumChannels; ++i) + outChannels.add ("Output channel " + String (i)); + + return outChannels; + } + + StringArray getInputChannelNames() override + { + StringArray inChannels; + + if (inputDevice != nullptr) + for (int i = 1; i <= inputDevice->actualNumChannels; ++i) + inChannels.add ("Input channel " + String (i)); + + return inChannels; + } + + Array getAvailableSampleRates() override { return sampleRates; } + Array getAvailableBufferSizes() override { return bufferSizes; } + int getDefaultBufferSize() override { return defaultBufferSize; } + + int getCurrentBufferSizeSamples() override { return currentBufferSizeSamples; } + double getCurrentSampleRate() override { return currentSampleRate; } + int getCurrentBitDepth() override { return 32; } + int getOutputLatencyInSamples() override { return latencyOut; } + int getInputLatencyInSamples() override { return latencyIn; } + BigInteger getActiveOutputChannels() const override { return outputDevice != nullptr ? outputDevice->channels : BigInteger(); } + BigInteger getActiveInputChannels() const override { return inputDevice != nullptr ? inputDevice->channels : BigInteger(); } + String getLastError() override { return lastError; } + int getXRunCount () const noexcept override { return inputDevice != nullptr ? inputDevice->xruns : -1; } + + String open (const BigInteger& inputChannels, const BigInteger& outputChannels, + double sampleRate, int bufferSizeSamples) override + { + close(); + lastError.clear(); + + if (sampleRates.size() == 0 && inputDevice != nullptr && outputDevice != nullptr) + { + lastError = TRANS("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; + lastKnownInputChannels = inputChannels; + lastKnownOutputChannels = outputChannels; + + if (inputDevice != nullptr && ! inputDevice->open (currentSampleRate, inputChannels, bufferSizeSamples)) + { + lastError = TRANS("Couldn't open the input device!"); + return lastError; + } + + if (outputDevice != nullptr && ! outputDevice->open (currentSampleRate, outputChannels, bufferSizeSamples)) + { + close(); + lastError = TRANS("Couldn't open the output device!"); + return lastError; + } + + if (useExclusiveMode) + { + // This is to make sure that the callback uses actualBufferSize in case of exclusive mode + if (inputDevice != nullptr && outputDevice != nullptr && inputDevice->actualBufferSize != outputDevice->actualBufferSize) + { + close(); + lastError = TRANS("Couldn't open the output device (buffer size mismatch)"); + return lastError; + } + + currentBufferSizeSamples = outputDevice != nullptr ? outputDevice->actualBufferSize + : inputDevice->actualBufferSize; + } + + if (inputDevice != nullptr) ResetEvent (inputDevice->clientEvent); + if (outputDevice != nullptr) ResetEvent (outputDevice->clientEvent); + + deviceBecameInactive = false; + + startThread (8); + Thread::sleep (5); + + if (inputDevice != nullptr && inputDevice->client != nullptr) + { + latencyIn = (int) (inputDevice->latencySamples + currentBufferSizeSamples); + + if (! inputDevice->start (currentBufferSizeSamples)) + { + close(); + lastError = TRANS("Couldn't start the input device!"); + return lastError; + } + } + + if (outputDevice != nullptr && outputDevice->client != nullptr) + { + latencyOut = (int) (outputDevice->latencySamples + currentBufferSizeSamples); + + if (! outputDevice->start()) + { + close(); + lastError = TRANS("Couldn't start the output device!"); + return lastError; + } + } + + isOpen_ = true; + return lastError; + } + + void close() override + { + 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() override { return isOpen_ && isThreadRunning(); } + bool isPlaying() override { return isStarted && isOpen_ && isThreadRunning(); } + + void start (AudioIODeviceCallback* call) override + { + 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() override + { + 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() override + { + setMMThreadPriority(); + + const int bufferSize = currentBufferSizeSamples; + const int numInputBuffers = getActiveInputChannels().countNumberOfSetBits(); + const int numOutputBuffers = getActiveOutputChannels().countNumberOfSetBits(); + bool sampleRateHasChanged = false; + + AudioSampleBuffer ins (jmax (1, numInputBuffers), bufferSize + 32); + AudioSampleBuffer outs (jmax (1, numOutputBuffers), bufferSize + 32); + float** const inputBuffers = ins.getArrayOfWritePointers(); + float** const outputBuffers = outs.getArrayOfWritePointers(); + ins.clear(); + outs.clear(); + + while (! threadShouldExit()) + { + if (outputDevice != nullptr && outputDevice->shouldClose) + deviceBecameInactive = true; + + if (inputDevice != nullptr && ! deviceBecameInactive) + { + if (inputDevice->shouldClose) + deviceBecameInactive = true; + + if (outputDevice == nullptr) + { + if (WaitForSingleObject (inputDevice->clientEvent, 1000) == WAIT_TIMEOUT) + break; + + inputDevice->handleDeviceBuffer(); + + if (inputDevice->getNumSamplesInReservoir() < bufferSize) + continue; + } + else + { + if (useExclusiveMode && WaitForSingleObject (inputDevice->clientEvent, 0) == WAIT_OBJECT_0) + inputDevice->handleDeviceBuffer(); + } + + inputDevice->copyBuffersFromReservoir (inputBuffers, numInputBuffers, bufferSize); + + if (inputDevice->sampleRateHasChanged) + { + sampleRateHasChanged = true; + sampleRateChangedByOutput = false; + } + } + + if (! deviceBecameInactive) + { + const ScopedTryLock sl (startStopLock); + + if (sl.isLocked() && isStarted) + callback->audioDeviceIOCallback (const_cast (inputBuffers), numInputBuffers, + outputBuffers, numOutputBuffers, bufferSize); + else + outs.clear(); + } + + if (outputDevice != nullptr && !deviceBecameInactive) + { + // Note that this function is handed the input device so it can check for the event and make sure + // the input reservoir is filled up correctly even when bufferSize > device actualBufferSize + outputDevice->copyBuffers (const_cast (outputBuffers), numOutputBuffers, bufferSize, inputDevice, *this); + + if (outputDevice->sampleRateHasChanged) + { + sampleRateHasChanged = true; + sampleRateChangedByOutput = true; + } + } + + if (sampleRateHasChanged || deviceBecameInactive) + { + triggerAsyncUpdate(); + break; // Quit the thread... will restart it later! + } + } + } + + //============================================================================== + 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; + bool sampleRateChangedByOutput, deviceBecameInactive; + + AudioIODeviceCallback* callback; + CriticalSection startStopLock; + + BigInteger lastKnownInputChannels, lastKnownOutputChannels; + + //============================================================================== + 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())); + } + + //============================================================================== + void handleAsyncUpdate() override + { + stop(); + + outputDevice = nullptr; + inputDevice = nullptr; + + // sample rate change + if (! deviceBecameInactive) + { + initialise(); + + open (lastKnownInputChannels, lastKnownOutputChannels, + getChangedSampleRate(), currentBufferSizeSamples); + + start (callback); + } + } + + double getChangedSampleRate() const + { + if (outputDevice != nullptr && sampleRateChangedByOutput) + return outputDevice->defaultSampleRate; + + if (inputDevice != nullptr && ! sampleRateChangedByOutput) + return inputDevice->defaultSampleRate; + + return 0.0; + } + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WASAPIAudioIODevice) +}; + + +//============================================================================== +class WASAPIAudioIODeviceType : public AudioIODeviceType, + private DeviceChangeDetector +{ +public: + WASAPIAudioIODeviceType (bool exclusive) + : AudioIODeviceType (exclusive ? "Windows Audio (Exclusive Mode)" : "Windows Audio"), + DeviceChangeDetector (L"Windows Audio"), + exclusiveMode (exclusive), + hasScanned (false) + { + } + + ~WASAPIAudioIODeviceType() + { + if (notifyClient != nullptr) + enumerator->UnregisterEndpointNotificationCallback (notifyClient); + } + + //============================================================================== + 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 + + if (WASAPIAudioIODevice* const d = dynamic_cast (device)) + return asInput ? inputDeviceIds.indexOf (d->inputDeviceId) + : outputDeviceIds.indexOf (d->outputDeviceId); + + return -1; + } + + bool hasSeparateInputsAndOutputs() const { return true; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + jassert (hasScanned); // need to call scanForDevices() before doing this + + 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, + getTypeName(), + outputDeviceIds [outputIndex], + inputDeviceIds [inputIndex], + exclusiveMode); + + if (! device->initialise()) + device = nullptr; + } + + return device.release(); + } + + //============================================================================== + StringArray outputDeviceNames, outputDeviceIds; + StringArray inputDeviceNames, inputDeviceIds; + +private: + bool exclusiveMode, hasScanned; + ComSmartPtr enumerator; + + //============================================================================== + class ChangeNotificationClient : public ComBaseClassHelper + { + public: + ChangeNotificationClient (WASAPIAudioIODeviceType& d) + : ComBaseClassHelper (0), device (d) {} + + HRESULT STDMETHODCALLTYPE OnDeviceAdded (LPCWSTR) { return notify(); } + HRESULT STDMETHODCALLTYPE OnDeviceRemoved (LPCWSTR) { return notify(); } + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR, DWORD) { return notify(); } + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged (EDataFlow, ERole, LPCWSTR) { return notify(); } + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged (LPCWSTR, const PROPERTYKEY) { return notify(); } + + private: + WASAPIAudioIODeviceType& device; + + HRESULT notify() { device.triggerAsyncDeviceChangeCallback(); return S_OK; } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChangeNotificationClient) + }; + + ComSmartPtr notifyClient; + + //============================================================================== + 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& outDeviceNames, + StringArray& inDeviceNames, + StringArray& outDeviceIds, + StringArray& inDeviceIds) + { + if (enumerator == nullptr) + { + if (! check (enumerator.CoCreateInstance (__uuidof (MMDeviceEnumerator)))) + return; + + notifyClient = new ChangeNotificationClient (*this); + enumerator->RegisterEndpointNotificationCallback (notifyClient); + } + + 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; + zerostruct (value); + + const PROPERTYKEY PKEY_Device_FriendlyName + = { { 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } }, 14 }; + + 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; + outDeviceIds.insert (index, deviceId); + outDeviceNames.insert (index, name); + } + else if (flow == eCapture) + { + const int index = (deviceId == defaultCapture) ? 0 : -1; + inDeviceIds.insert (index, deviceId); + inDeviceNames.insert (index, name); + } + } + + inDeviceNames.appendNumbersToDuplicates (false, false); + outDeviceNames.appendNumbersToDuplicates (false, false); + } + + //============================================================================== + void systemDeviceChanged() override + { + 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) +}; + +//============================================================================== +struct MMDeviceMasterVolume +{ + MMDeviceMasterVolume() + { + ComSmartPtr enumerator; + if (check (enumerator.CoCreateInstance (__uuidof (MMDeviceEnumerator)))) + { + ComSmartPtr device; + if (check (enumerator->GetDefaultAudioEndpoint (eRender, eConsole, device.resetAndGetPointerAddress()))) + check (device->Activate (__uuidof (IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, + (void**) endpointVolume.resetAndGetPointerAddress())); + } + } + + float getGain() const + { + float vol = 0.0f; + if (endpointVolume != nullptr) + check (endpointVolume->GetMasterVolumeLevelScalar (&vol)); + + return vol; + } + + bool setGain (float newGain) const + { + return endpointVolume != nullptr + && check (endpointVolume->SetMasterVolumeLevelScalar (jlimit (0.0f, 1.0f, newGain), nullptr)); + } + + bool isMuted() const + { + BOOL mute = 0; + return endpointVolume != nullptr + && check (endpointVolume->GetMute (&mute)) && mute != 0; + } + + bool setMuted (bool shouldMute) const + { + return endpointVolume != nullptr + && check (endpointVolume->SetMute (shouldMute, nullptr)); + } + + ComSmartPtr endpointVolume; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MMDeviceMasterVolume) +}; + +} + +//============================================================================== +AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_WASAPI (bool exclusiveMode) +{ + #if ! JUCE_WASAPI_EXCLUSIVE + if (exclusiveMode) + return nullptr; + #endif + + return SystemStats::getOperatingSystemType() >= SystemStats::WinVista + ? new WasapiClasses::WASAPIAudioIODeviceType (exclusiveMode) + : nullptr; +} + +//============================================================================== +#define JUCE_SYSTEMAUDIOVOL_IMPLEMENTED 1 +float JUCE_CALLTYPE SystemAudioVolume::getGain() { return WasapiClasses::MMDeviceMasterVolume().getGain(); } +bool JUCE_CALLTYPE SystemAudioVolume::setGain (float gain) { return WasapiClasses::MMDeviceMasterVolume().setGain (gain); } +bool JUCE_CALLTYPE SystemAudioVolume::isMuted() { return WasapiClasses::MMDeviceMasterVolume().isMuted(); } +bool JUCE_CALLTYPE SystemAudioVolume::setMuted (bool mute) { return WasapiClasses::MMDeviceMasterVolume().setMuted (mute); } + +} // namespace juce diff --git a/source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.cpp b/source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.cpp new file mode 100644 index 000000000..b03ca9a93 --- /dev/null +++ b/source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.cpp @@ -0,0 +1,185 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +AudioSourcePlayer::AudioSourcePlayer() + : source (nullptr), + sampleRate (0), + bufferSize (0), + lastGain (1.0f), + gain (1.0f) +{ +} + +AudioSourcePlayer::~AudioSourcePlayer() +{ + setSource (nullptr); +} + +void AudioSourcePlayer::setSource (AudioSource* newSource) +{ + if (source != newSource) + { + AudioSource* const oldSource = source; + + if (newSource != nullptr && bufferSize > 0 && sampleRate > 0) + newSource->prepareToPlay (bufferSize, sampleRate); + + { + const ScopedLock sl (readLock); + source = newSource; + } + + if (oldSource != nullptr) + oldSource->releaseResources(); + } +} + +void AudioSourcePlayer::setGain (const float newGain) noexcept +{ + gain = newGain; +} + +void AudioSourcePlayer::audioDeviceIOCallback (const float** inputChannelData, + int totalNumInputChannels, + float** outputChannelData, + int totalNumOutputChannels, + int numSamples) +{ + // these should have been prepared by audioDeviceAboutToStart()... + jassert (sampleRate > 0 && bufferSize > 0); + + const ScopedLock sl (readLock); + + if (source != nullptr) + { + int numActiveChans = 0, numInputs = 0, numOutputs = 0; + + // messy stuff needed to compact the channels down into an array + // of non-zero pointers.. + for (int i = 0; i < totalNumInputChannels; ++i) + { + if (inputChannelData[i] != nullptr) + { + inputChans [numInputs++] = inputChannelData[i]; + if (numInputs >= numElementsInArray (inputChans)) + break; + } + } + + for (int i = 0; i < totalNumOutputChannels; ++i) + { + if (outputChannelData[i] != nullptr) + { + outputChans [numOutputs++] = outputChannelData[i]; + if (numOutputs >= numElementsInArray (outputChans)) + break; + } + } + + if (numInputs > numOutputs) + { + // if there aren't enough output channels for the number of + // inputs, we need to create some temporary extra ones (can't + // use the input data in case it gets written to) + tempBuffer.setSize (numInputs - numOutputs, numSamples, + false, false, true); + + for (int i = 0; i < numOutputs; ++i) + { + channels[numActiveChans] = outputChans[i]; + memcpy (channels[numActiveChans], inputChans[i], sizeof (float) * (size_t) numSamples); + ++numActiveChans; + } + + for (int i = numOutputs; i < numInputs; ++i) + { + channels[numActiveChans] = tempBuffer.getWritePointer (i - numOutputs); + memcpy (channels[numActiveChans], inputChans[i], sizeof (float) * (size_t) numSamples); + ++numActiveChans; + } + } + else + { + for (int i = 0; i < numInputs; ++i) + { + channels[numActiveChans] = outputChans[i]; + memcpy (channels[numActiveChans], inputChans[i], sizeof (float) * (size_t) numSamples); + ++numActiveChans; + } + + for (int i = numInputs; i < numOutputs; ++i) + { + channels[numActiveChans] = outputChans[i]; + zeromem (channels[numActiveChans], sizeof (float) * (size_t) numSamples); + ++numActiveChans; + } + } + + AudioSampleBuffer buffer (channels, numActiveChans, numSamples); + + AudioSourceChannelInfo info (&buffer, 0, numSamples); + source->getNextAudioBlock (info); + + for (int i = info.buffer->getNumChannels(); --i >= 0;) + buffer.applyGainRamp (i, info.startSample, info.numSamples, lastGain, gain); + + lastGain = gain; + } + else + { + for (int i = 0; i < totalNumOutputChannels; ++i) + if (outputChannelData[i] != nullptr) + zeromem (outputChannelData[i], sizeof (float) * (size_t) numSamples); + } +} + +void AudioSourcePlayer::audioDeviceAboutToStart (AudioIODevice* device) +{ + prepareToPlay (device->getCurrentSampleRate(), + device->getCurrentBufferSizeSamples()); +} + +void AudioSourcePlayer::prepareToPlay (double newSampleRate, int newBufferSize) +{ + sampleRate = newSampleRate; + bufferSize = newBufferSize; + zeromem (channels, sizeof (channels)); + + if (source != nullptr) + source->prepareToPlay (bufferSize, sampleRate); +} + +void AudioSourcePlayer::audioDeviceStopped() +{ + if (source != nullptr) + source->releaseResources(); + + sampleRate = 0.0; + bufferSize = 0; + + tempBuffer.setSize (2, 8); +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.h b/source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.h new file mode 100644 index 000000000..d624b0a57 --- /dev/null +++ b/source/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.h @@ -0,0 +1,111 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Wrapper class to continuously stream audio from an audio source to an + AudioIODevice. + + This object acts as an AudioIODeviceCallback, so can be attached to an + output device, and will stream audio from an AudioSource. +*/ +class JUCE_API AudioSourcePlayer : public AudioIODeviceCallback +{ +public: + //============================================================================== + /** Creates an empty AudioSourcePlayer. */ + AudioSourcePlayer(); + + /** Destructor. + + Make sure this object isn't still being used by an AudioIODevice before + deleting it! + */ + virtual ~AudioSourcePlayer(); + + //============================================================================== + /** Changes the current audio source to play from. + + If the source passed in is already being used, this method will do nothing. + If the source is not null, its prepareToPlay() method will be called + before it starts being used for playback. + + If there's another source currently playing, its releaseResources() method + will be called after it has been swapped for the new one. + + @param newSource the new source to use - this will NOT be deleted + by this object when no longer needed, so it's the + caller's responsibility to manage it. + */ + void setSource (AudioSource* newSource); + + /** Returns the source that's playing. + May return nullptr if there's no source. + */ + AudioSource* getCurrentSource() const noexcept { return source; } + + /** Sets a gain to apply to the audio data. + @see getGain + */ + void setGain (float newGain) noexcept; + + /** Returns the current gain. + @see setGain + */ + float getGain() const noexcept { return gain; } + + //============================================================================== + /** Implementation of the AudioIODeviceCallback method. */ + void audioDeviceIOCallback (const float** inputChannelData, + int totalNumInputChannels, + float** outputChannelData, + int totalNumOutputChannels, + int numSamples) override; + + /** Implementation of the AudioIODeviceCallback method. */ + void audioDeviceAboutToStart (AudioIODevice* device) override; + + /** Implementation of the AudioIODeviceCallback method. */ + void audioDeviceStopped() override; + + /** An alternative method for initialising the source without an AudioIODevice. */ + void prepareToPlay (double sampleRate, int blockSize); + +private: + //============================================================================== + CriticalSection readLock; + AudioSource* source; + double sampleRate; + int bufferSize; + float* channels [128]; + float* outputChans [128]; + const float* inputChans [128]; + AudioSampleBuffer tempBuffer; + float lastGain, gain; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioSourcePlayer) +}; + +} // namespace juce diff --git a/source/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp b/source/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp new file mode 100644 index 000000000..bbc3d8ae4 --- /dev/null +++ b/source/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp @@ -0,0 +1,286 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +AudioTransportSource::AudioTransportSource() +{ +} + +AudioTransportSource::~AudioTransportSource() +{ + setSource (nullptr); + releaseMasterResources(); +} + +void AudioTransportSource::setSource (PositionableAudioSource* const newSource, + int readAheadSize, TimeSliceThread* readAheadThread, + double sourceSampleRateToCorrectFor, int maxNumChannels) +{ + if (source == newSource) + { + if (source == nullptr) + return; + + setSource (nullptr, 0, nullptr); // deselect and reselect to avoid releasing resources wrongly + } + + readAheadBufferSize = readAheadSize; + sourceSampleRate = sourceSampleRateToCorrectFor; + + ResamplingAudioSource* newResamplerSource = nullptr; + BufferingAudioSource* newBufferingSource = nullptr; + PositionableAudioSource* newPositionableSource = nullptr; + AudioSource* newMasterSource = nullptr; + + ScopedPointer oldResamplerSource (resamplerSource); + ScopedPointer oldBufferingSource (bufferingSource); + AudioSource* oldMasterSource = masterSource; + + if (newSource != nullptr) + { + newPositionableSource = newSource; + + if (readAheadSize > 0) + { + // If you want to use a read-ahead buffer, you must also provide a TimeSliceThread + // for it to use! + jassert (readAheadThread != nullptr); + + newPositionableSource = newBufferingSource + = new BufferingAudioSource (newPositionableSource, *readAheadThread, + false, readAheadSize, maxNumChannels); + } + + newPositionableSource->setNextReadPosition (0); + + if (sourceSampleRateToCorrectFor > 0) + newMasterSource = newResamplerSource + = new ResamplingAudioSource (newPositionableSource, false, maxNumChannels); + else + newMasterSource = newPositionableSource; + + if (isPrepared) + { + if (newResamplerSource != nullptr && sourceSampleRate > 0 && sampleRate > 0) + newResamplerSource->setResamplingRatio (sourceSampleRate / sampleRate); + + newMasterSource->prepareToPlay (blockSize, sampleRate); + } + } + + { + const ScopedLock sl (callbackLock); + + source = newSource; + resamplerSource = newResamplerSource; + bufferingSource = newBufferingSource; + masterSource = newMasterSource; + positionableSource = newPositionableSource; + + inputStreamEOF = false; + playing = false; + } + + if (oldMasterSource != nullptr) + oldMasterSource->releaseResources(); +} + +void AudioTransportSource::start() +{ + if ((! playing) && masterSource != nullptr) + { + { + const ScopedLock sl (callbackLock); + playing = true; + stopped = false; + inputStreamEOF = false; + } + + sendChangeMessage(); + } +} + +void AudioTransportSource::stop() +{ + if (playing) + { + { + const ScopedLock sl (callbackLock); + playing = false; + } + + int n = 500; + while (--n >= 0 && ! stopped) + Thread::sleep (2); + + sendChangeMessage(); + } +} + +void AudioTransportSource::setPosition (double newPosition) +{ + if (sampleRate > 0.0) + setNextReadPosition ((int64) (newPosition * sampleRate)); +} + +double AudioTransportSource::getCurrentPosition() const +{ + if (sampleRate > 0.0) + return (double) getNextReadPosition() / sampleRate; + + return 0.0; +} + +double AudioTransportSource::getLengthInSeconds() const +{ + if (sampleRate > 0.0) + return (double) getTotalLength() / sampleRate; + + return 0.0; +} + +void AudioTransportSource::setNextReadPosition (int64 newPosition) +{ + if (positionableSource != nullptr) + { + if (sampleRate > 0 && sourceSampleRate > 0) + newPosition = (int64) ((double) newPosition * sourceSampleRate / sampleRate); + + positionableSource->setNextReadPosition (newPosition); + + if (resamplerSource != nullptr) + resamplerSource->flushBuffers(); + + inputStreamEOF = false; + } +} + +int64 AudioTransportSource::getNextReadPosition() const +{ + if (positionableSource != nullptr) + { + const double ratio = (sampleRate > 0 && sourceSampleRate > 0) ? sampleRate / sourceSampleRate : 1.0; + return (int64) ((double) positionableSource->getNextReadPosition() * ratio); + } + + return 0; +} + +int64 AudioTransportSource::getTotalLength() const +{ + const ScopedLock sl (callbackLock); + + if (positionableSource != nullptr) + { + const double ratio = (sampleRate > 0 && sourceSampleRate > 0) ? sampleRate / sourceSampleRate : 1.0; + return (int64) ((double) positionableSource->getTotalLength() * ratio); + } + + return 0; +} + +bool AudioTransportSource::isLooping() const +{ + const ScopedLock sl (callbackLock); + return positionableSource != nullptr && positionableSource->isLooping(); +} + +void AudioTransportSource::setGain (const float newGain) noexcept +{ + gain = newGain; +} + +void AudioTransportSource::prepareToPlay (int samplesPerBlockExpected, double newSampleRate) +{ + const ScopedLock sl (callbackLock); + + sampleRate = newSampleRate; + blockSize = samplesPerBlockExpected; + + if (masterSource != nullptr) + masterSource->prepareToPlay (samplesPerBlockExpected, sampleRate); + + if (resamplerSource != nullptr && sourceSampleRate > 0) + resamplerSource->setResamplingRatio (sourceSampleRate / sampleRate); + + inputStreamEOF = false; + isPrepared = true; +} + +void AudioTransportSource::releaseMasterResources() +{ + const ScopedLock sl (callbackLock); + + if (masterSource != nullptr) + masterSource->releaseResources(); + + isPrepared = false; +} + +void AudioTransportSource::releaseResources() +{ + releaseMasterResources(); +} + +void AudioTransportSource::getNextAudioBlock (const AudioSourceChannelInfo& info) +{ + const ScopedLock sl (callbackLock); + + if (masterSource != nullptr && ! stopped) + { + masterSource->getNextAudioBlock (info); + + if (! playing) + { + // just stopped playing, so fade out the last block.. + for (int i = info.buffer->getNumChannels(); --i >= 0;) + info.buffer->applyGainRamp (i, info.startSample, jmin (256, info.numSamples), 1.0f, 0.0f); + + if (info.numSamples > 256) + info.buffer->clear (info.startSample + 256, info.numSamples - 256); + } + + if (positionableSource->getNextReadPosition() > positionableSource->getTotalLength() + 1 + && ! positionableSource->isLooping()) + { + playing = false; + inputStreamEOF = true; + sendChangeMessage(); + } + + stopped = ! playing; + + for (int i = info.buffer->getNumChannels(); --i >= 0;) + info.buffer->applyGainRamp (i, info.startSample, info.numSamples, lastGain, gain); + } + else + { + info.clearActiveBufferRegion(); + stopped = true; + } + + lastGain = gain; +} + +} // namespace juce diff --git a/source/modules/juce_audio_devices/sources/juce_AudioTransportSource.h b/source/modules/juce_audio_devices/sources/juce_AudioTransportSource.h new file mode 100644 index 000000000..90103dad6 --- /dev/null +++ b/source/modules/juce_audio_devices/sources/juce_AudioTransportSource.h @@ -0,0 +1,176 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + An AudioSource that takes a PositionableAudioSource and allows it to be + played, stopped, started, etc. + + This can also be told use a buffer and background thread to read ahead, and + if can correct for different sample-rates. + + You may want to use one of these along with an AudioSourcePlayer and AudioIODevice + to control playback of an audio file. + + @see AudioSource, AudioSourcePlayer +*/ +class JUCE_API AudioTransportSource : public PositionableAudioSource, + public ChangeBroadcaster +{ +public: + //============================================================================== + /** Creates an AudioTransportSource. + After creating one of these, use the setSource() method to select an input source. + */ + AudioTransportSource(); + + /** Destructor. */ + ~AudioTransportSource(); + + //============================================================================== + /** Sets the reader that is being used as the input source. + + This will stop playback, reset the position to 0 and change to the new reader. + + The source passed in will not be deleted by this object, so must be managed by + the caller. + + @param newSource the new input source to use. This may be a nullptr + @param readAheadBufferSize a size of buffer to use for reading ahead. If this + is zero, no reading ahead will be done; if it's + greater than zero, a BufferingAudioSource will be used + to do the reading-ahead. If you set a non-zero value here, + you'll also need to set the readAheadThread parameter. + @param readAheadThread if you set readAheadBufferSize to a non-zero value, then + you'll also need to supply this TimeSliceThread object for + the background reader to use. The thread object must not be + deleted while the AudioTransport source is still using it. + @param sourceSampleRateToCorrectFor if this is non-zero, it specifies the sample + rate of the source, and playback will be sample-rate + adjusted to maintain playback at the correct pitch. If + this is 0, no sample-rate adjustment will be performed + @param maxNumChannels the maximum number of channels that may need to be played + */ + void setSource (PositionableAudioSource* newSource, + int readAheadBufferSize = 0, + TimeSliceThread* readAheadThread = nullptr, + double sourceSampleRateToCorrectFor = 0.0, + int maxNumChannels = 2); + + //============================================================================== + /** Changes the current playback position in the source stream. + + The next time the getNextAudioBlock() method is called, this + is the time from which it'll read data. + + @see getPosition + */ + void setPosition (double newPosition); + + /** Returns the position that the next data block will be read from + This is a time in seconds. + */ + double getCurrentPosition() const; + + /** Returns the stream's length in seconds. */ + double getLengthInSeconds() const; + + /** Returns true if the player has stopped because its input stream ran out of data. */ + bool hasStreamFinished() const noexcept { return inputStreamEOF; } + + //============================================================================== + /** Starts playing (if a source has been selected). + + If it starts playing, this will send a message to any ChangeListeners + that are registered with this object. + */ + void start(); + + /** Stops playing. + + If it's actually playing, this will send a message to any ChangeListeners + that are registered with this object. + */ + void stop(); + + /** Returns true if it's currently playing. */ + bool isPlaying() const noexcept { return playing; } + + //============================================================================== + /** Changes the gain to apply to the output. + @param newGain a factor by which to multiply the outgoing samples, + so 1.0 = 0dB, 0.5 = -6dB, 2.0 = 6dB, etc. + */ + void setGain (float newGain) noexcept; + + /** Returns the current gain setting. + @see setGain + */ + float getGain() const noexcept { return gain; } + + //============================================================================== + /** Implementation of the AudioSource method. */ + void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override; + + /** Implementation of the AudioSource method. */ + void releaseResources() override; + + /** Implementation of the AudioSource method. */ + void getNextAudioBlock (const AudioSourceChannelInfo&) override; + + //============================================================================== + /** Implements the PositionableAudioSource method. */ + void setNextReadPosition (int64 newPosition) override; + + /** Implements the PositionableAudioSource method. */ + int64 getNextReadPosition() const override; + + /** Implements the PositionableAudioSource method. */ + int64 getTotalLength() const override; + + /** Implements the PositionableAudioSource method. */ + bool isLooping() const override; + +private: + //============================================================================== + PositionableAudioSource* source = nullptr; + ResamplingAudioSource* resamplerSource = nullptr; + BufferingAudioSource* bufferingSource = nullptr; + PositionableAudioSource* positionableSource = nullptr; + AudioSource* masterSource = nullptr; + + CriticalSection callbackLock; + float volatile gain = 1.0f, lastGain = 1.0f; + bool volatile playing = false, stopped = true; + double sampleRate = 44100.0, sourceSampleRate = 0; + int blockSize = 128, readAheadBufferSize = 0; + bool volatile isPrepared = false, inputStreamEOF = false; + + void releaseMasterResources(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioTransportSource) +}; + +} // namespace juce