Portaudio を Go 言語で触ってみた
仕事ではめっきり Ruby, TypeScript ばかりなので Go をちゃんと触らないとなと考えていた。そこで、2022 年度の年末年始はオーディオ関連の操作をしてみようと思い立った。 作ってみたいと思っていたソフトウェアの処理のうち、まずはマイクから音声を拾うところから触れてみようということで、今回は PortAudio を Go でラップしたパッケージを使って数秒間隔で音声の記録を試してみた。
パッケージの導入
go get github.com/gordonklaus/portaudio
コード
雑に書いた PCMRecorder 構造体とその関数たち
recorder/recorder.go
Start()
ではOpenDefaultStream()
によって初期設定になっているマイクから音声を拾うストリームを開き、後のループで Stream が受け取ったデータを読み込む。- 読み込まれたデータは
PCMRecorder.Input
に一時的に格納される。 - Stream の開始時間と今の時刻から経過時間を算出して、
PCMRecorder.Interval
で渡された秒数が経過したかチェックしている。 - 上記間隔で新規ファイルにバッファの内容を書き出す
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:
}
// Create a new file to record audio per PCMRecorder.Interval seconds.
if int(elapseTime.Seconds())%pr.Interval == 0 {
outputFileName := fmt.Sprintf(pr.FilePath+"_%d", 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)
}
}()
fmt.Println("A new LPCM file was created", outputFileName, elapseTime)
if err := binary.Write(pr.file, binary.BigEndian, pr.Data); err != nil {
log.Fatalf("Could not write data\n %v", err)
}
fmt.Printf("File is written successfully. length: %d\n", len(pr.Data))
pr.Data = nil
fmt.Println("tmp buffer initialized.")
}
}
}
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
}
main.go
package main
import (
"fmt"
"os"
"os/signal"
"time"
"github.com/killinsun/go-portaudio-study/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)
}
補足
経過時間について
elapseTime
の部分は、元々 サンプリングレートから割り出そうと思ったんだけど PortAudio 側で時間に関するインターフェースがあったのでそっちを使った。
ファイルの区切り方について(無音制御など)
たとえばこれを Google の Speech-to-text のような API に投げる場合を想定したとき、3〜5秒間隔で区切ってストリームに流すことが考えられるが、発話の途中で区切られてしまうと文字起こしの精度が悪くなると想像している。
その場合、pr.Input
で一時的に読み取ったバッファーの値がすべて特定の閾値*内であり、それが連続して受け取るようなら「話していない」と判断して pr.Data
へ格納するのをスキップするのが考えられる。
やってみて
オーディオインターフェース周りの処理は複雑そうで難しいというイメージだったが、PortAudio がうまく差異を吸収してくれるおかげで、プログラムを書く際は読み取った PCM のデジタルデータをどのように書き出すかを意識さえすれば良くなって非常に楽だった。
ただしサンプリングレートやチャンネルなど映像・音声で出てくる用語に馴染みがない、もしくはあっても私のように知識が曖昧な状態だと、ストリームの値を読み取ってもどのように処理すべきか最初は手こずる。
一旦ブログ用に LPCM 形式のデータを書き出すところだけかいつまんだが、実際は .wav 形式にして保存している。それは go-wav パッケージを使っているのだが、調べてもめぼしい記事がなかったので、別途記事に残す予定。
参考
/以上