前回の M5StickC + Speaker Hat で音声データを再生する では音声データを直接スケッチ上に書くというスマートとは言い難い方法でした。 今回は事前に SPIFFS へ保存した WAV ファイルを読み込んで再生します。

WAV ファイルの作成

まずは再生したい音声データを以下の形式の WAV ファイルへ変換します。

  • モノラル
  • サンプリング周波数8000Hz
  • Unsigned 8-bit PCM
  • メタデータなし

スピーカーが1つなのでモノラル、 M5StickC (というか ESP32) の DAC が 8 ビットなので WAV ファイルも 8 ビットです。 サンプリング周波数は 8000Hz じゃなくてもいいかもしれませんが、 一般的に使用されている 44100Hz ではうまく動作しなかったためこの値にしています。

また、今回のスケッチではメタデータを一切考慮していないため、 メタデータがあるとシリアルモニタへ “invalid wave file header” というエラーメッセージが表示されファイルが読み込まれません。

以下は FFmpeg で変換する場合のコマンド例です。 FFmpeg で変換すると「Lavf58.29.100」(数字部分はバージョンによる) というメタデータが標準で追加されるので、 オプション -fflags +bitexact を付けてこの動作を抑制します。

ffmpeg -i input.wav -ac 1 -ar 8000 -acodec pcm_u8 -fflags +bitexact output.wav

WAV ファイルを SPIFFS へ保存する

ESP32-WROOM-32 SPIFFS アップローダープラグインの使い方 | mgo-tec電子工作 に書かれている手順に従い、 Arduino ESP32 filesystem uploader を使用して事前に WAV ファイルを SPIFFS へ保存しておきます。

スケッチの書き込み

以下のスケッチを M5StickC へ書き込みます。 WAVE_FILE_NAME は SPIFFS へ保存した WAV ファイルのファイル名です。 書き込み後、Aボタン(正面の「M5」ボタン)を押すと WAV ファイルが再生されます。

#include <vector>
#include <M5StickC.h>
#include "FS.h"
#include "SPIFFS.h"

// WAVファイル名
const char WAVE_FILE_NAME[] = "/hoge.wav";

// スピーカー出力ピンの番号
const uint8_t SPEAKER_PIN = GPIO_NUM_26;
// LOWでLED点灯、HIGHでLED消灯
const uint8_t LED_ON = LOW;
const uint8_t LED_OFF = HIGH;
// 電源ボタンが1秒未満押された
const uint8_t AXP_WAS_PRESSED = 2;

// PWM出力のチャンネル
const uint8_t PWM_CHANNEL = 0;
// PWM出力の分解能(ビット数)
const uint8_t PWM_RESOLUTION = 8;
// PWM出力の周波数
const uint32_t PWM_FREQUENCY = getApbFrequency() / (1U << PWM_RESOLUTION);
// 音声データのサンプリング周波数(Hz)
const uint32_t SOUND_SAMPLING_RATE = 8000;
// 音声データ再生時の待ち時間(マイクロ秒)
const uint32_t DELAY_INTERVAL = 1000000 / SOUND_SAMPLING_RATE;

// WAVファイルのヘッダー
typedef struct {
    uint32_t riff;              // "RIFF" (0x52494646)
    uint32_t fileSize;          // ファイルサイズ-8
    uint32_t wave;              // "WAVE" (0x57415645)
    uint32_t fmt;               // "fmt " (0x666D7420)
    uint32_t fmtSize;           // fmtチャンクのバイト数
    uint16_t format;            // 音声フォーマット (非圧縮リニアPCMは1)
    uint16_t channels;          // チャンネル数
    uint32_t samplingRate;      // サンプリングレート
    uint32_t avgBytesPerSecond; // 1秒あたりのバイト数の平均
    uint16_t blockAlign;        // ブロックサイズ
    uint16_t bitsPerSample;     // 1サンプルあたりのビット数
    uint32_t data;              // "data" (0x64617461)
    uint32_t dataSize;          // 波形データのバイト数
} wavfileheader_t;
// PCMフォーマット
const uint16_t WAVE_FORMAT_PCM = 0x0001;
// モノラル
const uint16_t WAVE_MONAURAL = 0x0001;

// 音声データ
std::vector<uint8_t> soundData;


