@@ -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