Skip to content

Release v1.0

Latest

Choose a tag to compare

@marcomorosi06 marcomorosi06 released this 24 May 18:07
· 22 commits to master since this release

WiFi Audio Streaming Desktop - v1.0 🎉

Sorry for the two-month silence. I was studying C for an exam and, you know how it goes, I thought: "Why not rewrite the entire audio capture and processing engine from scratch for the third time?" Turns out that's a completely normal thing to do during exam season.

The good news: it paid off. This is a real 1.0. Not a "I slapped a new version number on it" 1.0. A "I rewrote the core, killed the driver requirement, added a tray icon, and taught the mic to mix itself (sort of)" 1.0.


⚠️ Breaking Changes

Before you upgrade, a heads-up on two things that changed under the hood:

New wire protocol. Every audio UDP packet now carries a 10-byte header (WF magic + version + flags + sequence number + sample position) prepended to the raw PCM payload. The client tells audio packets from control messages (PING/BYE/HELLO) by checking that WF magic instead of trying to parse text. Old clients won't talk to new servers and vice versa, update both sides at the same time.

experimentalFeaturesEnabled is gone. The feature flag that gated microphone access has been retired. Mic sharing is now a first-class citizen with its own dedicated routing system. If you were relying on that toggle for anything, just know: it doesn't exist anymore, and that's a good thing.


🚀 The Big One: Native Audio Engine (Windows & macOS)

The headline feature of 1.0. On Windows and macOS, audio capture no longer goes through FFmpeg + a virtual driver. There's now a native C library (AudioEngine) loaded via JNI that captures system audio directly, no VB-Cable, no BlackHole required to stream.

FFmpeg is still there, but it's been demoted: it only handles the AAC and Opus encoders for the HTTP server. The heavy lifting of capture is handled by the native engine now.

