楽器を作ってみる¶
本プロジェクトは木箱-kibaco-さんにご協力いただき進めているプロジェクトになります。 栗山町を拠点に活動する[木箱-kibaco-さん]と電子機器を組み合わせた新たな楽器を制作し、町内のライブ活動に活用したい、という個人の課題を、最終的に人口が減少している中で、アーティストを中心に「栗山町 = クリエイティブなまち」を広めることで関係人口創出し、人口増加をしていくという栗山町の課題の解決をに繋げていきたいと思います。
ステッピングモータを楽器として利用する¶
ステッピングモーターは時計の秒針のように、一定の角度ずつ回転するモーターです。 ステッピングモーターが回転する際に発生する振動を利用して音を鳴らしております。
MIDIについて¶
今回プロジェクトを進める上でMIDIについての知識が少なかったため以下のサイトで学習させていただき、とても参考になりました。
電子回路¶
本プロジェクトではArduinoとCNCシールドを用いてステッピングモーターの制御を行っております。
使用したCNCシールドとArduinoのピンの対応は以下になります。
Arduino Uno | CNCシールド |
---|---|
2 | Step Pulse X-Axis |
3 | Step Pulse Y-Axis |
4 | Step Pulse Z-Axis |
5 | Direction X-Axis |
6 | Direction Y-Axis |
7 | Direction Z-Axis |
8 | Stepper Enable/Disable |
ステッピングモータ・モータードライバの設定¶
モータードライバ(DRV8825)の電流調整¶
↓以下の資料が大変参考になります。
参考資料: Drv8825 Stepper Motor Driver Arduino Tutorial
ステッピングモーターに流れる最大電流は、定格電流を超えないように制限する必要がありますので、ドライバーのポテンショメーターで以下のように調整します。
この方法では、「ref」ピンの電圧(Vref)を測定することで電流制限を設定します。
-
NEMA 17 200steps/rev、12V 350mAを使用しているため、定格電流は
-
3本のマイクロステップ選択ピンを外して、ドライバをフルステップモードにします。
-
STEP入力をクロックしないようにして、モーターを定位置に保持する。
-
金属製のトリマーポット自体の電圧(Vref)を測定しながら調整する。
-
5.次の式でVref電圧を調整します。
Current Limit = Vref x 2
Nema 17ステッピングモーターの定格は350mAなので、基準電圧を175mVに調整することになります。
FLIPでファームフェアの書き換え¶
電子楽器を接続する際MIDI接続に比べUSBで接続できる方が汎用性が高いため、Arduinoのファームウェアを書き換え、USB接続のMIDIOUTができる端末にしていきたいと思います。
ArduinoをUSB MIDIデバイスとして設定したい場合、AVRチップのファームウェアを変更する必要があります。その際にATMEL製のファームウェア書き込み用ソフトウェア FLIP 3.4.7を使用します。
アプリのホーム画面は以下になります。
書き込み対象のデバイス名を選択します。
今回はArduino Unoを使用し作成しており、「ATmega16U2」が埋め込まれているためこちらを選択します。
以下のようにマイコンの表面に「ATmega16U2」のように印刷されております。
書き込み対象のマイコン名を選択後、USBマークを選択して「USB」を選択します。
「USB Port Connection」ウィンドウが開くため「Open」を選択します。
選択されるとHEX File欄の (ATMEL)の文字が青くなります。
「File」=>「Load HEX File」から読み込み対象のファイルを選択します。
「midi stepper.hex」を選択します。
「Run」ボタンをクリックします。
書き込みが完了すると、デバイスマネージャーで「MIDI Stepper」が確認できます。
デバイスをArduino Unoとして認識して書き込みできるようにするためには、以下の写真のように2本のピンを繋ぎます。
プログラム¶
プログラムはこちらのプロジェクトを参考にさせていただきました。
このプログラムでは、MIDIピッチ値を周波数に変換し、その周波数に対応するステッピングモーターの速度値を決定します。この速度値は、モーターが指定された周波数の音を生成するために必要なパルスの間隔を表します。
pitchs.hを読み込ませており、プログラムで周波数に対応するステッピングモーターの速度値を決定します。
MIDIメッセージに応じて、各ステッピングモーターの速度を変更することができます。これは、MIDIノートオンメッセージが受信された場合に、ピッチに基づいてスピードを設定します。また、MIDIノートオフメッセージが受信された場合に、モータースピードをゼロに設定します。
#include <MIDI.h>
#include "pitches.h"
#define stepPin_M1 2
#define stepPin_M2 3
#define stepPin_M3 4
#define dirPin_M1 5
#define dirPin_M2 6
#define dirPin_M3 7
#define enPin 8
#define TIMEOUT 10000
unsigned long motorSpeeds[] = {0, 0, 0, 0};
unsigned long prevStepMicros[] = {0, 0, 0, 0};
const bool motorDirection = LOW;
bool disableSteppers = HIGH;
unsigned long WDT;
MIDI_CREATE_DEFAULT_INSTANCE();
void setup()
{
pinMode(stepPin_M1, OUTPUT);
pinMode(stepPin_M2, OUTPUT);
pinMode(stepPin_M3, OUTPUT);
pinMode(dirPin_M1, OUTPUT);
pinMode(dirPin_M2, OUTPUT);
pinMode(dirPin_M3, OUTPUT);
digitalWrite(dirPin_M1, motorDirection);
digitalWrite(dirPin_M2, motorDirection);
digitalWrite(dirPin_M3, motorDirection);
pinMode(enPin, OUTPUT);
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
}
void loop()
{
MIDI.read();
digitalWrite(enPin, disableSteppers);
singleStep(1, stepPin_M1);
singleStep(2, stepPin_M2);
singleStep(3, stepPin_M3);
if (millis() - WDT >= TIMEOUT)
{
disableSteppers = HIGH;
}
}
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
disableSteppers = LOW;
motorSpeeds[channel] = pitchVals[pitch];
}
void handleNoteOff(byte channel, byte pitch, byte velocity)
{
motorSpeeds[channel] = 0;
}
void singleStep(byte motorNum, byte stepPin)
{
if ((micros() - prevStepMicros[motorNum] >= motorSpeeds[motorNum]) && (motorSpeeds[motorNum] != 0))
{
prevStepMicros[motorNum] += motorSpeeds[motorNum];
WDT = millis();
digitalWrite(stepPin, HIGH);
digitalWrite(stepPin, LOW);
}
}
MIDIライブラリとpitches.hを読み込んでます。
#include <MIDI.h>
#include "pitches.h"
ステッピングモータ1~3のstepピン、dirピンの番号をわかりやすいように定義します。
#define stepPin_M1 2
#define stepPin_M2 3
#define stepPin_M3 4
#define dirPin_M1 5
#define dirPin_M2 6
#define dirPin_M3 7
(EN)ピンにHIGHの信号が入力されると、モーターが動作します。8番ピンをわかりやすいようにenPinという名前で定義しております。
#define enPin 8
プログラムで使用されるタイムアウト値を定義するために必要な行です。このタイムアウト値は、何らかの処理が完了するまでに許容される最大待機時間、(このプログラムの場合10秒)を決定します。
#define TIMEOUT 10000
それぞれ3つのステッピングモータの速度と前のステップの時間を記録して格納するための配列を宣言しています。
motorSpeeds配列は、各モータの速度をマイクロ秒単位で格納するために使用されます。各要素は、各ステッピングモータの現在の速度を保持するために使用します。
prevStepMicros配列は、各モータの前回のステップ時刻を格納するために使用されます。これらの値は、次のステップの間隔を決定するために使用されます。前回のステップからの経過時間は、現在の時間と前回のステップ時刻の差分を取ることで計算されます。
それぞれ配列の一つ目の要素は使用しません。(index番号とモータの番号を合わせるため。)
unsigned long motorSpeeds[] = {0, 0, 0, 0};
unsigned long prevStepMicros[] = {0, 0, 0, 0};
以下の2つの変数は、ステッピングモーターの制御に関する情報を格納するために使用します。 motorDirectionは、ステッピングモーターの回転方向を指定するための変数で、LOWに設定されている場合は、モーターが正方向(通常は時計回り)に回転し、HIGHに設定されている場合は、逆方向(通常は反時計回り)に回転します。
disableSteppersは、ステッピングモーターの動作を有効または無効にするためのフラグで、HIGHに設定されている場合は、モーターは動作しなくなります。この変数は、handleNoteOn関数によってLOWに設定され、モーターを動作させるようになります。また、一定時間(TIMEOUTで定義された値)が経過すると、loop関数内の条件文によって再びHIGHに設定され、モーターを停止させます。
const bool motorDirection = LOW;
bool disableSteppers = HIGH;
WDT(Watchdog Timer)は、モーターを動かすステッピングモータードライバーが正しく動作していることを確認するために使用されます。この変数は、最後にステップが実行された時間を記録し、それが一定期間(TIMEOUT変数で定義されています)更新されなかった場合、モータードライバーが無効になります。これは、モータードライバーに問題がある場合、例えばステッピングモーターが詰まっている場合、モーターが破損するのを防ぐために役立ちます。
unsigned long WDT;
MIDIライブラリを使用するため、MIDIライブラリで定義されたデフォルト設定を使用して、MIDIインスタンスを作成します。
MIDI_CREATE_DEFAULT_INSTANCE();
次に、void setup() 関数の中の処理
このプログラムの setup() 関数で初期化を行います。 まず、pinMode() 関数が使用して、各ステッピングモーターのステップピンと方向ピンを出力ピンに設定します。
pinMode(stepPin_M1, OUTPUT);
pinMode(stepPin_M2, OUTPUT);
pinMode(stepPin_M3, OUTPUT);
pinMode(dirPin_M1, OUTPUT);
pinMode(dirPin_M2, OUTPUT);
pinMode(dirPin_M3, OUTPUT);
次に、各モーターの方向を設定します。ここで、digitalWrite() 関数を使用して、各方向ピンに motorDirection 変数の値を書き込んでいます。motorDirection の初期値は LOW であるため、すべてのモーターは同じ方向に回転するように設定されます。
digitalWrite(dirPin_M1, motorDirection);
digitalWrite(dirPin_M2, motorDirection);
digitalWrite(dirPin_M3, motorDirection);
タイムアウトが発生した場合にステッパーモータを無効にするために使用します。プログラムが長時間実行される場合、ステッパーモータを継続的に動かす必要がありますが、これはモータにとっては負荷が大きく、長時間使用すると過熱する場合があるため、一定の時間経過後、自動的にステッパーモーターを停止する必要があります。
このコードでは、millis()関数を使用して現在の時間とWDT変数の値の差を計算し、TIMEOUT定数で設定された時間(10秒)よりも大きい場合に、disableSteppers変数をHIGHに設定しています。その後、この変数はステッパーモーターを無効にするために使用されます。このようにして、プログラムの実行時間が長くなっても、ステッピングモータが安全に制御できるようにします。
if (millis() - WDT >= TIMEOUT)
{
disableSteppers = HIGH;
}
次に、Enピンを出力モードに設定します。
pinMode(enPin, OUTPUT);
MIDI ライブラリの初期化を行います。MIDI.begin() 関数は、MIDI 通信を開始し、MIDI_CHANNEL_OMNI を指定して、すべての MIDI チャンネルからの MIDI イベントを受信するように設定します。また、MIDI.setHandleNoteOn() 関数と MIDI.setHandleNoteOff() 関数は、ノートオンイベントとノートオフイベントを処理するためのコールバック関数を設定します。
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
次にvoid loop()内の処理を見ていきます。 このプログラムは、MIDIからの入力に応じてステッピングモーターを制御し、タイムアウトが発生した場合はモータを無効にする処理となっております。
MIDI.read() 関数を使用して、MIDIデータを読み取ります。
MIDI.read();
digitalWrite()関数を使用して、enPinピンの状態をdisableSteppers変数によって制御します。
digitalWrite(enPin, disableSteppers);
singleStep()関数を使用して、3つのステッピングモーターを制御します。 singleStep()関数はこの下で説明しております。
singleStep(1, stepPin_M1);
singleStep(2, stepPin_M2);
singleStep(3, stepPin_M3);
millis() 関数を使用して、WDT変数を更新します。 TIMEOUT(10秒)を過ぎた場合、disableSteppers変数をHIGHに設定して、モータを無効にします。
if (millis() - WDT >= TIMEOUT)
{
disableSteppers = HIGH;
}
handleNoteOn関数は、MIDI信号のノートオンメッセージを処理するためのハンドラーです。モータを有効にするために disableSteppers 変数を LOW に設定し、pitchVals 配列からピッチ値を取得して motorSpeeds 配列の対応するチャンネルに保存します。ピッチ値は、MIDIノート番号に対応しています。
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
disableSteppers = LOW;
motorSpeeds[channel] = pitchVals[pitch];
}
この関数は、MIDIノートオフコマンドが受信されたときに呼び出されます。MIDIノートオフコマンドは、MIDIノートオンコマンドとは対照的に、特定のノートが放されたことを示します。この関数では、引数として渡されたチャンネルとピッチのインデックスを使用して、ステッピングモータの速度を0に設定します。これにより、ノートオフコマンドが送信されたときに、モーターが停止するようになります。
void handleNoteOff(byte channel, byte pitch, byte velocity)
{
motorSpeeds[channel] = 0;
}
singleStep()関数で1つのステッピングモーターを1ステップだけ動かします。関数には2つの引数があります。motorNumはモータの番号を、stepPinはステップパルスを生成するピン番号を指定します。
関数内部では、現在の時間(マイクロ秒単位)と前回のステップからの経過時間を比較して、指定された速度に達したかどうかを確認します。速度に達している場合は、前回のステップ時間に速度を加算し、ウォッチドッグタイマーをリセットし、ステップパルスを生成します。ステップパルスは、ステップピンをHIGHに設定してからLOWに戻すことによって生成されます。ステップパルスを生成することで、モータは1ステップ進みます。
void singleStep(byte motorNum, byte stepPin)
{
if ((micros() - prevStepMicros[motorNum] >= motorSpeeds[motorNum]) && (motorSpeeds[motorNum] != 0))
{
prevStepMicros[motorNum] += motorSpeeds[motorNum];
WDT = millis();
digitalWrite(stepPin, HIGH);
digitalWrite(stepPin, LOW);
}
}
------------ UPDATE ----------------
MIDIノートイベントに基づいてステッピングモータを制御し、各モータが最大3つのノートを同時に扱えるようにアップデートしました。
モーター1が動いている間に別の鍵盤を押すとモーター2が動き、モーター2が動いている間に別の鍵盤を押すとモーター3が動き、モーター3が動いている間に別の鍵盤を押すと再びモーター1が動くようになります。
#include <MIDI.h>
#include "pitches.h"
#define stepPin_M1 2
#define stepPin_M2 3
#define stepPin_M3 4
#define dirPin_M1 5
#define dirPin_M2 6
#define dirPin_M3 7
#define enPin 8
#define TIMEOUT 10000
const int MAX_NOTES = 3;
unsigned long motorSpeeds[][MAX_NOTES] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
unsigned long prevStepMicros[][MAX_NOTES] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
const bool motorDirection = LOW;
bool disableSteppers = HIGH;
unsigned long WDT;
byte currentMotor = 0;
MIDI_CREATE_DEFAULT_INSTANCE();
void setup()
{
pinMode(stepPin_M1, OUTPUT);
pinMode(stepPin_M2, OUTPUT);
pinMode(stepPin_M3, OUTPUT);
pinMode(dirPin_M1, OUTPUT);
pinMode(dirPin_M2, OUTPUT);
pinMode(dirPin_M3, OUTPUT);
digitalWrite(dirPin_M1, motorDirection);
digitalWrite(dirPin_M2, motorDirection);
digitalWrite(dirPin_M3, motorDirection);
pinMode(enPin, OUTPUT);
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
}
void loop()
{
MIDI.read();
digitalWrite(enPin, disableSteppers);
singleStep(0, stepPin_M1);
singleStep(1, stepPin_M2);
singleStep(2, stepPin_M3);
if (millis() - WDT >= TIMEOUT)
{
disableSteppers = HIGH;
}
}
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
disableSteppers = LOW;
for (int i = 0; i < MAX_NOTES; i++) {
if (motorSpeeds[currentMotor][i] == 0) {
motorSpeeds[currentMotor][i] = pitchVals[pitch];
break;
}
}
currentMotor = (currentMotor + 1) % 3;
}
void handleNoteOff(byte channel, byte pitch, byte velocity)
{
for (int motorIndex = 0; motorIndex < 3; motorIndex++) {
for (int i = 0; i < MAX_NOTES; i++) {
if (motorSpeeds[motorIndex][i] == pitchVals[pitch]) {
motorSpeeds[motorIndex][i] = 0;
break;
}
}
}
}
void singleStep(int motorIndex, byte stepPin)
{
for (int i = 0; i < MAX_NOTES; i++) {
if ((micros() - prevStepMicros[motorIndex][i] >= motorSpeeds[motorIndex][i]) && (motorSpeeds[motorIndex][i] != 0))
{
prevStepMicros[motorIndex][i] += motorSpeeds[motorIndex][i];
WDT = millis();
digitalWrite(stepPin, HIGH);
digitalWrite(stepPin, LOW);
}
}
}
unsigned long motorSpeeds[][MAX_NOTES] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
unsigned long prevStepMicros[][MAX_NOTES] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
上記でそれぞれ3つのモーターに対応する2次元配列を定義しました。MAX_NOTESは3で定義しております。これらの配列は、モーターごとに最大3つのノートを同時に管理できるようアップデートしました。
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
disableSteppers = LOW;
for (int i = 0; i < MAX_NOTES; i++) {
if (motorSpeeds[currentMotor][i] == 0) {
motorSpeeds[currentMotor][i] = pitchVals[pitch];
break;
}
}
currentMotor = (currentMotor + 1) % 3;
}
アップデート後は、for (int i = 0; i < MAX_NOTES; i++) のループを使って、現在のモーターの各ノートスロットをチェックし、if (motorSpeeds[currentMotor][i] == 0) で、空きノートスロット(0の値を持つスロット)を見つけると、そのスロットに新しいノートのピッチ値を格納するようにしました。 新しいノートを格納した後、break; でループを終了します。これにより、最初に見つかった空きスロットにのみ新しいノートが格納されます。
3つのモーター間で新しいノートを循環させるため、currentMotor = (currentMotor + 1) % 3; で、次のノートオンイベントに対して次のモーターを選択するようにしております。
void handleNoteOff(byte channel, byte pitch, byte velocity)
{
for (int motorIndex = 0; motorIndex < 3; motorIndex++) {
for (int i = 0; i < MAX_NOTES; i++) {
if (motorSpeeds[motorIndex][i] == pitchVals[pitch]) {
motorSpeeds[motorIndex][i] = 0;
break;
}
}
}
}
アップデート後は、for (int motorIndex = 0; motorIndex < 3; motorIndex++) で、3つのモーターのそれぞれに対して処理を行っております。
内側のループ for (int i = 0; i < MAX_NOTES; i++) で、各モーターのノートスロットをチェックしております。
if (motorSpeeds[motorIndex][i] == pitchVals[pitch]) で、ノートオフイベントのピッチ値と一致するノートを見つけると、そのスロットの値を0にリセットします: motorSpeeds[motorIndex][i] = 0;。これにより、そのノートが終了したことが示されます。
対応するノートを見つけてリセットした後、break; で内側のループを終了します。これにより、見つかった最初の一致するノートのみがリセットされます。
void singleStep(int motorIndex, byte stepPin)
{
for (int i = 0; i < MAX_NOTES; i++) {
if ((micros() - prevStepMicros[motorIndex][i] >= motorSpeeds[motorIndex][i]) && (motorSpeeds[motorIndex][i] != 0))
{
prevStepMicros[motorIndex][i] += motorSpeeds[motorIndex][i];
WDT = millis();
digitalWrite(stepPin, HIGH);
digitalWrite(stepPin, LOW);
}
}
}
アップデート後は、for (int i = 0; i < MAX_NOTES; i++) で、各モーターのノートスロットをループ処理しております。 if ((micros() - prevStepMicros[motorIndex][i] >= motorSpeeds[motorIndex][i]) && (motorSpeeds[motorIndex][i] != 0)) で、現在のノートスロットがアクティブである(motorSpeeds[motorIndex][i] != 0)、かつステップを実行するタイミングが来たことをチェックしております。(micros() - prevStepMicros[motorIndex][i] >= motorSpeeds[motorIndex][i])。
Domino(MIDI音楽ソフト)と連動させる¶
Dominoを起動させると以下のようが画面が表示されます。
「ファイル」=>「環境設定」を開きます。
「MIDI-OUT」の項目で「ポート A」「MIDI OUT デバイス」に「MIDI Stepper」を選択します。
「トラックセレクトペインの表示切り替え」を選択すると左側にチャンネル設定の表示がされます。
今回は3つのステッピングモーターを使用しており、それぞれA-01, A-02, A-03の3つのチャンネルで制御しております。
また、キーボードなどのデバイスを接続する際には「MIDI-IN」の項目で対象のデバイス(今回はmicroKEY-25)を選択することができます。
ボディ設計¶
- プロトタイプ1
以下のようにFusion360でモデリングしました。
- ステッピングモーターの音を拡張させるため、入口が細く、出口が広がっていく構造を作成しました。
2.5mmの薄いMDFを敷いてその上にステッピングモーターを置いて振動音を増幅させるように作成しました。
レーザー加工機でパーツをカットしました。
レーザー加工機で以下のようにMDF5.5mmを重ね合わせてスピーカーを作成し、MDF2.5mmの板にステッピングモーターを固定しました。
以下のようにキーボードとステッピングモーターを接続します。
以下のようにキーボードと楽器をPCに接続して、キーボード(インプット)で弾いた音をデバイス(アウトプット)で演奏することができます。
パッケージング¶
マイコンボード、配線などを収納するため、箱の大きさを拡大し収納できるようにしました。
評価¶
チェックリスト¶
- 本プロトタイプはモータの振動による音を利用した楽器を作成できている
- 対象者が容易に楽器を使用することができる
データ¶
部品¶
部品 | 販売先 | 金額 | 必要個数 | 合計金額 | リンク |
---|---|---|---|---|---|
Arduino Uno R3 | switch science | ¥3,960 | 1個 | ¥3,960 | Link |
CNC Shield | Amazon | ¥689 | 1個 | ¥689 | Link |
ステッピングモータ(Stepper Motor) Nema 17 Bipolar 40mm | Amazon | ¥3,378 | 3個 | ¥10134 | Link |
DRV8825 4層 ステッパ モータ ドライバ モジュール | Amazon | ¥180 | 3個 | ¥540 | Link |
AC to DC 12V 2A アダプター | Amazon | ¥1,398 | 1個 | ¥1,398 | Link |