🎤

なんか録れた音..ヘンです! raw PCM 形式で録音する際の失敗とその対策

に公開

はじめに

こんにちは。Fairy Devices の hamamoto です。

録音を行うプログラム書いて動かしていると「なんか変な音が録れた!?」ということはありませんか。

当社で扱っている音声フォーマットの一つに raw PCM という形式があります。この形式は一般的な音声フォーマットではあるのですが、音声データの取り扱いに不慣れな方にとっては、ちょっとしたミスで思わぬデータ破損を起こしてしまうことがよくあります。

この記事では、過去に筆者が経験した「ありがちなミス」や「録音プログラムを書く際の注意点」など、raw PCM フォーマットを正しく扱う上でのヒントをご紹介します。

本記事の対象読者

  • これから録音プログラムなどで PCM データを触ってみようというかた。
  • これから当社の録音デバイス・音声 API を触るかた。
  • 適当に PCM 録音して再生してみたら耳が破壊されたかた。
  • 本記事の まとめ の内容がピンとこないかた。

raw PCM(パルス符号変調) とは

PCM とは「Pulse Code Modulation(パルス符号変調)」の略で、アナログの音波をデジタルのデータに変換する基本的な技術です。

「音声波形」と聞いてまずイメージされるような下図のような波形あったとします。
PCM はこの波形を 非常に短い間隔で分割(標本化、サンプリング)し、その一つ一つの音の振幅を数値として記録(量子化)したデータです。

PCMとは

PCM には代表的なものに WAV という形式が存在します。WAV ファイルには RIFF ヘッダと呼ばれる録音時のサンプリング数量子化数チャンネル数などが格納されたデータ領域があります。
raw PCM とは WAV 形式のファイルからヘッダ部分(RIFF ヘッダなど)を取り除いた「音声データ」部分 [1] のみを指します。

raw PCM データ取り扱い時の落とし穴

音声録音をするようなプログラムでは、音声信号をバイト列として読み込むケースが多く、必然的に raw PCM にふれることになります。

こんな感じのやつです
PyAudio Documentation より。
import pyaudio
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True)

while len(data := wf.readframes(CHUNK)): # data のバイト列は raw PCMデータである
    stream.write(data)

ここからは、実際に筆者が見かけた録音時やデータ取り扱いのミスや、そうならないためのヒントをいくつかご紹介します。

RIFF ヘッダが付いている

「raw PCM はヘッダなしの音声データです!」と言っておきながらヘッダが付いたままのデータでも実は音声データとしては問題はなく、 RIFF ヘッダ部分も音声データの一部として「それっぽく」再生ができてしまいます。[2]

# サンプリングレート 48kHz 量子化ビット数16 1チャンネルで録音する
rec -r 48k -esigned -b16 -c1 recording.wav

# ヘッダ除去を怠って raw PCM データを用意してみます。もちろん RIFF ヘッダは取り除かれません。
cp recording.wav with_header.raw


# それぞれ再生して聴き比べてみます。
# 後者の raw ファイルには本来再生に必要なメタ情報が存在しないはずなので、再生フォーマットを指定しています。
play recording.wav
play -r 48k -esigned -b16 -c1 with_header.raw

冒頭に何かノイズが聞こえませんでしたか?
同様に、このヘッダが付いたままの raw PCM を Audacity で サンプリングレート 48kHz / 量子化ビット数 16(Signed 16-bit PCM) / 1 チャンネル として「Raw データをインポート[3]」してみます。

import_audacity
ヘッダ部の文字列が音声データとして処理されたため 音声の先頭部分に一瞬の雑音が現れています。

with_header

ですが、実際の録音テスト中にこのような音声データに出会ったとして、この雑音が実際に録音された音の可能性も十分有り得ます。そこで..

このように「波形を見る・音声を聞く」だけでは問題に気づくことは困難なことがあります。些細な違いですがこの雑音がもたらす後の音声処理結果・精度への影響は見過ごせません。

使用する録音ライブラリによっては、 WAV ファイルでしか出力できないものもあるため注意が必要です。

録音時のサンプリングレートの誤り

録音時や、その後のサンプリングレート変換などの誤りによって意図せぬ音声波形となってしまう場合があります。

