2015-02-10 14:12:41 +01:00
|
|
|
// 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.
|
|
|
|
|
2015-03-03 10:34:19 -08:00
|
|
|
// +build darwin linux
|
2015-02-20 17:18:47 -05:00
|
|
|
|
2015-02-10 14:12:41 +01:00
|
|
|
// Package audio provides a basic audio player.
|
2015-03-06 13:30:33 -08:00
|
|
|
//
|
|
|
|
// 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
|
|
|
|
//
|
2015-04-13 14:06:16 -07:00
|
|
|
// 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.
|
2015-02-10 14:12:41 +01:00
|
|
|
package audio
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"golang.org/x/mobile/audio/al"
|
|
|
|
)
|
|
|
|
|
2015-02-25 23:54:59 -08:00
|
|
|
// Format represents an PCM data format.
|
2015-03-02 15:22:29 +11:00
|
|
|
type Format int
|
2015-02-10 14:12:41 +01:00
|
|
|
|
|
|
|
const (
|
2015-03-02 15:22:29 +11:00
|
|
|
Mono8 Format = iota
|
|
|
|
Mono16
|
|
|
|
Stereo8
|
|
|
|
Stereo16
|
2015-02-10 14:12:41 +01:00
|
|
|
)
|
|
|
|
|
2015-03-02 15:22:29 +11:00
|
|
|
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{
|
2015-02-25 23:54:59 -08:00
|
|
|
Mono8: al.FormatMono8,
|
|
|
|
Mono16: al.FormatMono16,
|
|
|
|
Stereo8: al.FormatStereo8,
|
|
|
|
Stereo16: al.FormatStereo16,
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
2015-03-02 15:22:29 +11:00
|
|
|
var formatStrings = [...]string{
|
|
|
|
Mono8: "mono8",
|
|
|
|
Mono16: "mono16",
|
|
|
|
Stereo8: "stereo8",
|
|
|
|
Stereo16: "stereo16",
|
|
|
|
}
|
|
|
|
|
2015-02-10 14:12:41 +01:00
|
|
|
// State indicates the current playing state of the player.
|
2015-03-02 15:22:29 +11:00
|
|
|
type State int
|
2015-02-10 14:12:41 +01:00
|
|
|
|
|
|
|
const (
|
2015-03-02 15:22:29 +11:00
|
|
|
Unknown State = iota
|
|
|
|
Initial
|
|
|
|
Playing
|
|
|
|
Paused
|
|
|
|
Stopped
|
2015-02-10 14:12:41 +01:00
|
|
|
)
|
|
|
|
|
2015-03-02 15:22:29 +11:00
|
|
|
func (s State) String() string { return stateStrings[s] }
|
|
|
|
|
|
|
|
var stateStrings = [...]string{
|
|
|
|
Unknown: "unknown",
|
|
|
|
Initial: "initial",
|
|
|
|
Playing: "playing",
|
|
|
|
Paused: "paused",
|
|
|
|
Stopped: "stopped",
|
|
|
|
}
|
|
|
|
|
2015-02-10 14:12:41 +01:00
|
|
|
var codeToState = map[int32]State{
|
2015-02-25 23:54:59 -08:00
|
|
|
0: Unknown,
|
|
|
|
al.Initial: Initial,
|
|
|
|
al.Playing: Playing,
|
|
|
|
al.Paused: Paused,
|
|
|
|
al.Stopped: Stopped,
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var device struct {
|
|
|
|
sync.Mutex
|
2015-02-26 22:39:38 -08:00
|
|
|
d *al.Device
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type track struct {
|
2015-02-25 23:54:59 -08:00
|
|
|
format Format
|
|
|
|
samplesPerSecond int64
|
|
|
|
src io.ReadSeeker
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2015-02-25 23:54:59 -08:00
|
|
|
mu sync.Mutex
|
|
|
|
prep bool
|
|
|
|
bufs []al.Buffer // buffers are created and queued to source during prepare.
|
|
|
|
sizeBytes int64 // size of the audio source
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewPlayer returns a new Player.
|
|
|
|
// It initializes the underlying audio devices and the related resources.
|
2015-02-25 23:54:59 -08:00
|
|
|
func NewPlayer(src io.ReadSeeker, format Format, samplesPerSecond int64) (*Player, error) {
|
2015-02-10 14:12:41 +01:00
|
|
|
device.Lock()
|
|
|
|
defer device.Unlock()
|
|
|
|
|
|
|
|
if device.d == nil {
|
2015-02-26 22:39:38 -08:00
|
|
|
device.d = al.Open("")
|
2015-02-10 14:12:41 +01:00
|
|
|
c := device.d.CreateContext(nil)
|
2015-04-06 15:57:04 -04:00
|
|
|
if !al.MakeContextCurrent(c) {
|
2015-02-25 23:54:59 -08:00
|
|
|
return nil, fmt.Errorf("audio: cannot initiate a new player")
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
s := al.GenSources(1)
|
|
|
|
if code := al.Error(); code != 0 {
|
2015-02-25 23:54:59 -08:00
|
|
|
return nil, fmt.Errorf("audio: cannot generate an audio source [err=%x]", code)
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
return &Player{
|
2015-02-25 23:54:59 -08:00
|
|
|
t: &track{format: format, src: src, samplesPerSecond: samplesPerSecond},
|
2015-02-10 14:12:41 +01:00
|
|
|
source: s[0],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Player) prepare(offset int64, force bool) error {
|
2015-02-25 23:54:59 -08:00
|
|
|
p.mu.Lock()
|
2015-02-10 14:12:41 +01:00
|
|
|
if !force && p.prep {
|
2015-02-25 23:54:59 -08:00
|
|
|
p.mu.Unlock()
|
2015-02-10 14:12:41 +01:00
|
|
|
return nil
|
|
|
|
}
|
2015-02-25 23:54:59 -08:00
|
|
|
p.mu.Unlock()
|
|
|
|
|
2015-02-10 14:12:41 +01:00
|
|
|
if _, err := p.t.src.Seek(offset, 0); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-02-25 23:54:59 -08:00
|
|
|
var bufs []al.Buffer
|
2015-02-10 14:12:41 +01:00
|
|
|
// 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)
|
2015-03-02 15:22:29 +11:00
|
|
|
b[0].BufferData(formatCodes[p.t.format], buf[:n], int32(p.t.samplesPerSecond))
|
2015-02-25 23:54:59 -08:00
|
|
|
bufs = append(bufs, b[0])
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2015-02-25 23:54:59 -08:00
|
|
|
|
|
|
|
p.mu.Lock()
|
2015-02-10 14:12:41 +01:00
|
|
|
if len(p.bufs) > 0 {
|
2015-02-25 23:54:59 -08:00
|
|
|
p.source.UnqueueBuffers(p.bufs)
|
|
|
|
al.DeleteBuffers(p.bufs)
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
2015-02-25 23:54:59 -08:00
|
|
|
p.sizeBytes = size
|
|
|
|
p.bufs = bufs
|
2015-02-10 14:12:41 +01:00
|
|
|
p.prep = true
|
2015-02-25 23:54:59 -08:00
|
|
|
if len(bufs) > 0 {
|
|
|
|
p.source.QueueBuffers(bufs)
|
|
|
|
}
|
|
|
|
p.mu.Unlock()
|
2015-02-10 14:12:41 +01:00
|
|
|
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 {
|
2015-02-25 23:54:59 -08:00
|
|
|
return 0
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
// 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)
|
2015-02-25 23:54:59 -08:00
|
|
|
return byteOffsetToDur(p.t, p.sizeBytes)
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Destroy 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) Destroy() {
|
|
|
|
if p == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if p.source != 0 {
|
|
|
|
al.DeleteSources(p.source)
|
|
|
|
}
|
2015-02-25 23:54:59 -08:00
|
|
|
p.mu.Lock()
|
2015-02-10 14:12:41 +01:00
|
|
|
if len(p.bufs) > 0 {
|
|
|
|
al.DeleteBuffers(p.bufs)
|
|
|
|
}
|
2015-02-25 23:54:59 -08:00
|
|
|
p.mu.Unlock()
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func byteOffsetToDur(t *track, offset int64) time.Duration {
|
2015-03-02 15:22:29 +11:00
|
|
|
return time.Duration(offset * formatBytes[t.format] * int64(time.Second) / t.samplesPerSecond)
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func durToByteOffset(t *track, dur time.Duration) int64 {
|
2015-03-02 15:22:29 +11:00
|
|
|
return int64(dur) * t.samplesPerSecond / (formatBytes[t.format] * int64(time.Second))
|
2015-02-10 14:12:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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): Destroy context, close the device.
|