お首が長いのよお首が長いのよ

チラシの裏よりお届けするソフトウェアエンジニアとして成長したい人のためのブログ

2023-01-07

Portaudio で録音した音声データを WAV 形式で保存する

前回の記事(Portaudio を Go 言語で触ってみた)では Portaudio を使って LPCM 形式のデータをヘッダ無しのまま保存するコードを書いたが、今度はサンプリングレート、チャンネルなどのヘッダ情報を付加して書き込んでみる。

コード

Portaudio 側処理

前回の記事(Portaudio を Go 言語で触ってみた)分から変えたのは主に 75 行目付近。前回直接ファイルに書き込んでいたが、go-wav パッケージの NewWriterio.Writer を投げられるので、今回はそのままファイルオブジェクト(のポインタ)を渡すことにした。

go: title=recorder/pcm.go
1package 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		if int(elapseTime.Seconds())%pr.Interval == 0 {
63			outputFileName := fmt.Sprintf(pr.FilePath+"_%d.wav", int(elapseTime.Seconds()))
64			if !exists(outputFileName) {
65				pr.file, err = os.Create(outputFileName)
66				if err != nil {
67					log.Fatalf("Could not create a new file to write \n %v", err)
68				}
69				defer func() {
70					if err := pr.file.Close(); err != nil {
71						log.Fatalf("Could not close output file \n %v", err)
72					}
73				}()
74
75				wav := NewWAVEncoder(pr.file, pr.Data)
76				wav.Encode()
77				pr.Data = nil
78			}
79		}
80	}
81
82	return nil
83}
84
85func changeVolume(input []int16, vol float32) (output []int16) {
86	output = make([]int16, len(input))
87
88	for i := 0; i < len(output); i++ {
89		output[i] = int16(float32(input[i]) * vol)
90	}
91
92	return output
93}
94
95func exists(fileName string) bool {
96	_, err := os.Stat(fileName)
97	return err == nil
98}
99

go-wav 側処理

go: title=recorder/wav.go
1
2package recorder
3
4import (
5	"fmt"
6	"log"
7	"os"
8
9	"github.com/youpy/go-wav"
10)
11
12type WAVEncoder struct {
13	writer     *wav.Writer
14	numSamples uint32
15	buf        []int16
16}
17
18func NewWAVEncoder(file *os.File, buf []int16) *WAVEncoder {
19	w := &WAVEncoder{
20		numSamples: uint32(len(buf)),
21		buf:        buf,
22	}
23
24	w.writer = wav.NewWriter(file, w.numSamples, 1, 44100, 16)
25	return w
26}
27
28func (w *WAVEncoder) Encode() {
29	samples := make([]wav.Sample, w.numSamples)
30	for i := 0; i < int(w.numSamples); i++ {
31		samples[i].Values[0] = int(w.buf[i])
32	}
33
34	if err := w.writer.WriteSamples(samples); err != nil {
35		fmt.Println(samples)
36		log.Fatalf("Could not write samples \n %v", err)
37	}
38}
39

Sample 形式への変換

Encode() の最初に、 []int16 から Sample型に書き換える処理をおこなっている。for ブロック内で samples[i].Volumes[0]Volumes[]はチャンネルを示しており、もし wav オブジェクトを初期化するときにチャンネル数が 2 つのステレオ形式で記録したいのであれば、 Volumes[0]に加えVolumes[1]にも値を代入すればよい。

WriteSamples を用いて書き込み

33 行目のWriteSamples()io.Writer の実装に対して書き込み処理を行う。今回は os.File を投げたので ファイルに書き込まれる。

main

特にこのへんは大きく変わるところはない。

go: title=main.go
1package main
2
3import (
4	"fmt"
5	"os"
6	"os/signal"
7	"time"
8
9	"github.com/killinsun/go-wav-sample/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
27

やってみて

go-wav パッケージのサンプルは wav形式のファイルを読み込む例しかなかったので、書き込み時の処理はどうすべきか、Sample型の構造を把握するのに時間がかかった。

ただ使ってみるととてもシンプルで使いやすいパッケージなので、2 チャンネルステレオ構造*の形式をいじる機会があれば活用していきたい。

※ この記事の投稿時点でこのパッケージでは 2 チャンネルまでしか対応していなかった

参考

/以上

よかったらシェアしてください!