気をつけておきたい事としては、使用しているライブラリや機材によっては録音時に指定したサンプリングレートが対応しておらず、予期せぬ値で(デフォルト値など)で録音されてしまう事が稀にあります。[4]

これに気づかず別の周波数のヘッダを付与してしまうと、音声長や音声の音程が変わるだけでなく、以降の音声処理で十分な結果や精度が得られない可能性があります。

実際に聞いてみて耳で「音程がおかしい!?」と判断できれば良いのですが、例えばよく使用されるサンプリングレートの 48kHz と 44.1kHz の違いは音程の変化も少なく判断が難しい場合があります。
48kHz録音の音声に16kHzのヘッダを付けた

(わかりやすい例) 音程がとても低くなっている。音声長も本来より 3 倍も長くなっている。

48kHz録音の音声に44.1kHzのヘッダを付けた

(わかりにくい例) 少しだけ音程が低くなっている。録音した音が人の声でない場合は問題に気が付かないかも。
ひょっとすると風邪をひいてるだけの人かも。

録音時の チャンネル数の誤り

サンプリングレート誤り同様、チャンネル数の誤りもよくあるトラブルの一つです。

事例 a-1: 「1ch で録音したつもりが 2ch (ステレオ) 録音だった」

多くの録音機器ではマイク間の距離の短さから異なるチャンネルでも似通った信号を持っている傾向があります。
複数チャンネルの音を同一チャンネルの音として扱ってしまうと音声波形が間延びしたような(本例では音声長が 2 倍)音声波形となり。これは前述の 録音時のサンプリングレートの誤り 同様の音程の変化を引き起こします。

チャンネル指定を誤った場合のスペクトログラム

事例 a-2: 「多チャンネルマイクで目的のマイクを取り違えた」

誤って収録対象から離れた位置にあるマイクで録音した場合、十分な音量が得られない可能性があります。

録音時のバイトオーダーの誤り / 量子化ビット数の誤り

録音機器や処理系によっては録音時のバイト列の並び(バイトオーダー)が異なる場合があります。
また、量子化ビット数[7]も録音機器によっては対応していないものもあります。

これらの誤りは概ね音声波形の破壊を引き起こします。迂闊な raw PCM の再生は再生機器や聴覚を痛める原因となります。👂️💥

番外編

DC オフセット

録音時にハードウェア上の電気的ノイズの影響で音声波形が中心(ゼロ)からズレた状態で取得される場合があります。これを DC オフセット(がズレた状態)と言います。
聴感上での判断は難しく、音声波形を可視化することでのみこの問題を発見することができます。

DCオフセットが0からズレている
※再現のため波形を加工しています。実際には録音開始の数秒にのみに現れるなど様々です。

この状態はオフセットがズレている方向に対して音割れ(最大値を超える)を起こしやすい状態にあります。
まずはハードウェアに問題がないか確認してみることをおすすめします。

一般に販売されている録音機器で発生するケースは稀ですが、開発中の録音ハードウェアなどを扱うようなかたは気に留めておくと良いでしょう。

(余談) Audacity を使用して DC オフセットのずれた録音済みファイルを補正する方法

録音後の対処となりますが、Audacity を使用して DC オフセットののずれを補正することができます。

  1. 音声波形を全選択、または DC オフセットを修正する範囲を選択
  2. メニューバー [エフェクト] から [音量と音圧] → [ノーマライズ] を選択
  3. [DC オフセットを消去(振幅の中央を 0.0)] をチェックし、[適用] ボタンを押下

DCオフセット補正

(Audacity バージョン 3.7.5 時点での手順)

音声が途切れる

途切れ方によっていくつかの要因が考えられます。

事例 b-1: 「音声が欠落している」

読み込みバッファサイズが小さく、録音スレッドでの読み込みに対して書き出し処理が間に合っていない可能性が考えられます。
まずは十分なサイズのバッファがあるか確認しましょう。

NG 例
極端な例
// 録音スレッドのループ処理
let mut buf = [0u8; 2]; //  読み込みバッファサイズが2byte(サンプル分)しかない
// 目安として読み込みバッファは 0.1 ~ 0.2 秒程度の音声が収まるサイズがあると安心です
// 例: 48kHz 2ch 量子化ビット数 16 (2byte) 録音の場合だと (48000 * 2ch * 2byte * 0.1sec =) 19200 byte程度

