仕事ではめっきり Ruby, TypeScript ばかりなので Go をちゃんと触らないとなと考えていた。そこで、2022 年度の年末年始はオーディオ関連の操作をしてみようと思い立った。 作ってみたいと思っていたソフトウェアの処理のうち、まずはマイクから音声を拾うところから触れてみようということで、今回は PortAudio を Go でラップしたパッケージを使って数秒間隔で音声の記録を試してみた。
パッケージの導入
bash1go get github.com/gordonklaus/portaudio 2
コード
雑に書いた PCMRecorder 構造体とその関数たち
recorder/recorder.go
Start()
ではOpenDefaultStream()
によって初期設定になっているマイクから音声を拾うストリームを開き、後のループで Stream が受け取ったデータを読み込む。- 読み込まれたデータは
PCMRecorder.Input
に一時的に格納される。 - Stream の開始時間と今の時刻から経過時間を算出して、
PCMRecorder.Interval
で渡された秒数が経過したかチェックしている。 - 上記間隔で新規ファイルにバッファの内容を書き出す
go1package recorder 2 3import ( 4 "fmt" 5 "log" 6 "os" 7 "time" 8 9 "github.com/gordonklaus/portaudio" 10) 11 12type PCMRecorder struct { 13 file *os.File 14 FilePath string 15 Interval int 16 Input []int16 17 Data []int16 18 stream *portaudio.Stream 19} 20 21func NewPCMRecorder(filePath string, interval int) *PCMRecorder { 22 var pr = &PCMRecorder{ 23 FilePath: filePath, 24 Interval: interval, 25 } 26 return pr 27} 28 29func (pr PCMRecorder) Start(sig chan os.Signal) error { 30 portaudio.Initialize() 31 defer portaudio.Terminate() 32 33 pr.Input = make([]int16, 64) 34 stream, err := portaudio.OpenDefaultStream(1, 0, 44100, len(pr.Input), pr.Input) 35 if err != nil { 36 log.Fatalf("Could not open default stream \n %v", err) 37 } 38 pr.stream = stream 39 pr.stream.Start() 40 defer pr.stream.Close() 41 42 startTime := pr.stream.Time() 43 44loop: 45 for { 46 elapseTime := (pr.stream.Time() - startTime).Round(time.Second) 47 48 if err := pr.stream.Read(); err != nil { 49 fmt.Println(err) 50 log.Fatalf("Could not read stream\n%v", err) 51 } 52 53 // Turn the volume up 54 pr.Data = append(pr.Data, changeVolume(pr.Input, 10)...) 55 56 select { 57 case <-sig: 58 break loop 59 default: 60 } 61 62 // Create a new file to record audio per PCMRecorder.Interval seconds. 63 if int(elapseTime.Seconds())%pr.Interval == 0 { 64 outputFileName := fmt.Sprintf(pr.FilePath+"_%d", int(elapseTime.Seconds())) 65 if !exists(outputFileName) { 66 pr.file, err = os.Create(outputFileName) 67 if err != nil { 68 log.Fatalf("Could not create a new file to write \n %v", err) 69 } 70 defer func() { 71 if err := pr.file.Close(); err != nil { 72 log.Fatalf("Could not close output file \n %v", err) 73 } 74 }() 75 76 fmt.Println("A new LPCM file was created", outputFileName, elapseTime) 77 if err := binary.Write(pr.file, binary.BigEndian, pr.Data); err != nil { 78 log.Fatalf("Could not write data\n %v", err) 79 } 80 fmt.Printf("File is written successfully. length: %d\n", len(pr.Data)) 81 82 pr.Data = nil 83 fmt.Println("tmp buffer initialized.") 84 } 85 } 86 } 87 88 return nil 89} 90 91func changeVolume(input []int16, vol float32) (output []int16) { 92 output = make([]int16, len(input)) 93 94 for i := 0; i < len(output); i++ { 95 output[i] = int16(float32(input[i]) * vol) 96 } 97 98 return output 99} 100 101func exists(fileName string) bool { 102 _, err := os.Stat(fileName) 103 return err == nil 104} 105
main.go
go1package main 2 3import ( 4 "fmt" 5 "os" 6 "os/signal" 7 "time" 8 9 "github.com/killinsun/go-portaudio-study/recorder" 10) 11 12func main() { 13 fmt.Println("Streaming. Press Ctrl + C to stop.") 14 15 sig := make(chan os.Signal, 1) 16 signal.Notify(sig, os.Interrupt, os.Kill) 17 18 outDir := time.Now().Format("audio_20060102_T150405") 19 if err := os.MkdirAll(outDir, 0755); err != nil { 20 panic("Could not create a new directory") 21 } 22 23 pr := recorder.NewPCMRecorder(fmt.Sprintf(outDir+"/file"), 5) 24 pr.Start(sig) 25} 26
補足
経過時間について
elapseTime
の部分は、元々 サンプリングレートから割り出そうと思ったんだけど PortAudio 側で時間に関するインターフェースがあったのでそっちを使った。
ファイルの区切り方について(無音制御など)
たとえばこれを Google の Speech-to-text のような API に投げる場合を想定したとき、3〜5秒間隔で区切ってストリームに流すことが考えられるが、発話の途中で区切られてしまうと文字起こしの精度が悪くなると想像している。
その場合、pr.Input
で一時的に読み取ったバッファーの値がすべて特定の閾値*内であり、それが連続して受け取るようなら「話していない」と判断して pr.Data
へ格納するのをスキップするのが考えられる。
やってみて
オーディオインターフェース周りの処理は複雑そうで難しいというイメージだったが、PortAudio がうまく差異を吸収してくれるおかげで、プログラムを書く際は読み取った PCM のデジタルデータをどのように書き出すかを意識さえすれば良くなって非常に楽だった。
ただしサンプリングレートやチャンネルなど映像・音声で出てくる用語に馴染みがない、もしくはあっても私のように知識が曖昧な状態だと、ストリームの値を読み取ってもどのように処理すべきか最初は手こずる。
一旦ブログ用に LPCM 形式のデータを書き出すところだけかいつまんだが、実際は .wav 形式にして保存している。それは go-wav パッケージを使っているのだが、調べてもめぼしい記事がなかったので、別途記事に残す予定。
参考
/以上
よかったらシェアしてください!