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
8 changes: 8 additions & 0 deletions Packages/AudioConductor/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v2.4.0 - 2026/06/11

- New Features
- Add `referenceSampleRate` to CueSheet: sample positions are automatically scaled at runtime when the clip's actual decoding frequency differs from the authored rate, preventing playback drift on platforms that resample audio (e.g. when Audio Import Settings do not use Preserve Sample Rate)
- Fix Issues
- Auto-pause and resume audio when the WebGL app loses focus
- Fix pause/resume not working correctly when Pause is called immediately after Play

## v2.3.1 - 2026/06/11

- Fix Issues
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#nullable enable

using System.Collections.Generic;
using System.Linq;
using AudioConductor.Core.Enums;
using AudioConductor.Core.Models;
using AudioConductor.Core.Shared;
Expand All @@ -19,17 +21,59 @@ internal sealed class CueSheetParameterPaneModel : ICueSheetParameterPaneModel
{
private readonly IAssetSaveService _assetSaveService;
private readonly AutoIncrementHistory _history;
private readonly CueSheet _rawCueSheet;
private readonly ObservableCueSheet _target;

public CueSheetParameterPaneModel([NotNull] CueSheet cueSheet,
[NotNull] AutoIncrementHistory history,
[NotNull] IAssetSaveService assetSaveService)
{
_rawCueSheet = cueSheet;
_target = new ObservableCueSheet(cueSheet);
_history = history;
_assetSaveService = assetSaveService;
}

public IReadOnlyObservableProperty<int> ReferenceSampleRateObservable => _target.ReferenceSampleRateObservable;

public bool CanApplyReferenceSampleRate => CollectClipFrequencies().Count == 1;

public void ApplyReferenceSampleRate()
{
var frequencies = CollectClipFrequencies();
if (frequencies.Count != 1)
return;
var frequency = frequencies.First();
var old = _target.ReferenceSampleRate;
_history.Register($"Set CueSheet {nameof(CueSheet.referenceSampleRate)} {frequency}", Redo, Undo);

#region LocalMethods

void Redo()
{
_target.ReferenceSampleRate = frequency;
_assetSaveService.Save();
}

void Undo()
{
_target.ReferenceSampleRate = old;
_assetSaveService.Save();
}

#endregion
}

private HashSet<int> CollectClipFrequencies()
{
var frequencies = new HashSet<int>();
foreach (var cue in _rawCueSheet.cueList)
foreach (var track in cue.trackList)
if (track.audioClip != null)
frequencies.Add(track.audioClip.frequency);
return frequencies;
}

#region Name

public string Name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,11 @@ internal interface ICueSheetParameterPaneModel
bool PitchInvert { get; set; }

IReadOnlyObservableProperty<bool> PitchInvertObservable { get; }

IReadOnlyObservableProperty<int> ReferenceSampleRateObservable { get; }

bool CanApplyReferenceSampleRate { get; }

void ApplyReferenceSampleRate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal sealed class ObservableCueSheet
private readonly ObservableProperty<string> _name;
private readonly ObservableProperty<float> _pitch;
private readonly ObservableProperty<bool> _pitchInvert;
private readonly ObservableProperty<int> _referenceSampleRate;
private readonly ObservableProperty<int> _throttleLimit;
private readonly ObservableProperty<ThrottleType> _throttleType;
private readonly ObservableProperty<float> _volume;
Expand All @@ -33,11 +34,20 @@ public ObservableCueSheet([NotNull] CueSheet cueSheet)
_volume = new(_cueSheet.volume);
_pitch = new(_cueSheet.pitch);
_pitchInvert = new(_cueSheet.pitchInvert);
_referenceSampleRate = new(_cueSheet.referenceSampleRate);
// ReSharper enable ArrangeObjectCreationWhenTypeNotEvident
}

public string Id => _cueSheet.Id;

public int ReferenceSampleRate
{
get => _referenceSampleRate.Value;
set => _referenceSampleRate.SetValueAndNotify(_cueSheet.referenceSampleRate = value);
}

public IReadOnlyObservableProperty<int> ReferenceSampleRateObservable => _referenceSampleRate;

public string Name
{
get => _name.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ private void Bind()
_model.PitchInvertObservable
.Subscribe(_view.SetPitchInvert)
.DisposeWith(_bindDisposable);
_model.ReferenceSampleRateObservable
.Subscribe(value =>
{
_view.SetReferenceSampleRate(value);
_view.SetApplyButtonEnabled(_model.CanApplyReferenceSampleRate);
})
.DisposeWith(_bindDisposable);
}

private void Unbind()
Expand Down Expand Up @@ -95,6 +102,9 @@ private void SetupViewEventHandlers()
_view.PitchInvertChangedAsObservable
.Subscribe(value => _model.PitchInvert = value)
.DisposeWith(_viewEventDisposable);
_view.ApplyReferenceSampleRateAsObservable
.Subscribe(_ => _model.ApplyReferenceSampleRate())
.DisposeWith(_viewEventDisposable);
}

