| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build darwin linux |
| |
| // Package audio provides a basic audio player. |
| // |
| // In order to use this package on Linux desktop distros, |
| // you will need OpenAL library as an external dependency. |
| // On Ubuntu 14.04 'Trusty', you may have to install this library |
| // by running the command below. |
| // |
| // sudo apt-get install libopenal-dev |
| // |
| // When compiled for Android, this package uses OpenAL Soft as a backend. |
| // Please add its license file to the open source notices of your |
| // application. |
| // OpenAL Soft's license file could be found at |
| // http://repo.or.cz/w/openal-soft.git/blob/HEAD:/COPYING. |
| package audio // import "golang.org/x/mobile/audio" |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "sync" |
| "time" |
| |
| "golang.org/x/mobile/audio/al" |
| ) |
| |
| // ReadSeekCloser is an io.ReadSeeker and io.Closer. |
| type ReadSeekCloser interface { |
| io.ReadSeeker |
| io.Closer |
| } |
| |
| // Format represents an PCM data format. |
| type Format int |
| |
| const ( |
| Mono8 Format = iota |
| Mono16 |
| Stereo8 |
| Stereo16 |
| ) |
| |
| func (f Format) String() string { return formatStrings[f] } |
| |
| // formatBytes is the product of bytes per sample and number of channels. |
| var formatBytes = [...]int64{ |
| Mono8: 1, |
| Mono16: 2, |
| Stereo8: 2, |
| Stereo16: 4, |
| } |
| |
| var formatCodes = [...]uint32{ |
| Mono8: al.FormatMono8, |
| Mono16: al.FormatMono16, |
| Stereo8: al.FormatStereo8, |
| Stereo16: al.FormatStereo16, |
| } |
| |
| var formatStrings = [...]string{ |
| Mono8: "mono8", |
| Mono16: "mono16", |
| Stereo8: "stereo8", |
| Stereo16: "stereo16", |
| } |
| |
| // State indicates the current playing state of the player. |
| type State int |
| |
| const ( |
| Unknown State = iota |
| Initial |
| Playing |
| Paused |
| Stopped |
| ) |
| |
| func (s State) String() string { return stateStrings[s] } |
| |
| var stateStrings = [...]string{ |
| Unknown: "unknown", |
| Initial: "initial", |
| Playing: "playing", |
| Paused: "paused", |
| Stopped: "stopped", |
| } |
| |
| var codeToState = map[int32]State{ |
| 0: Unknown, |
| al.Initial: Initial, |
| al.Playing: Playing, |
| al.Paused: Paused, |
| al.Stopped: Stopped, |
| } |
| |
| type track struct { |
| format Format |
| samplesPerSecond int64 |
| src ReadSeekCloser |
| |
| // hasHeader represents whether the audio source contains |
| // a PCM header. If true, the audio data starts 44 bytes |
| // later in the source. |
| hasHeader bool |
| } |
| |
| // Player is a basic audio player that plays PCM data. |
| // Operations on a nil *Player are no-op, a nil *Player can |
| // be used for testing purposes. |
| type Player struct { |
| t *track |
| source al.Source |
| |
| mu sync.Mutex |
| prep bool |
| bufs []al.Buffer // buffers are created and queued to source during prepare. |
| sizeBytes int64 // size of the audio source |
| } |
| |
| // NewPlayer returns a new Player. |
| // It initializes the underlying audio devices and the related resources. |
| // If zero values are provided for format and sample rate values, the player |
| // determines them from the source's WAV header. |
| // An error is returned if the format and sample rate can't be determined. |
| func NewPlayer(src ReadSeekCloser, format Format, samplesPerSecond int64) (*Player, error) { |
| if err := al.OpenDevice(); err != nil { |
| return nil, err |
| } |
| s := al.GenSources(1) |
| if code := al.Error(); code != 0 { |
| return nil, fmt.Errorf("audio: cannot generate an audio source [err=%x]", code) |
| } |
| p := &Player{ |
| t: &track{format: format, src: src, samplesPerSecond: samplesPerSecond}, |
| source: s[0], |
| } |
| if err := p.discoverHeader(); err != nil { |
| return nil, err |
| } |
| if p.t.format == 0 { |
| return nil, errors.New("audio: cannot determine the format") |
| } |
| if p.t.samplesPerSecond == 0 { |
| return nil, errors.New("audio: cannot determine the sample rate") |
| } |
| return p, nil |
| } |
| |
| // headerSize is the size of WAV headers. |
| // See http://www.topherlee.com/software/pcm-tut-wavformat.html. |
| const headerSize = 44 |
| |
| var ( |
| riffHeader = []byte("RIFF") |
| waveHeader = []byte("WAVE") |
| ) |
| |
| func (p *Player) discoverHeader() error { |
| buf := make([]byte, headerSize) |
| if n, _ := io.ReadFull(p.t.src, buf); n != headerSize { |
| // No header present or read error. |
| return nil |
| } |
| if !(bytes.Equal(buf[0:4], riffHeader) && bytes.Equal(buf[8:12], waveHeader)) { |
| return nil |
| } |
| p.t.hasHeader = true |
| var format Format |
| switch channels, depth := buf[22], buf[34]; { |
| case channels == 1 && depth == 8: |
| format = Mono8 |
| case channels == 1 && depth == 16: |
| format = Mono16 |
| case channels == 2 && depth == 8: |
| format = Stereo8 |
| case channels == 2 && depth == 16: |
| format = Stereo16 |
| default: |
| return fmt.Errorf("audio: unsupported format; num of channels=%d, bit rate=%d", channels, depth) |
| } |
| if p.t.format == 0 { |
| p.t.format = format |
| } |
| if p.t.format != format { |
| return fmt.Errorf("audio: given format %v does not match header %v", p.t.format, format) |
| } |
| sampleRate := int64(buf[24]) | int64(buf[25])<<8 | int64(buf[26])<<16 | int64(buf[27]<<24) |
| if p.t.samplesPerSecond == 0 { |
| p.t.samplesPerSecond = sampleRate |
| } |
| if p.t.samplesPerSecond != sampleRate { |
| return fmt.Errorf("audio: given sample rate %v does not match header", p.t.samplesPerSecond, sampleRate) |
| } |
| return nil |
| } |
| |
| func (p *Player) prepare(offset int64, force bool) error { |
| p.mu.Lock() |
| if !force && p.prep { |
| p.mu.Unlock() |
| return nil |
| } |
| p.mu.Unlock() |
| |
| if p.t.hasHeader { |
| offset += headerSize |
| } |
| if _, err := p.t.src.Seek(offset, 0); err != nil { |
| return err |
| } |
| var bufs []al.Buffer |
| // TODO(jbd): Limit the number of buffers in use, unqueue and reuse |
| // the existing buffers as buffers are processed. |
| buf := make([]byte, 128*1024) |
| size := offset |
| for { |
| n, err := p.t.src.Read(buf) |
| if n > 0 { |
| size += int64(n) |
| b := al.GenBuffers(1) |
| b[0].BufferData(formatCodes[p.t.format], buf[:n], int32(p.t.samplesPerSecond)) |
| bufs = append(bufs, b[0]) |
| } |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return err |
| } |
| } |
| |
| p.mu.Lock() |
| if len(p.bufs) > 0 { |
| p.source.UnqueueBuffers(p.bufs) |
| al.DeleteBuffers(p.bufs) |
| } |
| p.sizeBytes = size |
| p.bufs = bufs |
| p.prep = true |
| if len(bufs) > 0 { |
| p.source.QueueBuffers(bufs) |
| } |
| p.mu.Unlock() |
| return nil |
| } |
| |
| // Play buffers the source audio to the audio device and starts |
| // to play the source. |
| // If the player paused or stopped, it reuses the previously buffered |
| // resources to keep playing from the time it has paused or stopped. |
| func (p *Player) Play() error { |
| if p == nil { |
| return nil |
| } |
| // Prepares if the track hasn't been buffered before. |
| if err := p.prepare(0, false); err != nil { |
| return err |
| } |
| al.PlaySources(p.source) |
| return lastErr() |
| } |
| |
| // Pause pauses the player. |
| func (p *Player) Pause() error { |
| if p == nil { |
| return nil |
| } |
| al.PauseSources(p.source) |
| return lastErr() |
| } |
| |
| // Stop stops the player. |
| func (p *Player) Stop() error { |
| if p == nil { |
| return nil |
| } |
| al.StopSources(p.source) |
| return lastErr() |
| } |
| |
| // Seek moves the play head to the given offset relative to the start of the source. |
| func (p *Player) Seek(offset time.Duration) error { |
| if p == nil { |
| return nil |
| } |
| if err := p.Stop(); err != nil { |
| return err |
| } |
| size := durToByteOffset(p.t, offset) |
| if err := p.prepare(size, true); err != nil { |
| return err |
| } |
| al.PlaySources(p.source) |
| return lastErr() |
| } |
| |
| // Current returns the current playback position of the audio that is being played. |
| func (p *Player) Current() time.Duration { |
| if p == nil { |
| return 0 |
| } |
| // TODO(jbd): Current never returns the Total when the playing is finished. |
| // OpenAL may be returning the last buffer's start point as an OffsetByte. |
| return byteOffsetToDur(p.t, int64(p.source.OffsetByte())) |
| } |
| |
| // Total returns the total duration of the audio source. |
| func (p *Player) Total() time.Duration { |
| if p == nil { |
| return 0 |
| } |
| // Prepare is required to determine the length of the source. |
| // We need to read the entire source to calculate the length. |
| p.prepare(0, false) |
| return byteOffsetToDur(p.t, p.sizeBytes) |
| } |
| |
| // Volume returns the current player volume. The range of the volume is [0, 1]. |
| func (p *Player) Volume() float64 { |
| if p == nil { |
| return 0 |
| } |
| return float64(p.source.Gain()) |
| } |
| |
| // SetVolume sets the volume of the player. The range of the volume is [0, 1]. |
| func (p *Player) SetVolume(vol float64) { |
| if p == nil { |
| return |
| } |
| p.source.SetGain(float32(vol)) |
| } |
| |
| // State returns the player's current state. |
| func (p *Player) State() State { |
| if p == nil { |
| return Unknown |
| } |
| return codeToState[p.source.State()] |
| } |
| |
| // Close closes the device and frees the underlying resources |
| // used by the player. |
| // It should be called as soon as the player is not in-use anymore. |
| func (p *Player) Close() error { |
| if p == nil { |
| return nil |
| } |
| if p.source != 0 { |
| al.DeleteSources(p.source) |
| } |
| p.mu.Lock() |
| if len(p.bufs) > 0 { |
| al.DeleteBuffers(p.bufs) |
| } |
| p.mu.Unlock() |
| p.t.src.Close() |
| return nil |
| } |
| |
| func byteOffsetToDur(t *track, offset int64) time.Duration { |
| return time.Duration(offset * formatBytes[t.format] * int64(time.Second) / t.samplesPerSecond) |
| } |
| |
| func durToByteOffset(t *track, dur time.Duration) int64 { |
| return int64(dur) * t.samplesPerSecond / (formatBytes[t.format] * int64(time.Second)) |
| } |
| |
| // lastErr returns the last error or nil if the last operation |
| // has been succesful. |
| func lastErr() error { |
| if code := al.Error(); code != 0 { |
| return fmt.Errorf("audio: openal failed with %x", code) |
| } |
| return nil |
| } |
| |
| // TODO(jbd): Close the device. |