Skip to content

Commit f57d5f1

Browse files
committed
fix(core): restoring loop points functionality
- Proactively check for loop boundaries before audio generation
1 parent b8ed271 commit f57d5f1

1 file changed

Lines changed: 23 additions & 14 deletions

File tree

Src/Abstracts/SoundPlayerBase.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public abstract class SoundPlayerBase : SoundComponent, ISoundPlayer
1818
private float _playbackSpeed = 1.0f;
1919
private int _loopStartSamples;
2020
private int _loopEndSamples = -1;
21-
private bool _loopingSeekPending;
2221
private readonly WsolaTimeStretcher _timeStretcher;
2322
private readonly float[] _timeStretcherInputBuffer;
2423
private int _timeStretcherInputBufferValidSamples;
@@ -103,6 +102,14 @@ protected override void GenerateAudio(Span<float> output, int channels)
103102
output.Clear();
104103
return;
105104
}
105+
106+
// Proactively check for looping before generating audio. This handles loops where a specific end point is set.
107+
if (IsLooping && _loopEndSamples != -1)
108+
{
109+
// Ensure loop is valid and we've reached or passed the end point.
110+
if (_loopStartSamples < _loopEndSamples && _rawSamplePosition >= _loopEndSamples)
111+
Seek(_loopStartSamples, channels);
112+
}
106113

107114
// Directly read from provider when playback speed is 1.0
108115
if (Math.Abs(_playbackSpeed - 1.0f) < 0.001f)
@@ -319,7 +326,7 @@ private int FillResampleBuffer(int minSamplesRequiredInOutputBuffer, int channel
319326
int samplesWrittenToResample, samplesConsumedFromStretcherInputBuf, sourceSamplesForThisProcessCall;
320327

321328
// Determine how to call the time stretcher (Process or Flush).
322-
if (inputSpanForStretcher.IsEmpty && providerExhausted && !_loopingSeekPending)
329+
if (inputSpanForStretcher.IsEmpty && providerExhausted)
323330
{
324331
// If the input buffer for the stretcher is empty AND we know the provider is exhausted, flush the stretcher.
325332
samplesWrittenToResample = _timeStretcher.Flush(outputSpanForStretcher);
@@ -333,10 +340,6 @@ private int FillResampleBuffer(int minSamplesRequiredInOutputBuffer, int channel
333340
out samplesConsumedFromStretcherInputBuf,
334341
out sourceSamplesForThisProcessCall);
335342
}
336-
else if (_loopingSeekPending)
337-
{
338-
break;
339-
}
340343
else
341344
{
342345
break; // Not enough input to process, and the provider is not yet exhausted.
@@ -350,21 +353,21 @@ private int FillResampleBuffer(int minSamplesRequiredInOutputBuffer, int channel
350353
totalSourceSamplesRepresented += sourceSamplesForThisProcessCall;
351354

352355
// Break if no progress was made and no more data is expected.
353-
if (samplesWrittenToResample == 0 && samplesConsumedFromStretcherInputBuf == 0 &&
354-
providerExhausted && !_loopingSeekPending) break;
356+
if (samplesWrittenToResample == 0 && samplesConsumedFromStretcherInputBuf == 0 && providerExhausted) break;
355357
}
356358

357359
return totalSourceSamplesRepresented;
358360
}
359361

360362
/// <summary>
361363
/// Handles the end-of-stream condition, including looping and stopping.
364+
/// This is called when the data provider is fully exhausted (ReadBytes returns 0).
362365
/// </summary>
363366
/// <param name="remainingOutputBuffer">The buffer for remaining output.</param>
364367
/// <param name="channels">The number of channels.</param>
365368
protected virtual void HandleEndOfStream(Span<float> remainingOutputBuffer, int channels)
366369
{
367-
// For live streams with unknown length, don't treat buffer underflow as end-of-stream
370+
// Not looping, and it's a file with a known length. This is the definitive end.
368371
if (!IsLooping && _dataProvider.Length > 0)
369372
{
370373
// Original end-of-stream handling
@@ -406,30 +409,36 @@ protected virtual void HandleEndOfStream(Span<float> remainingOutputBuffer, int
406409
State = PlaybackState.Stopped;
407410
OnPlaybackEnded();
408411
}
412+
// Looping is enabled.
409413
else if (IsLooping)
410414
{
411-
// Original looping handling
412415
var targetLoopStart = Math.Max(0, _loopStartSamples);
413416
var actualLoopEnd = (_loopEndSamples == -1)
414417
? _dataProvider.Length
415418
: Math.Min(_loopEndSamples, _dataProvider.Length);
416419

420+
// Check if the loop is valid (start < end, and start is within bounds).
417421
if (targetLoopStart < actualLoopEnd && targetLoopStart < _dataProvider.Length)
418422
{
419-
_loopingSeekPending = true;
420423
Seek(targetLoopStart, channels);
421-
_loopingSeekPending = false;
422424
if (!remainingOutputBuffer.IsEmpty)
423425
GenerateAudio(remainingOutputBuffer, channels);
424426
}
427+
else
428+
{
429+
// Loop is not valid (e.g., start >= end), so treat as a normal end-of-stream.
430+
State = PlaybackState.Stopped;
431+
OnPlaybackEnded();
432+
remainingOutputBuffer.Clear();
433+
}
425434
}
426435
// For live streams (Length <= 0), just clear the buffer and continue
427436
else
428437
{
429438
remainingOutputBuffer.Clear();
430439
}
431440
}
432-
441+
433442
/// <summary>
434443
/// Invokes the PlaybackEnded event.
435444
/// </summary>
@@ -641,4 +650,4 @@ public override void Dispose()
641650
GC.SuppressFinalize(this);
642651
base.Dispose();
643652
}
644-
}
653+
}

0 commit comments

Comments
 (0)