Skip to content

Commit 381d6fb

Browse files
authored
v2.4.1 (#23)
1 parent 8b9173d commit 381d6fb

12 files changed

Lines changed: 402 additions & 230 deletions

‎Packages/AudioConductor/CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## v2.4.1 - 2026/07/24
4+
5+
- Fix Issues
6+
- Fix unnecessary ~0.1s playback start delay for non-loop playback; loop playback keeps the delay to preserve buffer-priming headroom.
7+
38
## v2.4.0 - 2026/06/26
49

510
- New Features
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// --------------------------------------------------------------
2+
// Copyright 2026 CyberAgent, Inc.
3+
// --------------------------------------------------------------
4+
5+
#nullable enable
6+
7+
namespace AudioConductor.Core
8+
{
9+
internal sealed partial class AudioClipPlayer
10+
{
11+
private void PauseLoop()
12+
{
13+
if (_sources[0].IsPlaying)
14+
{
15+
_sources[0].Pause();
16+
_pausedIndex = 0;
17+
_sources[1].Stop();
18+
}
19+
else if (_sources[1].IsPlaying)
20+
{
21+
_sources[0].Stop();
22+
_sources[1].Pause();
23+
_pausedIndex = 1;
24+
}
25+
else
26+
{
27+
// Neither source is playing yet (within PlayScheduleDelay window).
28+
// AudioSource.Pause() on a scheduled-but-not-yet-playing source has undefined
29+
// behavior per Unity docs, so stop both and reschedule fresh on Resume.
30+
_sources[0].Stop();
31+
_sources[1].Stop();
32+
#if UNITY_WEBGL
33+
CancelPendingBinds();
34+
#endif
35+
_wasStoppedBeforePlay = true;
36+
}
37+
}
38+
39+
private int GetCurrentSampleLoop()
40+
{
41+
var source = GetPlayingSource();
42+
return source == null ? 0 : source.TimeSamples;
43+
}
44+
45+
private bool SetCurrentSampleLoop(int sample)
46+
{
47+
var source = GetPlayingSource();
48+
if (source == null)
49+
return false;
50+
source.TimeSamples = sample;
51+
return true;
52+
}
53+
54+
private void ScheduleNextLoop()
55+
{
56+
SchedulePlayback(_scheduledEndTime, _loopStartSample);
57+
}
58+
}
59+
}

‎Packages/AudioConductor/Runtime/Core/AudioClipPlayer.Loop.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// --------------------------------------------------------------
2+
// Copyright 2026 CyberAgent, Inc.
3+
// --------------------------------------------------------------
4+
5+
#nullable enable
6+
7+
#if UNITY_WEBGL
8+
using UnityEngine;
9+
10+
namespace AudioConductor.Core
11+
{
12+
internal sealed partial class AudioClipPlayer
13+
{
14+
private bool _resumeFromLoopStart;
15+
16+
// Sample to resume from when _resumeFromLoopStart is set. Captured in PauseBySystem so an intro
17+
// region (start..loopStart) interrupted by a system pause is not truncated on resume.
18+
private int _resumeSample;
19+
20+
private bool _webGLNativeLoopActive;
21+
22+
// Always true for DecompressOnLoad; for CompressedInMemory/Streaming it depends on the
23+
// backend (buffer vs MediaElement) and is resolved by SetupNativeLoopMode /
24+
// ResolvePendingBackend. MediaElement-backed clips use the crossover instead.
25+
private bool _nativeLoopSupported;
26+
27+
// True while a loop clip whose backend is still unknown is waiting for the first source to be
28+
// created so its backend can be resolved (see ResolvePendingBackend).
29+
private bool _backendDetectPending;
30+
31+
// Decides the loop strategy for a clip at Setup. DecompressOnLoad is always a buffer (native
32+
// loop). For CompressedInMemory/Streaming the backend (buffer vs MediaElement) is not fixed by
33+
// loadType and is only knowable once a source exists, so it is resolved per clip after the
34+
// first play (see ResolvePendingBackend); start without a loop strategy and mark it pending.
35+
// Test seam: AudioClip.Create yields DecompressOnLoad in EditMode, so the
36+
// backend-detect-pending path (CompressedInMemory/Streaming) cannot be reached
37+
// through the clip. Null means "use the clip's loadType".
38+
internal AudioClipLoadType? LoadTypeOverride;
39+
40+
private void SetupNativeLoopMode(AudioClipLoadType loadType)
41+
{
42+
loadType = LoadTypeOverride ?? loadType;
43+
44+
if (loadType == AudioClipLoadType.DecompressOnLoad)
45+
{
46+
_backendDetectPending = false;
47+
_nativeLoopSupported = true;
48+
return;
49+
}
50+
51+
_nativeLoopSupported = false;
52+
_backendDetectPending = _isLoop;
53+
}
54+
55+
// Returns true once the backend is resolved; false while the source is not yet created.
56+
private bool ResolvePendingBackend()
57+
{
58+
#if !UNITY_EDITOR
59+
var slot = _lastScheduledSourceIndex;
60+
var backend = AudioConductor_SlotBackend(_webglPlayerId, slot);
61+
if (backend < 0)
62+
return false; // source not created yet; retry next update
63+
64+
_backendDetectPending = false;
65+
66+
if (backend == 1)
67+
{
68+
// Buffer source: loop the partial region natively, no crossover re-scheduling.
69+
_nativeLoopSupported = true;
70+
_webGLNativeLoopActive = true;
71+
_scheduledEndTime = double.MaxValue;
72+
AudioConductor_ApplyNativeLoopToSlot(_webglPlayerId, slot, (float)_loopStartSample / _frequency,
73+
(float)_endSample / _frequency);
74+
}
75+
else
76+
{
77+
// MediaElement source: arm the crossover now (deferred from the first play).
78+
_nativeLoopSupported = false;
79+
AudioConductor_SetScheduledEndTime(_webglPlayerId, slot, _scheduledEndTime);
80+
UpdateNextEventTime();
81+
}
82+
83+
return true;
84+
#else
85+
_backendDetectPending = false;
86+
return true;
87+
#endif
88+
}
89+
90+
#if !UNITY_EDITOR
91+
// Returns true if the loop was armed natively (AudioBufferSourceNode.loop), meaning the
92+
// caller must skip its own (non-loop) scheduled-end arming for this call.
93+
private bool TryArmNativeLoop(int sourceIndex)
94+
{
95+
if (!_isLoop || !_nativeLoopSupported)
96+
return false;
97+
98+
// Prevent ScheduleNextLoop from firing; AudioBufferSourceNode.loop handles itself.
99+
_scheduledEndTime = double.MaxValue;
100+
_webGLNativeLoopActive = true;
101+
_lastScheduledSourceIndex = sourceIndex;
102+
_armedWhileContextSuspended = !IsAudioContextRunning();
103+
return true;
104+
}
105+
#endif
106+
107+
private void PauseBySystemLoop()
108+
{
109+
if (_backendDetectPending)
110+
{
111+
// Backend not yet resolved: the source may turn out to be a buffer (native loop).
112+
// Pausing would risk the MediaElement catch-up glitch if it resolves to MediaElement,
113+
// so stop both and restart cleanly. Restart from the start sample (not loopStart) so a
114+
// cue with an intro region is not truncated; ResolvePendingBackend re-runs and commits
115+
// the loop strategy on resume.
116+
_sources[0].Stop();
117+
_sources[1].Stop();
118+
CancelPendingBinds();
119+
_wasStoppedBeforePlay = true;
120+
return;
121+
}
122+
123+
if (!_nativeLoopSupported)
124+
{
125+
// MediaElement crossover loop: do not Pause()/UnPause(). Unity's AudioContext-resume
126+
// catch-up fast-forwards a paused MediaElement by the suspended duration, overshooting
127+
// past the partial loop region (which a MediaElement cannot wrap natively), so it would
128+
// play out-of-region audio until the scheduled stop fires. Stop both and reschedule the
129+
// loop cleanly on resume instead.
130+
//
131+
// Capture the live position before stopping: a pause inside the intro region
132+
// (start..loopStart) must resume from there so the remaining intro is not truncated,
133+
// mirroring the _backendDetectPending branch. A pause inside the loop body resolves to
134+
// loopStart, the loop's own restart point (the crossfade may overlap two sources there,
135+
// making a single live position ambiguous).
136+
_resumeSample = ResolveSystemResumeSample();
137+
_sources[0].Stop();
138+
_sources[1].Stop();
139+
CancelPendingBinds();
140+
_resumeFromLoopStart = true;
141+
return;
142+
}
143+
144+
if (_sources[0].IsPlaying)
145+
{
146+
_sources[0].Pause();
147+
_pausedIndex = 0;
148+
_sources[1].Stop();
149+
}
150+
else if (_sources[1].IsPlaying)
151+
{
152+
_sources[0].Stop();
153+
_sources[1].Pause();
154+
_pausedIndex = 1;
155+
}
156+
else
157+
{
158+
// Neither source is playing yet (within PlayScheduleDelay window).
159+
// AudioSource.Pause() on a scheduled-but-not-yet-playing source has undefined
160+
// behavior per Unity docs, so stop both and reschedule fresh on ResumeBySystem.
161+
_sources[0].Stop();
162+
_sources[1].Stop();
163+
CancelPendingBinds();
164+
_wasStoppedBeforePlay = true;
165+
}
166+
}
167+
168+
// Resolves the sample a system-paused MediaElement crossover loop resumes from: the live
169+
// content position while still inside the intro region (start..loopStart) so its remainder is
170+
// not truncated, otherwise loopStart (the loop body's restart point).
171+
private int ResolveSystemResumeSample()
172+
{
173+
var source = GetPlayingSource();
174+
if (source == null) return _startSample;
175+
return source.TimeSamples < _loopStartSample ? source.TimeSamples : _loopStartSample;
176+
}
177+
178+
private void ResumeBySystemFromLoopStart()
179+
{
180+
_resumeFromLoopStart = false;
181+
_isSystemPaused = false;
182+
if (_isPlaybackActive && !IsPaused)
183+
{
184+
_nextPlayAudioSourceIndex = 0;
185+
SchedulePlayback(_dspClock.DspTime + PlayStartDelay, _resumeSample);
186+
}
187+
}
188+
}
189+
}
190+
191+
#endif

‎Packages/AudioConductor/Runtime/Core/AudioClipPlayer.WebGL.Loop.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)