Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/build-extensions-apm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ jobs:
matrix:
include:
# Windows builds
- os: windows-latest
- os: windows-2025
rid: win-x64
platform: Windows
arch: x64
toolchain: mingw
cross_file: "windows-x64.crossfile"

- os: windows-latest
- os: windows-2025
rid: win-x86
platform: Windows
arch: x86
Expand Down
27 changes: 25 additions & 2 deletions .github/workflows/build-ffmpeg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ jobs:
matrix:
include:
- arch: x64
os: windows-latest
os: windows-2025
msystem: MINGW64
mingw-package-prefix: mingw-w64-x86_64
- arch: x86
os: windows-latest
os: windows-2025
msystem: MINGW32
mingw-package-prefix: mingw-w64-i686
- arch: arm64
Expand Down Expand Up @@ -368,6 +368,29 @@ jobs:
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
cmake --build . --parallel --verbose

- name: Patch Info.plist for App Store Validation
run: |
PLIST="build/runtimes/ios-arm64/native/soundflow-ffmpeg.framework/Info.plist"
if [ -f "$PLIST" ]; then
echo "Patching $PLIST to meet altool/App Store requirements..."

# PlistBuddy is more reliable for CI scripting.
# 'Set' updates the key if it exists.
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString 1.4.0" "$PLIST"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

12. Future codec releases keep an old version 🐞 Bug ⚙ Maintainability

The plist patch hardcodes CFBundleShortVersionString to 1.4.0 independently of the release
version. When the release workflow packs a later FFmpeg package version, its bundled iOS framework
still identifies itself as 1.4.0.
Agent Prompt
## Issue description
The framework version is fixed even though package releases accept new versions.
## Fix Focus Areas
- .github/workflows/build-ffmpeg.yml[372-386]
- .github/workflows/release.yml[218-245]
## Recommended Fix
Pass the release version into the framework packaging step and write it to the plist instead of hardcoding 1.4.0.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

/usr/libexec/PlistBuddy -c "Set :CFBundleVersion 1" "$PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleName SoundFlow FFmpeg" "$PLIST"

# 'Add' creates the key if it doesn't exist (CMake sometimes omits MinimumOSVersion).
# We use '|| true' to ignore the error if it already exists.
/usr/libexec/PlistBuddy -c "Add :MinimumOSVersion string 12.0" "$PLIST" 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

11. The ios framework drops ios 11 support 🐞 Bug ≡ Correctness

The plist patch declares MinimumOSVersion as 12.0 although the same workflow builds the framework
with an iOS 11.0 deployment target. Successful artifacts therefore advertise a higher minimum
version than the binary was built for, excluding iOS 11 from the distributed framework.
Agent Prompt
## Issue description
The packaged plist declares a different minimum iOS version from the build target.
## Fix Focus Areas
- .github/workflows/build-ffmpeg.yml[364-386]
## Recommended Fix
Set MinimumOSVersion from the same deployment-target value used for compilation, or intentionally update both values together.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


echo "Patched Info.plist contents:"
cat "$PLIST"
else
echo "Warning: Info.plist not found at expected path: $PLIST"
fi

- name: Analyze Dependencies
run: |
ARTIFACT_DIR="build/runtimes/ios-arm64/native"
Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/build-miniaudio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,29 +144,29 @@ jobs:
matrix:
include:
# Windows builds
- os: windows-latest
- os: windows-2025
rid: win-x64
platform: Windows
arch: x64
cmake_target_arch: x64
lib_extension: ".dll"
toolchain: "Visual Studio 17 2022"
toolchain: "Visual Studio 18 2026"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

10. Windows builds request the wrong toolchain 🐞 Bug ☼ Reliability

The miniaudio Windows matrix requests the Visual Studio 2026 CMake generator while its
tool-installation step explicitly installs Visual Studio 2019 Build Tools. If the runner does not
separately provide the requested generator, every affected Windows configuration fails before
compilation; the PortMidi workflow now makes the same generator request without installing it.
Agent Prompt
## Issue description
Windows configuration requests a generator different from the explicitly installed toolchain.
## Fix Focus Areas
- .github/workflows/build-miniaudio.yml[147-169]
- .github/workflows/build-miniaudio.yml[260-278]
- .github/workflows/build-portmidi.yml[107-114]
## Recommended Fix
Request a generator that the workflows install or establish that the selected runner supplies the new generator for every Windows target.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


- os: windows-latest
- os: windows-2025
rid: win-x86
platform: Windows
arch: x86
cmake_target_arch: Win32
lib_extension: ".dll"
toolchain: "Visual Studio 17 2022"
toolchain: "Visual Studio 18 2026"

