audio: add Player.SetBufferSize

This change also adds examples/realtimepcm.

Closes #2026
This commit is contained in:
Hajime Hoshi 2022-03-25 03:03:46 +09:00
parent 798b60b67c
commit 08783542eb
6 changed files with 229 additions and 22 deletions

View File

@ -404,6 +404,14 @@ func (p *Player) UnplayedBufferSize() time.Duration {
return p.p.UnplayedBufferSize()
}
// SetBufferSize adjusts the buffer size of the player.
// If 0 is specified, the default buffer size is used.
// A small buffer size is useful if you want to play a real-time PCM for example.
// Note that the audio quality might be affected if you modify the buffer size.
func (p *Player) SetBufferSize(bufferSize int) {
p.p.SetBufferSize(bufferSize)
}
type hook interface {
OnSuspendAudio(f func() error)
OnResumeAudio(f func() error)

View File

@ -101,6 +101,9 @@ func (p *dummyPlayer) Err() error {
return nil
}
func (p *dummyPlayer) SetBufferSize(bufferSize int) {
}
func (p *dummyPlayer) Close() error {
p.m.Lock()
defer p.m.Unlock()

View File

@ -125,14 +125,15 @@ type Player struct {
}
type playerImpl struct {
context *Context
src io.Reader
volume float64
err atomicError
state playerState
tmpbuf []byte
buf []byte
eof bool
context *Context
src io.Reader
volume float64
err atomicError
state playerState
tmpbuf []byte
buf []byte
eof bool
bufferSize int
m sync.Mutex
}
@ -140,9 +141,10 @@ type playerImpl struct {
func newPlayer(context *Context, src io.Reader) *Player {
p := &Player{
p: &playerImpl{
context: context,
src: src,
volume: 1,
context: context,
src: src,
volume: 1,
bufferSize: context.defaultBufferSize(),
},
}
runtime.SetFinalizer(p, (*Player).Close)
@ -176,9 +178,27 @@ func (p *playerImpl) Play() {
<-ch
}
func (p *Player) SetBufferSize(bufferSize int) {
p.p.setBufferSize(bufferSize)
}
func (p *playerImpl) setBufferSize(bufferSize int) {
p.m.Lock()
defer p.m.Unlock()
orig := p.bufferSize
p.bufferSize = bufferSize
if bufferSize == 0 {
p.bufferSize = p.context.defaultBufferSize()
}
if orig != p.bufferSize {
p.tmpbuf = nil
}
}
func (p *playerImpl) ensureTmpBuf() []byte {
if p.tmpbuf == nil {
p.tmpbuf = make([]byte, p.context.defaultBufferSize())
p.tmpbuf = make([]byte, p.bufferSize)
}
return p.tmpbuf
}
@ -193,7 +213,7 @@ func (p *playerImpl) playImpl() {
if !p.eof {
buf := p.ensureTmpBuf()
for len(p.buf) < p.context.defaultBufferSize() {
for len(p.buf) < p.bufferSize {
n, err := p.src.Read(buf)
if err != nil && err != io.EOF {
p.setErrorImpl(err)
@ -360,7 +380,7 @@ func (p *playerImpl) canReadSourceToBuffer() bool {
if p.eof {
return false
}
return len(p.buf) < p.context.defaultBufferSize()
return len(p.buf) < p.bufferSize
}
func (p *playerImpl) readSourceToBuffer() {
@ -374,7 +394,7 @@ func (p *playerImpl) readSourceToBuffer() {
return
}
if len(p.buf) >= p.context.defaultBufferSize() {
if len(p.buf) >= p.bufferSize {
return
}

View File

@ -38,7 +38,7 @@ type contextProxy struct {
// NewPlayer implements context.
func (c *contextProxy) NewPlayer(r io.Reader) player {
return c.otoContext.NewPlayer(r)
return c.otoContext.NewPlayer(r).(player)
}
func otoContextToContext(ctx otoContext) context {

View File

@ -32,6 +32,7 @@ type player interface {
SetVolume(volume float64)
UnplayedBufferSize() int
Err() error
SetBufferSize(bufferSize int)
io.Closer
}
@ -63,12 +64,13 @@ func newPlayerFactory(sampleRate int) *playerFactory {
}
type playerImpl struct {
context *Context
player player
src io.Reader
stream *timeStream
factory *playerFactory
m sync.Mutex
context *Context
player player
src io.Reader
stream *timeStream
factory *playerFactory
initBufferSize int
m sync.Mutex
}
func (f *playerFactory) newPlayer(context *Context, src io.Reader) (*playerImpl, error) {
@ -156,6 +158,10 @@ func (p *playerImpl) ensurePlayer() error {
}
if p.player == nil {
p.player = p.factory.context.NewPlayer(p.stream)
if p.initBufferSize != 0 {
p.player.SetBufferSize(p.initBufferSize)
p.initBufferSize = 0
}
}
return nil
}
@ -292,6 +298,17 @@ func (p *playerImpl) UnplayedBufferSize() time.Duration {
return time.Duration(samples) * time.Second / time.Duration(p.factory.sampleRate)
}
func (p *playerImpl) SetBufferSize(bufferSize int) {
p.m.Lock()
defer p.m.Unlock()
if p.player == nil {
p.initBufferSize = bufferSize
return
}
p.player.SetBufferSize(bufferSize)
}
func (p *playerImpl) source() io.Reader {
return p.src
}

View File

@ -0,0 +1,159 @@
// Copyright 2022 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build example
// +build example
package main
import (
"log"
"math"
"sync"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
const (
screenWidth = 640
screenHeight = 480
sampleRate = 48000
)
type Game struct {
audioContext *audio.Context
player *audio.Player
sineWave *SineWave
}
type SineWave struct {
frequency int
minFrequency int
maxFrequency int
// position is the position in the wave length in the range of [0, 1).
position float64
remaining []byte
m sync.Mutex
}
func NewSineWave() *SineWave {
return &SineWave{
frequency: 440,
minFrequency: 440,
maxFrequency: 880,
}
}
func (s *SineWave) Update(raisePitch bool) {
s.m.Lock()
defer s.m.Unlock()
if raisePitch {
if s.frequency < s.maxFrequency {
s.frequency += 10
}
} else {
if s.frequency > s.minFrequency {
s.frequency -= 10
}
}
}
func (s *SineWave) Read(buf []byte) (int, error) {
s.m.Lock()
defer s.m.Unlock()
if len(s.remaining) > 0 {
n := copy(buf, s.remaining)
s.remaining = s.remaining[n:]
return n, nil
}
var origBuf []byte
if len(buf)%4 > 0 {
origBuf = buf
buf = make([]byte, len(origBuf)+4-len(origBuf)%4)
}
length := sampleRate / float64(s.frequency)
p := int64(length * s.position)
for i := 0; i < len(buf)/4; i++ {
const max = 32767
b := int16(math.Sin(2*math.Pi*float64(p)/float64(length)) * max)
buf[4*i] = byte(b)
buf[4*i+1] = byte(b >> 8)
buf[4*i+2] = byte(b)
buf[4*i+3] = byte(b >> 8)
p++
}
s.position = float64(p) / float64(length)
s.position = s.position - math.Floor(s.position)
if origBuf != nil {
n := copy(origBuf, buf)
s.remaining = buf[n:]
return n, nil
}
return len(buf), nil
}
func NewGame() *Game {
return &Game{
audioContext: audio.NewContext(sampleRate),
}
}
func (g *Game) Update() error {
if g.audioContext == nil {
g.audioContext = audio.NewContext(sampleRate)
}
if g.player == nil {
g.sineWave = NewSineWave()
p, err := g.audioContext.NewPlayer(g.sineWave)
if err != nil {
return err
}
g.player = p
g.player.Play()
// Adjust the buffer size to reflect the audio source changes in real time.
// Note that Ebiten doesn't guarantee the audio quality when the buffer size is modified.
// 8192 should work in most cases, but this might cause glitches in some environments.
g.player.SetBufferSize(8192)
}
g.sineWave.Update(ebiten.IsKeyPressed(ebiten.KeyA))
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "This is an example of a real time PCM.\nPress and hold the A key.")
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Real Time PCM (Ebiten Demo)")
if err := ebiten.RunGame(NewGame()); err != nil {
log.Fatal(err)
}
}