Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
694cb8d
Improve audio synchronization
polson Jul 11, 2026
010f50e
Refine audio synchronization
polson Jul 11, 2026
9bb1976
Fix transient sync delta after resume
polson Jul 11, 2026
16356ef
Improve audio synchronization on resume and fix teardown hangs
polson Jul 11, 2026
ebe46f7
Fix audio teardown and latency compensation
polson Jul 11, 2026
1610a33
More improvements
polson Jul 11, 2026
ab63ebf
Remove resume audio debug logs
polson Jul 11, 2026
0559bda
Document sync correction behavior
polson Jul 11, 2026
555513d
Keep sync correction state aligned with playback
polson Jul 11, 2026
2c746f5
Reduce playback sync polling overhead
polson Jul 11, 2026
e7f8a1f
Use ring buffer for sync correction history
polson Jul 12, 2026
660dad8
Improve audio sync and calibration timing
polson Jul 12, 2026
a7d00d8
Unify audio playback preparation
polson Jul 12, 2026
4543010
Improve audio sync correction
polson Jul 12, 2026
01c8ce9
More cleanup
polson Jul 13, 2026
b4baee4
Handle playback latency for metronome sample channel
polson Jul 13, 2026
1e8da02
Improved scheduling of metronome hits
polson Jul 13, 2026
83a4fc1
More oneshot refactoring
polson Jul 13, 2026
b9d9495
Refactor position handling to simplify song runner
polson Jul 13, 2026
c3ac846
Simplify handling playback/seek latency
polson Jul 13, 2026
2f5e770
Fix calibration input boxes for negatives
polson Jul 13, 2026
33063e9
Fix calibration issue
polson Jul 13, 2026
4ba5c65
First implementation of DSP backed metronome
polson Jul 14, 2026
7a4aade
Improved oneshot design
polson Jul 14, 2026
370f066
Update YARG.Core playback sync API
polson Jul 14, 2026
757ff37
Remove test cowbell asset
polson Jul 14, 2026
186e978
More cleanup
polson Jul 14, 2026
e9d54aa
Much simplified song runner
polson Jul 14, 2026
ae40c1c
More cleanup
polson Jul 14, 2026
2d38146
More cleanup
polson Jul 14, 2026
eee5dd3
More cleanup
polson Jul 14, 2026
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
19 changes: 7 additions & 12 deletions Assets/Script/Audio/Bass/BassAudioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public BassAudioManager()
}

var info = Bass.Info;
PlaybackLatency = info.Latency + Bass.DeviceBufferLength + devPeriod;
UpdatePlaybackLatency();
MinimumBufferLength = info.MinBufferLength + Bass.UpdatePeriod;
MaximumBufferLength = 5000;

Expand All @@ -178,8 +178,12 @@ public BassAudioManager()
Bass.UpdatePeriod, Bass.DeviceBufferLength, Bass.PlaybackBufferLength, PlaybackLatency);

YargLogger.LogFormatInfo("Current Device: {0}", Bass.GetDeviceInfo(Bass.CurrentDevice).Name);
}

Application.quitting += OnApplicationQuitting;
private void UpdatePlaybackLatency()
{
double playbackLatency = BassLatencyProvider.GetPlaybackStreamLatency();
PlaybackLatency = (int) Math.Round(playbackLatency * 1000.0);
}

protected override bool SetOutputDevice(string name)
Expand All @@ -198,6 +202,7 @@ protected override bool SetOutputDevice(string name)

_currentDevice?.Dispose();
_currentDevice = bassDevice.Use();
UpdatePlaybackLatency();

YargLogger.LogFormatInfo("Current BASS Device: {0}", Bass.GetDeviceInfo(Bass.CurrentDevice).Name);

Expand Down Expand Up @@ -587,21 +592,11 @@ protected override void SetBufferLength_Internal(int length)

protected override void DisposeUnmanagedResources()
{
Application.quitting -= OnApplicationQuitting;

YargLogger.LogInfo("Unloading BASS plugins");
Bass.PluginFree(0);
Bass.Free();
}

private void OnApplicationQuitting()
{
Application.quitting -= OnApplicationQuitting;
YargLogger.LogInfo("Application quitting: starting early BASS shutdown");
GlobalAudioHandler.Close();
YargLogger.LogInfo("Application quitting: early BASS shutdown complete");
}