// メッセージ出力
void showMessage(const char* message) {
    M5.Lcd.fillScreen(WHITE);
    M5.Lcd.setCursor(5, 30);
    M5.Lcd.setTextFont(4);
    M5.Lcd.setTextColor(BLACK);
    M5.Lcd.print(message);
}

// バイトオーダーを入れ替える
uint32_t reverseByteOrder(uint32_t x) {
    return ((x << 24 & 0xFF000000U) |
            (x <<  8 & 0x00FF0000U) |
            (x >>  8 & 0x0000FF00U) |
            (x >> 24 & 0x000000FFU));
}

// WAVファイルのヘッダーを検証する
bool validateWavHeader(wavfileheader_t& header) {
    Serial.printf("riff: 0x%x\n", header.riff);
    Serial.printf("wave: 0x%x\n", header.wave);
    Serial.printf("fmt : 0x%x\n", header.fmt);
    Serial.printf("data: 0x%x\n", header.data);
    Serial.printf("format: %d\n", header.format);
    Serial.printf("channels: %d\n", header.channels);
    Serial.printf("samplingRate: %d\n", header.samplingRate);
    Serial.printf("bitsPerSample: %d\n", header.bitsPerSample);

    return  header.riff             == 0x52494646
            && header.wave          == 0x57415645
            && header.fmt           == 0x666D7420
            && header.data          == 0x64617461
            && header.format        == WAVE_FORMAT_PCM
            && header.channels      == WAVE_MONAURAL
            && header.samplingRate  == SOUND_SAMPLING_RATE
            && header.bitsPerSample == PWM_RESOLUTION;
}

// WAVファイルを読み込む
void readWavFile(fs::FS& fs, const char* path, std::vector<uint8_t>& data) {
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if (!file || file.isDirectory()) {
        Serial.println("- failed to open file for reading");
        return;
    }

    // WAVファイルのヘッダー
    wavfileheader_t header;

    // ファイルサイズがヘッダーサイズ以下の場合は終了
    size_t fileSize = file.size();
    if (fileSize <= sizeof(header)) {
        Serial.println("invalid wave file");
        return;
    }

    // ヘッダーサイズ分読み込む
    file.read((uint8_t*)&header, sizeof(header));

    // バイトオーダーを入れ替え
    header.riff = reverseByteOrder(header.riff);
    header.wave = reverseByteOrder(header.wave);
    header.fmt  = reverseByteOrder(header.fmt);
    header.data = reverseByteOrder(header.data);

    // ヘッダーのチェック
    if (!validateWavHeader(header)) {
        Serial.println("invalid wave file header");
        return;
    }

    // ファイルの読み込み
    while (file.available()) {
        data.push_back(file.read());
    }
}

// 音声データを再生する
void playSound(std::vector<uint8_t>& soundData) {
    for (const auto& level : soundData) {
        ledcWrite(PWM_CHANNEL, level);
        delayMicroseconds(DELAY_INTERVAL);
    }

    ledcWrite(PWM_CHANNEL, 0);
}

void setup() {
    M5.begin();
    M5.Lcd.setRotation(1);
    showMessage("SPIFFS WAV");

    // シリアルモニターの設定
    Serial.begin(115200);

    // スピーカーの設定
    ledcSetup(PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
    ledcAttachPin(SPEAKER_PIN, PWM_CHANNEL);
    ledcWrite(PWM_CHANNEL, 0);

    // LEDの設定
    pinMode(M5_LED, OUTPUT);
    digitalWrite(M5_LED, LED_OFF);

    // SPIFFSの設定
    if (!SPIFFS.begin()) {
        Serial.println("SPIFFS Mount Failed");
        return;
    }

    // 音声データを読み込む
    readWavFile(SPIFFS, WAVE_FILE_NAME, soundData);
}

void loop() {
    delay(10);

    // ボタンの状態を更新
    M5.update();

    // Aボタンが押されたら音声データ再生
    if (M5.BtnA.wasPressed()) {
        // LED点灯
        digitalWrite(M5_LED, LED_ON);
        // 音声データ再生
        playSound(soundData);
        // LED消灯
        digitalWrite(M5_LED, LED_OFF);
    }

    // 電源ボタンが押されたらリセット
    if (M5.Axp.GetBtnPress() == AXP_WAS_PRESSED) {
        esp_restart();
    }
}

参考サイト