Portaudio で録音した音声データを WAV 形式で保存する
前回の記事(Portaudio を Go 言語で触ってみた)では Portaudio を使って LPCM 形式のデータをヘッダ無しのまま保存するコードを書いたが、今度はサンプリングレート、チャンネルなどのヘッダ情報を付加して書き込んでみる。
コード
Portaudio 側処理
前回の記事(Portaudio を Go 言語で触ってみた)分から変えたのは主に 75 行目付近。前回直接ファイルに書き込んでいたが、go-wav
パッケージの NewWriter
に io.Writer
を投げられるので、今回はそのままファイルオブジェクト(のポインタ)を渡すことにした。
package recorder
import (
"fmt"
"log"
"os"
"time"
"github.com/gordonklaus/portaudio"
)
type PCMRecorder struct {
file *os.File
FilePath string
Interval int
Input []int16
Data []int16
stream *portaudio.Stream
}
func NewPCMRecorder(filePath string, interval int) *PCMRecorder {
var pr = &PCMRecorder{
FilePath: filePath,
Interval: interval,
}
return pr
}
func (pr PCMRecorder) Start(sig chan os.Signal) error {
portaudio.Initialize()
defer portaudio.Terminate()
pr.Input = make([]int16, 64)
stream, err := portaudio.OpenDefaultStream(1, 0, 44100, len(pr.Input), pr.Input)
if err != nil {
log.Fatalf("Could not open default stream \n %v", err)
}
pr.stream = stream
pr.stream.Start()
defer pr.stream.Close()
startTime := pr.stream.Time()
loop:
for {
elapseTime := (pr.stream.Time() - startTime).Round(time.Second)
if err := pr.stream.Read(); err != nil {
fmt.Println(err)
log.Fatalf("Could not read stream\n%v", err)
}
// Turn the volume up
pr.Data = append(pr.Data, changeVolume(pr.Input, 10)...)
select {
case <-sig:
break loop
default:
}
if int(elapseTime.Seconds())%pr.Interval == 0 {
outputFileName := fmt.Sprintf(pr.FilePath+"_%d.wav", int(elapseTime.Seconds()))
if !exists(outputFileName) {
pr.file, err = os.Create(outputFileName)
if err != nil {
log.Fatalf("Could not create a new file to write \n %v", err)
}
defer func() {
if err := pr.file.Close(); err != nil {
log.Fatalf("Could not close output file \n %v", err)
}
}()
wav := NewWAVEncoder(pr.file, pr.Data)
wav.Encode()
pr.Data = nil
}
}
}
return nil
}
func changeVolume(input []int16, vol float32) (output []int16) {
output = make([]int16, len(input))
for i := 0; i < len(output); i++ {
output[i] = int16(float32(input[i]) * vol)
}
return output
}
func exists(fileName string) bool {
_, err := os.Stat(fileName)
return err == nil
}
go-wav 側処理
package recorder
import (
"fmt"
"log"
"os"
"github.com/youpy/go-wav"
)
type WAVEncoder struct {
writer *wav.Writer
numSamples uint32
buf []int16
}
func NewWAVEncoder(file *os.File, buf []int16) *WAVEncoder {
w := &WAVEncoder{
numSamples: uint32(len(buf)),
buf: buf,
}
w.writer = wav.NewWriter(file, w.numSamples, 1, 44100, 16)
return w
}
func (w *WAVEncoder) Encode() {
samples := make([]wav.Sample, w.numSamples)
for i := 0; i < int(w.numSamples); i++ {
samples[i].Values[0] = int(w.buf[i])
}
if err := w.writer.WriteSamples(samples); err != nil {
fmt.Println(samples)
log.Fatalf("Could not write samples \n %v", err)
}
}
Sample 形式への変換
Encode()
の最初に、 []int16
から Sample
型に書き換える処理をおこなっている。for ブロック内で samples[i].Volumes[0]
の Volumes[]
はチャンネルを示しており、もし wav
オブジェクトを初期化するときにチャンネル数が 2 つのステレオ形式で記録したいのであれば、 Volumes[0]
に加えVolumes[1]
にも値を代入すればよい。
WriteSamples を用いて書き込み
33 行目のWriteSamples()
で io.Writer
の実装に対して書き込み処理を行う。今回は os.File
を投げたので ファイルに書き込まれる。
main
特にこのへんは大きく変わるところはない。
package main
import (
"fmt"
"os"
"os/signal"
"time"
"github.com/killinsun/go-wav-sample/recorder"
)
func main() {
fmt.Println("Streaming. Press Ctrl + C to stop.")
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, os.Kill)
outDir := time.Now().Format("audio_20060102_T150405")
if err := os.MkdirAll(outDir, 0755); err != nil {
panic("Could not create a new directory")
}
pr := recorder.NewPCMRecorder(fmt.Sprintf(outDir+"/file"), 5)
pr.Start(sig)
}
やってみて
go-wav
パッケージのサンプルは wav
形式のファイルを読み込む例しかなかったので、書き込み時の処理はどうすべきか、Sample
型の構造を把握するのに時間がかかった。
ただ使ってみるととてもシンプルで使いやすいパッケージなので、2 チャンネルステレオ構造*の形式をいじる機会があれば活用していきたい。
※ この記事の投稿時点でこのパッケージでは 2 チャンネルまでしか対応していなかった
参考
/以上