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.
- Issuing a
player.Reset()
always causes one Read()
call
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.
- 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.
- 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.