シェルスクリプトとAWKでWAV生成
概要
シェルスクリプトとAWKでWAV生成してみたい。
シェルスクリプトでバイナリを扱う
最終的にWAVを生成するので、バイナリを扱える必要がある。
シェルスクリプトでバイナリを扱うとコマンド置換などいろんな箇所で問題が出る。
なので、最終段階までは、バイナリをテキストデータで扱うことにする。
最も汎用性が高そうな方式として、1バイトを16進数2文字 00
~ff
で表す方式で扱う。
これを本記事ではHexdump形式と呼ぶ。
文字列からHexdump形式への変換は、od
コマンドを使って、
stringToHexdump() {
od -An -tx1 | tr -d ' \n'
}
とできる。
逆に、Hexdump形式からバイナリへの変換は、
hexdumpToBinary() {
fold -w2 | sed 's/^/0x/' | xargs printf '\\\\0%o\n' | xargs printf '%b'
}
とできる。
まず、文字列を2文字ずつ改行して、先頭に 0x
を付ける。
これを、printf
での %o
により、8進数に変換しつつ先頭に \0
を付加している。
printf
と xargs
それぞれでエスケープが必要になるため、\
は4つになっている。
printf
は、
https://pubs.opengroup.org/onlinepubs/000095399/utilities/printf.html
に書かれているように、%b
を使うことで引数に8進数を指定し1バイトのバイナリを出力できる。
このときの引数に与える8進数は、\0111
のような表記になる。
これによって、Hexdump形式がバイナリに変換できるようになった。
なお、汎用性は下がるが、xxd
を使えばそれぞれ、xxd -p
、xxd -p -r
とすることもできる。
WAVファイルのヘッダをHexdump形式で生成
WAVファイルのヘッダは、16bit値、32bit値をリトルエンディアンで書き込める必要がある。
そのための関数を準備する。
uint8Hexdump() {
printf '%02x' $1
}
uint16LeHexdump() {
uint8Hexdump $(($1 % 0x100))
uint8Hexdump $((($1 / 0x100) % 0x100))
}
uint32LeHexdump() {
uint16LeHexdump $(($1 % 0x10000))
uint16LeHexdump $((($1 / 0x10000) % 0x10000))
}
出力は、ここまでの解説の通り、Hexdump形式になっている。
ここまでの関数を使って、WAVファイルのヘッダは、
getHeaderHexdump() (
sampleRate=$1
samplesLen=$2
bytesPerSample=$3
printf 'RIFF' | stringToHexdump
uint32LeHexdump $((32 + samplesLen * bytesPerSample))
printf 'WAVE' | stringToHexdump
printf 'fmt ' | stringToHexdump
uint32LeHexdump 16
uint16LeHexdump 1
uint16LeHexdump 1
uint32LeHexdump $sampleRate
uint32LeHexdump $((sampleRate * bytesPerSample))
uint16LeHexdump $bytesPerSample
uint16LeHexdump $((8 * bytesPerSample))
printf 'data' | stringToHexdump
uint32LeHexdump $((samplesLen * bytesPerSample))
)
と取得できるようになった。これもHexdump形式だ。
オーディオデータの生成
オーディオデータはHexdump形式で生成してもよいのだが、もう少し汎用性を上げるために、
1行ごとに-1.0~1.0が書き込まれた形式を中間で用意することにする。
これを本記事ではAudioData形式と呼ぶ。
16bitモノラルとして、AudioData形式からHexdump形式への変換は、
AWKを使って、
audioDataToHexdump() {
awk '
{
i = int(0x7fff * $1);
if (i < 0) i += 0xffff;
printf("%02x%02x", i % 0x100, (i / 0x100) % 0x100);
}
'
}
と書ける。-1.0~1.0の値を、リトルエンディアン16bit値にして Hexdump
形式で書き込んでいる。
処理の流れ
これまでの流れを組み合わせて、
sampleRate=22050
durationSeconds=5
samplesLen=$((durationSeconds * sampleRate))
bytesPerSample=2
{
getHeaderHexdump $sampleRate $samplesLen $bytesPerSample
getAudioData $sampleRate $samplesLen | audioDataToHexdump
} | hexdumpToBinary > output.wav
とすれば、WAVファイルを出力できるようになった。
あとは、AudioData形式を出力する getAudioData
を自由に実装すればよい。
波形の生成
https://qiita.com/htsnul/items/3e001e70e7144dda07a6 の前半の1秒ずつドレミを鳴らす処理を、
AWKを使ったシェルスクリプトに移植しよう。
getAudioData() {
sampleRate=$1
samplesLen=$2
seq 0 $((samplesLen - 1)) | awk -v sampleRate=$sampleRate '
BEGIN {
PI = atan2(0, -1);
}
function angularVelFromNoteNumber(nn) {
return 2 * PI * 440 * 2 ** ((nn - 69) / 12);
}
function toneSimple(t, duration, nn,
angVel \
) {
if (t < 0 || duration < t) return 0;
angVel = angularVelFromNoteNumber(nn);
return 0.05 * sin( \
angVel * t + \
4 * sin(1 * angVel * t) \
);
}
function sample(t,
d \
) {
d = 0;
d += toneSimple(t - 0, 1, 72);
d += toneSimple(t - 1, 1, 74);
d += toneSimple(t - 2, 1, 76);
return d;
}
{
t = $1 / sampleRate;
print sample(t);
}
'
}
これでドレミと鳴るWAVファイルが生成できた。
全コード
#!/bin/bash
stringToHexdump() {
od -An -tx1 | tr -d ' \n'
}
hexdumpToBinary() {
fold -w2 | sed 's/^/0x/' | xargs printf '\\\\0%o\n' | xargs printf '%b'
}
uint8Hexdump() {
printf '%02x' $1
}
uint16LeHexdump() {
uint8Hexdump $(($1 % 0x100))
uint8Hexdump $((($1 / 0x100) % 0x100))
}
uint32LeHexdump() {
uint16LeHexdump $(($1 % 0x10000))
uint16LeHexdump $((($1 / 0x10000) % 0x10000))
}
getHeaderHexdump() (
sampleRate=$1
samplesLen=$2
bytesPerSample=$3
printf 'RIFF' | stringToHexdump
uint32LeHexdump $((32 + samplesLen * bytesPerSample))
printf 'WAVE' | stringToHexdump
printf 'fmt ' | stringToHexdump
uint32LeHexdump 16
uint16LeHexdump 1
uint16LeHexdump 1
uint32LeHexdump $sampleRate
uint32LeHexdump $((sampleRate * bytesPerSample))
uint16LeHexdump $bytesPerSample
uint16LeHexdump $((8 * bytesPerSample))
printf 'data' | stringToHexdump
uint32LeHexdump $((samplesLen * bytesPerSample))
)
audioDataToHexdump() {
awk '
{
i = int(0x7fff * $1);
if (i < 0) i += 0xffff;
printf("%02x%02x", i % 0x100, (i / 0x100) % 0x100);
}
'
}
getAudioData() {
sampleRate=$1
samplesLen=$2
seq 0 $((samplesLen - 1)) | awk -v sampleRate=$sampleRate '
BEGIN {
PI = atan2(0, -1);
}
function angularVelFromNoteNumber(nn) {
return 2 * PI * 440 * 2 ** ((nn - 69) / 12);
}
function toneSimple(t, duration, nn,
angVel \
) {
if (t < 0 || duration < t) return 0;
angVel = angularVelFromNoteNumber(nn);
return 0.05 * sin( \
angVel * t + \
4 * sin(1 * angVel * t) \
);
}
function sample(t,
d \
) {
d = 0;
d += toneSimple(t - 0, 1, 72);
d += toneSimple(t - 1, 1, 74);
d += toneSimple(t - 2, 1, 76);
return d;
}
{
t = $1 / sampleRate;
print sample(t);
}
'
}
sampleRate=22050
durationSeconds=5
samplesLen=$((durationSeconds * sampleRate))
bytesPerSample=2
{
getHeaderHexdump $sampleRate $samplesLen $bytesPerSample
getAudioData $sampleRate $samplesLen | audioDataToHexdump
} | hexdumpToBinary > output.wav
パイプで加工
データ形式が素直なので、パイプでAudioData形式を加工することができる。
例えば、
- getAudioData $sampleRate $samplesLen | audioDataToHexdump
+ getAudioData $sampleRate $samplesLen | tac | audioDataToHexdump
のように tac
コマンドを挟むことで出力を逆再生に変換できる。
getAudioData $sampleRate $samplesLen | awk '{ print $1 / 4; }' | audioDataToHexdump
とすれば、音量を1/4に下げることができる。
getAudioData $sampleRate $samplesLen \
| awk '{ d = $1 + b[(NR + 1) % 5000] / 2; b[NR % 5000] = d; print d; }' \
| audioDataToHexdump
とすればディレイ効果を掛けることができる。
コマンドやAWKをオーディオ効果を適用するノードのように見立てることができる。
まとめ
実用性はともかく、シェルスクリプトとAWKでWAV生成できるようになった。
Author And Source
この問題について(シェルスクリプトとAWKでWAV生成), 我々は、より多くの情報をここで見つけました https://qiita.com/htsnul/items/bbf230b8628598851e52著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .