hajimehoshi / ebiten

Ebitengine - A dead simple 2D game engine for Go
https://ebitengine.org
Apache License 2.0
10.98k stars 657 forks source link

Add audio.Context.BufferSize() method #2727

Open elgopher opened 1 year ago

elgopher commented 1 year ago

Operating System

What feature would you like to be added?

Add BufferSize() method to audio.Context which will return the buffer size used at runtime. The size of the context buffer is arbitrarily set by Oto, depending on the architecture. I would like to know this value so that I can create an audio.Player instance with the same buffer size.

Why is this needed?

I make a real-time PCM audio. I am using audio.Player instance with source generated real-time. To avoid audio lag I would like to use Player's buffer which is as smallest as possible. Ideally exactly the same size as audio.Context. But I don't know the buffer size of audio.Context.

The workaround is to just hardcode buffer size for all possible architectures.

hajimehoshi commented 1 year ago

Thanks, let me think more...

tinne26 commented 1 year ago

Notice that there are other internal details that would still affect your strategy to achieve minimum latency. For example, if we assume that you fill your player buffer on Read() instantaneously, you will actually get two initial calls to Read() consecutively. So, in practice, even if the context buffer size was small enough, the latency is always closer to Player.BufferSize*2, and if you really want to achieve minimum latency, you would intentionally wait before serving the second read and check the bytes left in the internal buffer (if that was exposed).

So, while I understand that having Context.BufferSize() exposed solves the problem of having to figure out by hand the minimum player buffer size for each platform, it doesn't solve your primary issue, which is about minimal audio latency. There are other hidden factors here that are relevant, like the "player buffer read policy", being able to set context latency manually and so on.

I'm also very interested in lower latencies, especially in a consistent way across the different platforms, but I don't think Context.BufferSize() is a sufficiently... context aware step towards that. While it's very possible that exposing it is a necessary part of most solutions, the problem is quite complex and we don't have a well thought-out and complete plan for it.

elgopher commented 1 year ago

You are right @tinne26. I've spent the last few days trying to use Player in such a way as to synthesize the sound with as little latency as possible. Unfortunately, I ran into more problems:

In my case, it would be perfect if I could just somehow plug into the Oto rendering thread. That is, I would like Oto/Ebiten to run my callback where I would fill the buffer with data. And it's about a callback that will be triggered just before sending samples to the sound card. Such a low-level player.

I even managed to create a proof-of-concept for such a player in Oto. I named it the working name LivePlayer. My proposed changes are backwards compatible. However, I don't know if this idea is in line with Oto's vision. But I hope it will be useful in some way. You can find the code here: https://github.com/elgopher/oto.

hajimehoshi commented 1 year ago

Player buffer fills up even when audio Context is not ready. This means that when it is ready, old samples are sent to the sound card.

This is on purpose in order to avoid glitches due to lack of buffer data. I'd create a player just after the actual sound starts.

sound is generated by my own go-routine. I use a cyclic queue where I put samples on a regular basis. When Player asks me for samples, I pull them out of the queue. However, this is all complicated and consumes precious CPU cycles.

I am not sure. You mean implementing your io.Reader doesn't match with your implementaiton, even though the buffer is small?

In my case, it would be perfect if I could just somehow plug into the Oto rendering thread. That is, I would like Oto/Ebiten to run my callback where I would fill the buffer with data. And it's about a callback that will be triggered just before sending samples to the sound card. Such a low-level player.

I don't plan to expose the internal state of Oto, so unfortunately I would not accept such a change. I still think giving a callback is the same as giving an io.Reader with a small buffer size Player. I might be wrong.

elgopher commented 1 year ago

This is on purpose in order to avoid glitches due to lack of buffer data. I'd create a player just after the actual sound starts.

Good idea. I can create a Player when Context is ready. I can poll Context by executing IsReady().

I am not sure. You mean implementing your io.Reader doesn't match with your implementaiton, even though the buffer is small?

Basically I mean two things:

I don't plan to expose the internal state of Oto, so unfortunately I would not accept such a change. I still think giving a callback is the same as giving an io.Reader with a small buffer size Player. I might be wrong.

I understand. As I said, I can do workarounds. I just wanted to make sure that I wasn't writing redundant code. I can also use 2 times bigger Player buffer size, to avoid audio glitches. This is not ideal, but maybe not to so bad for my retro game engine.