見出し画像

プログラムと電子工作・開発ツール wav2h.py

プログラムと電子工作・楽曲演奏(3)SpeakerHATへDAC出力」編で、WAVE形式の音楽ファイルのデータを const uint8_t wav[] = "・・・"; として埋め込みました。.wavファイルから .hファイルへの変換は python で行っています。

作業手順

  1. 音楽ファイルは、著作権、著作隣接権ともに切れていると説明のある下記の Webサイト https://classix.sitefactory.info/ からダウンロードします。例では、ヴィヴァルディ「春」をダウンロードしました。

  2. 長すぎて M5StickC Plus のメモリに入り切らないので、Audio Cutter https://mp3cut.net/ja/ を使用して、適当な部分を切り出します。
    このとき、音量も調整します。例では、冒頭 8秒でカットし、音量はそのままにしました。

  3. Audio Converter https://online-audio-converter.com/ja/ で、ファイル形式を変換します。

    1. 出力ファイル形式を wav にします。

    2. [詳細設定]を押します。

    3. サンプルレートを設定します。小さい数字にするほど、M5StickC Plus のメモリを消費しません。例では 16000Hzにしました。

    4. チャンネル数を 1(モノラル)にします。

  4. python スクリプト wav2h.py で .wavファイルから .hファイルを生成します。Windows のコマンドプロンプトで、下記のように入力します。例では、.wavファイル名が vivaldi_8sec_1ch_16000hz.wav、.hファイル名が vivaldi_1ch_16000hz_uint8.h です。オプション --vol で音量調整できます。

  5. 生成した .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データ

ライセンス

このページのソースコードは、複製・改変・配布が自由です。営利目的にも使用してかまいませんが、何ら責任を負いません。


この記事が気に入ったらサポートをしてみませんか?