- os: windows-latest
- os: windows-2025
rid: win-arm64
platform: Windows
arch: ARM64
cmake_target_arch: ARM64
lib_extension: ".dll"
toolchain: "Visual Studio 17 2022"
toolchain: "Visual Studio 18 2026"

# Linux builds
- os: ubuntu-22.04
Expand Down Expand Up @@ -284,10 +284,16 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y cmake gcc g++ file binutils

- name: Set up JDK 17
uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: '17'

- name: Install Dependencies (Android)
if: matrix.platform == 'Android'
uses: android-actions/setup-android@v2
uses: android-actions/setup-android@v4

- name: Install Cross-Compilation tools (Linux)
if: matrix.platform == 'Linux' && (matrix.arch == 'armv7l' || matrix.arch == 'aarch64')
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build-portmidi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ jobs:
matrix:
include:
# Windows (MSVC)
- os: windows-latest
- os: windows-2025
rid: win-x64
platform: Windows
arch: x64
cmake_generator_arch: x64
- os: windows-latest
- os: windows-2025
rid: win-x86
platform: Windows
arch: x86
cmake_generator_arch: Win32
- os: windows-latest
- os: windows-2025
rid: win-arm64
platform: Windows
arch: ARM64
Expand Down Expand Up @@ -107,7 +107,7 @@ jobs:
run: |
CMAKE_ARGS=()
if [[ "$RUNNER_OS" == "Windows" ]]; then
CMAKE_ARGS+=("-G" "Visual Studio 17 2022" "-A" "${{ matrix.cmake_generator_arch }}")
CMAKE_ARGS+=("-G" "Visual Studio 18 2026" "-A" "${{ matrix.cmake_generator_arch }}")
elif [[ "$RUNNER_OS" == "macOS" ]]; then
CMAKE_ARGS+=("-DCMAKE_OSX_ARCHITECTURES=${{ matrix.arch }}")
fi
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ jobs:
inputs.release_webrtc == true ||
inputs.release_portmidi == true
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -238,13 +242,23 @@ jobs:
update_and_pack "Midi/SoundFlow.Midi.PortMidi/SoundFlow.Midi.PortMidi.csproj"
fi

- name: NuGet login (OIDC -> temporary API key)
if: inputs.publish_to_nuget == true
uses: NuGet/login@v1
id: nuget_login
with:
user: ${{ secrets.NUGET_USER }}

- name: Push Packages to NuGet.org
if: inputs.publish_to_nuget == true
run: |
if [ -z "$(ls -A packages/*.nupkg 2>/dev/null)" ]; then
echo "No packages were created. Skipping NuGet push."
else
dotnet nuget push "packages/*.nupkg" --source "https://api.nuget.org/v3/index.json" --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
dotnet nuget push "packages/*.nupkg" \
--source "https://api.nuget.org/v3/index.json" \
--api-key ${{ steps.nuget_login.outputs.NUGET_API_KEY }} \
--skip-duplicate
fi

