ループから音を作る

ShellBeginner
オンラインで実践に進む

はじめに

短い音があれば、作業の終了を知らせられます。このラボでは音そのものを作ります。C のループで数値の列を計算し、それを再生可能な WAV ファイルにします。まず動作する 440 Hz の例を聴き、音の高さだけを変えてから、再利用可能なサンプリング関数を完成させます。

C の変数、算術、ループ、配列、関数と、基本的なターミナルでの移動操作を前提とします。音声のサンプリング、ラジアン、バイナリ出力はここで説明します。ポインタや信号処理の知識は不要です。コンパイラ、小さく仕組みの分かる WAV 書き込み処理、実際のファイルだけを読むブラウザプレーヤーは準備済みです。音声はすべて独自に合成するため、録音機器は不要です。50 分は、読解や通常のミスへの対応を含めた作者の目安です。

動作する音を聴く

このステップでは、動作する 0.5 秒の音を再生し、値を 1 つ変えて音を高くします。Terminal から始めます。cd はディレクトリを変更します。この絶対パスは、作業ディレクトリを間違えた場合の復帰にも使えます。ここで作業を続けてください。

cd /home/labex/project/sound-studio

サンプルとは、ある瞬間の波形を表す符号付きの数値です。PCM はパルス符号変調を意味し、ファイルはこれらの値を直接保存します。このラボではモノラルと呼ばれる 1 チャネルを使い、毎秒 16,000 個のサンプルを保存します。符号付き 16 ビットのサンプルは -32,768 から 32,767 の整数を表せます。正と負の値は波形の両側を表し、ゼロが中心です。振幅はゼロからどの程度離れるかを決めます。スピーカーやヘッドホンで聴く場合は、快適な音量から始めてください。

準備された sample.c は 440 Hz、つまり毎秒 440 周期の正弦波を作ります。<math.h> で宣言される sin はラジアンで表した角度を受け取り、-1 から 1 の値を返します。1 周期は 2 * pi ラジアンです。10,000 を掛けると適度な振幅に拡大できます。時間の計算は音を聴いた後に見ていきます。

提供された例をコンパイルします。gcc は C をコンパイルし、-std=c11 は C11 を選択し、-Wall -Wextra は有用な警告を有効にし、-o sample は実行ファイル名を指定します。pcm.c は WAV 書き込み処理を提供します。ソースファイルの後ろに置く -lm は、sin が使う数学ライブラリをリンクします。これらのファイルでコンパイラ警告は出ないはずです。

gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample

./sample は現在のディレクトリの実行ファイルを起動します。プログラムはバイナリの WAV バイト列を標準出力に書き込みます。> はそのバイト列をファイルにリダイレクトします。音声バイトは読める端末テキストではないため、リダイレクトを省略して実行しないでください。outputs は準備済みのディレクトリです。次の新しい名前を使います。

./sample > outputs/sample-440.wav

成功時にはテキストは表示されません。Sound Studio タブへ切り替えます。Refresh files をクリックし、WAV file で sample-440.wav を選び、Load sound をクリックします。Ready to play と 8000 samples · 16000 Hz · 0.500 s が表示されるはずです。Play をクリックし、Pause と Playback position も試します。Tab で位置コントロールにフォーカスを移し、Home、End、矢印キーを使えます。標準の音声コントロールは 1 秒未満の長さを 0:00 と丸めて表示することがあります。上部のテキストと秒単位の位置表示で正確な長さを確認してください。

Download WAV をクリックして実際のファイルを保存します。プレーヤーは作成したファイルを読み、欠けたサンプルを生成しません。Cannot load sound はファイルが存在しないか未対応であることを示します。スタジオのページ自体が開かない接続エラーとは別です。パスを間違えた場合は Terminal に戻り、上記の cd を使い、ファイル名を確認して対応するコマンドを再実行し、ファイル一覧を更新します。

Sound Studio の最初の実際の音

選択中の sample-440.wav、0.5 秒のメタデータ、Play、Playback position、Download WAV の各コントロールに注目してください。