private void CleanupViewEventHandlers()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ namespace AudioConductor.Editor.Core.Tools.CueSheetEditor.Views
{
internal sealed class CueSheetParameterPaneView : VisualElement, IDisposable
{
private readonly Button _applyReferenceSampleRateButton;
private readonly Subject<Empty> _applyReferenceSampleRateSubject = new();
private readonly Subject<string> _nameChangedSubject = new();
private readonly TextField _nameField;
private readonly Subject<float> _pitchChangedSubject = new();
private readonly SliderAndFloatField _pitchField;
private readonly Subject<bool> _pitchInvertChangedSubject = new();
private readonly Toggle _pitchInvertField;
private readonly IntegerField _referenceSampleRateField;
private readonly HelpBox _referenceSampleRateWarning;
private readonly Subject<int> _throttleLimitChangedSubject = new();
private readonly IntegerField _throttleLimitField;
private readonly Subject<ThrottleType> _throttleTypeChangedSubject = new();
Expand All @@ -45,6 +49,13 @@ public CueSheetParameterPaneView()
_pitchField.lowValue = ValueRangeConst.Pitch.Min;
_pitchField.highValue = ValueRangeConst.Pitch.Max;

_referenceSampleRateField = this.Q<IntegerField>("ReferenceSampleRate");
_referenceSampleRateField.SetEnabled(false);
_referenceSampleRateWarning = this.Q<HelpBox>("ReferenceSampleRateWarning");
_referenceSampleRateWarning.SetDisplay(false);
_applyReferenceSampleRateButton = this.Q<Button>("ApplyReferenceSampleRateButton");
_applyReferenceSampleRateButton.SetDisplay(false);

ApplyTooltips();
}

Expand All @@ -54,10 +65,12 @@ public CueSheetParameterPaneView()
internal IObservable<float> VolumeChangedAsObservable => _volumeChangedSubject;
internal IObservable<float> PitchChangedAsObservable => _pitchChangedSubject;
internal IObservable<bool> PitchInvertChangedAsObservable => _pitchInvertChangedSubject;
internal IObservable<Empty> ApplyReferenceSampleRateAsObservable => _applyReferenceSampleRateSubject;

public void Dispose()
{
CleanupEventHandlers();
_applyReferenceSampleRateSubject.Dispose();
Localization.Localization.LanguageChanged -= OnLanguageChanged;
}

Expand Down Expand Up @@ -85,6 +98,12 @@ private void ApplyTooltips()
_volumeField.tooltip = Localization.Localization.Tr("cue_sheet_parameter.volume");
_pitchField.tooltip = Localization.Localization.Tr("cue_sheet_parameter.pitch");
_pitchInvertField.tooltip = Localization.Localization.Tr("cue_sheet_parameter.pitch_invert");
_referenceSampleRateField.tooltip =
Localization.Localization.Tr("cue_sheet_parameter.reference_sample_rate");
_referenceSampleRateWarning.text =
Localization.Localization.Tr("cue_sheet_parameter.reference_sample_rate_warning");
_applyReferenceSampleRateButton.text =
Localization.Localization.Tr("cue_sheet_parameter.apply_reference_sample_rate");
}

private void SetupEventHandlers()
Expand All @@ -95,10 +114,12 @@ private void SetupEventHandlers()
_volumeField.RegisterValueChangedCallback(OnVolumeChanged);
_pitchField.RegisterValueChangedCallback(OnPitchChanged);
_pitchInvertField.RegisterValueChangedCallback(OnPitchInvertChanged);
_applyReferenceSampleRateButton.clicked += OnApplyReferenceSampleRateClicked;
}

private void CleanupEventHandlers()
{
_applyReferenceSampleRateButton.clicked -= OnApplyReferenceSampleRateClicked;
_pitchInvertField.UnregisterValueChangedCallback(OnPitchInvertChanged);
_pitchField.UnregisterValueChangedCallback(OnPitchChanged);
_volumeField.UnregisterValueChangedCallback(OnVolumeChanged);
Expand Down Expand Up @@ -139,6 +160,18 @@ internal void SetPitchInvert(bool value)
_pitchInvertField.SetValueWithoutNotify(value);
}

internal void SetReferenceSampleRate(int value)
{
_referenceSampleRateField.SetValueWithoutNotify(value);
_referenceSampleRateWarning.SetDisplay(value == 0);
_applyReferenceSampleRateButton.SetDisplay(value == 0);
}

internal void SetApplyButtonEnabled(bool enabled)
{
_applyReferenceSampleRateButton.SetEnabled(enabled);
}

#endregion

#region Methods - EventHandler
Expand Down Expand Up @@ -173,6 +206,11 @@ private void OnPitchInvertChanged(ChangeEvent<bool> evt)
_pitchInvertChangedSubject.OnNext(evt.newValue);
}