private static string GetBassDirectory()
{
string pluginDirectory = Path.Combine(Application.dataPath, "Plugins");
Expand Down
15 changes: 15 additions & 0 deletions Assets/Script/Audio/Bass/BassHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using UnityEngine;
using YARG.Core.Audio;
using YARG.Core.Logging;
using YARG.Settings;

namespace YARG.Audio.BASS
{
Expand All @@ -17,6 +18,9 @@ public static class BassHelpers

public const int FADE_TIME_MILLISECONDS = 1000;

public static int ConfiguredPlaybackBufferLength => ClampPlaybackBufferLength(
SettingsManager.Settings?.PlaybackBufferLength.Value ?? 0);

public const int REVERB_SLIDE_IN_MILLISECONDS = 300;
public const int REVERB_SLIDE_OUT_MILLISECONDS = 500;

Expand Down Expand Up @@ -65,6 +69,17 @@ public static class BassHelpers
fDryMix = 0.5f, fWetMix = 1.0f, fRoomSize = 0.8f, fDamp = 0.5f, fWidth = 1.0f, lMode = 0
};

public static int ClampPlaybackBufferLength(int length)
{
int minimumLength = GlobalAudioHandler.MinimumBufferLength;
if (length > 0 && minimumLength > 0 && length < minimumLength)
{
return minimumLength;
}

return length;
}

public static int FXAddParameters(int streamHandle, EffectType type, IEffectParameter parameters,
int priority = 0)
{
Expand Down
56 changes: 56 additions & 0 deletions Assets/Script/Audio/Bass/BassLatencyProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using ManagedBass;

namespace YARG.Audio.BASS
{
/// <summary>
/// Provides estimated BASS playback and tempo stream latencies in seconds.
/// </summary>
internal static class BassLatencyProvider
{
private static double DeviceOutputLatency => Math.Max(0, Bass.Info.Latency) / 1000.0;

// Estimate command timing at midpoint of BASS's update period.
private static double CommandLatency => Math.Max(0, Bass.UpdatePeriod) / 2000.0;

/// <summary>
/// Gets estimated playback stream output latency.
/// </summary>
public static double GetPlaybackStreamLatency()
{
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
// CoreAudio is pull-based; info.Latency already encapsulates the full hardware pipeline.
return DeviceOutputLatency;
#else
double deviceBufferLatency = Math.Max(0, Bass.DeviceBufferLength) / 1000.0;
return DeviceOutputLatency + deviceBufferLatency;
#endif
}

/// <summary>
/// Gets estimated tempo stream latency, including buffered audio and BASS command latency.
/// </summary>
public static double GetTempoStreamLatency(int tempoStreamHandle)
{
return GetOutputBufferLatency(tempoStreamHandle) + CommandLatency;
}

private static double GetOutputBufferLatency(int tempoStreamHandle)
{
double configuredBufferLatency = BassHelpers.ConfiguredPlaybackBufferLength / 1000.0;
if (configuredBufferLatency <= 0)
{
return 0;
}

int availableBytes = Bass.ChannelGetData(tempoStreamHandle, IntPtr.Zero, (int) DataFlags.Available);
if (availableBytes < 0)
{
return configuredBufferLatency;
}

double bufferLatency = Bass.ChannelBytes2Seconds(tempoStreamHandle, availableBytes);
return bufferLatency >= 0 ? bufferLatency : configuredBufferLatency;
}
}
}
2 changes: 2 additions & 0 deletions Assets/Script/Audio/Bass/BassLatencyProvider.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 16 additions & 3 deletions Assets/Script/Audio/Bass/BassMetronomeSampleChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public sealed class BassMetronomeSampleChannel : MetronomeSampleChannel
private readonly int _hiChannel;
private readonly int _loHandle;
private readonly int _loChannel;
private double _volumeSetting = 1;

#nullable enable
private BassMetronomeSampleChannel(MetronomeSample sample, int hiHandle, int hiChannel, string hiPath, int loHandle, int loChannel, string loPath,
Expand Down Expand Up @@ -82,9 +81,23 @@ protected override void PlayLo_Internal()
}
}

protected override int CreateStream_Internal(MetronomePitch pitch)
{
int sampleHandle = pitch == MetronomePitch.Hi ? _hiHandle : _loHandle;
int stream = Bass.SampleGetChannel(sampleHandle,
BassFlags.SampleChannelStream | BassFlags.SampleChannelNew | BassFlags.Decode);
if (stream == 0)
{
YargLogger.LogFormatError("Failed to create {0} {1} stream: {2}!", Sample, pitch,
Bass.LastError);
return 0;
}

return stream;
}

protected override void SetVolume_Internal(double volume)
{
_volumeSetting = volume;
volume *= AudioHelpers.MetronomeSamples[(int) Sample].Volume;

if (!Bass.ChannelSetAttribute(_hiChannel, ChannelAttribute.Volume, volume))
Expand Down Expand Up @@ -112,4 +125,4 @@ protected override void DisposeUnmanagedResources()
Bass.SampleFree(_loHandle);
}
}
}
}
57 changes: 40 additions & 17 deletions Assets/Script/Audio/Bass/BassNormalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class BassNormalizer : IDisposable