Terminal に戻ります。cat はテキストを表示します。短い例を読みます。

cat sample.c

ループはサンプルのインデックス 0 から 7999 を処理します。seconds = (double)i / rate はインデックスを時間に変換します。インデックス 0 は 0 秒、160 は 0.010 秒です。(double) は整数除算ではなく小数を保つ除算を指定します。位相 2.0 * pi * frequency * seconds は周期を進む角度を表します。(int) は拡大した正弦値から小数部を取り除き、整数にします。提供された wav_sample はその数の PCM 2 バイトを書くだけで、数そのものを選んではくれません。

次に周波数だけを変えます。次のコマンドは、特定の置換を行うテキストエディタ sed を使います。-i は指定ファイルをその場で編集し、引用符内の s/old/new/ は最初のスラッシュの組の間にあるリテラルを、次の組の間の文字列に置き換えます。変更するのは sample.c だけです。

sed -i 's/frequency = 440/frequency = 660/' sample.c

変更を実行ファイルに反映するため再コンパイルし、別のファイルに保存します。最初の WAV も残してください。

gcc -std=c11 -Wall -Wextra sample.c pcm.c -lm -o sample
./sample > outputs/sample-660.wav

Sound Studio に切り替え、更新して sample-660.wav を読み込み、再生します。サンプル数は同じ 8,000 個、長さは同じ 0.5 秒ですが、音は高くなります。変えたのは周波数だけです。編集後の sample.c、sample 実行ファイル、両方の WAV を残します。この比較は準備段階です。次は自分の再利用可能なプログラムと、それによる別の成果物を作ります。

すべてのサンプルを自分で計算する

このステップでは、音の高さ、長さ、振幅が異なる場合にも 1 つの C 関数で対応できるように write_tone を完成させます。Terminal の /home/labex/project/sound-studio から始めます。

毎秒 16,000 サンプルでは、300 ミリ秒に 16000 * 300 / 1000 = 4800 個のサンプルが必要です。1 ミリ秒は 1 秒の 1,000 分の 1 です。このレートでは整数ミリ秒の長さが正確な整数サンプル数になります。i = 0 から i < count の間続くループは、ちょうど count 個の値を出力します。<= だと余計なサンプルが 1 個増えます。

もっと小さな例を考えます。毎秒 8 サンプルで毎秒 1 周期の正弦波を取ると、最初の 8 インデックスはおよそ 0, 0.71, 1, 0.71, 0, -0.71, -1, -0.71 になります。インデックスが時間を進め、sin が縦方向の位置を与えます。実際の 16,000 Hz では、1 周期にもっと多くの値が含まれます。

提供されたインターフェースと初期関数を読みます。

cat tone.h tone.c

write_tone(frequency, milliseconds, amplitude) は全サンプルを生成しなければなりません。初期コードはゼロを書くため、コンパイルできても、構造は正しいのに無音の WAV になる場合があります。ファイル名、サイズ、正常終了だけでは音の存在を証明できません。

main.c、tone.h、pcm.h、pcm.c、inspect_wav.py、audio.py は変更しないでください。準備された main.c は 3 引数を読み、検証し、ヘッダーを書いて関数を呼びます。ポインタを使う引数解析処理を変更する必要はありません。自分の実装は tone.c です。ブラウザは準備済みです。

書き込み処理はファイルへの封入と音の合成を分けます。wav_begin(count) はヘッダーを 1 回書き、wav_sample(value) は渡された整数を 1 個書きます。どちらも波形を作りません。小さな pcm.c は putchar で標準出力に 1 バイトを書き、fputs で固定のチャンク名を書きます。C 構造体をそのまま出力せず、下位バイト、上位バイトの順に出力します。負のサンプルは 16 ビット表現に符号化されます。書き込み処理は符号付き範囲外の値を拒否し、クリップや置換はしません。

書き込むフィールドは次のとおりです。fmt という名前には末尾の空白が含まれます。

