diff --git a/CMakeLists.txt b/CMakeLists.txt index a8e35c0ec..f3e0f1511 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -600,18 +600,19 @@ function(sono_add_custom_plugin_target target_name product_name formats is_instr ${LIB_PATHS} ) - target_link_libraries("${target_name}" + target_link_libraries("${target_name}" PRIVATE juce::juce_audio_utils juce::juce_dsp juce::juce_cryptography juce::juce_audio_plugin_client - + ff_meters - + ${target_name}_SBData opus + $<$:mmdevapi> PUBLIC juce::juce_recommended_config_flags juce::juce_recommended_lto_flags diff --git a/FORK_README.md b/FORK_README.md new file mode 100644 index 000000000..6fbb67f80 --- /dev/null +++ b/FORK_README.md @@ -0,0 +1,111 @@ +# SonoBus Fork — WASAPI Loopback Capture + +A fork of [SonoBus](https://github.com/sonosaurus/sonobus) that adds native Windows audio capture features, eliminating the need for Voicemeeter, VB-Cable, or any virtual audio cable software. + +## New Features + +### WASAPI Loopback Capture +Every output device (speakers, headphones, monitors) now appears as a **(Loopback)** input device in SonoBus. Select one to capture all audio playing through that device — lossless, digital, no extra software. + +### Save Direct Connect Address +The last used direct connect IP:port is saved and pre-filled on next launch. + +## Direct Ethernet Setup Guide + +You can connect two PCs with a single ethernet cable for ultra-low-latency audio streaming. No router or internet needed. + +### 1. Physical Connection +Plug an ethernet cable directly between the two PCs. + +### 2. Set Static IPs +On each PC, configure the ethernet adapter with a static IP: + +**PC 1 (e.g. receiver with speakers):** +1. Open **Settings > Network & Internet > Ethernet** (or the direct connection adapter) +2. Click **Edit** next to IP assignment, switch to **Manual** +3. Enable **IPv4** and set: + - IP address: `192.168.1.1` + - Subnet mask: `255.255.255.0` + - Gateway: leave blank + - DNS: leave blank +4. Save + +**PC 2 (e.g. sender with the game):** +- Same steps, but set IP to `192.168.1.2` + +### 3. Connect in SonoBus +1. Open SonoBus on both PCs +2. On either PC, note the **Local Address** shown in the Direct Connect dialog (e.g. `192.168.1.1:58893`) +3. On the other PC, click **Direct Connect** and enter the address (e.g. `192.168.1.2:58893`) +4. The port changes each time SonoBus starts — always check the Local Address shown + +### 4. Audio Settings + +**Sending PC (e.g. running SnowRunner):** +- Audio Device Type: **Windows Audio** +- Input: **Your output device (Loopback)** — e.g. "Speakers (Realtek) (Loopback)" +- Output: **<< none >>** +- Set Windows default output to any device, mute it in Windows if you don't want local sound + + +**Receiving PC (with DAC/amp/speakers):** +- Audio Device Type: **Windows Audio** +- Input: **<< none >>** +- Output: **Your DAC/speakers** (e.g. USB-C audio adapter) + +### 5. Audio Format +In SonoBus, set the send quality to **PCM 32-bit** for completely lossless audio over the direct ethernet link. Bandwidth is not an issue on a local cable. + +## Sending All System Audio (No Real Output Device) + +Three options, from easiest to most involved: + +1. **Mute a real device** — Set any real output device (e.g. Realtek onboard) as Windows default, mute it in Windows. WASAPI loopback still captures audio even when muted. Easiest option. + +2. **Virtual Audio Driver** — If your PC genuinely has no audio output at all, install [Virtual-Audio-Driver](https://github.com/VirtualDrivers/Virtual-Audio-Driver) (open source, MIT) to create a virtual output, then use its loopback. Requires test signing mode. + +## Download + +Grab `SonoBus.exe` from the [Releases](https://github.com/mthwJsmith/sonobus/releases) page, or build from source. + +## Building from Source + +### Requirements +- CMake 3.15+ +- Visual Studio 2022 +- Windows SDK +- [ASIO SDK](https://github.com/audiosdk/asio) — clone to `../asiosdk` (sibling directory) + +### Steps + +```bash +# Clone +git clone --recursive https://github.com/mthwJsmith/sonobus.git +cd sonobus + +# Clone ASIO SDK +git clone https://github.com/audiosdk/asio.git ../asiosdk + +# Configure (unset Android toolchain if you have Android SDK installed) +CMAKE_TOOLCHAIN_FILE="" cmake -B build -G "Visual Studio 17 2022" -A x64 + +# Build +CMAKE_TOOLCHAIN_FILE="" cmake --build build --config Release --target SonoBus_Standalone + +# Output: build/SonoBus_artefacts/Release/Standalone/SonoBus.exe +``` + +## Technical Details + +### WASAPI Loopback +- `AUDCLNT_STREAMFLAGS_LOOPBACK` on `IAudioClient::Initialize` +- Lossless PCM capture, digitally before the DAC +- Forces shared mode (required by Windows) +- Changes in `deps/juce/modules/juce_audio_devices/native/juce_WASAPI_windows.cpp` + +## Upstream Issues Addressed +- [#241 — WASAPI Loopback as Input Device](https://github.com/sonosaurus/sonobus/issues/241) +- [#53 — Save previous direct connection IP](https://github.com/sonosaurus/sonobus/issues/53) + +## License +GPLv3 — same as upstream SonoBus. diff --git a/Source/ConnectView.cpp b/Source/ConnectView.cpp index 7458703f2..568f1883c 100644 --- a/Source/ConnectView.cpp +++ b/Source/ConnectView.cpp @@ -92,7 +92,9 @@ publicGroupsListModel(this) mAddRemoteHostEditor = std::make_unique("remaddredit"); mAddRemoteHostEditor->setTitle(TRANS("Remote Host:Port")); mAddRemoteHostEditor->setFont(Font(16 * SonoLookAndFeel::getFontScale())); - mAddRemoteHostEditor->setText("", false); // 100.36.128.246:11000 + // Pre-fill with last used direct connect address + String lastAddr = processor.getLastDirectConnectAddress(); + mAddRemoteHostEditor->setText(lastAddr, false); mAddRemoteHostEditor->setTextToShowWhenEmpty(TRANS("IPaddress:port"), Colour(0x44ffffff)); @@ -1006,6 +1008,8 @@ void ConnectView::buttonClicked (Button* buttonThatWasClicked) if (host.isNotEmpty() && port != 0) { if (processor.connectRemotePeer(host, port, "", "", processor.getValueTreeState().getParameter(SonobusAudioProcessor::paramMainRecvMute)->getValue() == 0)) { + // Save last used direct connect address + processor.setLastDirectConnectAddress(hostport); setVisible(false); if (auto * callout = dynamic_cast(directConnectCalloutBox.get())) { callout->dismiss(); diff --git a/Source/SonoStandaloneFilterWindow.h b/Source/SonoStandaloneFilterWindow.h index 5beaef324..56d6cf48f 100644 --- a/Source/SonoStandaloneFilterWindow.h +++ b/Source/SonoStandaloneFilterWindow.h @@ -37,6 +37,7 @@ // HACK #include "SonobusPluginEditor.h" + #include #include @@ -446,6 +447,7 @@ class StandalonePluginHolder : private AudioIODeviceCallback, preferredDefaultDeviceName, prefSetupOptions.get()); + #if JUCE_IOS // get current audio device and change a setting if necessary if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) { diff --git a/Source/SonobusPluginProcessor.cpp b/Source/SonobusPluginProcessor.cpp index f67bcfd48..758808f4a 100644 --- a/Source/SonobusPluginProcessor.cpp +++ b/Source/SonobusPluginProcessor.cpp @@ -112,6 +112,7 @@ static String lastWindowWidthKey("lastWindowWidth"); static String lastWindowHeightKey("lastWindowHeight"); static String autoresizeDropRateThreshKey("autoDropRateThreshNew"); static String reconnectServerLossKey("reconnServLoss"); +static String lastDirectConnectAddressKey("lastDirectConnectAddr"); static String compressorStateKey("CompressorState"); static String expanderStateKey("ExpanderState"); @@ -8545,6 +8546,7 @@ void SonobusAudioProcessor::getStateInformationWithOptions(MemoryBlock& destData extraTree.setProperty(lastWindowHeightKey, var((int)mPluginWindowHeight), nullptr); extraTree.setProperty(autoresizeDropRateThreshKey, var((float)mAutoresizeDropRateThresh), nullptr); extraTree.setProperty(reconnectServerLossKey, mReconnectAfterServerLoss.get(), nullptr); + extraTree.setProperty(lastDirectConnectAddressKey, mLastDirectConnectAddress, nullptr); extraTree.appendChild(mVideoLinkInfo.getValueTree(), nullptr); @@ -8716,7 +8718,8 @@ void SonobusAudioProcessor::setStateInformationWithOptions (const void* data, in setReconnectAfterServerLoss(extraTree.getProperty(reconnectServerLossKey, mReconnectAfterServerLoss.get())); - + mLastDirectConnectAddress = extraTree.getProperty(lastDirectConnectAddressKey, "").toString(); + ValueTree videoinfo = extraTree.getChildWithName(videoLinkInfoKey); if (videoinfo.isValid()) { mVideoLinkInfo.setFromValueTree(videoinfo); @@ -8846,9 +8849,12 @@ void SonobusAudioProcessor::ServerReconnectTimer::timerCallback() bool SonobusAudioProcessor::reconnectToMostRecent() { + // Note: direct peer auto-reconnect is not feasible because SonoBus uses + // ephemeral UDP ports that change on each launch. The saved address only + // pre-fills the Direct Connect text field for convenience. Array recents; getRecentServerConnectionInfos(recents); - + if (recents.size() > 0) { const auto & info = recents.getReference(0); diff --git a/Source/SonobusPluginProcessor.h b/Source/SonobusPluginProcessor.h index 0ddcbb680..45b901307 100644 --- a/Source/SonobusPluginProcessor.h +++ b/Source/SonobusPluginProcessor.h @@ -315,6 +315,9 @@ class SonobusAudioProcessor : public AudioProcessor, public AudioProcessorValue int getRecentServerConnectionInfos(Array & retarray); void clearRecentServerConnectionInfos(); + void setLastDirectConnectAddress(const String & address) { mLastDirectConnectAddress = address; } + String getLastDirectConnectAddress() const { return mLastDirectConnectAddress; } + bool setCurrentUsername(const String & name); String getCurrentUsername() const { return mCurrentUsername; } @@ -1122,6 +1125,7 @@ class SonobusAudioProcessor : public AudioProcessor, public AudioProcessorValue Array mRecentConnectionInfos; CriticalSection mRecentsLock; + String mLastDirectConnectAddress; AooServerConnectionInfo mPendingReconnectInfo; bool mPendingReconnect = false; diff --git a/audio.md b/audio.md new file mode 100644 index 000000000..7338ea624 --- /dev/null +++ b/audio.md @@ -0,0 +1,134 @@ +# SonoBus Fork — Audio Improvements Plan + +## Goal +Fork SonoBus to add WASAPI loopback capture and UX improvements for a direct ethernet audio streaming setup (two PCs, 192.168.1.x, one-way lossless audio → Apple USB-C DAC → amp → speakers). + +## Fork +- **Upstream**: https://github.com/sonosaurus/sonobus +- **Fork**: https://github.com/mthwJsmith/sonobus +- **Branch**: `feature/wasapi-loopback-and-improvements` +- **WIP Branch**: `feature/application-audio-wip-broken` (Application Audio — works but audio quality is broken) +- **Base**: JUCE 7 (sono7good) — JUCE 8 had shared mode loopback issues, reverted +- **PR**: https://github.com/sonosaurus/sonobus/pull/280 +- **Release**: https://github.com/mthwJsmith/sonobus/releases/tag/v1.7.2-loopback + +## Current Status + +### SHIPPED (v1.7.2-loopback) +- **WASAPI Loopback Capture** — loopback devices show in input list, audio levels confirmed, sounds great +- **Save Direct Connect Address** — pre-fills last used IP:port +- **Build system** — compiles with VS2022, ASIO SDK, on JUCE 7 +- **Direct peer connection** — working after clearing stale %APPDATA% state + +### SHELVED — Application Audio (per-process capture) +- Moved to `feature/application-audio-wip-broken` branch +- Win11 API works, shows running audio apps, captures audio +- **Audio quality is broken** — sounds super glitchy/choppy, likely sample rate or buffer mismatch +- Code preserved for future investigation + +### Connection Debugging Results (2025-03-25) +Built v0–v3 test versions to isolate a connection issue: +- **v0-clean** (upstream, no changes) — didn't connect +- **v1-loopback** (WASAPI changes only) — worked +- **v2-connect** (+ save address) — didn't connect initially +- **v3-full** (+ Application Audio) — didn't connect initially +- **Root cause**: stale SonoBus state in `%APPDATA%`. After clearing, all versions worked. +- The WASAPI `else if` → `if` change in `createDevices()` may have incidentally helped device initialization. + +### Known Issues +- **Loopback + same output device conflict** — in shared mode, can't use same device as both output AND loopback input. Set output to `<< none >>` on sending PC (which is correct for the use case anyway) +- **JUCE 8 shared mode loopback broken** — reverted to JUCE 7 +- **Auto-reconnect direct peer not feasible** — ports are ephemeral, can't auto-reconnect (saved address only pre-fills the text field) + +## Features Detail + +### 1. WASAPI Loopback Capture — DONE +- Eliminates need for Voicemeeter/VB-Cable entirely +- Adds render devices as "(Loopback)" input devices in JUCE's WASAPI backend +- SonoBus can directly capture desktop/game audio (lossless PCM) +- File: `deps/juce/modules/juce_audio_devices/native/juce_WASAPI_windows.cpp` +- Changes: + - Added loopback device ID helpers (`JUCE_LOOPBACK::` prefix scheme) + - Added `isLoopbackDevice` flag to `WASAPIDeviceBase` + - Modified `getStreamFlags()` to add `AUDCLNT_STREAMFLAGS_LOOPBACK` (0x00020000) + - Modified `scan()` to enumerate render devices as loopback inputs with "(Loopback)" suffix + - Modified `createDevices()` to detect loopback IDs and open render endpoint as capture + - Modified `initialiseStandardClient()` to force shared mode for loopback + - Modified `tryFormat()`, `findSupportedFormat()`, `querySupportedSampleRates()` to force shared mode for loopback + - `WASAPIInputDevice` constructor accepts loopback flag + +### 2. Save Direct Connect Address — DONE +- Saves last used IP:port to processor state (extraState tree) +- Pre-fills the direct connect text field on next launch +- Auto-reconnect removed (ephemeral ports make it impossible) +- Files changed: `SonobusPluginProcessor.h`, `SonobusPluginProcessor.cpp`, `ConnectView.cpp` + +### 3. Per-Process Audio Capture (Win11 API) — SHELVED +- On `feature/application-audio-wip-broken` branch +- Files: `Source/ProcessAudioCapture.h`, `Source/ProcessAudioCapture.cpp`, `Source/ApplicationAudioDevice.h` +- Uses `AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS` + `ActivateAudioInterfaceAsync` +- Audio quality broken — glitchy/choppy output, needs investigation + +## Architecture Notes + +### WASAPI Loopback (how it works) +- `IAudioClient::Initialize` with `AUDCLNT_STREAMFLAGS_LOOPBACK` flag (0x00020000) +- Must use `AUDCLNT_SHAREMODE_SHARED` (exclusive mode not supported) +- Captures post-mix PCM audio digitally before DAC — completely lossless +- Event-driven supported on Win10 1703+ +- Loopback device IDs use `JUCE_LOOPBACK::` prefix to distinguish from regular capture devices +- **Cannot use same device as both output AND loopback input in shared mode** — set output to none on sender + +### SonoBus Network Protocol +- Audio over OSC (AoO) — UDP peer-to-peer +- Supports PCM 16/24/32-bit and Opus codec +- Direct connect: `processor.connectRemotePeer(host, port, ...)` + +## Build + +**Important:** If you have Android SDK/NDK installed, you MUST unset the CMAKE_TOOLCHAIN_FILE +env var or CMake will try to use the Android NDK Clang compiler instead of MSVC. + +**ASIO SDK:** Clone https://github.com/audiosdk/asio.git to `../asiosdk` (sibling of sonobus dir). + +```bash +# Clone ASIO SDK (one-time) +git clone https://github.com/audiosdk/asio.git ../asiosdk + +# Unset Android toolchain and configure +CMAKE_TOOLCHAIN_FILE="" cmake -B build -G "Visual Studio 17 2022" -A x64 + +# Build standalone app +CMAKE_TOOLCHAIN_FILE="" cmake --build build --config Release --target SonoBus_Standalone + +# Output: build/SonoBus_artefacts/Release/Standalone/SonoBus.exe (25MB) +``` +Requires: CMake 3.15+, Visual Studio 2022, Windows SDK, ASIO SDK + +## How to Use + +### WASAPI Loopback (Sending PC) +1. Audio Device Type: **Windows Audio** +2. Input: **Your output device (Loopback)** — e.g. "Speakers (Realtek) (Loopback)" +3. Output: **<< none >>** (IMPORTANT: don't set output to same device as loopback input) +4. Direct Connect to other PC's IP:port + +### Receiving PC +1. Audio Device Type: **Windows Audio** +2. Input: **<< none >>** +3. Output: **Apple DAC / speakers** + +## Key Files +- `Source/ConnectView.cpp` — direct connect UI, saves last address +- `Source/SonobusPluginProcessor.cpp` — state save/load, connection logic +- `Source/SonoStandaloneFilterWindow.h` — device type registration +- `deps/juce/.../juce_WASAPI_windows.cpp` — WASAPI device enumeration & loopback capture +- `deps/aoo/` — Audio over OSC networking library +- `CMakeLists.txt` — build config + +## Bugs Found & Fixed +- **Stale %APPDATA% state breaks connections** — clearing SonoBus appdata fixes direct connect issues between different builds +- **JUCE 8 shared mode loopback** — broken, reverted to JUCE 7 + +## Resources +- Virtual Audio Driver (for PCs with no real output): https://github.com/VirtualDrivers/Virtual-Audio-Driver diff --git a/deps/juce/.gitrepo b/deps/juce/.gitrepo deleted file mode 100644 index e01352ad9..000000000 --- a/deps/juce/.gitrepo +++ /dev/null @@ -1,12 +0,0 @@ -; DO NOT EDIT (unless you know what you are doing) -; -; This subdirectory is a git "subrepo", and this file is maintained by the -; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme -; -[subrepo] - remote = https://github.com/essej/JUCE.git - branch = sono7good - commit = 925e37b00500afe60ad64cacdbcbc8c85f1f2ec5 - method = merge - cmdver = 0.4.5 - parent = 5ebf788e14e46a9987266eae81878e9094452cd3 diff --git a/deps/juce/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/deps/juce/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index f7b63216a..df9ead908 100644 --- a/deps/juce/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/deps/juce/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -928,7 +928,7 @@ class AndroidProjectExporter final : public ProjectExporter mo << " implementation files('libs/" << File (d).getFileName() << "')" << newLine; if (isInAppBillingEnabled()) - mo << " implementation 'com.android.billingclient:billing:5.0.0'" << newLine; + mo << " implementation 'com.android.billingclient:billing:7.0.0'" << newLine; if (areRemoteNotificationsEnabled()) { diff --git a/deps/juce/modules/juce_audio_devices/native/juce_WASAPI_windows.cpp b/deps/juce/modules/juce_audio_devices/native/juce_WASAPI_windows.cpp index c92548694..bb7af3c0e 100644 --- a/deps/juce/modules/juce_audio_devices/native/juce_WASAPI_windows.cpp +++ b/deps/juce/modules/juce_audio_devices/native/juce_WASAPI_windows.cpp @@ -415,12 +415,30 @@ static bool supportsSampleRateConversion (WASAPIDeviceMode deviceMode) noexcept } //============================================================================== +static const String loopbackDeviceIdPrefix ("JUCE_LOOPBACK::"); + +static bool isLoopbackDeviceId (const String& deviceId) +{ + return deviceId.startsWith (loopbackDeviceIdPrefix); +} + +static String makeLoopbackDeviceId (const String& renderDeviceId) +{ + return loopbackDeviceIdPrefix + renderDeviceId; +} + +static String getRenderDeviceIdFromLoopback (const String& loopbackDeviceId) +{ + return loopbackDeviceId.fromFirstOccurrenceOf (loopbackDeviceIdPrefix, false, false); +} + class WASAPIDeviceBase { public: - WASAPIDeviceBase (const ComSmartPtr& d, WASAPIDeviceMode mode) + WASAPIDeviceBase (const ComSmartPtr& d, WASAPIDeviceMode mode, bool loopback = false) : device (d), - deviceMode (mode) + deviceMode (mode), + isLoopbackDevice (loopback) { clientEvent = CreateEvent (nullptr, false, false, nullptr); @@ -439,7 +457,7 @@ class WASAPIDeviceBase rates.addUsingDefaultSort (defaultSampleRate); defaultFormatChannelMask = format->dwChannelMask; - if (isExclusiveMode (deviceMode)) + if (isExclusiveMode (deviceMode) && ! isLoopbackDevice) if (auto optFormat = findSupportedFormat (tempClient, defaultNumChannels, defaultSampleRate)) format = optFormat; @@ -542,6 +560,7 @@ class WASAPIDeviceBase ComSmartPtr client; WASAPIDeviceMode deviceMode; + bool isLoopbackDevice = false; double sampleRate = 0, defaultSampleRate = 0; int numChannels = 0, actualNumChannels = 0, maxNumChannels = 0, defaultNumChannels = 0; @@ -699,11 +718,12 @@ class WASAPIDeviceBase WAVEFORMATEX* nearestFormat = nullptr; - if (SUCCEEDED (audioClient->IsFormatSupported (isExclusiveMode (deviceMode) ? AUDCLNT_SHAREMODE_EXCLUSIVE - : AUDCLNT_SHAREMODE_SHARED, + const bool useExclusiveSR = isExclusiveMode (deviceMode) && ! isLoopbackDevice; + if (SUCCEEDED (audioClient->IsFormatSupported (useExclusiveSR ? AUDCLNT_SHAREMODE_EXCLUSIVE + : AUDCLNT_SHAREMODE_SHARED, (WAVEFORMATEX*) &format, - isExclusiveMode (deviceMode) ? nullptr - : &nearestFormat))) + useExclusiveSR ? nullptr + : &nearestFormat))) { if (nearestFormat != nullptr) rate = (double) nearestFormat->nSamplesPerSec; @@ -736,7 +756,7 @@ class WASAPIDeviceBase static std::optional tryFormat (const AudioSampleFormat sampleFormat, IAudioClient* clientToUse, WASAPIDeviceMode mode, int newNumChannels, double newSampleRate, - DWORD newMixFormatChannelMask) + DWORD newMixFormatChannelMask, bool loopback = false) { WAVEFORMATEXTENSIBLE format; zerostruct (format); @@ -762,11 +782,12 @@ class WASAPIDeviceBase WAVEFORMATEX* nearestFormat = nullptr; - HRESULT hr = clientToUse->IsFormatSupported (isExclusiveMode (mode) ? AUDCLNT_SHAREMODE_EXCLUSIVE - : AUDCLNT_SHAREMODE_SHARED, + const bool useExclusive = isExclusiveMode (mode) && ! loopback; + HRESULT hr = clientToUse->IsFormatSupported (useExclusive ? AUDCLNT_SHAREMODE_EXCLUSIVE + : AUDCLNT_SHAREMODE_SHARED, (WAVEFORMATEX*) &format, - isExclusiveMode (mode) ? nullptr - : &nearestFormat); + useExclusive ? nullptr + : &nearestFormat); logFailure (hr); auto supportsSRC = supportsSampleRateConversion (mode); @@ -803,7 +824,7 @@ class WASAPIDeviceBase auto mixFormatChannelMask = (ch == defaultNumChannels ? defaultFormatChannelMask : maskWithLowestNBitsSet); for (auto const& sampleFormat: formatsToTry) - if (auto format = tryFormat (sampleFormat, clientToUse, deviceMode, ch, newSampleRate, mixFormatChannelMask)) + if (auto format = tryFormat (sampleFormat, clientToUse, deviceMode, ch, newSampleRate, mixFormatChannelMask, isLoopbackDevice)) return format; } @@ -826,7 +847,7 @@ class WASAPIDeviceBase for (auto rate : rates) for (auto const& sampleFormat: formatsToTry) - if (auto format = tryFormat (sampleFormat, clientToUse, deviceMode, ch, rate, channelMask)) + if (auto format = tryFormat (sampleFormat, clientToUse, deviceMode, ch, rate, channelMask, isLoopbackDevice)) result = jmax (static_cast (format->Format.nChannels), result); } @@ -837,9 +858,18 @@ class WASAPIDeviceBase { DWORD streamFlags = 0x40000; /*AUDCLNT_STREAMFLAGS_EVENTCALLBACK*/ - if (supportsSampleRateConversion (deviceMode)) + if (isLoopbackDevice) + { + streamFlags |= 0x00020000; /*AUDCLNT_STREAMFLAGS_LOOPBACK*/ + // Loopback always needs sample rate conversion flags since it must use shared mode streamFlags |= (0x80000000 /*AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM*/ | 0x8000000); /*AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY*/ + } + else if (supportsSampleRateConversion (deviceMode)) + { + streamFlags |= (0x80000000 /*AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM*/ + | 0x8000000); /*AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY*/ + } return streamFlags; } @@ -861,17 +891,19 @@ class WASAPIDeviceBase check (client->GetDevicePeriod (&defaultPeriod, &minPeriod)); - if (isExclusiveMode (deviceMode) && bufferSizeSamples > 0) + const bool useExclusive = isExclusiveMode (deviceMode) && ! isLoopbackDevice; + + if (useExclusive && bufferSizeSamples > 0) defaultPeriod = jmax (minPeriod, samplesToRefTime (bufferSizeSamples, format.Format.nSamplesPerSec)); for (;;) { GUID session; - auto hr = client->Initialize (isExclusiveMode (deviceMode) ? AUDCLNT_SHAREMODE_EXCLUSIVE - : AUDCLNT_SHAREMODE_SHARED, + auto hr = client->Initialize (useExclusive ? AUDCLNT_SHAREMODE_EXCLUSIVE + : AUDCLNT_SHAREMODE_SHARED, getStreamFlags(), defaultPeriod, - isExclusiveMode (deviceMode) ? defaultPeriod : 0, + useExclusive ? defaultPeriod : 0, (WAVEFORMATEX*) &format, &session); @@ -926,8 +958,8 @@ class WASAPIDeviceBase class WASAPIInputDevice final : public WASAPIDeviceBase { public: - WASAPIInputDevice (const ComSmartPtr& d, WASAPIDeviceMode mode) - : WASAPIDeviceBase (d, mode) + WASAPIInputDevice (const ComSmartPtr& d, WASAPIDeviceMode mode, bool loopback = false) + : WASAPIDeviceBase (d, mode, loopback) { } @@ -1646,6 +1678,11 @@ class WASAPIAudioIODevice final : public AudioIODevice, if (! check (deviceCollection->GetCount (&numDevices))) return false; + // Check if input is a loopback device + const bool inputIsLoopback = isLoopbackDeviceId (inputDeviceId); + const String actualInputDeviceId = inputIsLoopback ? getRenderDeviceIdFromLoopback (inputDeviceId) + : inputDeviceId; + for (UINT32 i = 0; i < numDevices; ++i) { ComSmartPtr device; @@ -1660,9 +1697,17 @@ class WASAPIAudioIODevice final : public AudioIODevice, auto flow = getDataFlow (device); - if (deviceId == inputDeviceId && flow == eCapture) + if (inputIsLoopback && deviceId == actualInputDeviceId && flow == eRender) + { + // Open the render device as a loopback capture device (shared mode forced) + inputDevice.reset (new WASAPIInputDevice (device, deviceMode, true)); + } + else if (! inputIsLoopback && deviceId == inputDeviceId && flow == eCapture) + { inputDevice.reset (new WASAPIInputDevice (device, deviceMode)); - else if (deviceId == outputDeviceId && flow == eRender) + } + + if (deviceId == outputDeviceId && flow == eRender) outputDevice.reset (new WASAPIOutputDevice (device, deviceMode)); } @@ -1930,6 +1975,10 @@ class WASAPIAudioIODeviceType final : public AudioIODeviceType const int index = (deviceId == defaultRenderer) ? 0 : -1; result.outputDeviceIds.insert (index, deviceId); result.outputDeviceNames.insert (index, name); + + // Also add as a loopback input device + result.inputDeviceIds.add (makeLoopbackDeviceId (deviceId)); + result.inputDeviceNames.add (name + " (Loopback)"); } else if (flow == eCapture) { diff --git a/deps/juce/modules/juce_gui_basics/juce_gui_basics.cpp b/deps/juce/modules/juce_gui_basics/juce_gui_basics.cpp index 518f39397..89435e3ef 100644 --- a/deps/juce/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/deps/juce/modules/juce_gui_basics/juce_gui_basics.cpp @@ -52,6 +52,10 @@ #import #import +#if defined (MAC_OS_VERSION_14_4) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_14_4 + #import +#endif + #elif JUCE_IOS #if JUCE_PUSH_NOTIFICATIONS #import diff --git a/deps/juce/modules/juce_gui_basics/native/juce_Windowing_mac.mm b/deps/juce/modules/juce_gui_basics/native/juce_Windowing_mac.mm index ad459d483..b559a0f17 100644 --- a/deps/juce/modules/juce_gui_basics/native/juce_Windowing_mac.mm +++ b/deps/juce/modules/juce_gui_basics/native/juce_Windowing_mac.mm @@ -520,37 +520,100 @@ static Image createNSWindowSnapshot (NSWindow* nsWindow) { JUCE_AUTORELEASEPOOL { - // CGWindowListCreateImage is replaced by functions in the ScreenCaptureKit framework, but - // that framework is only available from macOS 12.3 onwards. - // A suitable @available check should be added once the minimum build OS is 12.3 or greater, - // so that ScreenCaptureKit can be weak-linked. - #if defined (MAC_OS_VERSION_14_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_14_0 - JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") - #define JUCE_DEPRECATION_IGNORED 1 - #endif + const auto createImageFromCGImage = [&] (CGImageRef cgImage) + { + jassert (cgImage != nullptr); - CGImageRef screenShot = CGWindowListCreateImage (CGRectNull, - kCGWindowListOptionIncludingWindow, - (CGWindowID) [nsWindow windowNumber], - kCGWindowImageBoundsIgnoreFraming); + const auto width = CGImageGetWidth (cgImage); + const auto height = CGImageGetHeight (cgImage); + const auto cgRect = CGRectMake (0, 0, (CGFloat) width, (CGFloat) height); + const Image image (Image::ARGB, (int) width, (int) height, true); - #if JUCE_DEPRECATION_IGNORED - JUCE_END_IGNORE_WARNINGS_GCC_LIKE - #undef JUCE_DEPRECATION_IGNORED - #endif + CGContextDrawImage (juce_getImageContext (image), cgRect, cgImage); - NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage: screenShot]; + return image; + }; - Image result (Image::ARGB, (int) [bitmapRep size].width, (int) [bitmapRep size].height, true); + #if defined (MAC_OS_VERSION_14_4) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_14_4 - selectImageForDrawing (result); - [bitmapRep drawAtPoint: NSMakePoint (0, 0)]; - releaseImageAfterDrawing(); + if (dlopen ("/System/Library/Frameworks/ScreenCaptureKit.framework/ScreenCaptureKit", RTLD_LAZY) == nullptr) + { + DBG (dlerror()); + jassertfalse; + return {}; + } - [bitmapRep release]; - CGImageRelease (screenShot); + std::promise result; - return result; + const auto windowId = nsWindow.windowNumber; + const auto windowRect = [nsWindow.screen convertRectToBacking: nsWindow.frame].size; + + const auto onSharableContent = [&] (SCShareableContent* content, NSError* contentError) + { + if (contentError != nullptr) + { + jassertfalse; + result.set_value (Image{}); + return; + } + + const auto window = [&]() -> SCWindow* + { + for (SCWindow* w in content.windows) + if (w.windowID == windowId) + return w; + + return nullptr; + }(); + + if (window == nullptr) + { + jassertfalse; + result.set_value (Image{}); + return; + } + + Class contentFilterClass = NSClassFromString (@"SCContentFilter"); + SCContentFilter* filter = [[[contentFilterClass alloc] initWithDesktopIndependentWindow: window] autorelease]; + + Class streamConfigurationClass = NSClassFromString (@"SCStreamConfiguration"); + SCStreamConfiguration* config = [[[streamConfigurationClass alloc] init] autorelease]; + config.colorSpaceName = kCGColorSpaceSRGB; + config.showsCursor = NO; + config.ignoreShadowsSingleWindow = YES; + config.captureResolution = SCCaptureResolutionBest; + config.ignoreGlobalClipSingleWindow = YES; + config.includeChildWindows = NO; + config.width = (size_t) windowRect.width; + config.height = (size_t) windowRect.height; + + const auto onScreenshot = [&] (CGImageRef screenshot, NSError* screenshotError) + { + jassert (screenshotError == nullptr); + result.set_value (screenshotError == nullptr ? createImageFromCGImage (screenshot) : Image{}); + }; + + Class screenshotManagerClass = NSClassFromString (@"SCScreenshotManager"); + [screenshotManagerClass captureImageWithFilter: filter + configuration: config + completionHandler: onScreenshot]; + }; + + Class shareableContentClass = NSClassFromString (@"SCShareableContent"); + [shareableContentClass getCurrentProcessShareableContentWithCompletionHandler: onSharableContent]; + + return result.get_future().get(); + + #else + + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + return createImageFromCGImage ((CGImageRef) CFAutorelease (CGWindowListCreateImage (CGRectNull, + kCGWindowListOptionIncludingWindow, + (CGWindowID) [nsWindow windowNumber], + kCGWindowImageBoundsIgnoreFraming))); + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + + #endif } } diff --git a/deps/juce/modules/juce_product_unlocking/native/juce_InAppPurchases_android.cpp b/deps/juce/modules/juce_product_unlocking/native/juce_InAppPurchases_android.cpp index 5915a0560..0d7d6b69b 100644 --- a/deps/juce/modules/juce_product_unlocking/native/juce_InAppPurchases_android.cpp +++ b/deps/juce/modules/juce_product_unlocking/native/juce_InAppPurchases_android.cpp @@ -90,9 +90,9 @@ DECLARE_JNI_CLASS (BillingFlowParamsBuilder, "com/android/billingclient/api/Bill #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams;") \ - METHOD (setOldPurchaseToken, "setOldPurchaseToken", "(Ljava/lang/String;)Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;") \ - METHOD (setReplaceProrationMode, "setReplaceProrationMode", "(I)Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;") + METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams;") \ + METHOD (setOldPurchaseToken, "setOldPurchaseToken", "(Ljava/lang/String;)Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;") \ + METHOD (setSubscriptionReplacementMode, "setSubscriptionReplacementMode", "(I)Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;") DECLARE_JNI_CLASS (BillingFlowParamsSubscriptionUpdateParamsBuilder, "com/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder") #undef JNI_CLASS_MEMBERS @@ -847,9 +847,10 @@ struct InAppPurchases::Pimpl if (! creditForUnusedSubscription) { + constexpr auto WITHOUT_PRORATION = 3; env->CallObjectMethod (subscriptionBuilder.get(), - BillingFlowParamsSubscriptionUpdateParamsBuilder.setReplaceProrationMode, - 3 /*IMMEDIATE_WITHOUT_PRORATION*/); + BillingFlowParamsSubscriptionUpdateParamsBuilder.setSubscriptionReplacementMode, + WITHOUT_PRORATION); } const LocalRef subscriptionParams { env->CallObjectMethod (subscriptionBuilder.get(),