oto: Player reads infinitely even after being paused/reset

Hello (again 😉)

I found some weird behavior with the player (intentional?) that can cause issues and wanted to discuss them here (tested on Windows 11).

Note that the following behaviors happen when the player is paused, but do not happen if the player has never played. They will start to happen even if the player was started then immediately paused, or if the sound played completely.

  1. Issuing a player.Reset() always causes one Read() call
  2. Reset followed by a seek (or vice versa) to io.SeekStart (or similar) will cause the player to call Read() infinitely! Once the infinite reading starts even seeking to io.SeekEnd doesn’t stop it. The only thing that seems to stop that is to call Play() and let the sound finish.
  3. While point 2 is active (Read is being called by Oto), a user calling seek can cause the Read and Seek functions to be called concurrently which is many times not safe.
  4. Infinite calling of Read() will start with reset+seek, or by playing and pausing before a sound finishes playing.

I had issues with sounds not re-playing properly because of this, although a mutex in read/seek seems to help in some cases.

Here is simple code to reproduce:

//This is a io.ReadSeeker wrapper that will log when Read/Seek is called, and will panic on concurrent use
type ReadSeekerFileWrapper struct {
	F *os.File
	M sync.Mutex
}

//If mutex is already locked this will panic
func (fw *ReadSeekerFileWrapper) Check(name string) {
	locked := fw.M.TryLock()
	if locked {
		fw.M.Unlock()
	} else {
		panic("Concurrent use by: " + name)
	}
}

var shouldPrint = false

func (fw *ReadSeekerFileWrapper) Read(outBuf []byte) (bytesRead int, err error) {

	//mp3 decoding calls read, so we don't want to log that
	if shouldPrint {
		println("Read called")
	}

	fw.Check("Read")
	fw.M.Lock()
	defer fw.M.Unlock()

	n, err := fw.F.Read(outBuf)
	if shouldPrint {
		fmt.Printf("Read %d bytes\n", n)
	}
	return n, err
}

func (fw *ReadSeekerFileWrapper) Seek(offset int64, whence int) (int64, error) {

	if shouldPrint {
		fmt.Printf("Seek called: offset=%d, whence=%d\n", offset, whence)
	}

	fw.Check("Seek")
	fw.M.Lock()
	defer fw.M.Unlock()

	return fw.F.Seek(offset, whence)
}

func main() {

	//Load some mp3
	file, _ := os.Open("./test_audio_files/camera.mp3")
	fw := &ReadSeekerFileWrapper{F: file, M: sync.Mutex{}}
	decodedMp3, _ := mp3.NewDecoder(fw)
	shouldPrint = true

	//Init Oto
	otoCtx, readyChan, _ := oto.NewContext(44100, 2, 2)
	<-readyChan
	player := otoCtx.NewPlayer(decodedMp3)

        //Play once
       	player.Play()
	for player.IsPlaying() {
		time.Sleep(time.Millisecond)
	}

	//This will cause `Read` to be called infinitely (you will see many logs)
	//Doing reset first then seek does the same thing
	fw.Seek(0, io.SeekStart)
	player.Reset()

	//Instead of playing fully then reset+seek we could have done:
	//player.Play(); player.Pause()

	//Simulate a user thinking Oto isn't doing anything and wanting to seek.
	//At some point this will panic because the wrapper detects concurrent use (seek/read called at the same time)
	time.Sleep(100 * time.Millisecond)
	for {

		//Start/End both will panic

		// fw.Seek(0, io.SeekStart)
		fw.Seek(0, io.SeekEnd)
	}
}

I don’t think this is intended behavior. Not only can this cause concurrent access to resources which might not be safe, but will cause very high CPU usage.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 19 (7 by maintainers)

Commits related to this issue

Most upvoted comments

I think I have fixed this. Thank you for reporting this!

Whether this is reproducible with any audio or not matters. I’ll try later. Thanks