Add audio/wav package

This commit is contained in:
Hajime Hoshi 2016-03-27 19:10:16 +09:00
parent f8e984a62a
commit 27911b8156
4 changed files with 169 additions and 19 deletions

Binary file not shown.

View File

@ -1,8 +1,19 @@
# License # License
## jab.wav
```
http://soundbible.com/995-Jab.html
Recorded by Mike Koenig
Attribution 3.0: https://creativecommons.org/licenses/by/3.0/
```
## ragtime.ogg ## ragtime.ogg
``` ```
https://soundcloud.com/jacaranda-trilhas-sonoras/james-scott-01-frog-legs-rag
Title: Frog Legs Rag (1906, piano roll) Title: Frog Legs Rag (1906, piano roll)
Artist: James Scott Artist: James Scott
Album: Frog Legs: Ragtime Era Favorites Album: Frog Legs: Ragtime Era Favorites

View File

@ -15,8 +15,10 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"image/color" "image/color"
"io"
"log" "log"
"time" "time"
@ -24,6 +26,7 @@ import (
"github.com/hajimehoshi/ebiten/ebitenutil" "github.com/hajimehoshi/ebiten/ebitenutil"
"github.com/hajimehoshi/ebiten/exp/audio" "github.com/hajimehoshi/ebiten/exp/audio"
"github.com/hajimehoshi/ebiten/exp/audio/vorbis" "github.com/hajimehoshi/ebiten/exp/audio/vorbis"
"github.com/hajimehoshi/ebiten/exp/audio/wav"
) )
const ( const (
@ -58,8 +61,10 @@ type Player struct {
var ( var (
audioContext *audio.Context audioContext *audio.Context
player *Player musicPlayer *Player
playerCh = make(chan *Player) seStream *wav.Stream
musicCh = make(chan *Player)
seCh = make(chan *wav.Stream)
mouseButtonState = map[ebiten.MouseButton]int{} mouseButtonState = map[ebiten.MouseButton]int{}
keyState = map[ebiten.Key]int{} keyState = map[ebiten.Key]int{}
) )
@ -71,6 +76,33 @@ func playerBarRect() (x, y, w, h int) {
return return
} }
func (p *Player) updateSE() error {
if seStream == nil {
return nil
}
if !ebiten.IsKeyPressed(ebiten.KeyP) {
keyState[ebiten.KeyP] = 0
return nil
}
keyState[ebiten.KeyP]++
if keyState[ebiten.KeyP] != 1 {
return nil
}
// Clone the stream
b := &bytes.Buffer{}
if _, err := seStream.Seek(0, 0); err != nil {
return err
}
if _, err := io.Copy(b, seStream); err != nil {
return err
}
sePlayer, err := audioContext.NewPlayer(bytes.NewReader(b.Bytes()))
if err != nil {
return err
}
return sePlayer.Play()
}
func (p *Player) updatePlayPause() error { func (p *Player) updatePlayPause() error {
if p.audioPlayer == nil { if p.audioPlayer == nil {
return nil return nil
@ -115,17 +147,26 @@ func (p *Player) updateBar() error {
func update(screen *ebiten.Image) error { func update(screen *ebiten.Image) error {
audioContext.Update() audioContext.Update()
if player == nil { if musicPlayer == nil {
select { select {
case player = <-playerCh: case musicPlayer = <-musicCh:
default: default:
} }
} }
if player != nil { if seStream == nil {
if err := player.updateBar(); err != nil { select {
case seStream = <-seCh:
default:
}
}
if musicPlayer != nil {
if err := musicPlayer.updateBar(); err != nil {
return err return err
} }
if err := player.updatePlayPause(); err != nil { if err := musicPlayer.updatePlayPause(); err != nil {
return err
}
if err := musicPlayer.updateSE(); err != nil {
return err return err
} }
} }
@ -135,8 +176,8 @@ func update(screen *ebiten.Image) error {
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
screen.DrawImage(playerBarImage, op) screen.DrawImage(playerBarImage, op)
currentTimeStr := "00:00" currentTimeStr := "00:00"
if player != nil { if musicPlayer != nil {
c := player.audioPlayer.Current() c := musicPlayer.audioPlayer.Current()
// Current Time // Current Time
m := (c / time.Minute) % 100 m := (c / time.Minute) % 100
@ -145,7 +186,7 @@ func update(screen *ebiten.Image) error {
// Bar // Bar
cw, ch := playerCurrentImage.Size() cw, ch := playerCurrentImage.Size()
cx := int(time.Duration(w)*c/player.total) + x - cw/2 cx := int(time.Duration(w)*c/musicPlayer.total) + x - cw/2
cy := y - (ch-h)/2 cy := y - (ch-h)/2
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(cx), float64(cy)) op.GeoM.Translate(float64(cx), float64(cy))
@ -154,8 +195,9 @@ func update(screen *ebiten.Image) error {
msg := fmt.Sprintf(`FPS: %0.2f msg := fmt.Sprintf(`FPS: %0.2f
Press S to toggle Play/Pause Press S to toggle Play/Pause
Press P to play SE
%s`, ebiten.CurrentFPS(), currentTimeStr) %s`, ebiten.CurrentFPS(), currentTimeStr)
if player == nil { if musicPlayer == nil {
msg += "\nNow Loading..." msg += "\nNow Loading..."
} }
ebitenutil.DebugPrint(screen, msg) ebitenutil.DebugPrint(screen, msg)
@ -163,16 +205,26 @@ Press S to toggle Play/Pause
} }
func main() { func main() {
// Use a FLAC file so far: I couldn't find any good OGG/Vorbis decoder in pure Go. wavF, err := ebitenutil.OpenFile("_resources/audio/jab.wav")
f, err := ebitenutil.OpenFile("_resources/audio/ragtime.ogg") if err != nil {
log.Fatal(err)
}
oggF, err := ebitenutil.OpenFile("_resources/audio/ragtime.ogg")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// TODO: sampleRate should be obtained from the ogg file.
audioContext = audio.NewContext(22050) audioContext = audio.NewContext(22050)
// TODO: This doesn't work synchronously on browsers because of decoding. Fix this.
go func() { go func() {
s, err := vorbis.Decode(audioContext, f) s, err := wav.Decode(audioContext, wavF)
if err != nil {
log.Fatal(err)
return
}
seCh <- s
close(seCh)
}()
go func() {
s, err := vorbis.Decode(audioContext, oggF)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
return return
@ -182,11 +234,12 @@ func main() {
log.Fatal(err) log.Fatal(err)
return return
} }
playerCh <- &Player{ musicCh <- &Player{
audioPlayer: p, audioPlayer: p,
total: s.Len(), total: s.Len(),
} }
close(playerCh) close(musicCh)
// TODO: Is this goroutine-safe?
p.Play() p.Play()
}() }()
if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Audio (Ebiten Demo)"); err != nil { if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Audio (Ebiten Demo)"); err != nil {

86
exp/audio/wav/decode.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright 2016 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wav
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"time"
"github.com/hajimehoshi/ebiten/exp/audio"
)
const (
headerSize = 44
riffHeader = "RIFF"
waveHeader = "WAVE"
)
type Stream struct {
buf *bytes.Reader
sampleRate int
}
func (s *Stream) Read(p []byte) (int, error) {
return s.buf.Read(p)
}
func (s *Stream) Seek(offset int64, whence int) (int64, error) {
return s.buf.Seek(offset, whence)
}
func (s *Stream) Len() time.Duration {
const bytesPerSample = 4
return time.Duration(s.buf.Len()/bytesPerSample) * time.Second / time.Duration(s.sampleRate)
}
func Decode(context *audio.Context, src io.Reader) (*Stream, error) {
buf := make([]byte, headerSize)
n, err := io.ReadFull(src, buf)
if n != headerSize {
return nil, fmt.Errorf("wav: invalid header")
}
if err != nil {
return nil, err
}
if !bytes.Equal(buf[0:4], []byte(riffHeader)) {
return nil, fmt.Errorf("wav: invalid header: RIFF not found")
}
if !bytes.Equal(buf[8:12], []byte(waveHeader)) {
return nil, fmt.Errorf("wav: invalid header: WAVE not found")
}
channels, depth := buf[22], buf[34]
if channels != 2 {
return nil, fmt.Errorf("wav: invalid header: channel num must be 2")
}
if depth != 16 {
return nil, fmt.Errorf("wav: invalid header: depth must be 16")
}
sampleRate := int(buf[24]) | int(buf[25])<<8 | int(buf[26])<<16 | int(buf[27]<<24)
if context.SampleRate() != sampleRate {
return nil, fmt.Errorf("wav: sample rate must be %d but %d", context.SampleRate(), sampleRate)
}
b, err := ioutil.ReadAll(src)
if err != nil {
return nil, err
}
s := &Stream{
buf: bytes.NewReader(b),
sampleRate: sampleRate,
}
return s, nil
}