プログラムと電子工作・開発ツール wav2h.py
「プログラムと電子工作・楽曲演奏(3)SpeakerHATへDAC出力」編で、WAVE形式の音楽ファイルのデータを const uint8_t wav[] = "・・・"; として埋め込みました。.wavファイルから .hファイルへの変換は python で行っています。
作業手順
音楽ファイルは、著作権、著作隣接権ともに切れていると説明のある下記の Webサイト https://classix.sitefactory.info/ からダウンロードします。例では、ヴィヴァルディ「春」をダウンロードしました。
長すぎて M5StickC Plus のメモリに入り切らないので、Audio Cutter https://mp3cut.net/ja/ を使用して、適当な部分を切り出します。
このとき、音量も調整します。例では、冒頭 8秒でカットし、音量はそのままにしました。Audio Converter https://online-audio-converter.com/ja/ で、ファイル形式を変換します。
出力ファイル形式を wav にします。
[詳細設定]を押します。
サンプルレートを設定します。小さい数字にするほど、M5StickC Plus のメモリを消費しません。例では 16000Hzにしました。
チャンネル数を 1(モノラル)にします。
python スクリプト wav2h.py で .wavファイルから .hファイルを生成します。Windows のコマンドプロンプトで、下記のように入力します。例では、.wavファイル名が vivaldi_8sec_1ch_16000hz.wav、.hファイル名が vivaldi_1ch_16000hz_uint8.h です。オプション --vol で音量調整できます。
生成した .hファイルをエディタで開き、Output Min、Output Max を確認します。いずれかが 100%を超えていると、波形が歪んでいます。Audio Cutter の音量調整か、wav2h.py の --vol オプションで、100%以下になるように調整してください。
Windows のコマンドプロンプト(楽曲演奏(3)SpeakerHATへDAC出力)
> python3 wav2h.py --uint --bit 8 --vol 1.75 vivaldi_8sec_1ch_16000hz.wav > vivaldi_1ch_16000hz_uint8.h
wav2h.py
import sys
import io
import argparse
#------------------------------------------------------------------------------
# function main:メイン
#------------------------------------------------------------------------------
def main():
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
parser = argparse.ArgumentParser()
parser.add_argument("file", type=str)
parser.add_argument("--bit", type=int)
parser.add_argument("--vol", type=float)
parser.add_argument("--uint", action='store_true')
parser.add_argument("--head", action='store_true')
if len(sys.argv) < 2:
print(f"使用方法:python3 {sys.argv[0]} [--bit 8|16] [--vol 1.0] [--head] <filename.wav> > <output.h>")
print(f".wavファイルのデータ部のみを取り出し、ビット数を変換する。")
print(f"--bit 8 のときは 16ビットデータを符号なし 8ビットに変換する。")
print(f"--bit 16(デフォルト)のときは 16ビットデータをそのまま 8ビットデータ 2個に書き出す。")
print(f"配列の定義文を出力する。")
print(f"--vol 1.0(デフォルト) 音量倍率")
print(f"--uint 強制的に符号なしにする")
print(f"--head ファイルヘッダを出力する(デフォルトは出力しない)。")
exit()
else:
args = parser.parse_args()
FILENAME = args.file
BITNUM = args.bit #default file header
VOL = args.vol if args.vol else 1.0 #default 1.0
UINT = args.uint #default false
HEAD = args.head #default false
with open(FILENAME, 'rb') as f:
# ファイルヘッダを読む。
file_kind = f.read(4).decode()
file_size = int.from_bytes(f.read(4), 'little') + 8
riff_kind = f.read(4).decode()
if riff_kind != 'WAVE':
print(f"RIFF識別子:{riff_kind}")
exit()
# ファイルヘッダの解析-dataチャンクまで読み飛ばす。
while True:
chunc_kind = f.read(4).decode()
if chunc_kind == 'data':
wave_size = int.from_bytes(f.read(4), 'little')
break
elif chunc_kind == 'fmt ':
chunc_size = int.from_bytes(f.read(4), 'little')
f.read(2) #(読み飛ばす)エンコーディング方式
NUM_CH = int.from_bytes(f.read(2), 'little') #チャネル数、1 モノラル、2 ステレオ
SMPL_FREQ = int.from_bytes(f.read(4), 'little') #サンプリング周波数、CDは 44100
f.read(4) #(読み飛ばす)1秒当りのバイト数
f.read(2) #(読み飛ばす)ブロックサイズ
if not BITNUM:
BITNUM = int.from_bytes(f.read(2), 'little') #音データ当りのビット数
else:
f.read(2) #(読み飛ばす)音データ当りのビット数
f.read(chunc_size - (2+2+4+4+2+2))
else:
chunc_size = int.from_bytes(f.read(4), 'little')
f.read(chunc_size)
data_pos = f.tell() #data chunk position
# 演奏データの取得-dataチャンクを読む。
# ・ 1データ 16ビットと仮定する。
wave_data = []
for i in range(wave_size):
w = f.read(2)
if w:
wave_data.append(int.from_bytes(w, 'little', signed=True))
else:
break
# --head オプションの場合のみ、ファイルヘッダの取得
if HEAD:
# ファイルを巻き戻す。
f.seek(0);
file_header = []
for i in range(data_pos):
d = f.read(1)
if d:
file_header.append(int.from_bytes(d, 'little'))
else:
break
# 自動的にファイルは閉じられる。
# --- ここまで、
# リスト wave_data 演奏データ(1データ 2バイト)
# リスト file_header ファイルヘッダ(1データ 1バイト)
# 標準出力へC言語配列定義形式で出力する。
n = 0
# --head オプションの処理
if HEAD:
# --- ファイルヘッダとデータチャンクの両方を出力する。
output = f"const uint8_t wav[{len(file_header) + (len(wave_data) << (BITNUM >> 4))}] PROGMEM = " "{\n"
for i in range(len(file_header)):
if n % 16 == 0:
output += f" 0x{file_header[i]:02x}, "
elif n % 16 == 15:
output += f"0x{file_header[i]:02x}," "\n"
else:
output += f"0x{file_header[i]:02x}, "
n += 1
else:
# --- データチャンクのみ出力する。
output = f"const uint8_t wav[{len(wave_data) << (BITNUM >> 4)}] PROGMEM = " "{\n"
# データチャンクを出力する。
# 最大値、最小値を計算する準備をする。
max_origin = -32768
min_origin = +32767
max_output = -32768
min_output = +32767
for i in range(len(wave_data)):
w = wave_data[i]
max_origin = w if w > max_origin else max_origin
min_origin = w if w < min_origin else min_origin
# 音量を調節する。
w = int(w * VOL)
# --uint オプションでは強制的に符号なしに変換する。
if UINT:
# --- 強制的に符号なし
w += 32768
max_output = w if w > max_output else max_output
min_output = w if w < min_output else min_output
dM = int(w) & 0xff
dL = (int(w) >> 8) & 0xff
if BITNUM == 8:
# --- 8ビット
# 符号付き16ビット整数を符号なし 8ビット整数に変換する。
if i != len(wave_data) - 1:
# --- 途中のデータ
if n % 16 == 0:
output += f" 0x{dL:02x}, "
elif n % 16 == 15:
output += f"0x{dL:02x}," "\n"
else:
output += f"0x{dL:02x}, "
n += 1
else:
# --- 最後のデータ
if n % 16 == 0:
output += f" 0x{dL:02x}" "};\n"
elif n % 16 == 15:
output += f"0x{dL:02x}" "};\n"
else:
output += f"0x{dL:02x}" "};\n"
n += 1
else:
# --- 16ビット
if i != len(wave_data) - 1:
# --- 途中のデータ
if n % 16 == 0:
output += f" 0x{dM:02x}, "
elif n % 16 == 15:
output += f"0x{dM:02x}," "\n"
else:
output += f"0x{dM:02x}, "
n += 1
if n % 16 == 0:
output += f" 0x{dL:02x}, "
elif n % 16 == 15:
output += f"0x{dL:02x}," "\n"
else:
output += f"0x{dL:02x}, "
n += 1
else:
# --- 最後のデータ
if n % 16 == 0:
output += f" 0x{dM:02x}, "
elif n % 16 == 15:
output += f"0x{dM:02x}," "\n"
else:
output += f"0x{dM:02x}, "
n += 1
if n % 16 == 0:
output += f" 0x{dL:02x}" "};\n"
elif n % 16 == 15:
output += f"0x{dL:02x}" "};\n"
else:
output += f"0x{dL:02x}" "};\n"
n += 1
print(f"// {FILENAME}")
if HEAD:
print(f"// with Header")
print(f"// Sampling : {SMPL_FREQ}")
print(f"// Number of Channel : {NUM_CH}")
print(f"// Bit : {BITNUM}")
print(f"// Original Min : {min_origin:.0f}({min_origin/(-32768)*100:.1f}%)")
print(f"// Original Max : {max_origin:.0f}({max_origin/(+32767)*100:.1f}%)")
if not UINT:
# --- 符号付き
print(f"// Output Min : {min_output:.0f}({min_output/(-32768)*100:.1f}%)")
print(f"// Output Max : {max_output:.0f}({max_output/(+32767)*100:.1f}%)")
else:
# --- 符号なし
print(f"// Output Min : {min_output:.0f}({min_output/(65535)*100:.1f}%)")
print(f"// Output Max : {max_output:.0f}({max_output/(65535)*100:.1f}%)")
print()
print("// Wave File with Header Dump") if HEAD else print("// Wave File Data Chunk Dump")
print(output, end="")
if __name__ == '__main__':
main()
参考
WAVEファイルの構造を簡単にメモします。
Littleエンディアンであることに注意。
ファイルヘッダ
識別子 [4バイト]:'RIFF'
ファイルサイズ[4バイト]:ファイルサイズ-8(bytes)
RIFF識別子 [4バイト]:'WAVE'
チャンク1
チャンク識別子[4バイト]:半角文字列
チャンクサイズ[4バイト]:チャンクデータのサイズ L
チャンクデータ[可変長L]:チャンクデータ
チャンク2
:チャンクn
チャンク識別子[4バイト]:'data'
チャンクサイズ[4バイト]:dataチャンクのデータのサイズ M
チャンクデータ[可変長M]:WAVEデータ
ライセンス
このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。
この記事が気に入ったらサポートをしてみませんか?