フィールド このラボでの意味
RIFF, size, WAVE WAV コンテナを識別。size はファイルの総バイト数から先頭 8 バイトを除いた値
fmt, size 16 続く 16 バイトの形式情報を記述
Format 1, channels 1 非圧縮 PCM、モノラル
Sample rate 16000 毎秒のサンプル数
Byte rate 32000 毎秒 16,000 フレーム × 1 フレーム 2 バイト
Block alignment 2, sample bits 16 1 フレームは 2 バイトのサンプル 1 個
data, size 2 * count 続く実際の PCM バイトを識別し、その数を示す

cat pcm.c でこの提供済みラッパーを読めます。フィールドは出力を説明するだけで、ヘッダーだけ変えても無音サンプルは音になりません。

次の短い関数を実装します。<math.h> は sin を宣言します。pi はローカルな小数定数なので、非標準マクロ M_PI に依存しません。amplitude は整数変換前の倍率です。解析処理が最大 12,000 に制限するため、この正弦サンプルは安全に 16 ビット範囲内に収まります。各呼び出しの時間インデックスはゼロから始まります。

cat > tone.c はこのソースファイルだけを置き換えます。here-document <<'C' は、C だけがある最終行まで、後続行を cat に渡します。区切り文字を引用すると C テキストがそのまま保たれます。最後の行を含め、ブロック全体を貼り付けてください。

cat > tone.c <<'C'
#include <math.h>
#include "tone.h"
#include "pcm.h"

void write_tone(int frequency, int milliseconds, int amplitude) {
    const int rate = 16000;
    const double pi = 3.14159265358979323846;
    int count = rate * milliseconds / 1000;
    for (int i = 0; i < count; i++) {
        double seconds = (double)i / rate;
        double phase = 2.0 * pi * frequency * seconds;
        int sample = (int)(amplitude * sin(phase));
        wav_sample(sample);
    }
}
C

関数を準備済みの呼び出し元と書き込み処理とともにコンパイルします。行エラーが出た場合は tone.c を直すか、置換ブロック全体を再入力して再コンパイルしてください。コンパイル失敗後に古い実行ファイルを動かしても、試すのは古いコードです。

gcc -std=c11 -Wall -Wextra main.c tone.c pcm.c -lm -o tone

実行ファイルは Hz 単位の周波数、整数ミリ秒の長さ、振幅 を順に受け取ります。両端を含む範囲は、周波数 200–1200、長さ 100–1000、振幅 1000–12000 です。プログラムで 2 つの新しい出力を作ります。

./tone 440 300 8000 > outputs/tone-440.wav
./tone 660 500 10000 > outputs/tone-660.wav

準備された読み取りツールで実際に保存されたバイトを調べます。python3 は指定した読み取り専用 Python ツールを起動します。Python を書く必要はありません。ファイルがない、不完全、未対応の場合はエラーを報告し、再生成は一切しません。

python3 inspect_wav.py outputs/tone-440.wav

期待される出力:

Format: signed 16-bit mono PCM
Rate: 16000 Hz
Frames: 4800
Duration: 0.300 s
Peak: 8000

Sound Studio に切り替えます。更新して tone-440.wav を読み込み再生し、次に tone-660.wav を読み込み再生します。表示される長さは 0.300 秒と 0.500 秒です。新しい出力のどちらかもダウンロードします。これらは関数の成果であり、sample-440.wav、sample-660.wav とは別です。

自分で生成した音をプレーヤーで確認

選択中のファイルと Ready to play の両方が tone-660.wav を示します。メタデータは保存済みの実サンプルを説明します。

tone.c、動作する tone 実行ファイル、2 つの新しい出力を残してください。チェックは保存された WAV と提出済み実行ファイルを調べ、別の有効パラメータを使って、変更しないソースを使い捨てディレクトリで再コンパイルします。3 つすべてが動く必要があります。ソースだけ、古い実行ファイル、コピーした例だけでは不十分です。同等の計算と整数丸めは許可されます。近似された音の振幅の誤差は要求振幅の 2% に 2 サンプル単位を加えた値まで、ピークの超過は 2 単位までです。要求周波数の正弦波に対する残差の二乗平均平方根(RMS)は振幅の 0.5% に 2 単位を加えた値までです。RMS 残差は平均的な大きさのサンプル誤差を表します。この数値許容差は丸めの差を認めるもので、要求された音の高さを変える許可ではありません。正確なフレーム数と PCM 形式も必要です。提供した式は十分にこの制限を満たします。

