diff --git a/example/audio/main.go b/example/audio/main.go index d1ebcf7d6..0e7036f80 100644 --- a/example/audio/main.go +++ b/example/audio/main.go @@ -113,7 +113,7 @@ func addNote() { vol := 1.0 / 16.0 square(l, vol, freq, 0.25) square(r, vol, freq, 0.25) - audio.Queue(toBytes(l, r)) + audio.Queue(toBytes(l, r), sampleRate) } func update(screen *ebiten.Image) error { diff --git a/example/piano/main.go b/example/piano/main.go index 1e0edf460..9dc89e703 100644 --- a/example/piano/main.go +++ b/example/piano/main.go @@ -72,7 +72,7 @@ func toBytes(l, r []int16) []byte { func addNote(freq float64, vol float64) { f := int(freq) if n, ok := noteCache[f]; ok { - audio.Queue(n) + audio.Queue(n, sampleRate) return } length := len(pcm) * baseFreq / f @@ -89,7 +89,7 @@ func addNote(freq float64, vol float64) { } n := toBytes(l, r) noteCache[f] = n - audio.Queue(n) + audio.Queue(n, sampleRate) } var keys = []ebiten.Key{ diff --git a/exp/audio/audio.go b/exp/audio/audio.go index 04aa703d0..ef9d6a6d6 100644 --- a/exp/audio/audio.go +++ b/exp/audio/audio.go @@ -22,12 +22,12 @@ import ( // The given data is queued to the end of the buffer. // This may not be played immediately when data already exists in the buffer. // -// data's format must be linear PCM (44100Hz, 16bits, 2 channel stereo, little endian) +// data's format must be linear PCM (16bits, 2 channel stereo, little endian) // without a header (e.g. RIFF header). // // TODO: Pass sample rate and num of channels. -func Queue(data []byte) bool { - return audio.Queue(data) +func Queue(data []byte, sampleRate int) error { + return audio.Queue(data, sampleRate) } // TODO: Add Clear function diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 774aacc37..958d05339 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -14,106 +14,16 @@ package audio -import ( - "sync" -) - var audioEnabled = false -const SampleRate = 44100 - -type channel struct { - buffer []byte -} - -const MaxChannel = 32 - -var channels = make([]*channel, MaxChannel) - -func init() { - for i, _ := range channels { - channels[i] = &channel{ - buffer: []byte{}, - } - } +type chunk struct { + buffer []byte + sampleRate int } func Init() { initialize() } - -var channelsMutex = sync.Mutex{} - -func withChannels(f func()) { - channelsMutex.Lock() - defer channelsMutex.Unlock() - f() -} - -func emptyChannel() *channel { - for i, _ := range channels { - if !isPlaying(i) { - return channels[i] - } - } - return nil -} - -// TODO: Accept sample rate -func Queue(data []byte) bool { - result := true - withChannels(func() { - if !audioEnabled { - result = false - return - } - ch := emptyChannel() - if ch == nil { - result = false - return - } - ch.buffer = append(ch.buffer, data...) - }) - return result -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func isChannelsEmpty() bool { - result := false - withChannels(func() { - if !audioEnabled { - result = true - return - } - - for _, ch := range channels { - if 0 < len(ch.buffer) { - return - } - } - result = true - return - }) - return result -} - -func loadChannelBuffer(channel int, bufferSize int) []byte { - var r []byte - withChannels(func() { - if !audioEnabled { - return - } - ch := channels[channel] - length := min(len(ch.buffer), bufferSize) - input := ch.buffer[:length] - ch.buffer = ch.buffer[length:] - r = input - }) - return r +func Queue(data []byte, sampleRate int) error { + return playChunk(data, sampleRate) } diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go index 2d688a343..1daabc57f 100644 --- a/internal/audio/audio_js.go +++ b/internal/audio/audio_js.go @@ -17,21 +17,17 @@ package audio import ( - "time" - "github.com/gopherjs/gopherjs/js" ) var context *js.Object type audioProcessor struct { - channel int + data []byte sampleRate int position float64 } -var audioProcessors [MaxChannel]*audioProcessor - func toLR(data []byte) ([]int16, []int16) { l := make([]int16, len(data)/4) r := make([]int16, len(data)/4) @@ -64,14 +60,15 @@ func (a *audioProcessor) playChunk(buf []byte) { a.position += b.Get("duration").Float() } -func isPlaying(channel int) bool { - ch := channels[channel] - if 0 < len(ch.buffer) { - return true +// TOOD: Implement IO version +func playChunk(data []byte, sampleRate int) error { + a := &audioProcessor{ + data: data, + sampleRate: sampleRate, + position: context.Get("currentTime").Float(), } - a := audioProcessors[channel] - c := context.Get("currentTime").Float() - return c < a.position + a.playChunk(data) + return nil } func initialize() { @@ -89,25 +86,4 @@ func initialize() { } context = class.New() audioEnabled = true - for i := 0; i < len(audioProcessors); i++ { - audioProcessors[i] = &audioProcessor{ - channel: i, - sampleRate: 44100, // TODO: Change this for each chunks - position: 0, - } - } - go func() { - for { - const bufferSize = 2048 - c := context.Get("currentTime").Float() - for _, a := range audioProcessors { - if a.position < c { - a.position = c - } - // TODO: 4 is a magic number - a.playChunk(loadChannelBuffer(a.channel, bufferSize*4)) - } - time.Sleep(0) - } - }() } diff --git a/internal/audio/audio_openal.go b/internal/audio/audio_openal.go index afc4d5fa9..802287248 100644 --- a/internal/audio/audio_openal.go +++ b/internal/audio/audio_openal.go @@ -17,76 +17,47 @@ package audio import ( - "log" - "runtime" + "bytes" "time" - "golang.org/x/mobile/exp/audio/al" + "golang.org/x/mobile/exp/audio" ) -func isPlaying(channel int) bool { - ch := channels[channel] - return 0 < len(ch.buffer) +type src struct { + *bytes.Reader +} + +func (s *src) Close() error { + return nil +} + +var players = map[*audio.Player]struct{}{} + +func playChunk(data []byte, sampleRate int) error { + s := &src{bytes.NewReader(data)} + p, err := audio.NewPlayer(s, audio.Stereo16, int64(sampleRate)) + if err != nil { + return err + } + players[p] = struct{}{} + return p.Play() } func initialize() { - // Creating OpenAL device must be done after initializing UI. I'm not sure the reason. - ch := make(chan struct{}) + audioEnabled = true go func() { - runtime.LockOSThread() - - if err := al.OpenDevice(); err != nil { - log.Printf("OpenAL initialize error: %v", err) - close(ch) - // Graceful ending: Audio is not available on Travis CI. - return - } - - audioEnabled = true - sources := al.GenSources(MaxChannel) - close(ch) - - const bufferSize = 2048 - emptyBytes := make([]byte, bufferSize) - - for _, source := range sources { - // 3 is the least number? - // http://stackoverflow.com/questions/14932004/play-sound-with-openalstream - const bufferNum = 4 - buffers := al.GenBuffers(bufferNum) - for _, buffer := range buffers { - buffer.BufferData(al.FormatStereo16, emptyBytes, SampleRate) - source.QueueBuffers(buffer) - } - al.PlaySources(source) - } - for { - oneProcessed := false - for ch, source := range sources { - processed := source.BuffersProcessed() - if processed == 0 { - continue - } - - oneProcessed = true - buffers := make([]al.Buffer, processed) - source.UnqueueBuffers(buffers...) - for _, buffer := range buffers { - b := make([]byte, bufferSize) - copy(b, loadChannelBuffer(ch, bufferSize)) - buffer.BufferData(al.FormatStereo16, b, SampleRate) - source.QueueBuffers(buffer) - } - if source.State() == al.Stopped { - al.RewindSources(source) - al.PlaySources(source) + deleted := []*audio.Player{} + for p, _ := range players { + if p.State() == audio.Stopped { + p.Close() + deleted = append(deleted, p) } } - if !oneProcessed { - time.Sleep(1 * time.Millisecond) + for _, p := range deleted { + delete(players, p) } + time.Sleep(1 * time.Millisecond) } }() - <-ch }