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

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

コード

Portaudio 側処理

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

recorder/pcm.go
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 側処理

recorder/wav.go
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

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

main.go
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 チャンネルまでしか対応していなかった

参考


/以上