長さを確認し、拒否から復旧する

このステップでは、短く端数のある長さにも同じ関数を使い、無効な入力と壊れた音声ファイルを区別します。Terminal から始めます。

733 Hz、101 ミリ秒、振幅 7,373 を生成します。周期が区間の長さを割り切る必要はありません。サンプル数は引き続き count が定めます。

./tone 733 101 7373 > outputs/short-tone.wav
python3 inspect_wav.py outputs/short-tone.wav

Frames: 1616 と Duration: 0.101 s を確認します。これは 16000 * 101 / 1000 から求められます。最後のサンプルは区間の終わりの少し前にあり、終点にサンプルを追加してはいけません。この最初のラボでは振幅のフェードをまだ行わないため、急な開始や終了でクリック音が生じることがあります。次のガイド付きラボでゲインの緩やかな変化と音の間の休止を学びます。

次のコマンドは最小値未満の 99 ミリ秒を意図的に要求します。新しい使い捨てファイル名を使います。シェルはプログラムの引数検証前にリダイレクト先を開くため、拒否されると空ファイルが残る場合があります。同名の以前のファイルが保持される保証はありません。

./tone 440 99 10000 > outputs/rejected.wav

echo $? は直前のコマンドのネイティブ終了ステータスを即座に表示します。ゼロは成功、ゼロ以外は拒否または失敗です。試行とステータス取得の間に別のコマンドを実行しないでください。

echo $?

公開した範囲の説明とステータス 2 が表示されるはずです。これは通常の入力拒否であり、環境をリセットする理由にはなりません。99 が無効な長さの引数です。既存の正常な WAV は別ファイルなので引き続き利用できます。

Sound Studio に切り替え、更新して rejected.wav を選び、読み込みます。ページには Cannot load sound と表示され、完全な対応 PCM WAV ではないと説明されるはずです。再生とダウンロードは利用不可のままです。空ファイルは、名前が .wav で終わるだけでは音になりません。

Terminal に戻ります。rm -- は、オプション終端を表す -- の後の指定ファイルだけを削除します。この失敗した使い捨て出力だけを削除します。

rm -- outputs/rejected.wav

対応する最小値と別の新しい出力名を使って復旧します。

./tone 440 100 10000 > outputs/minimum-tone.wav
python3 inspect_wav.py outputs/minimum-tone.wav

今度は 1,600 フレーム、0.100 秒になるはずです。Sound Studio に戻り、更新して minimum-tone.wav を再生します。次に short-tone.wav を読み込み、Download WAV を使います。キーボード確認では Tab で表示中のコントロールを移動し、Play で Enter を押し、位置スライダーで Home、End、矢印キーを使います。別のファイルへ移る前に一時停止してください。

再利用可能なソース、それと一致する実行ファイル、実際に再生可能な出力ができました。比較用の例と自分の出力を残します。チェックは使い捨てディレクトリで実行し、現在のファイルを読み取るだけで、プログラムを修復したり不足分を作ったりしません。ブラウザでのデコードと再生は物理スピーカーの接続を証明しません。また、この限定された WAV サブセットは汎用音声エンコーダーではありません。

まとめ

C のループの時間インデックスを実際の音に結び付けました。例の音の高さを変更し、再利用可能な正弦波サンプリングを実装し、振幅を符号付き 16 ビットの範囲に保ち、正確なフレーム数の WAV を保存しました。実際のファイルを調べ、再生、ダウンロードし、意図的なパラメータ拒否から復旧しました。次は音、無音、緩やかなフェードを組み合わせられます。