From 3859bc742109e84af2673579cc0437dfc9d76c30 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 3 Mar 2016 11:57:25 +0900 Subject: [PATCH] audio: Mixing by Go --- examples/piano/main.go | 6 -- exp/audio/audio.go | 114 ++++++++++++++++++++++++++++++++++---- exp/audio/audio_js.go | 6 +- exp/audio/audio_openal.go | 29 ++-------- 4 files changed, 113 insertions(+), 42 deletions(-) diff --git a/examples/piano/main.go b/examples/piano/main.go index 63aae0826..305a32230 100644 --- a/examples/piano/main.go +++ b/examples/piano/main.go @@ -88,9 +88,6 @@ func addNote(freq float64, vol float64) error { f := int(freq) if n, ok := noteCache[f]; ok { p, err := audioContext.NewPlayer(&stream{bytes.NewReader(n)}) - if err == audio.ErrTooManyPlayers { - return nil - } if err != nil { return err } @@ -112,9 +109,6 @@ func addNote(freq float64, vol float64) error { n := toBytes(l, r) noteCache[f] = n p, err := audioContext.NewPlayer(&stream{bytes.NewReader(n)}) - if err == audio.ErrTooManyPlayers { - return nil - } if err != nil { return err } diff --git a/exp/audio/audio.go b/exp/audio/audio.go index a0878c193..248700e58 100644 --- a/exp/audio/audio.go +++ b/exp/audio/audio.go @@ -15,24 +15,102 @@ package audio import ( - "errors" + "fmt" "io" + "sync" ) +// TODO: In JavaScript, mixing should be done by WebAudio for performance. +type mixedPlayersStream struct { + context *Context +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func (s *mixedPlayersStream) Read(b []byte) (int, error) { + s.context.Lock() + defer s.context.Unlock() + + l := len(b) + if len(s.context.players) == 0 { + copy(b, make([]byte, l)) + return l, nil + } + closed := []*Player{} + for p := range s.context.players { + ll := l - len(p.buf) + if ll <= 0 { + continue + } + b := make([]byte, ll) + n, err := p.src.Read(b) + if 0 < n { + p.buf = append(p.buf, b[:n]...) + } + if err == io.EOF { + if len(p.buf) < l { + p.buf = append(p.buf, make([]byte, l-len(p.buf))...) + } + closed = append(closed, p) + } + } + resultLen := l + for p := range s.context.players { + resultLen = min(len(p.buf)/2*2, resultLen) + } + for i := 0; i < resultLen/2; i++ { + x := int16(0) + for p := range s.context.players { + // TODO: Overflow check to avoid distortion + x += int16(p.buf[2*i]) | (int16(p.buf[2*i+1]) << 8) + } + b[2*i] = byte(x) + b[2*i+1] = byte(x >> 8) + } + for p := range s.context.players { + p.buf = p.buf[resultLen:] + } + for _, p := range closed { + delete(s.context.players, p) + } + return resultLen, nil +} + // TODO: Enable to specify the format like Mono8? type Context struct { - sampleRate int + sampleRate int + stream *mixedPlayersStream + innerPlayer *player + players map[*Player]struct{} + sync.Mutex } func NewContext(sampleRate int) *Context { - return &Context{ + // TODO: Panic if one context exists. + c := &Context{ sampleRate: sampleRate, + players: map[*Player]struct{}{}, } + c.stream = &mixedPlayersStream{c} + var err error + c.innerPlayer, err = newPlayer(c.stream, c.sampleRate) + if err != nil { + panic(fmt.Sprintf("audio: NewContext error: %v", err)) + } + c.innerPlayer.play() + return c } type Player struct { - player *player + context *Context + src io.ReadSeeker + buf []byte } // NewPlayer creates a new player with the given data to the given channel. @@ -44,15 +122,31 @@ type Player struct { // // TODO: Pass sample rate and num of channels. func (c *Context) NewPlayer(src io.ReadSeeker) (*Player, error) { - return newPlayer(src, c.sampleRate) -} + c.Lock() + defer c.Unlock() -var ErrTooManyPlayers = errors.New("audio: too many players exist") + p := &Player{ + context: c, + src: src, + buf: []byte{}, + } + return p, nil +} func (p *Player) Play() error { - return p.player.play() + p.context.Lock() + defer p.context.Unlock() + + p.context.players[p] = struct{}{} + return nil } -func (p *Player) Close() error { - return p.player.close() +// TODO: IsPlaying + +func (p *Player) Stop() error { + p.context.Lock() + defer p.context.Unlock() + + delete(p.context.players, p) + return nil } diff --git a/exp/audio/audio_js.go b/exp/audio/audio_js.go index 875437bc5..78b537b8e 100644 --- a/exp/audio/audio_js.go +++ b/exp/audio/audio_js.go @@ -26,7 +26,7 @@ import ( var context *js.Object type player struct { - src io.ReadSeeker + src io.Reader sampleRate int position float64 bufferSource *js.Object @@ -49,7 +49,7 @@ func initialize() bool { return true } -func newPlayer(src io.ReadSeeker, sampleRate int) (*Player, error) { +func newPlayer(src io.Reader, sampleRate int) (*player, error) { if context == nil { if !initialize() { panic("audio couldn't be initialized") @@ -62,7 +62,7 @@ func newPlayer(src io.ReadSeeker, sampleRate int) (*Player, error) { position: context.Get("currentTime").Float(), bufferSource: nil, } - return &Player{p}, nil + return p, nil } func toLR(data []byte) ([]int16, []int16) { diff --git a/exp/audio/audio_openal.go b/exp/audio/audio_openal.go index c561559eb..d495c7242 100644 --- a/exp/audio/audio_openal.go +++ b/exp/audio/audio_openal.go @@ -27,39 +27,23 @@ import ( ) const ( - maxSourceNum = 32 maxBufferNum = 8 ) var totalBufferNum = 0 -var playerCache = []*player{} - type player struct { alSource al.Source alBuffers []al.Buffer - source io.ReadSeeker + source io.Reader sampleRate int isClosed bool } var m sync.Mutex -func newPlayerFromCache(src io.ReadSeeker, sampleRate int) (*player, error) { - for _, p := range playerCache { - if p.sampleRate != sampleRate { - continue - } - if !p.isClosed { - continue - } - p.source = src - p.isClosed = false - return p, nil - } - if maxSourceNum <= len(playerCache) { - return nil, ErrTooManyPlayers - } +// TODO: Unify this to newPlayer +func newPlayerFromCache(src io.Reader, sampleRate int) (*player, error) { s := al.GenSources(1) if err := al.Error(); err != 0 { panic(fmt.Sprintf("audio: al.GenSources error: %d", err)) @@ -71,11 +55,10 @@ func newPlayerFromCache(src io.ReadSeeker, sampleRate int) (*player, error) { sampleRate: sampleRate, } runtime.SetFinalizer(p, (*player).close) - playerCache = append(playerCache, p) return p, nil } -func newPlayer(src io.ReadSeeker, sampleRate int) (*Player, error) { +func newPlayer(src io.Reader, sampleRate int) (*player, error) { m.Lock() defer m.Unlock() @@ -87,7 +70,7 @@ func newPlayer(src io.ReadSeeker, sampleRate int) (*Player, error) { if err != nil { return nil, err } - return &Player{p}, nil + return p, nil } const bufferSize = 1024 @@ -147,7 +130,7 @@ func (p *player) play() error { if 0 < n { p.alBuffers = append(p.alBuffers, al.GenBuffers(n)...) totalBufferNum += n - if maxSourceNum*maxBufferNum < totalBufferNum { + if maxBufferNum < totalBufferNum { panic("audio: too many buffers are created") } }