nup-headless looks like it still takes ~4.3% CPU on the crappy Celeron where I have it running while playback is paused (compared to ~5.3% while playing). I suspect that this is from the audio library that I'm using (maybe I looked at it before?), but I should still do some profiling and see if there's some way to prevent this.
Reduce nup-headless CPU usage when idle #139
I think that this is probably from github.com/hajimehoshi/ebiten/v2/audio.NewContext.func4, which calls github.com/hajimehoshi/ebiten/v2/audio.(*Context).updatePlayers every 100 ms.
This sounds like it's done for macOS game engine reasons that aren't relevant to me ("In the current Ebitengine implementation, update might not be called when the window is in background"). There's probably nothing I can do about this, and I'm still not aware of any other audio playback libraries for Go. Maybe I could use Cgo to wrap some other library (miniaudio?), but I'm not sure that it's worth the effort since the current gapless playback code is very tied to Ebitengine/Oto's needs.
Maybe I could close the audio.Player when the playlist is empty/ended or playback has been paused for a while and then create a new one when needed. I should test first that this actually reduces the idle CPU usage.
I don't think that will help. Even just the following uses ~5% CPU:
packagemainimport("time""github.com/hajimehoshi/ebiten/v2/audio")funcmain(){audio.NewContext(44_100)time.Sleep(time.Minute)}
Go bindings for miniaudio: https://github.com/gen2brain/malgo
Hmm. Audio playback seems to be far more performant with miniaudio (via malgo) than with Ebitengine. In a container on a crappy Chromebook, a trivial program that decodes an MP3 using minimp3 and plays it consumes 2-3% CPU with miniaudio, compared to 20-30% with Ebitengine. When I stop the miniaudio device that drops to 0%; when I pause the Ebitengine player it remains at 20-30%.
(Note that this device is slower than the one in the initial comment -- the 20-30% CPU here corresponds to the ~5% in the original comment.)
Ebitengine gives me the current playback position and lets me seek to new positions, and I don't think that there's anything similar in miniaudio (at least exposed by the malgo bindings); it just calls a read callback as long as the device is active. I could probably build my own abstraction on top of a miniaudio device that implements the audioPlayer interface used by gaplessPlayer:
typeaudioPlayerinterface{Position()time.DurationIsPlaying()boolPlay()Pause()SetPosition(time.Duration)error}I'm not completely sure how to get the current playback position rather than what's been buffered, though. At least on the Chromebook, the buffer size passed to the read callback is just 3306 samples at 44.1 kHz stereo (30 ms, if I'm getting the math right), so maybe it doesn't really matter.
I'm also using Ebitengine for resampling on the rare occasion when it's needed, so I'd probably need to either keep using Ebitengine for that, extend malgo to wrap miniaudio's ma_resampler, or find some other Go audio resampling library.
I... think I mostly have this working? It was much easier than I expected.
There's some trickiness due to synchronization differences between Ebitengine and miniaudio. Ebitengine allows calling play/pause from anywhere (I think) and automatically pauses when it gets an EOF. miniaudio keeps reading until you stop the device, which deadlocks if you do it from within the read callback.
I added support for selecting a particular miniaudio backend, but I'm not sure that it was worthwhile.
The "null" and OSS backends look like they aren't compiled in.
The ALSA backend makes nup-headless consume close to 100% CPU and frequently logs EPIPE (write) messages.
The JACK backend doesn't work on my Chromebook; I think I don't have the server (or PipeWire emulation) running.
So I guess the PulseAudio backend is my only option, at least in my dev environment. I don't think there's a native PipeWire backend (or if one would be more efficient).
I was too cocky. This is a racy mess and I need to replace gaplessPlayer with something that's designed with miniaudio's API in mind.
No due date set.
No dependencies set.
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?