loop {
    let read_length = input.read(&mut buf)?;
    if read_length == 0 {
        break;
    }
    output.write_all(&buf[..read_length])?; // 録音スレッド内でファイル書き出しのような処理は避けたい
}

録音スレッド内で音声サンプルに関する他の操作をしている場合は、スレッドセーフなキューやリングバッファを経由し、別に用意した書き出し用スレッドに処理を移すことで改善することがあります。
基本的には、録音スレッド内ではサンプルの読み込み以上のことをさせないことが重要です。

スウィープ音の再生方法
# sox コマンドで生成した2つのスウィープ音を(上昇:300Hz~8kHz と 下降:8kHz~300Hz 各4秒間隔) パイプで連結してループ再生します
# 音割れ防止の為、音量調整を行っています(`gain -6.0`)
# 連結部分にプツッというノイズが入るのを小さくするためフェードイン・フェードアウト処理を入れています(`fade 0.01 4 0.01`)
# Ctrl+C(SIGINT)押下までリピート再生を行います (`repeat -`)

play -n synth 4 sin 300-16000 sin 16000-300 delay 0 4.0 remix 1,2 repeat -

# (Ctrl+C で再生停止)

# 音声の欠損を見つけやすくするには、更にスウィープの間隔を狭くする(短時間に周波数変化の大きいスウィープにする)ことも有効です。
# `synth 4 → synth 2` / `delay 0 4.0 → delay 0 2.0`

スウィープ音で音声の途切れを可視化する

厳密には上記方法で生成したスウィープ音は周波数の折り返し部分に無音が存在するため、その付近では音声の途切れを判断することは難しいです。
長時間の録音や、複数回録音を行うことで周波数変化の途中で音声の欠落を補足できるようにしましょう。

事例 b-2: 「無音の区間や雑音の音声区間がある」

録音スレッドが読み込んだバイト長と異なるバイト長を書き込んでいる可能性が考えられます。
read 関数が読み込みサイズを返す場合などにやりがちです。
バッファ長読み込みバイト長や未初期化の区間を書き出さないようにしましょう。

例としては以下のような read 関数の誤用です。

(例: Rust)
const READ_BUFFER_SIZE: usize = 1024;
let mut buf = [0u8; READ_BUFFER_SIZE];

// 実際に read で読み込めたバイト数を返す
let read_length = input.read(&mut buf)?;

// OK例: 読み込めた分だけ書き込む
output.write_all(&buf[..read_length])?;

// NG例: バッファサイズ分を常に書き込んでしまうと、未初期化領域(無音や以前に読み込んだ音声)が混ざることがあります
// output.write_all(&buf)?; // ←読み込みバッファの全長を書き込むようなことは避けましょう

誤った取り扱いをしていても、録音ハードウェアなどの環境によっては稀にしか顕在化しないという事も十分あり得ます。バッファを読み書きするサイズは特に注意が必要です。

サンプル数の間引きによるダウンサンプリング

ダウンサンプリングを行う必要がある場合、単純に「サンプルを間引けば良いんだな」と思いがちですが、実は注意すべきポイントがあります。

例として次のようなケースです。

  • 16kHz で録音したかったが録音機器が対応しておらず、48kHz で録音した。
  • 16k/48k == 1/3 なので 3 サンプルに 1 回だけサンプルを保存すれば 16kHz の音声として保存できるな。ヨシ。

問題ない手順のように思えますが、実はヨシではありません。

このような間引きのみによるダウンサンプリングを行った際にはエイリアシング(折り返し雑音)と呼ばれる雑音が発生します。

下図にエイリアシングが発生している例を示します。ダウンサンプリング前後の 8kHz の位置に注目してください。

エイリアシングノイズ
テスト用のサイン波が 8kHz を境に線対称に反転したような音声(折り返し雑音)が生まれてしまっている。[8] (比較の際は縦軸のスケールに注意)

折り返し雑音可視化のため、収録時に再生していたサイン波を生成するコマンド
# 5秒間かけて 8kHz から 15kHz まで連続変化するサイン波 と
# 10kHz のサイン波を合成してモノラル再生を行います
play -n -c2 -r48000 synth 5 sine 8000-15000 sine 10000 remix -

