Audio playback not in sink? #45
Replies: 5 comments
|
Hi David, Not a mixer bug — it's recording round-trip latency. You record with Option A — record through OwnAudio (recommended) Same engine, same device clock. For overdub you want input-only, so pull the capture ring and stream it out: private async Task _recordInputTake(string path)
{
var _writer = new WaveFileWriter(path, OwnaudioNet.Engine.Config);
int _skip = OwnaudioNet.InputLatencyFrames * OwnaudioNet.Engine.Config.Channels;
while (_recording)
{
float[]? _buf = OwnaudioNet.Receive(out int _count);
if (_buf == null) { await Task.Delay(2); continue; }
var _span = _buf.AsSpan(0, _count);
if (_skip > 0)
{
int _drop = Math.Min(_skip, _span.Length);
_span = _span.Slice(_drop);
_skip -= _drop;
}
if (_span.Length > 0) _writer.WriteSamples(_span);
OwnaudioNet.ReturnInputBuffer(_buf);
}
_writer.Dispose();
}Check ( Option B — keep Plugin.Maui.Audio, compensate with StartOffset Measure the round trip once per device (play a click through the mixer while recording, find the peak track.Source = this.SetAudioSource(track);
if (track.Source is FileSource _fs) { _fs.StartOffset = -LooperConstants.RecordLatencySeconds; }
this.Mixer.AddSourcePrepared(track.Source);Set it before Other things worth fixing
regards |
|
excellent!.
I have been trying all sorts of stuff . moving frames , trying to
compensate for latency.
i tried recording - but that is for a final mix eh?
didn't see wavefilewriter in the source - d'oh.
thanks ever so.
I will try it.
David.
…On Sun, 23 Aug 2026 at 10:45, ModernMusician ***@***.***> wrote:
Hi David,
Not a mixer bug — it's recording round-trip latency. You record with
Plugin.Maui.Audio and play back
with OwnAudio, so two independent device clocks. The first sample of the
WAV is not timeline zero, it is
output latency + input latency + recorder start skew later. Add it at 0
and it lands late by exactly that.
*Option A — record through OwnAudio (recommended)*
Same engine, same device clock. For overdub you want input-only, so pull
the capture ring and stream it out:
private async Task _recordInputTake(string path){
var _writer = new WaveFileWriter(path, OwnaudioNet.Engine.Config);
int _skip = OwnaudioNet.InputLatencyFrames * OwnaudioNet.Engine.Config.Channels;
while (_recording)
{
float[]? _buf = OwnaudioNet.Receive(out int _count);
if (_buf == null) { await Task.Delay(2); continue; }
var _span = _buf.AsSpan(0, _count);
if (_skip > 0)
{
int _drop = Math.Min(_skip, _span.Length);
_span = _span.Slice(_drop);
_skip -= _drop;
}
if (_span.Length > 0) _writer.WriteSamples(_span);
OwnaudioNet.ReturnInputBuffer(_buf);
}
_writer.Dispose();}
Check OwnaudioNet.TotalInputOverflowFrames afterwards — anything above 0
means the take has a hole.
(Mixer.StartRecording(path, compensateInputLatency: true) does the
latency trim for you, but it captures
the master mix, so it's not what you want for overdub.)
*Option B — keep Plugin.Maui.Audio, compensate with StartOffset*
Measure the round trip once per device (play a click through the mixer
while recording, find the peak
position in the take — that's your latency), store it, then pull the track
earlier. StartOffset accepts
negative values:
track.Source = this.SetAudioSource(track);if (track.Source is FileSource _fs) { _fs.StartOffset = -LooperConstants.RecordLatencySeconds; }this.Mixer.AddSourcePrepared(track.Source);
Set it *before* AddSourcePrepared — that call already attaches the source
to the master clock.
*Other things worth fixing*
- Drop the AttachToClock calls in PlayTracks() — AddSourcePrepared
already attached them; calling it
again detaches and re-seeks each source at a slightly different moment.
- In the byPassMasterPosition path, StartPreparedSources(0) only moves
the clock, not the tracks. Call
Mixer.Seek(0) first, then StartPreparedSources(0).
- TrimTrailingSilenceAsync trims the wrong end — the offset is at the
head. Don't trim leading silence by
detection either, a quiet intro would shift the take; use the
calibrated constant.
- Verify the actual sample rate of the recorded WAV. If the recorder
fell back to the device rate instead of
the one you asked for (common on Android), you get growing drift over
the loop, not a constant offset.
regards
ModernMube
—
Reply to this email directly, view it on GitHub
<#45?email_source=notifications&email_token=BFOAGWEBMQXWE33CDW4QABD5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVRTG633UMVZF6Y3MNFRWW#discussioncomment-18122910>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/BFOAGWC5QBVC23MAPQZZRHD5LK4JZAVCNFSNUABIKJSXA33TNF2G64TZHM4TKMZRGUYDENJVHNCGS43DOVZXG2LPNY5TCMBWGY3DEMZSUF3AE>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/BFOAGWEZZ46UTXR5RJAPJTD5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVJTG633UMVZF62LPOM>
and Android
<https://github.com/notifications/mobile/android/BFOAGWDLTY4HBUN3J2ZJMWT5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***
com>
|
|
hmm do i enable input ?
var globalConfig = new AudioConfig()
{
SampleRate = 48000,
Channels = 2,
BufferSize = 512,
HostType = OperatingSystem.IsWindows()
? EngineHostType.WASAPI
: EngineHostType.AAUDIO,
EnableOutput = true,* EnableInput = true*
};
because if i do the the
this.waveFileWriter.Dispose();
this.waveFileWriter = null;
if (OwnaudioNet.TotalInputOverflowFrames > 0)
{
// some enourmous number!
}
if i don't it only contains the wav header (44 bytes)
maybe i should enable input at the start and disable it at the end ?
sorry to be a nuisance .
David
On Sun, 23 Aug 2026 at 16:30, David Nuttall ***@***.***>
wrote:
… excellent!.
I have been trying all sorts of stuff . moving frames , trying to
compensate for latency.
i tried recording - but that is for a final mix eh?
didn't see wavefilewriter in the source - d'oh.
thanks ever so.
I will try it.
David.
On Sun, 23 Aug 2026 at 10:45, ModernMusician ***@***.***>
wrote:
> Hi David,
>
> Not a mixer bug — it's recording round-trip latency. You record with
> Plugin.Maui.Audio and play back
> with OwnAudio, so two independent device clocks. The first sample of the
> WAV is not timeline zero, it is
> output latency + input latency + recorder start skew later. Add it at 0
> and it lands late by exactly that.
>
> *Option A — record through OwnAudio (recommended)*
>
> Same engine, same device clock. For overdub you want input-only, so pull
> the capture ring and stream it out:
>
> private async Task _recordInputTake(string path){
> var _writer = new WaveFileWriter(path, OwnaudioNet.Engine.Config);
> int _skip = OwnaudioNet.InputLatencyFrames * OwnaudioNet.Engine.Config.Channels;
>
> while (_recording)
> {
> float[]? _buf = OwnaudioNet.Receive(out int _count);
> if (_buf == null) { await Task.Delay(2); continue; }
>
> var _span = _buf.AsSpan(0, _count);
> if (_skip > 0)
> {
> int _drop = Math.Min(_skip, _span.Length);
> _span = _span.Slice(_drop);
> _skip -= _drop;
> }
>
> if (_span.Length > 0) _writer.WriteSamples(_span);
> OwnaudioNet.ReturnInputBuffer(_buf);
> }
>
> _writer.Dispose();}
>
> Check OwnaudioNet.TotalInputOverflowFrames afterwards — anything above 0
> means the take has a hole.
>
> (Mixer.StartRecording(path, compensateInputLatency: true) does the
> latency trim for you, but it captures
> the master mix, so it's not what you want for overdub.)
>
> *Option B — keep Plugin.Maui.Audio, compensate with StartOffset*
>
> Measure the round trip once per device (play a click through the mixer
> while recording, find the peak
> position in the take — that's your latency), store it, then pull the
> track earlier. StartOffset accepts
> negative values:
>
> track.Source = this.SetAudioSource(track);if (track.Source is FileSource _fs) { _fs.StartOffset = -LooperConstants.RecordLatencySeconds; }this.Mixer.AddSourcePrepared(track.Source);
>
> Set it *before* AddSourcePrepared — that call already attaches the
> source to the master clock.
>
> *Other things worth fixing*
>
> - Drop the AttachToClock calls in PlayTracks() — AddSourcePrepared
> already attached them; calling it
> again detaches and re-seeks each source at a slightly different
> moment.
> - In the byPassMasterPosition path, StartPreparedSources(0) only
> moves the clock, not the tracks. Call
> Mixer.Seek(0) first, then StartPreparedSources(0).
> - TrimTrailingSilenceAsync trims the wrong end — the offset is at the
> head. Don't trim leading silence by
> detection either, a quiet intro would shift the take; use the
> calibrated constant.
> - Verify the actual sample rate of the recorded WAV. If the recorder
> fell back to the device rate instead of
> the one you asked for (common on Android), you get growing drift over
> the loop, not a constant offset.
>
> regards
> ModernMube
>
> —
> Reply to this email directly, view it on GitHub
> <#45?email_source=notifications&email_token=BFOAGWEBMQXWE33CDW4QABD5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVRTG633UMVZF6Y3MNFRWW#discussioncomment-18122910>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/BFOAGWC5QBVC23MAPQZZRHD5LK4JZAVCNFSNUABIKJSXA33TNF2G64TZHM4TKMZRGUYDENJVHNCGS43DOVZXG2LPNY5TCMBWGY3DEMZSUF3AE>
> .
> Triage notifications, keep track of coding agent tasks and review pull
> requests on the go with GitHub Mobile for iOS
> <https://github.com/notifications/mobile/ios/BFOAGWEZZ46UTXR5RJAPJTD5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVJTG633UMVZF62LPOM>
> and Android
> <https://github.com/notifications/mobile/android/BFOAGWDLTY4HBUN3J2ZJMWT5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
> Download it today!
> You are receiving this because you authored the thread.Message ID:
> ***@***.***
> com>
>
|
|
i have enabled the input - and it traps buffers - but when i try and load
it into a filesource - i get ?
StreamingAudioDecoder failed: failed to open or probe the audio file
(code 12) — Failed to open audio file: malformed stream: riff:
chunk length exceeds parent (list) chunk length
using Nutstone.Looper.Controls.Master;using
Nutstone.Looper.Extensions;using Nutstone.Looper.Models;using
Nutstone.Looper.Services.Mixer;using Ownaudio.Core;using
OwnaudioNET;using OwnaudioNET.Mixing;using OwnaudioNET.Sources;using
System;using System.Collections.Generic;using System.Diagnostics;using
System.Resources;using System.Text;
namespace Nutstone.Looper.Services.Recorder
{
public class RecorderService : IRecorderService
{
private IMixerService mixerService;
private TrackModel currentTrack;
private IDispatcherTimer recordTimer;
private Task? recordingTask = null;
public MasterPosition TrackPosition { get; set; }
private CancellationTokenSource? recordingStopped = null;
private Action<long>? onRecordedFile = null;
private WaveFileWriter? waveFileWriter;
public Exception LastException { get; set; }
public RecorderService(IMixerService mixerService)
{
this.mixerService = mixerService;
}
public async Task Record(TrackModel track, bool shouldLoop)
{
/*
this.TrackPosition.SetUserInteractionAction(false);
if (this.currentTrack != null || audioRecorder.IsRecording)
{
return;
}
this.recordTimer = Application.Current.Dispatcher.CreateTimer();
this.recordTimer.Interval = TimeSpan.FromMilliseconds(100);
var startedAt = DateTime.Now;
this.recordTimer.Tick += (s, e) =>
{
this.TrackPosition.SetCurrentPosition(TimeSpan.FromTicks(DateTime.Now.Ticks
- startedAt.Ticks).TotalMilliseconds);
};
this.TrackPosition?.SetForRecording(600 * 1000);
this.recordTimer.Start();
*/
this.currentTrack = track;
if (this.mixerService.HaveTracks())
{
await this.mixerService.Play(true);
}
var currentConfig = OwnaudioNet.Engine.Config;
List<AudioDeviceInfo> inputs = await
OwnaudioNet.GetInputDevicesAsync();
if (inputs.Count == 1)
{
currentConfig.InputDeviceId = inputs[0].DeviceId;
}
// Initialize and assign the cancellation token source
this.recordingStopped = new CancellationTokenSource();
var cancellationToken = this.recordingStopped.Token;
this.waveFileWriter = new
WaveFileWriter(this.currentTrack.SourcePath.EnsurePath(),
OwnaudioNet.Engine.Config);
int _skip = OwnaudioNet.InputLatencyFrames *
OwnaudioNet.Engine.Config.Channels;
this.recordingTask = Task.Run(async () =>
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
float[]? _buf = OwnaudioNet.Receive(out int _count);
if (_buf == null)
{
// Use the token in Delay so it exits
immediately if cancelled
try { await Task.Delay(2,
cancellationToken); } catch (TaskCanceledException) { }
continue;
}
var _span = _buf.AsSpan(0, _count);
if (_skip > 0)
{
int _drop = Math.Min(_skip, _span.Length);
_span = _span.Slice(_drop);
_skip -= _drop;
}
if (_span.Length > 0)
{
this.waveFileWriter?.WriteSamples(_span);
}
OwnaudioNet.ReturnInputBuffer(_buf);
}
}
finally
{
this.waveFileWriter?.Dispose();
this.onRecordedFile?.Invoke(OwnaudioNet.TotalInputOverflowFrames);
this.waveFileWriter = null;
}
}, cancellationToken);
}
public async Task<bool> StopRecord(Action<string> onError)
{
var state = false;
this.TrackPosition?.Reset();
this.TrackPosition?.SetUserInteractionAction(true);
this.recordTimer?.Stop();
if (this.currentTrack != null)
{
await this.mixerService.Stop();
this.onRecordedFile = async (totalOverrunFrames) =>
{
// fails when loading into filesource
state = await
this.mixerService.AddTrackToMixer(this.currentTrack, onError);
this.currentTrack.Volume = LooperConstants.DefaultVolume;
this.currentTrack?.StoppedRecording?.Invoke(this.currentTrack);
this.currentTrack?.Recording = false;
this.currentTrack = null;
await this.mixerService.SetMasterPositionAfterRecord();
};
this.recordingStopped?.Cancel();
if (recordingTask != null)
{
await this.recordingTask;
}
}
return state;
}
}
}
sorry to be a pain - !
David.
P.S promise this is the last time i will bother you - honest
On Sun, 23 Aug 2026 at 21:07, David Nuttall ***@***.***>
wrote:
… hmm do i enable input ?
var globalConfig = new AudioConfig()
{
SampleRate = 48000,
Channels = 2,
BufferSize = 512,
HostType = OperatingSystem.IsWindows()
? EngineHostType.WASAPI
: EngineHostType.AAUDIO,
EnableOutput = true,* EnableInput = true*
};
because if i do the the
this.waveFileWriter.Dispose();
this.waveFileWriter = null;
if (OwnaudioNet.TotalInputOverflowFrames > 0)
{
// some enourmous number!
}
if i don't it only contains the wav header (44 bytes)
maybe i should enable input at the start and disable it at the end ?
sorry to be a nuisance .
David
On Sun, 23 Aug 2026 at 16:30, David Nuttall ***@***.***>
wrote:
> excellent!.
> I have been trying all sorts of stuff . moving frames , trying to
> compensate for latency.
>
> i tried recording - but that is for a final mix eh?
>
> didn't see wavefilewriter in the source - d'oh.
>
> thanks ever so.
>
> I will try it.
>
> David.
>
> On Sun, 23 Aug 2026 at 10:45, ModernMusician ***@***.***>
> wrote:
>
>> Hi David,
>>
>> Not a mixer bug — it's recording round-trip latency. You record with
>> Plugin.Maui.Audio and play back
>> with OwnAudio, so two independent device clocks. The first sample of the
>> WAV is not timeline zero, it is
>> output latency + input latency + recorder start skew later. Add it at 0
>> and it lands late by exactly that.
>>
>> *Option A — record through OwnAudio (recommended)*
>>
>> Same engine, same device clock. For overdub you want input-only, so pull
>> the capture ring and stream it out:
>>
>> private async Task _recordInputTake(string path){
>> var _writer = new WaveFileWriter(path, OwnaudioNet.Engine.Config);
>> int _skip = OwnaudioNet.InputLatencyFrames * OwnaudioNet.Engine.Config.Channels;
>>
>> while (_recording)
>> {
>> float[]? _buf = OwnaudioNet.Receive(out int _count);
>> if (_buf == null) { await Task.Delay(2); continue; }
>>
>> var _span = _buf.AsSpan(0, _count);
>> if (_skip > 0)
>> {
>> int _drop = Math.Min(_skip, _span.Length);
>> _span = _span.Slice(_drop);
>> _skip -= _drop;
>> }
>>
>> if (_span.Length > 0) _writer.WriteSamples(_span);
>> OwnaudioNet.ReturnInputBuffer(_buf);
>> }
>>
>> _writer.Dispose();}
>>
>> Check OwnaudioNet.TotalInputOverflowFrames afterwards — anything above
>> 0 means the take has a hole.
>>
>> (Mixer.StartRecording(path, compensateInputLatency: true) does the
>> latency trim for you, but it captures
>> the master mix, so it's not what you want for overdub.)
>>
>> *Option B — keep Plugin.Maui.Audio, compensate with StartOffset*
>>
>> Measure the round trip once per device (play a click through the mixer
>> while recording, find the peak
>> position in the take — that's your latency), store it, then pull the
>> track earlier. StartOffset accepts
>> negative values:
>>
>> track.Source = this.SetAudioSource(track);if (track.Source is FileSource _fs) { _fs.StartOffset = -LooperConstants.RecordLatencySeconds; }this.Mixer.AddSourcePrepared(track.Source);
>>
>> Set it *before* AddSourcePrepared — that call already attaches the
>> source to the master clock.
>>
>> *Other things worth fixing*
>>
>> - Drop the AttachToClock calls in PlayTracks() — AddSourcePrepared
>> already attached them; calling it
>> again detaches and re-seeks each source at a slightly different
>> moment.
>> - In the byPassMasterPosition path, StartPreparedSources(0) only
>> moves the clock, not the tracks. Call
>> Mixer.Seek(0) first, then StartPreparedSources(0).
>> - TrimTrailingSilenceAsync trims the wrong end — the offset is at
>> the head. Don't trim leading silence by
>> detection either, a quiet intro would shift the take; use the
>> calibrated constant.
>> - Verify the actual sample rate of the recorded WAV. If the recorder
>> fell back to the device rate instead of
>> the one you asked for (common on Android), you get growing drift
>> over the loop, not a constant offset.
>>
>> regards
>> ModernMube
>>
>> —
>> Reply to this email directly, view it on GitHub
>> <#45?email_source=notifications&email_token=BFOAGWEBMQXWE33CDW4QABD5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVRTG633UMVZF6Y3MNFRWW#discussioncomment-18122910>,
>> or unsubscribe
>> <https://github.com/notifications/unsubscribe-auth/BFOAGWC5QBVC23MAPQZZRHD5LK4JZAVCNFSNUABIKJSXA33TNF2G64TZHM4TKMZRGUYDENJVHNCGS43DOVZXG2LPNY5TCMBWGY3DEMZSUF3AE>
>> .
>> Triage notifications, keep track of coding agent tasks and review pull
>> requests on the go with GitHub Mobile for iOS
>> <https://github.com/notifications/mobile/ios/BFOAGWEZZ46UTXR5RJAPJTD5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVJTG633UMVZF62LPOM>
>> and Android
>> <https://github.com/notifications/mobile/android/BFOAGWDLTY4HBUN3J2ZJMWT5LK4JZA5CNFSNUABIM5UWIORPF5TWS5BNNB2WEL2ENFZWG5LTONUW63SDN5WW2ZLOOQXTCOBRGIZDSMJQUZZGKYLTN5XKMYLVORUG64VFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE>.
>> Download it today!
>> You are receiving this because you authored the thread.Message ID:
>> ***@***.***
>> com>
>>
>
|
|
Hi David, That error was our bug, not yours — and it is fixed in 4.0.5, which went up on NuGet today. WaveFileWriter patched the RIFF chunk size with the file length minus 16 instead of minus 8, so every file it wrote declared itself 8 bytes shorter than it actually is. Lenient players ignore that; Symphonia (our decoder) checks it, and reports exactly what you saw — the data chunk runs past the end of the RIFF chunk that is supposed to contain it. Update the package and the takes will load. To your two questions: Yes, EnableInput = true is required. Without it nothing is ever pushed into the capture ring, so Receive always returns null and you end up with the 44-byte header and nothing else. long _before = OwnaudioNet.TotalInputOverflowFrames;
wait _recordInputTake(path);
long _dropped = OwnaudioNet.TotalInputOverflowFrames - _before; // this one mattersThe rest of the earlier advice still stands — calibrate the round trip once per device and keep the constant, don't detect leading silence. regards |
Uh oh!
There was an error while loading. Please reload this page.
Hi owen, this is probably not a bug.. just me being dim.
when a playback multiple tracks - they are not in sink. they a few milliseconds off
I am using Plugin.Maui.Audio to record the audio ..
and then adding it to the mixer ..
and when I play ..
Any ideas?. Sorry for the long post .. it really is a great library
cheers
David.
All reactions