On Linux, the FFmpeg + PulseAudio backend is unchanged. (Linux users: you're not forgotten, the architecture is just different enough that the native path needs more work there.)

A personal note on macOS audio quality. In previous versions, using a Mac as the server was a somewhat traumatic experience; the audio on the receiving end ranged from "serviceable" to "sounds like the mic is submerged in a bucket of reverb." With the native engine, after granting the approximately twelve thousand privacy permissions that macOS requires before letting any app breathe near your audio subsystem, the stream is actually clean and crisp. Genuinely crisp. I was surprised too.

The Gradle build system grew accordingly: new compileNative, buildNative, and copyNativeLib tasks compile the C library via CMake and bundle it into the JAR under resources/native/<os>/<arch>/. There's even an automatic cmake.exe finder for Windows that searches every known path (Visual Studio, CLion, Scoop, Chocolatey, winget) so you don't have to fiddle with environment variables.


🎙️ Microphone Routing, Properly

The experimental microphone feature has been redesigned from the ground up with a proper three-mode selector:

Mode What it does
Off Ignores any incoming mic stream
Virtual Microphone Exposes the received audio as a virtual input device; shows up in Discord, Zoom, games as a real mic
Mix Into Stream Blends the mic directly into the outgoing audio so everyone hears it without any extra app setup

Platform details for Virtual Mic:

  • Linux: a PulseAudio null sink named WFAS_VirtualMic is created automatically via pactl. Open pavucontrol, point your app at it, done.
  • Windows: uses the new native engine's micSinkOpen() to write directly to a WASAPI endpoint, with a SourceDataLine fallback if that fails.
  • macOS: routes through BlackHole or any other SourceDataLine-compatible output.

There's also a new local mic mix mode where the server picks up a physical microphone on its own machine and blends it into the stream, no client involvement needed.

And of course, there's a mute button. Press it during streaming and the mic volume drops to zero instantly, propagated via StateFlow to every active job. No reconnect, no restart.

The UI includes step-by-step setup guides for VB-CABLE (Windows), BlackHole (macOS), and the automatic virtual sink (Linux), with direct download links.


🔔 Connection & Disconnection Sounds

The client now plays a sound when it connects to a server and another when it disconnects. Both are loaded from /raw/connection_sound.wav and /raw/disconnection_sound.wav in the classpath. If those files aren't present, the app synthesizes them on the fly (880 + 1320 Hz chord for connection, 660 + 440 Hz for disconnection). Both can be toggled independently in settings.


🗂️ System Tray

The app now lives in your system tray.

Two new settings:

  • Start Minimized to Tray: launch hidden. Combined with Launch at Startup, the app behaves like AirPlay or Chromecast: always on, never in your face.
  • Close to Tray (default: on): clicking the ✕ button hides the window instead of quitting. Right-click the tray icon → Quit to actually exit.

🐧 Arch Linux friends, this one's for you

No more excuses. A .tar, a ready-to-use AppImage, and the package is also on the AUR, one command away (I hope, I don't use arch guys I'm sorry):

yay -S wifi-audio-streaming-desktop

Any other AUR helper works just as well. If you're building manually with makepkg, you already know what to do.


🔧 Other Improvements

Smarter network interface detection. getActiveNetworkInterface() now tries to match the interface by actual local IP address first, before falling back to the name-based filter. Much less likely to pick a VPN adapter or a VMware bridge on machines with several NICs.

Audio settings in the discovery beacon. The server now broadcasts its sample rate, channel count, and bit depth (sr=, ch=, bd=) in the multicast beacon. Clients in multicast mode use the server's audio settings to open the output line, which eliminates glitch artifacts caused by sample rate mismatches.

Multicast socket binding fixed. The client-side multicast socket now uses explicit bind() + joinGroup() with a specific NetworkInterface. The old approach broke silently on certain JVM/OS combinations.

Lifecycle management hardened. A lifecycleMutex + generation counter prevents race conditions during rapid stop/restart cycles. requestStopCurrentStream() returns a Job for fire-and-forget use; stopCurrentStream() is now a wrapper around it.

CLIENT_BYE message. When the client disconnects cleanly, it now sends a CLIENT_BYE message to the server. The server tears down immediately instead of waiting out three missed PINGs. Makes reconnecting feel snappier.

RTP and HTTP force multicast. If either protocol is enabled, isMulticastMode is forced to true automatically. Previously this was just a UI hint with no enforcement.

Buffer size default changed. The default bufferSize in settings dropped from 6400 to 512 samples. With the native engine, structural latency is near zero, so a large buffer only adds delay. If you're on a high-jitter network and hear dropouts, bump it up in Settings → Audio Quality.

Output line buffer back to ×4. The client-side SourceDataLine buffer was set to ×1 in the beta in a perhaps overzealous latency hunt. It's back to ×4; ×1 was causing underruns on any network with more than a few milliseconds of jitter.

Persistent multicast preference. The last position of the multicast toggle is now saved and restored on next launch.

EUPL 1.2 license headers added to all source files, see below for the full story.


⚖️ License Change: MIT → EUPL 1.2

This project started as a personal hack to solve a problem I had. I threw it on GitHub under MIT because I wasn't thinking about it much; it was a script that grew legs, and MIT is what you pick when you don't really care.

Then I kept working on it. Then I started caring. Then I rewrote the audio engine from scratch during exam season, which is a fairly reliable indicator that you've become emotionally invested in something. At that point I figured the license deserved a second thought.

The spirit of the change: using this project, modifying it, and building on top of it is absolutely welcome, as long as it benefits the commons. If you take this code and make it better, that improvement should flow back to everyone, not disappear into a proprietary product.

The [European Union Public Licence 1.2](https://joinup.ec.europa.eu/software/page/eupl) is essentially a copyleft licence with a few practical differences from MIT worth knowing:

MIT EUPL 1.2
Use freely
Modify
Distribute ✅, but source must be provided
Use in proprietary software ❌, derivatives must stay open
Network use triggers copyleft ✅, if you run a modified version as a service, you must release the source
Compatible with GPL v2/v3 varies ✅ explicitly
Legally reviewed for EU law ✅, drafted and approved by the European Commission

The short version: do whatever you want with it, just keep it open.


🛠️ For Developers

The project now builds the native C library as part of the Gradle lifecycle. Before your first build (or after pulling changes to src/main/native/), run:

./gradlew copyNativeLib

This configures CMake, compiles audio_engine, and copies the output into src/main/resources/native/. The compileKotlin and processResources tasks depend on this automatically, so a plain ./gradlew build will do the right thing once you've run it once.

On Windows, Gradle will search for cmake.exe in all the usual spots. If it can't find it, the error message tells you exactly which package manager commands to run.


📦 Supported Platforms

Platform Architecture Native Engine Tray
Windows 10/11 x86_64 ✅ Native
macOS 13+ (Ventura) x86_64, arm64 ✅ Native
Linux x86_64 ❌ (FFmpeg) ✅ Dorkbox

macOS 12.2 and earlier are not supported by the native engine (ScreenCaptureKit requires 12.3+). You can still use the legacy FFmpeg + BlackHole path by disabling the native engine in Settings → Advanced, even though i don't reccommend it lol.


Thank you to everyone who reported bugs, opened issues, and tested the beta builds. The app is better because of you, but there's still room for improvement, as always. ;)

If you'd like to support the project, you can buy me a coffee on Ko-fi, it goes a long way! ☕

Support on Ko-fi