// Undocumented BASS attribute to set max processing threads for a mixer
private const int MAX_THREADS_ATTRIB = 86017;
private const int GAIN_CALC_SHUTDOWN_TIMEOUT_MS = 1000;

private int _mixer;
private readonly List<Stream> _streams = new();
Expand All @@ -55,7 +56,11 @@ public class BassNormalizer : IDisposable
/// </summary>
public bool AddStream(Stream stream, params StemMixer.StemInfo[] stemInfos)
{
StopGainCalculation();
if (!StopGainCalculation())
{
YargLogger.LogError("Previous gain calculation did not stop; refusing to start another one.");
return false;
}

if (_mixer == 0)
{
Expand Down Expand Up @@ -172,39 +177,54 @@ private void StartGainCalculation()
_gainCalcCts = new CancellationTokenSource();
var token = _gainCalcCts.Token;

var progress = new Progress<double>(gain =>
Action<double> progress = gain =>
{
OnGainAdjusted?.Invoke((float) gain);
});
};

_gainCalcTask = Task.Run(() => CalculateRms(progress, token), token);
}

private void StopGainCalculation()
private bool StopGainCalculation()
{
if (_gainCalcCts == null)
{
return;
return true;
}

_gainCalcCts.Cancel();
var cts = _gainCalcCts;
var task = _gainCalcTask;
cts.Cancel();

bool stopped = false;
try
{
_gainCalcTask.GetAwaiter().GetResult();
stopped = task.Wait(GAIN_CALC_SHUTDOWN_TIMEOUT_MS);
}
catch (OperationCanceledException)
catch (AggregateException)
{
// Cancellation before the worker starts is expected.
// The worker has stopped; its exception must not abort Unity teardown.
stopped = true;
}
finally
{
_gainCalcCts.Dispose();
_gainCalcCts = null;
_gainCalcTask = Task.CompletedTask;
if (stopped || task.IsCompleted)
{
cts.Dispose();
_gainCalcCts = null;
_gainCalcTask = Task.CompletedTask;
}
}

if (!stopped)
{
YargLogger.LogError("Gain calculation did not stop during audio teardown; leaving its BASS handles intact.");
}

return stopped;
}

private void CalculateRms(IProgress<double> progress, CancellationToken token)
private void CalculateRms(Action<double> progress, CancellationToken token)
{
double cumulativeSumSquares = 0.0;
long totalSamples = 0;
Expand Down Expand Up @@ -240,15 +260,18 @@ private void CalculateRms(IProgress<double> progress, CancellationToken token)
float targetGain = (float) Math.Min(MAX_GAIN, TARGET_RMS / rms);
float delta = Math.Clamp(targetGain - Gain, -MAX_GAIN_STEP, MAX_GAIN_STEP);
Gain += delta;
progress?.Report(Gain);
progress?.Invoke(Gain);
}
}
}

public void Dispose()
{
// BASS calls cannot be interrupted mid-call. Wait before freeing handles used by the worker.
StopGainCalculation();
// BASS calls cannot be interrupted mid-call. Do not free handles while the worker is still using them.
if (!StopGainCalculation())
{
return;
}

foreach (var handle in _handles)
{
Expand All @@ -270,4 +293,4 @@ public void Dispose()
_handles.Clear();
}
}
}
}
Loading