M5StickC Plusでバックグラウンド音楽再生

概要

M5StickC Plusに搭載されているブザーを、マルチタスクを利用してバックグラインド再生を試してみました。実際の音楽データはBLE-MIDIを使って、レコーディングしました。

BLE-MIDIでレコーディング

再生する元データを作るって結構面倒ですよね?

なので、今回はBLE-MIDIを使ってパソコンから音データをM5Stick Plusに流し込んで、そのデータを使って再生することにしました。

スケッチ

#include <M5StickCPlus.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

#define MIDI_SERVICE_UUID         "03b80e5a-ede8-4b33-a751-6ce34ec4c700"
#define MIDI_CHARACTERISTIC_UUID  "7772e5db-3868-4112-a1a9-f2669d106bf3"
#define DEVIVE_NAME               "M5StickC"

BLEServer *pServer;
BLEAdvertising *pAdvertising;
BLECharacteristic *pCharacteristic;
int pos = 0;
char midi[5];

uint16_t frequency_list[] = {
  9, // 0
  9, // 1
  9, // 2
  10, // 3
  10, // 4
  11, // 5
  12, // 6
  12, // 7
  13, // 8
  14, // 9
  15, // 10
  15, // 11
  16, // 12
  17, // 13
  18, // 14
  19, // 15
  21, // 16
  22, // 17
  23, // 18
  24, // 19
  26, // 20
  28, // 21
  29, // 22
  31, // 23
  33, // 24
  35, // 25
  37, // 26
  39, // 27
  41, // 28
  44, // 29
  46, // 30
  49, // 31
  52, // 32
  55, // 33
  58, // 34
  62, // 35
  65, // 36
  69, // 37
  73, // 38
  78, // 39
  82, // 40
  87, // 41
  92, // 42
  98, // 43
  104, // 44
  110, // 45
  117, // 46
  123, // 47
  131, // 48
  139, // 49
  147, // 50
  156, // 51
  165, // 52
  175, // 53
  185, // 54
  196, // 55
  208, // 56
  220, // 57
  233, // 58
  247, // 59
  262, // 60
  277, // 61
  294, // 62
  311, // 63
  330, // 64
  349, // 65
  370, // 66
  392, // 67
  415, // 68
  440, // 69
  466, // 70
  494, // 71
  523, // 72
  554, // 73
  587, // 74
  622, // 75
  659, // 76
  698, // 77
  740, // 78
  784, // 79
  831, // 80
  880, // 81
  932, // 82
  988, // 83
  1047, // 84
  1109, // 85
  1175, // 86
  1245, // 87
  1319, // 88
  1397, // 89
  1480, // 90
  1568, // 91
  1661, // 92
  1760, // 93
  1865, // 94
  1976, // 95
  2093, // 96
  2217, // 97
  2349, // 98
  2489, // 99
  2637, // 100
  2794, // 101
  2960, // 102
  3136, // 103
  3322, // 104
  3520, // 105
  3729, // 106
  3951, // 107
  4186, // 108
  4435, // 109
  4699, // 110
  4978, // 111
  5274, // 112
  5588, // 113
  5920, // 114
  6272, // 115
  6645, // 116
  7040, // 117
  7459, // 118
  7902, // 119
  8372, // 120
  8870, // 121
  9397, // 122
  9956, // 123
  10548, // 124
  11175, // 125
  11840, // 126
  12544, // 127
};

int frequency = 0;
int duration = 0;
unsigned long startTime = 0;

class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      M5.Lcd.setTextSize(1);
      M5.Lcd.setCursor(10, 0);
      M5.Lcd.printf("BLE MIDI Connected. ");
    };

    void onDisconnect(BLEServer* pServer) {
      M5.Lcd.setTextSize(1);
      M5.Lcd.setCursor(10, 0);
      M5.Lcd.printf("BLE MIDI Disconnect.");
    }
};

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();
      pos = 0;

      if (rxValue.length() > 0) {
        for (int i = 0; i < rxValue.length(); i++) {
          midi[pos] = rxValue[i];
          pos++;
          if (pos == 5) {
            M5.Lcd.setTextSize(3);
            M5.Lcd.setCursor(10, 30);
            M5.Lcd.printf("%02x %02x %02x", midi[2], midi[3], midi[4]);
            pos = 0;
            if (midi[2] == 0x80) {
              // Note Off
              ledcWriteTone(1, 0);
              int duration = millis() - startTime;
              Serial.printf("{%5d, %5d},\n", frequency, duration);
              frequency = 0;
              startTime = millis();
            } else if (midi[2] == 0x90) {
              // Note On
              if (startTime) {
                int duration = millis() - startTime;
                Serial.printf("{%5d, %5d},\n", frequency, duration);
              }
              frequency = frequency_list[midi[3]];
              ledcWriteTone(1, frequency);
              startTime = millis();
            }
          }
        }
      }
    }
};

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(10, 0);
  M5.Lcd.printf("BLE MIDI Disconnect.");

  BLEDevice::init(DEVIVE_NAME);
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  BLEService *pService = pServer->createService(BLEUUID(MIDI_SERVICE_UUID));
  pCharacteristic = pService->createCharacteristic(
                      BLEUUID(MIDI_CHARACTERISTIC_UUID),
                      BLECharacteristic::PROPERTY_READ   |
                      BLECharacteristic::PROPERTY_WRITE  |
                      BLECharacteristic::PROPERTY_NOTIFY |
                      BLECharacteristic::PROPERTY_WRITE_NR
                    );
  pCharacteristic->setCallbacks(new MyCallbacks());
  pService->start();

  BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
  oAdvertisementData.setFlags(0x04);
  oAdvertisementData.setCompleteServices(BLEUUID(MIDI_SERVICE_UUID));
  oAdvertisementData.setName(DEVIVE_NAME);
  pAdvertising = pServer->getAdvertising();
  pAdvertising->setAdvertisementData(oAdvertisementData);
  pAdvertising->start();

  pinMode(2, OUTPUT);
  ledcSetup(1, 12000, 8);
  ledcAttachPin(2, 1);

}