- name: Upload NuGet Packages as Artifact
Expand Down
4 changes: 3 additions & 1 deletion Native/miniaudio-backend/library.c
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ MA_API ma_device_config* sf_allocate_device_config(const ma_device_type deviceTy

// Allocate memory for a decoder configuration struct.
MA_API ma_decoder_config *sf_allocate_decoder_config(const ma_format outputFormat, const ma_uint32 outputChannels,
const ma_uint32 outputSampleRate) {
const ma_uint32 outputSampleRate, const ma_uint32 seekPointCount) {
ma_decoder_config *pConfig = sf_create(ma_decoder_config);
if (pConfig == NULL) {
return NULL;
Expand All @@ -177,6 +177,8 @@ MA_API ma_decoder_config *sf_allocate_decoder_config(const ma_format outputForma
MA_ZERO_OBJECT(pConfig);
*pConfig = ma_decoder_config_init(outputFormat, outputChannels, outputSampleRate);

pConfig->seekPointCount = seekPointCount;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Seek settings differ by platform

The native function now accepts seekPointCount, but the packaged Android, Intel macOS, and FreeBSD libraries were not updated. Those unchanged libraries still use the previous implementation, which does not apply the fourth argument. Rebuild the remaining runtime assets so users on those platforms also get the new seek-point setting.

Knowledge Base Used: Audio devices and MiniAudio backend

Prompt To Fix With AI
This is a comment left during a code review.
Path: Native/miniaudio-backend/library.c
Line: 180

Comment:
**Seek settings differ by platform**

The native function now accepts `seekPointCount`, but the packaged Android, Intel macOS, and FreeBSD libraries were not updated. Those unchanged libraries still use the previous implementation, which does not apply the fourth argument. Rebuild the remaining runtime assets so users on those platforms also get the new seek-point setting.

**Knowledge Base Used:** [Audio devices and MiniAudio backend](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/audio-device-backend.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holy fuck, I actually built the native libraries and forgot to replace them in the Src/Backends/MiniAudio/runtimes, well, no one should use them anyway. Users either directly use the NuGet package (which runtimes get automatically updated on release) or just use the release/v1.4.2 branch.

It’s not that I’m too lazy to do another git commit and git push, but I really hate cluttering my Git history with random stuff.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right—the release workflow rebuilds the native libraries for all RIDs, replaces Src/Backends/MiniAudio/runtimes, and then packs NuGet and updates the release branch. My comment assumed those checked-in runtime binaries were the distribution source; for your supported release paths, that isn't the case. Please disregard it, and sorry for the unnecessary noise.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.


return pConfig;
}

Expand Down
2 changes: 1 addition & 1 deletion Native/miniaudio-backend/library.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ MA_API ma_device_config *sf_allocate_device_config(ma_device_type deviceType, ma

// Allocate memory for a decoder configuration struct.
MA_API ma_decoder_config *sf_allocate_decoder_config(ma_format outputFormat, ma_uint32 outputChannels,
ma_uint32 outputSampleRate);
ma_uint32 outputSampleRate, ma_uint32 seekPointCount);

// Allocate memory for an encoder configuration struct.
MA_API ma_encoder_config *sf_allocate_encoder_config(ma_format format,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ public static void DisplayPlaybackControls(SoundPlayer player)
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write(new string(' ', Console.WindowWidth - 1)); // Clear the line
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write($"\rTime: {TimeSpan.FromSeconds(player.Time):mm\\:ss\\.ff} / {TimeSpan.FromSeconds(player.Duration):mm\\:ss\\.ff} | Speed: {player.PlaybackSpeed:F1}x | Vol: {player.Volume:F1} ");
Console.Write($"\rTime: {FormatTime(TimeSpan.FromSeconds(player.Time))} / {FormatTime(TimeSpan.FromSeconds(player.Duration))} | Speed: {player.PlaybackSpeed:F1}x | Vol: {player.Volume:F1} ");
if (originalLeft < Console.WindowWidth)
{
Console.SetCursorPosition(originalLeft, originalTop);
}

string FormatTime(TimeSpan t) =>
t.TotalHours >= 1 ? t.ToString(@"hh\:mm\:ss\.ff") : t.ToString(@"mm\:ss\.ff");
}
};
timer.Start();
Expand All @@ -59,7 +62,7 @@ public static void DisplayPlaybackControls(SoundPlayer player)
player.PlaybackSpeed = Math.Min(player.PlaybackSpeed + 0.1f, 4.0f);
break;
case ConsoleKey.OemMinus or ConsoleKey.Subtract:
player.PlaybackSpeed = Math.Max(0.1f, player.PlaybackSpeed - 0.1f);
player.PlaybackSpeed = Math.Max(-1f, player.PlaybackSpeed - 0.1f);
break;
case ConsoleKey.T:
currentQuality = (WsolaPerformancePreset)(((int)currentQuality + 1) % 4);
Expand Down
26 changes: 1 addition & 25 deletions Src/Abstracts/Devices/AudioCaptureDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,11 @@ namespace SoundFlow.Abstracts.Devices;
/// </summary>
public abstract class AudioCaptureDevice : AudioDevice
{
/// <summary>
/// Occurs when new audio samples are captured from this device.
/// </summary>
public event AudioProcessCallback? OnAudioProcessed;

/// <summary>
/// Initializes a new instance of the <see cref="AudioCaptureDevice"/> class.
/// </summary>
/// <param name="engine">The parent audio engine.</param>
/// <param name="format">The desired audio format.</param>
/// <param name="config">The device configuration.</param>
protected AudioCaptureDevice(AudioEngine engine, AudioFormat format, DeviceConfig config) : base(engine, format, config) { }

/// <summary>
/// Invokes the <see cref="OnAudioProcessed"/> event with the captured samples.
/// This method is intended to be called by the backend implementation.
/// </summary>
/// <param name="samples">The captured audio samples.</param>
protected virtual void InvokeOnAudioProcessed(Span<float> samples)
{
OnAudioProcessed?.Invoke(samples, Capability);
}

/// <summary>
/// Gets the invocation list of the OnAudioProcessed event. For internal engine use only.
/// </summary>
/// <returns>An array of delegates subscribed to the event.</returns>
internal Delegate[] GetEventSubscribers()
{
return OnAudioProcessed?.GetInvocationList() ?? [];
}
}
}
29 changes: 28 additions & 1 deletion Src/Abstracts/Devices/AudioDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ public abstract class AudioDevice : IDisposable
/// </summary>
public EventHandler? OnDisposed;

/// <summary>
/// Occurs when audio samples are processed by this device.
/// For capture devices, this is raised when new audio is captured from the hardware.
/// For playback devices, this is raised after the master mixer renders the final output buffer,
/// just before it is sent to the hardware.
/// </summary>
public event AudioProcessCallback? OnAudioProcessed;

/// <summary>
/// Initializes a new instance of the <see cref="AudioDevice"/> class.
/// </summary>
Expand Down Expand Up @@ -76,8 +84,27 @@ protected AudioDevice(AudioEngine engine, AudioFormat format, DeviceConfig confi
/// </summary>
public abstract void Dispose();

/// <summary>
/// Invokes the <see cref="OnAudioProcessed"/> event with the processed samples.
/// This method is intended to be called by the backend implementation.
/// </summary>
/// <param name="samples">The processed audio samples.</param>
protected virtual void InvokeOnAudioProcessed(Span<float> samples)
{
OnAudioProcessed?.Invoke(samples, Capability);
}

/// <summary>
/// Gets the invocation list of the OnAudioProcessed event. For internal engine use only.
/// </summary>
/// <returns>An array of delegates subscribed to the event.</returns>
internal virtual Delegate[] GetEventSubscribers()
{
return OnAudioProcessed?.GetInvocationList() ?? [];
}

/// <summary>
/// Called when the audio device is disposed.
/// </summary>
protected virtual void OnDisposedHandler() => OnDisposed?.Invoke(this, EventArgs.Empty);
}
}
27 changes: 22 additions & 5 deletions Src/Abstracts/Devices/FullDuplexDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ public sealed class FullDuplexDevice : AudioDevice, IDisposable
public Mixer MasterMixer => PlaybackDevice.MasterMixer;

/// <summary>
/// Occurs when audio data is processed by the capture device.
/// Occurs when audio data is processed by either the capture or playback device.
/// Subscribers will receive both near-end (capture) and far-end (playback) frames.
/// </summary>
public event AudioProcessCallback? OnAudioProcessed
public new event AudioProcessCallback? OnAudioProcessed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Full-duplex callbacks disappear

FullDuplexDevice hides the new base event with new, so subscribing through an AudioDevice reference attaches to a separate event that never fires. For example, AudioDevice device = fullDuplexDevice; device.OnAudioProcessed += handler; receives neither capture nor playback samples. Make the base event overridable and override its accessors, or forward both child events into the base event.

Knowledge Base Used: Audio devices and MiniAudio backend

Prompt To Fix With AI
This is a comment left during a code review.
Path: Src/Abstracts/Devices/FullDuplexDevice.cs
Line: 33

Comment:
**Full-duplex callbacks disappear**

`FullDuplexDevice` hides the new base event with `new`, so subscribing through an `AudioDevice` reference attaches to a separate event that never fires. For example, `AudioDevice device = fullDuplexDevice; device.OnAudioProcessed += handler;` receives neither capture nor playback samples. Make the base event overridable and override its accessors, or forward both child events into the base event.

**Knowledge Base Used:** [Audio devices and MiniAudio backend](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/audio-device-backend.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

{
add => CaptureDevice.OnAudioProcessed += value;
remove => CaptureDevice.OnAudioProcessed -= value;
add
{
CaptureDevice.OnAudioProcessed += value;
PlaybackDevice.OnAudioProcessed += value;
Comment on lines +33 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

9. Base-typed duplex listeners hear nothing 🐞 Bug ≡ Correctness

FullDuplexDevice hides the new base OnAudioProcessed event with a separate forwarding event
instead of overriding subscription behavior. A listener holding the duplex device as AudioDevice
subscribes to its never-raised base event, rather than either child device's capture or playback
event.
Agent Prompt
## Issue description
The hidden duplex event and the base event have different subscriber lists.
## Fix Focus Areas
- Src/Abstracts/Devices/AudioDevice.cs[51-103]
- Src/Abstracts/Devices/FullDuplexDevice.cs[29-45]
## Recommended Fix
Use a common overridable subscription mechanism so subscriptions through either static device type route to both children; preserve that behavior during switching.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
remove
{
CaptureDevice.OnAudioProcessed -= value;
PlaybackDevice.OnAudioProcessed -= value;
}
}

/// <summary>
Expand Down Expand Up @@ -87,4 +96,12 @@ public override void Dispose()

IsDisposed = true;
}
}

/// <inheritdoc />
internal override Delegate[] GetEventSubscribers()
{
// Subscribers are routed to the child devices, so we retrieve them from there.
// Returning CaptureDevice's list is sufficient as both will have the same subscribers.
return CaptureDevice.GetEventSubscribers();
}
}
Loading
Loading