Help us understand the problem. What is going on with this article?

A tiny MML parserを使ってArduino UNOで音を鳴らしてみる

はじめに

いまどきのひとはMMLなんて知らないんでしょうけど、昔はPCで音楽を鳴らすために、楽譜を文字列で表現するということをしてました。その時使っていた文字列をMML (Music Macro Laguage) といいました。昔のPCは矩形波3音だったので、文字列で書いても大した量ではなかったんですね。
童謡の「チューリップ」の冒頭はこんな感じ(↓)...

PLAY "O4L8CDERCDERGEDCDEDR","O3L2CRCRGCRC"

ま、ちょっと音楽を鳴らすならこちらのほうが敷居の低い世代もいるのですね。>> 私。
で、簡易的なMMLのパーサー(A tiny MML parser)を作っていらっしゃる方がいたので使わせていただこうと思います。
Arduinoでも実装実績があるというので、ちゃちゃっとやってみましょう。

お題目

  • A tiny MML parser を Arduino 環境にインストール
  • サンプルコードを Arduino UNO で鳴らしてみる。
  • 応用編として、ArduinoのシリアルコンソールからMMLを入力(送信)して鳴らしてみる。

A tiny MML parser の場所

A tiny MML parser @www.cubeatsystems.com/tinymml/
Arduino に限定されていないようですが、MMLを解釈してイベント発生時にコールバックする実装になってます。
Shinichiro Nakamura 様、Special Thanks です!

Arduinoへインストール

今回は、最新版 (Rev 0.50) をインストールします。
上記 URL のdownload ページから tar.gz を取ってきて展開します。

  • ライブラリのインストール

1.1 Arduino のシステムのライブラリフォルダ (C:\Arduino\arduino-1.8.xx\libraries\ か C:\Program Files (x86)\Arduino\libraries\ の下) に tiny_mml_parser などのフォルダを作成する。

1.2. tinymml-0.5/src/lib/ の下にある *.h *.c ファイルを 1.1. で作ったフォルダにコピーする。

1.3. ここで注意: MML を SRAM に置くか、FLASHに置くか、MML.h に定義することになります。デフォルトは SRAMです。

識別子 (MML.h) 意味
MML_CONFIG_USE_AVR_PROGMEM (0) MMLをSRAMに配置している
MML_CONFIG_USE_AVR_PROGMEM (1) MMLをFLASHに配置している

AVRでは読み出し方に差があるため、このような実装になっていると思われます。小規模だけど動的に変更しやすいSRAMと、変更が難しいけど大規模データを扱えるFLASHと選べるのですね。実際の実装と値があっていないと音が鳴らないので、注意。

  • Arduino用のサンプルコードもコピーしてしまいましょう。

2.1. Arduino のシステムのサンプルコードフォルダ (C:\Arduino\arduino-1.8.xx\examples\ か C:\Program Files (x86)\Arduino\examples\ の下) に 12.tiny_mml_parser などのフォルダを作成する。

2.2. tinymml-0.5/src/sample/ の下にある arduino* というフォルダごと 2.1. で作ったフォルダにコピーする。

2.3. これらのサンプルコードは、FLASHにMMLを置くことを前提に作られていますので MML.h を変更する必要があります。1.3. 参照のこと。

サンプルコードのコンパイル

ここでは arduino_chord_example を例に挙げます。
Arduino IDE を起動後、サンプル 12.tiny_mml_parser (先ほど作ったディレクトリ名ですね) の下の arduino_chord_example を選択します。

arduino_chord_example.ino
 // @note Please change MML_CONFIG_USE_AVR_PROGMEM in mml.h to (1) for this sample codes.

とあるとおり、librariesの下にコピーした MML.h の該当行の値を (1)に変えて保存しましょう。

MML.h
#define MML_CONFIG_USE_AVR_PROGMEM  (1)

音を鳴らすためにスピーカーなどをつながないといけませんが、

chord.cpp
#define SOUND_SYSTEM_TONE(FREQ, MS) do { tone(11, (FREQ)); delay(MS); } while (0)
#define SOUND_SYSTEM_REST(MS)       do { noTone(11); delay(MS); } while (0)

