exp/audio: remove the high-level player
Since nothing about this high-level player will stay the same after the audio work core types are finalized, there is no good point in keeping the naive implementation around. Removing also the audio example. Updates golang/go#9551. Change-Id: I5a7666c77e043aeacf44356e20e8d90822fd78e7 Reviewed-on: https://go-review.googlesource.com/27671 Reviewed-by: David Crawshaw <crawshaw@golang.org> Run-TryBot: Jaana Burcu Dogan <jbd@google.com>
This commit is contained in:
parent
ed036a869f
commit
7573efae75
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
|
@ -1,206 +0,0 @@
|
|||
// 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
|
||||
|
||||
// An app that makes a sound as the gopher hits the walls of the screen.
|
||||
//
|
||||
// Note: This demo is an early preview of Go 1.5. In order to build this
|
||||
// program as an Android APK using the gomobile tool.
|
||||
//
|
||||
// See http://godoc.org/golang.org/x/mobile/cmd/gomobile to install gomobile.
|
||||
//
|
||||
// Get the audio example and use gomobile to build or install it on your device.
|
||||
//
|
||||
// $ go get -d golang.org/x/mobile/example/audio
|
||||
// $ gomobile build golang.org/x/mobile/example/audio # will build an APK
|
||||
//
|
||||
// # plug your Android device to your computer or start an Android emulator.
|
||||
// # if you have adb installed on your machine, use gomobile install to
|
||||
// # build and deploy the APK to an Android target.
|
||||
// $ gomobile install golang.org/x/mobile/example/audio
|
||||
//
|
||||
// Additionally, you can run the sample on your desktop environment
|
||||
// by using the go tool.
|
||||
//
|
||||
// $ go install golang.org/x/mobile/example/audio && audio
|
||||
//
|
||||
// On Linux, you need to install OpenAL developer library by
|
||||
// running the command below.
|
||||
//
|
||||
// $ apt-get install libopenal-dev
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "image/jpeg"
|
||||
|
||||
"golang.org/x/mobile/app"
|
||||
"golang.org/x/mobile/asset"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/exp/audio"
|
||||
"golang.org/x/mobile/exp/f32"
|
||||
"golang.org/x/mobile/exp/gl/glutil"
|
||||
"golang.org/x/mobile/exp/sprite"
|
||||
"golang.org/x/mobile/exp/sprite/clock"
|
||||
"golang.org/x/mobile/exp/sprite/glsprite"
|
||||
"golang.org/x/mobile/gl"
|
||||
)
|
||||
|
||||
const (
|
||||
width = 72
|
||||
height = 60
|
||||
)
|
||||
|
||||
var (
|
||||
startTime = time.Now()
|
||||
|
||||
images *glutil.Images
|
||||
eng sprite.Engine
|
||||
scene *sprite.Node
|
||||
|
||||
player *audio.Player
|
||||
|
||||
sz size.Event
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Main(func(a app.App) {
|
||||
var glctx gl.Context
|
||||
for e := range a.Events() {
|
||||
switch e := a.Filter(e).(type) {
|
||||
case lifecycle.Event:
|
||||
switch e.Crosses(lifecycle.StageVisible) {
|
||||
case lifecycle.CrossOn:
|
||||
glctx, _ = e.DrawContext.(gl.Context)
|
||||
onStart(glctx)
|
||||
a.Send(paint.Event{})
|
||||
case lifecycle.CrossOff:
|
||||
onStop()
|
||||
glctx = nil
|
||||
}
|
||||
case size.Event:
|
||||
sz = e
|
||||
case paint.Event:
|
||||
if glctx == nil || e.External {
|
||||
continue
|
||||
}
|
||||
onPaint(glctx)
|
||||
a.Publish()
|
||||
a.Send(paint.Event{}) // keep animating
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func onStart(glctx gl.Context) {
|
||||
images = glutil.NewImages(glctx)
|
||||
eng = glsprite.Engine(images)
|
||||
loadScene()
|
||||
|
||||
rc, err := asset.Open("boing.wav")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
player, err = audio.NewPlayer(rc, 0, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func onStop() {
|
||||
eng.Release()
|
||||
images.Release()
|
||||
player.Close()
|
||||
}
|
||||
|
||||
func onPaint(glctx gl.Context) {
|
||||
glctx.ClearColor(1, 1, 1, 1)
|
||||
glctx.Clear(gl.COLOR_BUFFER_BIT)
|
||||
now := clock.Time(time.Since(startTime) * 60 / time.Second)
|
||||
eng.Render(scene, now, sz)
|
||||
}
|
||||
|
||||
func newNode() *sprite.Node {
|
||||
n := &sprite.Node{}
|
||||
eng.Register(n)
|
||||
scene.AppendChild(n)
|
||||
return n
|
||||
}
|
||||
|
||||
func loadScene() {
|
||||
gopher := loadGopher()
|
||||
scene = &sprite.Node{}
|
||||
eng.Register(scene)
|
||||
eng.SetTransform(scene, f32.Affine{
|
||||
{1, 0, 0},
|
||||
{0, 1, 0},
|
||||
})
|
||||
|
||||
var x, y float32
|
||||
dx, dy := float32(1), float32(1)
|
||||
|
||||
n := newNode()
|
||||
// TODO: Shouldn't arranger pass the size.Event?
|
||||
n.Arranger = arrangerFunc(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
|
||||
eng.SetSubTex(n, gopher)
|
||||
|
||||
if x < 0 {
|
||||
dx = 1
|
||||
boing()
|
||||
}
|
||||
if y < 0 {
|
||||
dy = 1
|
||||
boing()
|
||||
}
|
||||
if x+width > float32(sz.WidthPt) {
|
||||
dx = -1
|
||||
boing()
|
||||
}
|
||||
if y+height > float32(sz.HeightPt) {
|
||||
dy = -1
|
||||
boing()
|
||||
}
|
||||
|
||||
x += dx
|
||||
y += dy
|
||||
|
||||
eng.SetTransform(n, f32.Affine{
|
||||
{width, 0, x},
|
||||
{0, height, y},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func boing() {
|
||||
player.Seek(0)
|
||||
player.Play()
|
||||
}
|
||||
|
||||
func loadGopher() sprite.SubTex {
|
||||
a, err := asset.Open("gopher.jpeg")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer a.Close()
|
||||
|
||||
img, _, err := image.Decode(a)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
t, err := eng.LoadTexture(img)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return sprite.SubTex{t, image.Rect(0, 0, 360, 300)}
|
||||
}
|
||||
|
||||
type arrangerFunc func(e sprite.Engine, n *sprite.Node, t clock.Time)
|
||||
|
||||
func (a arrangerFunc) Arrange(e sprite.Engine, n *sprite.Node, t clock.Time) { a(e, n, t) }
|
|
@ -1,10 +0,0 @@
|
|||
// 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 main
|
||||
|
||||
func main() {
|
||||
}
|
|
@ -1,388 +0,0 @@
|
|||
// 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/exp/audio"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mobile/exp/audio/al"
|
||||
)
|
||||
|
||||
// ReadSeekCloser is an io.ReadSeeker and io.Closer.
|
||||
type ReadSeekCloser interface {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Format represents a PCM data format.
|
||||
type Format int
|
||||
|
||||
const (
|
||||
Mono8 Format = iota + 1
|
||||
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{
|
||||
0: "unknown",
|
||||
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.
|
||||
//
|
||||
// The audio package is only designed for small audio sources.
|
||||
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.
|
|
@ -1,40 +0,0 @@
|
|||
// 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,!android
|
||||
|
||||
package audio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNoOp(t *testing.T) {
|
||||
var p *Player
|
||||
if err := p.Play(); err != nil {
|
||||
t.Errorf("no-op player failed to play: %v", err)
|
||||
}
|
||||
if err := p.Pause(); err != nil {
|
||||
t.Errorf("no-op player failed to pause: %v", err)
|
||||
}
|
||||
if err := p.Stop(); err != nil {
|
||||
t.Errorf("no-op player failed to stop: %v", err)
|
||||
}
|
||||
if c := p.Current(); c != 0 {
|
||||
t.Errorf("no-op player returns a non-zero playback position: %v", c)
|
||||
}
|
||||
if tot := p.Total(); tot != 0 {
|
||||
t.Errorf("no-op player returns a non-zero total: %v", tot)
|
||||
}
|
||||
if vol := p.Volume(); vol != 0 {
|
||||
t.Errorf("no-op player returns a non-zero volume: %v", vol)
|
||||
}
|
||||
if s := p.State(); s != Unknown {
|
||||
t.Errorf("playing state: %v", s)
|
||||
}
|
||||
p.SetVolume(0.1)
|
||||
p.Seek(1 * time.Second)
|
||||
p.Close()
|
||||
}
|
Loading…
Reference in New Issue