Repository navigation
feat(player): add reverse playback and fix device/decoder issues - #117
Conversation
- Add negative playback speed support with reverse audio generation, looping, and WSOLA bypass - Unify OnAudioProcessed event in AudioDevice base class; fix WebRTC AEC far-end reference (#116) - Improve MP3 seeking in MiniAudio decoder with configurable seek points and stream-length-agnostic seeking - Improve StreamDataProvider length calculation with better fallback logic - Rename DeviceSwitcher methods to PreserveEventSubscribers/RestoreEventSubscribers - Fix iOS FFMpeg framework Info.plist for App Store validation (#113) - Update CI: Windows 2025 runners, VS 2026, OIDC NuGet auth, Android setup v4, JDK 17 - Minor fixes: PlaybackEnded placement, time format with hours, reverse EOS handling Fixes #116, #115, #114, #113
PR Summary by QodoAdd reverse playback and fix audio device and decoder behavior
AI Description
Diagram
High-Level Assessment
Files changed (22)
|
Code Review by Qodo
1. Long reverse buffers repeat audio
|
| var samplesToRead = spaceAvailableInResampleBuffer; | ||
| var targetPos = _rawSamplePosition - samplesToRead; |
There was a problem hiding this comment.
1. Long reverse buffers repeat audio 🐞 Bug ≡ Correctness
FillResampleBuffer computes every reverse read position from _rawSamplePosition, which GenerateAudio updates only after finishing the output buffer. When one output call requires another fill after exhausting the first, it reads the same source region again.
Agent Prompt
## Issue description
Multiple reverse fills in one output call can reread the same samples.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[279-335]
- Src/Abstracts/SoundPlayerBase.cs[380-425]
## Recommended Fix
Maintain a cursor for the next reverse source read and move it after each successful fill, independently of the position of audio already rendered.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| tempSpan[..read].CopyTo(_resampleBuffer.AsSpan(_resampleBufferValidSamples)); | ||
| _resampleBufferValidSamples += read; | ||
| totalSourceSamplesRepresented -= read; // Negative to indicate reverse movement |
There was a problem hiding this comment.
7. Reverse playback time jumps ahead 🐞 Bug ≡ Correctness
FillResampleBuffer reports every prefetched reverse sample as source movement even when most of those samples remain buffered. After a small output callback fills the much larger resample buffer, Time jumps backward by the entire prefetched block rather than the audio played.
Agent Prompt
## Issue description
Reverse read-ahead is reported as elapsed playback position.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[103-108]
- Src/Abstracts/SoundPlayerBase.cs[279-335]
- Src/Abstracts/SoundPlayerBase.cs[416-425]
## Recommended Fix
Keep the reverse read cursor separate from the playhead and update the reported position according to source frames actually rendered.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| var actualLoopEnd = (_loopEndSamples == -1) ? _dataProvider.Length : Math.Min(_loopEndSamples, _dataProvider.Length); | ||
| if (_loopStartSamples < actualLoopEnd && _rawSamplePosition <= _loopStartSamples) | ||
| Seek(actualLoopEnd, channels); |
There was a problem hiding this comment.
2. Reverse loops play outside their range 🐞 Bug ≡ Correctness
GenerateAudio checks the reverse loop start only before generating a buffer, but its reverse reads can continue below _loopStartSamples. A callback that crosses the configured start emits out-of-loop samples before the next callback can wrap.
Agent Prompt
## Issue description
A reverse read can cross the loop start within one output callback.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[171-177]
- Src/Abstracts/SoundPlayerBase.cs[203-250]
- Src/Abstracts/SoundPlayerBase.cs[387-425]
## Recommended Fix
Limit each reverse source read to the configured loop start, then continue filling the same output buffer from the loop end.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Forward playback seeks to start. Reverse seeks to end. | ||
| Seek(_playbackSpeed < 0 ? actualLoopEnd : targetLoopStart, channels); |
There was a problem hiding this comment.
3. One-frame reverse loops overflow the stack 🐞 Bug ☼ Reliability
Reverse looping seeks to its end, but Seek(int) clamps that exclusive end position to `Length - channels`. For a one-frame clip this leaves the playhead at zero, so reverse generation reaches end-of-stream and recursively seeks and generates without making progress; longer clips also omit their last frame.
Agent Prompt
## Issue description
The forward seek clamp makes the reverse loop end inaccessible.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[203-216]
- Src/Abstracts/SoundPlayerBase.cs[626-634]
- Src/Abstracts/SoundPlayerBase.cs[738-752]
## Recommended Fix
Allow an aligned, exclusive end position for reverse seeks, including the full provider length, and ensure looping advances before recursively filling output.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (targetPos < 0) | ||
| { | ||
| // We are at the beginning, only read what's left | ||
| samplesToRead = _rawSamplePosition; | ||
| targetPos = 0; | ||
| } | ||
|
|
||
| if (samplesToRead == 0) | ||
| { | ||
| HandleEndOfStream(outputSlice, channels); |
There was a problem hiding this comment.
8. New reverse playback starts with silence 🐞 Bug ≡ Correctness
The direct reverse path reduces its requested read to _rawSamplePosition and invokes end-of-stream handling when that position is zero. A newly created, non-looping player starts at zero, and Play() does not move it, so setting a negative speed before the first play stops without output unless the caller seeks first.
Agent Prompt
## Issue description
A new player has no source samples behind its initial playhead.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[31-56]
- Src/Abstracts/SoundPlayerBase.cs[203-218]
- Src/Abstracts/SoundPlayerBase.cs[674-680]
## Recommended Fix
For a seekable new player started in reverse, position its playhead at the exclusive end before rendering, without overriding an explicit caller seek.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Reverse playback bypasses the time stretcher entirely and relies on linear interpolation. | ||
| if (isReverse) | ||
| { | ||
| while (_resampleBufferValidSamples < minSamplesRequiredInOutputBuffer) |
There was a problem hiding this comment.
5. Reverse speeds other than -1x all play at -1x 🐞 Bug ≡ Correctness
For negative speeds, FillResampleBuffer reverses and copies source frames into _resampleBuffer without WSOLA, while GenerateAudio advances _currentFractionalFrame by a fixed 1.0f rather than Math.Abs(_playbackSpeed). Consequently, reverse speeds such as -0.3x, -0.5x, -0.9x, and -2x all consume approximately one source frame per output frame and play at about -1x, including in the SimplePlayer sample and turntable use case.
Agent Prompt
## Issue description
Reverse playback bypasses WSOLA but consumes one source frame per output frame regardless of the magnitude of the negative playback speed.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[268-335]
- Src/Abstracts/SoundPlayerBase.cs[378-426]
## Recommended Fix
In `GenerateAudio`, advance `_currentFractionalFrame` by `Math.Abs(_playbackSpeed)` when `_playbackSpeed < 0`, while retaining the `1.0f` step for forward playback, whose speed is already applied by WSOLA. Preserve fractional frame positions across calls and ensure `_rawSamplePosition` reflects the source frames actually consumed from the buffer.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (Math.Sign(_playbackSpeed) != Math.Sign(value) && _playbackSpeed != 0) | ||
| { | ||
| _timeStretcher.Reset(); | ||
| _resampleBufferValidSamples = 0; | ||
| _timeStretcherInputBufferValidSamples = 0; | ||
| _timeStretcherInputBufferReadOffset = 0; | ||
| _currentFractionalFrame = 0f; | ||
| } |
There was a problem hiding this comment.
13. Switching reverse to forward skips ahead 🐞 Bug ≡ Correctness
When the direction changes, the PlaybackSpeed setter clears the resample and WSOLA buffers but never calls _dataProvider.Seek(_rawSamplePosition). The reverse paths leave the provider at the end of the chunk they last read (the old position), while _rawSamplePosition has already moved back by read. Forward playback reads straight from the provider without seeking, so after a reverse-to-forward switch it jumps ahead by up to one buffer (about 0.1 s in the resample path). From then on, Time, the loop-end checks and the real stream position disagree permanently.
Agent Prompt
## Issue description
After reverse playback the data provider is ahead of `_rawSamplePosition`. Switching to forward resumes from the wrong spot.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[39-46]
## Recommended Fix
In the sign-change block, call `_dataProvider.Seek(_rawSamplePosition)` (aligned to a frame and guarded by `_dataProvider.CanSeek`), or call the private `Seek(_rawSamplePosition, Format.Channels)`, which already resets every buffer.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (samplesToRead == 0) break; // Reached beginning | ||
|
|
||
| _dataProvider.Seek(targetPos); | ||
| var tempBuf = System.Buffers.ArrayPool<float>.Shared.Rent(samplesToRead); |
There was a problem hiding this comment.
6. Reverse playback of live streams crashes 🐞 Bug ☼ Reliability
Both reverse paths in SoundPlayerBase call _dataProvider.Seek(targetPos) on every read without checking _dataProvider.CanSeek. Setting a negative playback speed for a queue, microphone, or non-seekable stream reaches those calls; their providers throw from Seek, and neither the player nor MiniAudioPlaybackDevice catches the exception in the native audio callback.
Agent Prompt
## Issue description
Reverse playback seeks on every read even when the provider cannot seek, allowing an exception to escape from the audio callback.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[31-56]
- Src/Abstracts/SoundPlayerBase.cs[203-225]
- Src/Abstracts/SoundPlayerBase.cs[387-404]
## Recommended Fix
In the `PlaybackSpeed` setter, throw `NotSupportedException` when `value < 0 && !_dataProvider.CanSeek`. Add a defensive `CanSeek` check in both reverse branches so an unsupported provider emits silence and ends the stream instead of calling `Seek` from the audio callback.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| _dataProvider.Seek(targetPos); | ||
| var tempBuf = System.Buffers.ArrayPool<float>.Shared.Rent(samplesToRead); |
There was a problem hiding this comment.
14. -1x reverse on mp3s re-seeks every callback 🐞 Bug ➹ Performance
The |speed| == 1 reverse branch calls _dataProvider.Seek(targetPos) for every device buffer (around 10 ms). With the default seekPointCount = 100, each MiniAudio MP3 seek has to decode forward from the nearest seek point, which in long files can be tens of seconds of audio. That decode cost lands on the real-time audio thread every callback and can cause underruns and crackling when reverse-playing compressed files.
Agent Prompt
## Issue description
Reverse playback at -1x seeks the decoder once per audio callback, which is expensive for compressed formats.
## Fix Focus Areas
- Src/Abstracts/SoundPlayerBase.cs[202-256]
## Recommended Fix
Route -1x reverse through the buffered reverse path, or read a large reversed block (for example 0.25–1 s) into a cache and serve successive callbacks from it, seeking again only once the cache is used up.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| return decoderLength switch | ||
| { | ||
| // Prefer the larger value when both are available. See the XML doc above for the rationale. | ||
| // Special case: if one of them is 0 (unknown), use the other. | ||
| > 0 when metadataLength > 0 => Math.Max(decoderLength, metadataLength), | ||
| > 0 => decoderLength, | ||
| _ => metadataLength | ||
| }; |
There was a problem hiding this comment.
15. Overestimated length breaks reverse loops 🐞 Bug ≡ Correctness
StreamDataProvider.Length now returns Math.Max(decoderLength, metadataLength), even when the decoder reports an exact length and the metadata comes from a DurationAccuracy.FastEstimate read. Reverse looping and HandleEndOfStream seek to actualLoopEnd = _dataProvider.Length, so an inflated estimate starts reverse playback past the real end of the audio. There, reads return 0 or come from the wrong position, and with looping on, HandleEndOfStream → Seek(end) → GenerateAudio can recurse without ever making progress.
Agent Prompt
## Issue description
When metadata overestimates the length, reverse playback and looping start beyond the real audio.
## Fix Focus Areas
- Src/Providers/StreamDataProvider.cs[137-172]
- Src/Abstracts/SoundPlayerBase.cs[224-230]
## Recommended Fix
Use `_decoder.Length` whenever it is > 0 and fall back to metadata only when it is unknown. In the reverse path, when a read returns 0 just after a loop seek, stop instead of recursing: for example, pull `_rawSamplePosition` back to the real end, or end playback.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
[High impact] The PR reaches core audio playback and device paths, stream decoding, and package release workflows. Fix reverse playback, converted-stream lengths, and full-duplex event routing before merging. Findings
Fix with agent prompt### Issue 1
Src/Abstracts/SoundPlayerBase.cs:380-381
Negative speeds other than `-1` ignore their magnitude. This branch bypasses `_timeStretcher`, but `GenerateAudio` still advances `_currentFractionalFrame` by exactly `1.0f` per output frame. As a result, `-0.5` and `-2` both play backward at normal speed. Use `Math.Abs(_playbackSpeed)` to advance through the reversed buffer.
### Issue 2
Src/Abstracts/SoundPlayerBase.cs:388-389
A second reverse fill within one `GenerateAudio` call seeks from the same `_rawSamplePosition` as the first fill. That position is updated only after the output loop finishes. At 8 kHz, for example, the buffer holds 800 frames, so a larger output block can reread the first source range instead of continuing backward. Advance a separate read position after each fill, or include the pending movement when calculating `targetPos`.
### Issue 3
Src/Abstracts/SoundPlayerBase.cs:39-45
Switching from reverse to forward clears the buffers but leaves the provider at the wrong position. A reverse read seeks to `targetPos`, reads forward, and leaves the provider at the end of that block, while `_rawSamplePosition` moves to its beginning. Switching to `1.0f` then reads from the provider's old position and skips a block. Seek the provider to the current playback position when changing direction.
### Issue 4
Src/Abstracts/SoundPlayerBase.cs:204-211
Reverse reads stop at zero rather than at `_loopStartSamples`. If a block starts just above a nonzero loop start, it plays audio before the selected region; the loop check runs only before the block starts. Both reverse read paths need to stop at the loop start and wrap while filling the remaining output.
### Issue 5
Src/Providers/StreamDataProvider.cs:149-151
`metadataLength` counts samples in the file's original format, while `_decoder.Length` counts samples in the requested output format. The constructor accepting `AudioFormat` allows those formats to differ. A ten-second, 48 kHz stereo file decoded to 24 kHz mono therefore returns `960000` samples instead of `240000`, making `Duration` forty seconds and allowing seeks beyond the decoded audio. Convert the metadata duration to the decoder's output rate and channel count before comparing lengths.
### Issue 6
Src/Abstracts/Devices/FullDuplexDevice.cs:33
`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.
### Issue 7
Native/miniaudio-backend/library.c:180
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.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryAdds reverse playback, device-wide audio callbacks, decoder seeking changes, and build and release updates.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
Provider[Audio provider] --> Direction{Playback direction}
Direction -->|Forward| Wsola[WSOLA for changed speed]
Direction -->|Reverse| Reverse[Seek and reverse frames]
Wsola --> Mixer[Master mixer]
Reverse --> Mixer
Mixer --> Playback[Playback device]
Playback --> Output[Hardware output]
Playback --> Event[Audio callbacks]
Capture[Capture device] --> Event
Event --> Consumers[Microphone and echo cancellation]
Reviews (1) · Last reviewed commit: "feat(player): add reverse playback and f..." · Reviewed by Greptile |
| // Reverse playback bypasses the time stretcher entirely and relies on linear interpolation. | ||
| if (isReverse) |
There was a problem hiding this comment.
Negative speeds other than -1 ignore their magnitude. This branch bypasses _timeStretcher, but GenerateAudio still advances _currentFractionalFrame by exactly 1.0f per output frame. As a result, -0.5 and -2 both play backward at normal speed. Use Math.Abs(_playbackSpeed) to advance through the reversed buffer.
Knowledge Base Used: Playback, mixing, and recording components
Prompt To Fix With AI
This is a comment left during a code review.
Path: Src/Abstracts/SoundPlayerBase.cs
Line: 380-381
Comment:
**Reverse speed is ignored**
Negative speeds other than `-1` ignore their magnitude. This branch bypasses `_timeStretcher`, but `GenerateAudio` still advances `_currentFractionalFrame` by exactly `1.0f` per output frame. As a result, `-0.5` and `-2` both play backward at normal speed. Use `Math.Abs(_playbackSpeed)` to advance through the reversed buffer.
**Knowledge Base Used:** [Playback, mixing, and recording components](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/playback-mixing-and-recording.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| var samplesToRead = spaceAvailableInResampleBuffer; | ||
| var targetPos = _rawSamplePosition - samplesToRead; |
There was a problem hiding this comment.
Reverse playback repeats chunks
A second reverse fill within one GenerateAudio call seeks from the same _rawSamplePosition as the first fill. That position is updated only after the output loop finishes. At 8 kHz, for example, the buffer holds 800 frames, so a larger output block can reread the first source range instead of continuing backward. Advance a separate read position after each fill, or include the pending movement when calculating targetPos.
Knowledge Base Used: Playback, mixing, and recording components
Prompt To Fix With AI
This is a comment left during a code review.
Path: Src/Abstracts/SoundPlayerBase.cs
Line: 388-389
Comment:
**Reverse playback repeats chunks**
A second reverse fill within one `GenerateAudio` call seeks from the same `_rawSamplePosition` as the first fill. That position is updated only after the output loop finishes. At 8 kHz, for example, the buffer holds 800 frames, so a larger output block can reread the first source range instead of continuing backward. Advance a separate read position after each fill, or include the pending movement when calculating `targetPos`.
**Knowledge Base Used:** [Playback, mixing, and recording components](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/playback-mixing-and-recording.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if (Math.Sign(_playbackSpeed) != Math.Sign(value) && _playbackSpeed != 0) | ||
| { | ||
| _timeStretcher.Reset(); | ||
| _resampleBufferValidSamples = 0; | ||
| _timeStretcherInputBufferValidSamples = 0; | ||
| _timeStretcherInputBufferReadOffset = 0; | ||
| _currentFractionalFrame = 0f; |
There was a problem hiding this comment.
Changing direction skips audio
Switching from reverse to forward clears the buffers but leaves the provider at the wrong position. A reverse read seeks to targetPos, reads forward, and leaves the provider at the end of that block, while _rawSamplePosition moves to its beginning. Switching to 1.0f then reads from the provider's old position and skips a block. Seek the provider to the current playback position when changing direction.
Knowledge Base Used: Playback, mixing, and recording components
Prompt To Fix With AI
This is a comment left during a code review.
Path: Src/Abstracts/SoundPlayerBase.cs
Line: 39-45
Comment:
**Changing direction skips audio**
Switching from reverse to forward clears the buffers but leaves the provider at the wrong position. A reverse read seeks to `targetPos`, reads forward, and leaves the provider at the end of that block, while `_rawSamplePosition` moves to its beginning. Switching to `1.0f` then reads from the provider's old position and skips a block. Seek the provider to the current playback position when changing direction.
**Knowledge Base Used:** [Playback, mixing, and recording components](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/playback-mixing-and-recording.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| var samplesToRead = outputSlice.Length; | ||
| var targetPos = _rawSamplePosition - samplesToRead; | ||
|
|
||
| if (targetPos < 0) | ||
| { | ||
| // We are at the beginning, only read what's left | ||
| samplesToRead = _rawSamplePosition; | ||
| targetPos = 0; |
There was a problem hiding this comment.
Reverse loops escape their bounds
Reverse reads stop at zero rather than at _loopStartSamples. If a block starts just above a nonzero loop start, it plays audio before the selected region; the loop check runs only before the block starts. Both reverse read paths need to stop at the loop start and wrap while filling the remaining output.
Knowledge Base Used: Playback, mixing, and recording components
Prompt To Fix With AI
This is a comment left during a code review.
Path: Src/Abstracts/SoundPlayerBase.cs
Line: 204-211
Comment:
**Reverse loops escape their bounds**
Reverse reads stop at zero rather than at `_loopStartSamples`. If a block starts just above a nonzero loop start, it plays audio before the selected region; the loop check runs only before the block starts. Both reverse read paths need to stop at the loop start and wrap while filling the remaining output.
**Knowledge Base Used:** [Playback, mixing, and recording components](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/playback-mixing-and-recording.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| var metadataLengthSamples = FormatInfo.Duration.TotalSeconds | ||
| * FormatInfo.SampleRate | ||
| * FormatInfo.ChannelCount; |
There was a problem hiding this comment.
Converted files show wrong durations
metadataLength counts samples in the file's original format, while _decoder.Length counts samples in the requested output format. The constructor accepting AudioFormat allows those formats to differ. A ten-second, 48 kHz stereo file decoded to 24 kHz mono therefore returns 960000 samples instead of 240000, making Duration forty seconds and allowing seeks beyond the decoded audio. Convert the metadata duration to the decoder's output rate and channel count before comparing lengths.
Knowledge Base Used: Audio data providers
Prompt To Fix With AI
This is a comment left during a code review.
Path: Src/Providers/StreamDataProvider.cs
Line: 149-151
Comment:
**Converted files show wrong durations**
`metadataLength` counts samples in the file's original format, while `_decoder.Length` counts samples in the requested output format. The constructor accepting `AudioFormat` allows those formats to differ. A ten-second, 48 kHz stereo file decoded to 24 kHz mono therefore returns `960000` samples instead of `240000`, making `Duration` forty seconds and allowing seeks beyond the decoded audio. Convert the metadata duration to the decoder's output rate and channel count before comparing lengths.
**Knowledge Base Used:** [Audio data providers](https://app.greptile.com/lsxprime/-/custom-context/knowledge-base/lsxprime/soundflow/-/docs/audio-data-providers.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| /// Subscribers will receive both near-end (capture) and far-end (playback) frames. | ||
| /// </summary> | ||
| public event AudioProcessCallback? OnAudioProcessed | ||
| public new event AudioProcessCallback? OnAudioProcessed |
There was a problem hiding this 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
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.| MA_ZERO_OBJECT(pConfig); | ||
| *pConfig = ma_decoder_config_init(outputFormat, outputChannels, outputSampleRate); | ||
|
|
||
| pConfig->seekPointCount = seekPointCount; |
There was a problem hiding this 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
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
I really like reading AI reviewers comments, it's so beautiful and makes me impressed with how technology has advanced, but like if I give a fuck, just ship. Jokes aside, these simple modifications took three months because I couldn’t dedicate enough time (let’s hope March 1st comes quickly). I’ve had them running on my machine for a while, but I couldn’t commit until now. So, for now, no further changes, it’s working, and any issues can be fixed later. |
Adds reverse playback, device-level audio processing events, and several decoder and distribution fixes.
Playback:
PlaybackSpeedvalues for reverse playback, including reverse looping.SimplePlayersample project and show hours for long files.Devices:
OnAudioProcessedfromAudioCaptureDevicetoAudioDeviceso playback devices raise it too, fixing WebRTC AEC not receiving the far-end stream.FullDuplexDevicesubscriptions to both child devices.DeviceSwitcher.PreserveCaptureState/RestoreCaptureStatetoPreserveEventSubscribers/RestoreEventSubscribersand acceptAudioDevice.Decoders:
seekPointCounttoMiniAudioDecoder(default 100) so seeking no longer requires a known stream length.StreamDataProvider.Lengthto prevent premature end-of-stream andTime > Durationstates.Build:
Info.plistfor App Store validation (CFBundleShortVersionString,CFBundleVersion,CFBundleName,MinimumOSVersion).windows-2025and Visual Studio generator to18 2026.NuGet/login@v1.v4with explicit JDK 17.Related Issues