Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
$<$<PLATFORM_ID:Windows>:mmdevapi>
PUBLIC
juce::juce_recommended_config_flags
juce::juce_recommended_lto_flags
Expand Down
111 changes: 111 additions & 0 deletions FORK_README.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion Source/ConnectView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ publicGroupsListModel(this)
mAddRemoteHostEditor = std::make_unique<TextEditor>("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));


Expand Down Expand Up @@ -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<CallOutBox*>(directConnectCalloutBox.get())) {
callout->dismiss();
Expand Down
2 changes: 2 additions & 0 deletions Source/SonoStandaloneFilterWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
// HACK
#include "SonobusPluginEditor.h"


#include <limits>
#include <algorithm>

Expand Down Expand Up @@ -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<iOSAudioIODevice*> (deviceManager.getCurrentAudioDevice())) {
Expand Down
10 changes: 8 additions & 2 deletions Source/SonobusPluginProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<AooServerConnectionInfo> recents;
getRecentServerConnectionInfos(recents);

if (recents.size() > 0) {
const auto & info = recents.getReference(0);

Expand Down
4 changes: 4 additions & 0 deletions Source/SonobusPluginProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ class SonobusAudioProcessor : public AudioProcessor, public AudioProcessorValue
int getRecentServerConnectionInfos(Array<AooServerConnectionInfo> & 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; }

Expand Down Expand Up @@ -1122,6 +1125,7 @@ class SonobusAudioProcessor : public AudioProcessor, public AudioProcessorValue

Array<AooServerConnectionInfo> mRecentConnectionInfos;
CriticalSection mRecentsLock;
String mLastDirectConnectAddress;

AooServerConnectionInfo mPendingReconnectInfo;
bool mPendingReconnect = false;
Expand Down
134 changes: 134 additions & 0 deletions audio.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 0 additions & 12 deletions deps/juce/.gitrepo

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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())
{
Expand Down
Loading