こういった場合は、折り返し雑音を取り除くため、サンプルを間引く前に特定の周波数(ナイキスト周波数[9])より高い周波数の音をカットする手順(ローパスフィルター処理)が必要になります。

サンプルを間引くだけのダウンサンプリングは、処理負荷が軽量で魅力的ではありますが、このような折り返し雑音を生まないよう音声編集ライブラリなどでローパスフィルターないしは、同様の手順を行ってくれるリサンプラーを使用する必要があることを覚えておくと良いでしょう。

(余談) 折り返し雑音はどの様に発生するのか

折り返し雑音が音声波形上ではどの様に発生しているかをグラフにしてみました。

  • ⚫️ 黒い線: アナログ信号 (2Hz ~ 32Hz)
  • 🟠 オレンジの点: アナログ信号を 32Hz で標本化した位置に描画
    • ナイキスト周波数は 16Hz
  • 🔵 青色折れ線: オレンジの点を結んだもの。ここでは仮に「 デジタル信号 」と呼称します。
    • PCM データから表される音声波形はこの青色折れ線グラフのような状態です。

折り返し雑音が発生するさま

ナイキスト周波数である 16Hz を超えるにつれ「🔵 デジタル信号の波形」が実際の「⚫️ アナログ信号の波形」と比較してゆったりとした波形になっていく(周波数が下がっていく)のが見えると思います。
この例では「28Hz は 4Hz の折り返し雑音」として現れ「30Hz は 2Hz の折り返し雑音」として現れますが、音声波形として見ても先述した実測データ(周波数を表示したもの)のようにナイキスト周波数を境に線対称になっています。

また、現れた折り返し雑音では振幅が上下反転し、逆位相 になっているのが面白いですね。

ナイキスト周波数である 16Hz は信号が残るかと思いきや、きれいに 0hz になってしまいました。これは先程の振幅が逆位相になっていることが影響していそうですがはたして・・。

まとめ

本記事で挙げたヒントは以下のようなことでした。

  • 録音時に指定したパラメータを一旦疑ってみよう。実際と異なるかも。
  • 録音した音声を「耳で聞く」「目で見る(音声波形)」「目で見る(スペクトログラム)」を怠らない。
  • サイン波・スウィープ音を同時に録音することを試そう。
  • 録音時はバッファサイズに十分注意し、スレッドを適切に使用しよう。音声を取りこぼしているかも。
  • 「折り返し雑音」って言うものがあるのね。

raw PCM 音源はちょっとしたミスで思わぬ音声になりがちです。この記事が PCM 録音時のヒントとして少しでも役立てば幸いです。

👂️ は大切に!![10]

脚注
  1. 「header-less WAV ファイル」とも呼ばれる。メタ情報を知らない限りは謎のバイナリデータ ↩︎

  2. ヘッダ部は常に偶数バイトであるため、データ部の構造が壊れないこともその理由。 ↩︎

  3. このウインドウの左下に「検出」というボタンを見つけたので何度か使ってみましたが、検出精度は低いようです。(ヘッダなしの正常なデータでもめっちゃ間違えとる!) ↩︎

  4. 筆者の環境(Mac)では Bluetooth ヘッドセットとオーディオインターフェイスを同時に接続した場合に、Bluetooth マイクのサンプリングレート(通話用 HFP プロファイル 8kHz)がすべての録音機器に適用され、思わぬサンプリングレートで録音されることがありました。 ↩︎

  5. https://mimi.fairydevices.jp/technology/device/mic_array/ ↩︎

  6. 上から下にかけて順に 1ch, 2ch, ... となっています。 ↩︎

  7. 16bit, 24bit, 32bit など。整数・浮動小数の差などもある。 ↩︎

  8. ナイキスト周波数である 8kHz を境界に、10kHz のサイン波は 6kHz の折り返し雑音に、8kHz〜15kHz のサイン波は 8kHz〜1kHz の折り返し雑音として現れている。 ↩︎

  9. 16kHz へのダウンサンプリングの際には 16kHz/2 となり ナイキスト周波数 は 「8kHz」となる。 ↩︎

  10. この記事を書く際に sox コマンドで色々やってたら耳を痛めました。内耳の有毛細胞は損傷しても再生しないんですって!こわい! ↩︎

フェアリーデバイセズ公式

Discussion