private void OnApplyReferenceSampleRateClicked()
{
_applyReferenceSampleRateSubject.OnNext(Empty.Default);
}

private void OnLanguageChanged()
{
ApplyTooltips();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
using System.Linq;
using AudioConductor.Core.Models;
using UnityEditor;
using UnityEngine;

namespace AudioConductor.Editor.Core.Tools.Shared
{
[InitializeOnLoad]
internal class CueSheetAssetImportChecker : AssetPostprocessor
{
private const string ReferenceSampleRateMigrationSkipKey =
"AudioConductor.ReferenceSampleRateMigration.Skip";

private static readonly Dictionary<string, CueSheetAsset> CueSheetAssets;
private static readonly HashSet<string> CueSheetIds;

Expand All @@ -35,6 +39,8 @@ static CueSheetAssetImportChecker()
CueSheetIds.Add(asset.cueSheet.Id);
MigrateCueIds(asset);
}

EditorApplication.delayCall += () => MigrateReferenceSampleRates(CueSheetAssets.Values);
}

private static void OnPostprocessAllAssets(string[] importedAssets,
Expand Down Expand Up @@ -165,5 +171,76 @@ private static void MigrateCueIds(CueSheetAsset asset)
EditorUtility.SetDirty(asset);
AssetDatabase.SaveAssets();
}

internal static void MigrateReferenceSampleRates(IEnumerable<CueSheetAsset> assets)
{
if (EditorPrefs.GetBool(ReferenceSampleRateMigrationSkipKey, false))
return;

var toMigrate = new List<(CueSheetAsset asset, int frequency)>();
var inconsistentNames = new List<string>();

foreach (var asset in assets)
{
if (asset.cueSheet.referenceSampleRate != 0)
continue;

var frequencies = CollectClipFrequencies(asset.cueSheet);
if (frequencies.Count == 0)
continue;

if (frequencies.Count == 1)
toMigrate.Add((asset, frequencies.First()));
else
inconsistentNames.Add(asset.name);
}

foreach (var name in inconsistentNames)
Debug.LogWarning(string.Format(
Localization.Localization.Tr("migration.reference_sample_rate.inconsistent_warning"),
name));

if (toMigrate.Count == 0)
return;

var names = string.Join("\n", toMigrate.Select(x => $" {x.asset.name} ({x.frequency} Hz)"));
var message =
$"{Localization.Localization.Tr("migration.reference_sample_rate.dialog_message")}\n\n{names}";

// 2: Don't show again
var result = EditorUtility.DisplayDialogComplex(
Localization.Localization.Tr("migration.reference_sample_rate.dialog_title"),
message,
Localization.Localization.Tr("migration.reference_sample_rate.apply"),
Localization.Localization.Tr("migration.reference_sample_rate.skip"),
Localization.Localization.Tr("migration.reference_sample_rate.dont_show_again"));

if (result == 2)
{
EditorPrefs.SetBool(ReferenceSampleRateMigrationSkipKey, true);
return;
}

if (result != 0)
return;

foreach (var (asset, frequency) in toMigrate)
{
asset.cueSheet.referenceSampleRate = frequency;
EditorUtility.SetDirty(asset);
}

AssetDatabase.SaveAssets();
}

internal static HashSet<int> CollectClipFrequencies(CueSheet cueSheet)
{
var frequencies = new HashSet<int>();
foreach (var cue in cueSheet.cueList)
foreach (var track in cue.trackList)
if (track.audioClip != null)
frequencies.Add(track.audioClip.frequency);
return frequencies;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// --------------------------------------------------------------
// Copyright 2026 CyberAgent, Inc.
// --------------------------------------------------------------

#nullable enable

using AudioConductor.Core.Models;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;

namespace AudioConductor.Editor.Core.Tools.Shared
{
internal sealed class ReferenceSampleRateBuildPreprocessor : IPreprocessBuildWithReport
{
public int callbackOrder => 1;

public void OnPreprocessBuild(BuildReport report)
{
var guids = AssetDatabase.FindAssets("t:" + nameof(CueSheetAsset), new[] { "Assets" });
if (guids == null || guids.Length == 0)
return;

foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<CueSheetAsset>(path);
if (asset == null || asset.cueSheet.referenceSampleRate != 0)
continue;

if (HasAnyClip(asset.cueSheet))
Debug.LogWarning(
$"[AudioConductor] CueSheet '{asset.name}' has no referenceSampleRate set. Sample positions may drift on platforms with different audio decoding frequencies (e.g. WebGL).");
}
}

private static bool HasAnyClip(CueSheet cueSheet)
{
foreach (var cue in cueSheet.cueList)
foreach (var track in cue.trackList)
if (track.audioClip != null)
return true;
return false;
}
}
}
Loading