diff --git a/Packages/AudioConductor/CHANGELOG.md b/Packages/AudioConductor/CHANGELOG.md index 96a658a..d10fbeb 100644 --- a/Packages/AudioConductor/CHANGELOG.md +++ b/Packages/AudioConductor/CHANGELOG.md @@ -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 diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs index 7df6d83..cfbfa2f 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/CueSheetParameterPaneModel.cs @@ -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; @@ -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 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 CollectClipFrequencies() + { + var frequencies = new HashSet(); + 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 diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs index 3fb8dc0..af66cd1 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/Interfaces/ICueSheetParameterPaneModel.cs @@ -34,5 +34,11 @@ internal interface ICueSheetParameterPaneModel bool PitchInvert { get; set; } IReadOnlyObservableProperty PitchInvertObservable { get; } + + IReadOnlyObservableProperty ReferenceSampleRateObservable { get; } + + bool CanApplyReferenceSampleRate { get; } + + void ApplyReferenceSampleRate(); } } diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs index e09f1d4..c84e501 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Models/ObservableCueSheet.cs @@ -18,6 +18,7 @@ internal sealed class ObservableCueSheet private readonly ObservableProperty _name; private readonly ObservableProperty _pitch; private readonly ObservableProperty _pitchInvert; + private readonly ObservableProperty _referenceSampleRate; private readonly ObservableProperty _throttleLimit; private readonly ObservableProperty _throttleType; private readonly ObservableProperty _volume; @@ -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 ReferenceSampleRateObservable => _referenceSampleRate; + public string Name { get => _name.Value; diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs index 6c8c643..d390386 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Presenters/CueSheetParameterPanePresenter.cs @@ -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() @@ -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() diff --git a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs index f8207df..acc1feb 100644 --- a/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs +++ b/Packages/AudioConductor/Editor/Core/Tools/CueSheetEditor/Views/CueSheetParameterPaneView.cs @@ -15,12 +15,16 @@ namespace AudioConductor.Editor.Core.Tools.CueSheetEditor.Views { internal sealed class CueSheetParameterPaneView : VisualElement, IDisposable { + private readonly Button _applyReferenceSampleRateButton; + private readonly Subject _applyReferenceSampleRateSubject = new(); private readonly Subject _nameChangedSubject = new(); private readonly TextField _nameField; private readonly Subject _pitchChangedSubject = new(); private readonly SliderAndFloatField _pitchField; private readonly Subject _pitchInvertChangedSubject = new(); private readonly Toggle _pitchInvertField; + private readonly IntegerField _referenceSampleRateField; + private readonly HelpBox _referenceSampleRateWarning; private readonly Subject _throttleLimitChangedSubject = new(); private readonly IntegerField _throttleLimitField; private readonly Subject _throttleTypeChangedSubject = new(); @@ -45,6 +49,13 @@ public CueSheetParameterPaneView() _pitchField.lowValue = ValueRangeConst.Pitch.Min; _pitchField.highValue = ValueRangeConst.Pitch.Max; + _referenceSampleRateField = this.Q("ReferenceSampleRate"); + _referenceSampleRateField.SetEnabled(false); + _referenceSampleRateWarning = this.Q("ReferenceSampleRateWarning"); + _referenceSampleRateWarning.SetDisplay(false); + _applyReferenceSampleRateButton = this.Q