Repository navigation
Release Notes - SoundFlow v1.4.2
Reverse playback, an AEC fix that actually matters, and some long-overdue performance work.
This one started as a simple answer to a feature request and spiraled into something bigger. Here's what happened:
- Someone asked for reverse playback. I said "sure, probably doable." Then I remembered WSOLA doesn't do negative speeds, looping needs to work in both directions, and buffers need to be reset when you flip direction mid-stream. It got interesting.
- The WebRTC AEC was silently doing nothing (#116). The far-end reference stream—the playback audio that the echo canceller compares against—was never being fed to the APM. The event for it only existed on capture devices. So the AEC was essentially comparing the microphone to silence and calling it a day.
- MP3 seeking got slower the longer the file (#115). MiniAudio's internal seek points were set to zero, so every seek became a binary search through the entire stream. Long VBR files without a Xing header were the worst offenders.
This release fixes all three. There's a small rename in DeviceSwitcher that I'll flag below, but otherwise nothing should break.
🔧 What Changed
Reverse Playback (Issue #114)
PlaybackSpeed now accepts negative values. Setting it to -1.0f plays the audio backwards at normal speed.
- Time-stretching (WSOLA) is bypassed for negative speeds—it doesn't support them—and linear interpolation is used instead. Pitch will be affected in reverse. This is documented on
ISoundPlayer.PlaybackSpeed. - Looping works in both directions. In reverse, the loop starts at the end and wraps back to the start.
- Changing direction mid-playback resets the internal resample and time-stretcher buffers, so you don't get stale samples bleeding through.
SimplePlayersample project now allows speeds down to-1.0x, and shows hours in the time display for long files.
Unified OnAudioProcessed Event (Issue #116)
The event moved from AudioCaptureDevice to the base AudioDevice class. Playback devices now raise it with the final mixed buffer just before it hits the hardware—which is exactly what WebRTC AEC needs as a far-end reference.
FullDuplexDeviceroutes subscriptions to both its capture and playback children, so a single subscription covers both directions.DeviceSwitcherrenames below support the new base-class type.
MP3 Seeking Performance (Issue #115)
MiniAudioDecoder now takes a seekPointCount parameter (default: 100). This tells MiniAudio how many internal seek points to build while decoding, which turns seeking from a full binary search into a much shorter one.
- Seeking no longer requires the decoder to know the total stream length upfront. If
ma_decoder_get_length_in_pcm_framesfails (VBR MP3 without a Xing header, for example), seeking still works. - Fixed a relative seek offset calculation in the decoder's seek callback. MiniAudio's binary search uses
SeekPoint.FromCurrentwith a negative offset; the old code was passing it straight through instead of computing an absolute target first. - Seek targets are now clamped to the stream's actual bounds.
StreamDataProvider.Length Reliability
Length now returns the larger of the decoder's length estimate and the metadata-derived estimate. When they disagree, the smaller one is usually wrong—and a too-small length causes impossible states like Time > Duration, breaks seeking near the end, and can trigger premature end-of-stream handling.
iOS FFmpeg Info.plist (Issue #113)
The FFMpeg framework generated by the CMake build had an invalid Info.plist for App Store validation. The workflow now patches it with:
CFBundleShortVersionString→1.4.0CFBundleVersion→1CFBundleName→SoundFlow FFmpegMinimumOSVersion→12.0(if missing)
DeviceSwitcher Renames
| Old | New | Why |
|---|---|---|
PreserveCaptureState(AudioCaptureDevice) |
PreserveEventSubscribers(AudioDevice) |
Works for playback devices now too |
RestoreCaptureState(AudioCaptureDevice, Delegate[]) |
RestoreEventSubscribers(AudioDevice, Delegate[]) |
Same |
The old names were misleading—they only preserved event subscribers, not full device state. If you were calling these directly, update the names and the parameter types will accept any AudioDevice.
Minor
MiniAudioDecodernow passesnullfor the seek callback when the stream doesn't support seeking, instead of passing a callback that would fail at runtime.
⚠️ Breaking Changes
Only one, and it's a rename:
DeviceSwitcher.PreserveCaptureState→DeviceSwitcher.PreserveEventSubscribersDeviceSwitcher.RestoreCaptureState→DeviceSwitcher.RestoreEventSubscribers
Both now accept AudioDevice instead of AudioCaptureDevice. If you're only using them with capture devices, this is a find-and-replace.
The OnAudioProcessed event move from AudioCaptureDevice to AudioDevice is not breaking—existing subscriptions on capture devices continue to work. You just get playback device support now, too.
A Note on Timing
I'm still in service. This was put together on a weekends passes (in the unit, lol) with a lot of coffee. If something's off, open an issue and I'll get to it when I can.
The project is still in hibernation until February/March 2027. But if a critical bug shows up—something that makes a shipped feature silently do nothing—I'd rather fix it than leave you all guessing why your echo cancellation sounds like it's not there.
Thank you for the bug reports, the feature requests, and the patience while I'm away.
— Ahmed Abdallah (SoundFlow's Maintainer)