void loop() {
  delay(1);
}

長いのは音に対する周波数一覧です。他の処理はほぼ昔のブログのままになります。

M5StickC Plusにスケッチを入れて動かして、パソコンからBLEの追加をすると「M5StickC」がいるので追加します。MIDIberryを使って、出力先を「M5StickC」を選択します。左上にあるボタンを押すと鍵盤がでますので、チャンネルを1してから鍵盤を叩くと音がでます。

{    0,  3299},
{  349,   300},
{    0,   240},
{  392,   240},
{    0,   240},
{  440,   780},
{    0,   240},
{  392,   180},
{    0,   300},
{  349,   120},
{    0,   720},
{  349,   239},
{    0,   240},
{  392,   180},
{    0,   240},
{  440,   179},
{    0,   240},
{  392,   179},
{    0,   300},
{  349,   179},
{    0,   300},
{  392,   779},

シリアル出力に、上記のようなログがでますのでこちらを利用します。1つ目の数値が周波数で、2つ目が継続時間です。周波数0が無音になります。

最初の0が長いところは無音なのでカットして、2行目から下がレコーディングされた音楽となります。

バックグラインド再生

再生したいデータをキューに送ることで再生を実現しています。停止はキューの中身を空にしています。他にも方法はあると思いますが、キューを使うのが単純なわりに排他処理などが安全になります。

スケッチ

#include <M5StickCPlus.h>

struct BeepData {
  uint16_t frequency;
  uint16_t duration;
};

BeepData sound1[] = {
  {  349,   300},
  {    0,   240},
  {  392,   240},
  {    0,   240},
  {  440,   780},
  {    0,   240},
  {  392,   180},
  {    0,   300},
  {  349,   120},
  {    0,   720},
  {  349,   239},
  {    0,   240},
  {  392,   180},
  {    0,   240},
  {  440,   179},
  {    0,   240},
  {  392,   179},
  {    0,   300},
  {  349,   179},
  {    0,   300},
  {  392,   779},
};

#define QUEUE_BEEP_LENGTH 1000

QueueHandle_t xQueueBeep;     // キュー
TaskHandle_t taskBeepHandle;  // タスク管理

// タスク
void taskBeep(void *pvParameters) {
  while (1) {
    BeepData data;
    xQueueReceive(xQueueBeep, &data, portMAX_DELAY);
    ledcWriteTone(1, data.frequency); // Play
    delay(data.duration);
    ledcWriteTone(1, 0);              // Mute
  }
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
 
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("Beep Test");
  M5.Lcd.println();
  M5.Lcd.println("BtnA : PLAY");
  M5.Lcd.println("BtnB : STOP");
  
  pinMode(2, OUTPUT);
  ledcSetup(1, 12000, 8);
  ledcAttachPin(2, 1);

  // キュー作成
  xQueueBeep = xQueueCreate(QUEUE_BEEP_LENGTH, sizeof(BeepData));

  // Core1の優先度3でタスク起動
  xTaskCreateUniversal(
    taskBeep,         // タスク関数
    "taskBeep",       // タスク名(あまり意味はない)
    8192,             // スタックサイズ
    NULL,             // 引数
    3,                // 優先度(大きい方が高い)
    &taskBeepHandle,  // タスクハンドル
    APP_CPU_NUM       // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );
}

void loop() {
  M5.update();
  if (M5.BtnA.wasPressed()) {
    // Play
    for (int i = 0; i < sizeof(sound1) / sizeof(BeepData); i++) {
      xQueueSend(xQueueBeep, &sound1[i], 0);
    }
  }
  if (M5.BtnB.wasPressed()) {
    // Stop
    BeepData data;
    while (xQueueReceive(xQueueBeep, &data, 0)) {
      // 空にする
    }
    ledcWriteTone(1, 0); // Mute
  }

  M5.Lcd.setCursor(0, 108);
  M5.Lcd.println(millis());

  delay(1);
}

1000個まで入るキューを作成して、再生したい音のリストを登録すると再生します。マルチタスクで動いていますので、再生中に他の処理もできます。

ちなみに再生時はキューに追加だけしているので、どんどん後ろに追加されています。本当は今のを消してから追加したほうが好ましいと思います。

おまけ UIFlowでのブザー

こんな感じでPWMをGPIO2に鳴らしたい音を周波数にセットして50%で出力することで音ができます。ただし、PWM出力中に再起動をした場合になりっぱなしになります!

初期化処理でPWMを止める処理が入っていないんですよね、、、なのでアプリ起動直後に音を止める処理を入れないと意図せずなりっぱなしになりますので注意してください。

こんな感じで、起動直後にPWMの初期化をして、その直後に停止をすると安全です。音を鳴らす場合には関数化して呼び出すと便利だと思います。処理の途中で音を鳴らすと処理が止まってしまうので、ボタン系のイベントの中か、タイマー処理などを使って別スレッドで動かすようにしたほうがいいと思います。

この辺はUIFlowの標準ブロックで今後サポートしてくれるとは思います!

まとめ

音楽データ作成は本当はMIDIファイル(SMF)を解析しようとして途中まで作業していたのですが、そもそも単音のデータがほとんどないのと、結構変なデータがあったので諦めました。。。

サンプルの音は、へろへろですがあまりちゃんとした曲をいれると、いろいろあれなんで、、、

コメント