と定義されてるとおり、このサンプルコードでは音声(PWM)は tone() 関数を使って pin11 から出てきます。圧電ブザーを pin11 と GND につなげば音が鳴ります。もちろん、アンプ経由させればスピーカーからも鳴ります。

このサンプルコードは 8声まで鳴らせるようになっていますが、同時に鳴らすのではなく、14ミリ秒ごと時分割して鳴らしています。速いアルペジオの繰り返しですね。音数が多いと、本当にアルペジオになってしまいます。

chord.h
#define CHORD_MAX_NOTES             (8)
#define CHORD_SPLIT_TIME_MS         (14)

でも、お手軽に鳴らすには十分だし、選曲もナイスですね~。(歳がバレる...)
このサンプル 実装では、音長0の音符を並べて最後に本当の音長を与えて和声化するようになっています。
例えば、4分音符のドミソの和音を鳴らすために "L0CEG4" ("C0E0G4"と同じ) という与え方をしてます。

シリアル経由でMMLを送って鳴らしてみよう

先の和声発生サンプル (arduino_chord_example.ino) を改良して シリアルコンソールから MMLを打ち込んでインタラクティブに鳴らすことを考えてみましょう。使ったデバイスは Arduino UNOです。音はサンプルコードからいじらずにpin11からのままとします。
+ song.h は不要です。
+ chord.h と chord.cpp はそのまま使います。
+ シリアルコンソールから送ったMMLは、変数(SRAM上) に存在することになるので ライブラリの MML.h の MML_CONFIG_USE_AVR_PROGMEM の値は (0) に戻します。書き換えを忘れると、鳴りません。

MML.h
#define MML_CONFIG_USE_AVR_PROGMEM  (0)

シリアルからの入力は、Serial.readStringUntil() という関数を使いました。こんな関数知りませんでした。すぐtimeoutするようなので、Serial.setTimeout() で 10sec を取ってます。
入力した MML は mmlcmd というString型変数に入れた後、.c_str() を使って String → char * に変換して mml_setup() に渡します。

arduino_mml_via_serial.ino
#include <mml.h>
#include "chord.h"

CHORD chord;
MML mml;
MML_OPTION mml_opt;
String mmlcmd ;

static void mml_callback(MML_INFO *p, void *extobj)
{
  switch (p->type) {
    case MML_TYPE_TEMPO:
      {
        MML_ARGS_TEMPO *args = &(p->args.tempo);
        chord_init(&chord, args->value, mml_opt.bticks);
      }
      break;
    case MML_TYPE_NOTE:
      {
        MML_ARGS_NOTE *args = &(p->args.note);
        chord_note(&chord, args->number, args->ticks);
      }
      break;
    case MML_TYPE_REST:
      {
        MML_ARGS_REST *args = &(p->args.rest);
        chord_rest(&chord, args->ticks);
      }
      break;
    case MML_TYPE_USER_EVENT:
      {
        MML_ARGS_USER_EVENT *args = &(p->args.user_event);
        char *p = args->name;
        while (*p) {
          Serial.write(*p);
          p++;
        }
        Serial.write('\n');
      }
      break;
  }
}

void setup()
{
  Serial.begin(9600);
  Serial.setTimeout(10000UL); // 10sec
  mml_init(&mml, mml_callback, 0);
  MML_OPTION_INITIALIZER_DEFAULT(&mml_opt);
  int tempo_default = 120;
  chord_init(&chord, tempo_default, mml_opt.bticks);
}

void loop()
{
  if (Serial.available()>0) {
    mmlcmd = Serial.readStringUntil('\r');
    // Serial.println(mmlcmd.c_str());
    mml_setup(&mml, &mml_opt, mmlcmd.c_str());
    while (mml_fetch(&mml) == MML_RESULT_OK) {
    }
  }
}

おまけ

オクターブを上下するのに不等号(<, >)を使いますが、上がるのが < か > か、機種によって違いましたよね(昔話)。libraries にある MML.h の下記定義を変えることで変更できるようになってます。

MML.h
#define MML_CONFIG_OCTAVE_REVERSE   (0)

手元に AY-3-8910 (PSG) があるので、作ってみるかな。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした