Spresenseと3軸ジャイロセンサーを使った、チャクラム型ゆる楽器「sound chakram」
はじめに
世の中には数多の楽器があり、様々な楽器を使った演奏会や演奏動画・ライブなどが普及してきています。しかし、楽器の殆どは多くの練習が必要であり、障がい者の方や楽器に馴染みのない人にとっては、演奏は難しかったり、あまり楽しくなかったりするパターンが見受けられます。より多くの人に先入観なく楽器に触れてみてほしい、演奏自体の楽しさを知って音楽に興味を持ってほしいという考えから、Spresenseを利用して「ゆる楽器」を制作しました。
Spresenseと3軸ジャイロセンサーを利用して、「傾ける」動作を用いて音を出力するメインボードを作成し、これをチャクラム状の物体に取り付けたゆる楽器「sound chakram」
の制作過程を示します。
なお、当ゆる楽器の開発は、障がい者の2名のお子さんとそのご家族をパートナーとして、フィードバックなどのご協力を得た上で行っています。
パートナーAさん : 四肢が不自由なお子さん。普段は車椅子を用いて家族と移動している。
パートナーBさん : 聴覚補助として補聴器が必要で、発声器官が不自由なお子さん。普段は手話で会話をしている。
ゆる楽器とは、世界ゆるミュージック協会が定めた言葉で、「人が楽器をやらない理由をなくす新しい楽器」「誰もがすぐに演奏・合奏できる楽器」の事を指します。
コンセプト
「単純な動作で演奏が出来る、触ってみるとなんとなく楽しいゆる楽器」
が作成した楽器のコンセプトです。
これは持論になりますが、私は楽器に限らず物事に興味を持ったり続けたりするコツは「(なんとなく)楽しいこと」であると考えています。そのため、楽器でありながら適当に扱ってもなんとなく楽しく、触ってみたくなるようなデザインである事をメインに据えて楽器を制作しました。
制作する楽器がゆる楽器たり得るために、「障がい者の方も、誰でも演奏が可能」「演奏初心者も演奏しやすい」「音階の制御ができる」の3点を特に留意しました。
結果、ある程度適当に振り回しても大丈夫であり、「なんか触ってみたいな」と思わせるデザインであるチャクラムを形として採用し、そこに「傾き」というトリガーを使って演奏できる機構を内蔵したsound chakram
を作成しました。
チャクラム(chakram)は、古代インドの投擲武器です。
しかし、sound chakramにおいては形のみの採用であるため、投げないでください。
演奏方法
片手でグリップを「握り」「傾ける」だけです。
より端的に言えば、「傾ける」だけ
です。
最も簡単な演奏方法は、チャクラムのグリップを握った状態で手首を右あるいは左に曲げる方法です。
グリップを握ることが難しいユーザーは、チャクラムを固定した物体を回転させる(傾ける)ことによっても演奏が可能です。
現在、鳴らせる音は1オクターブで「ド~シ」までの音階を鳴らすことが出来て、本体を傾ければ傾けるほど「シ」に近い音階になり、最初の角度に戻せばより「ド」の音階に近づきます。
また、「ド~シ」までのそれぞれの音が鳴ったタイミングで、「ド=赤」「レ=オレンジ」「ミ=黄色」といったように、LEDテープが虹色を順に発光します。
「ド」の音色が鳴る角度を任意に調整したい場合は、本体を手前もしくは奥に倒すことによって「ド」の鳴る角度をキャリブレーションすることができます。
なお、現在鳴らすことが出来る音色はビープ音に限られます。
※音を鳴らすためには、事前にSPRESENSE本体をスピーカーと電源に接続する必要があります。
環境によって音が出る最初の瞬間に割れる可能性があるため、
動作をさせる際には、スピーカーの音量を小さくしてから徐々に上げていく事を推奨します。
制作環境など
制作環境
- MacBook Air (BigSur 11.7.10)
- Arduino IDE 2.2.2
用いた材料
名称 | 概要 |
---|---|
SPRESENSEメインボード | Arduino互換のソニーのボードコンピュータ |
SPRESENSE 拡張ボード | Arduino Uno 互換のピンソケットやヘッドホンジャック、micro SD カードスロット等を備えた拡張ボード |
3軸加速度・3軸ジャイロセンサー アドオンボード | Bosch Sensortec製 BMI160(3軸加速度・3軸ジャイロセンサー)BMP280(気圧・温度センサ)を備えたアドオンボードを搭載した基板 |
BTF-LIGHTING WS2812B 1m 144LEDs | 144LEDs/1m のLEDテープ。Arduinoにて個別アドレスまでプログラム可能であり、256高輝度表示と24-bitカラー表示を持っている |
ジャンパー線 | LEDテープとSPRESENSE 拡張ボードを接続するジャンパー線 |
スピーカー | SPRESENSEメインボードにて生成された音源を出力するために用いる。パワード(アクティブ)かつAUX入力があればどれでも可。制作時に使用したのはこのスピーカーです |
AUXケーブル | 拡張ボードのヘッドホンジャックとスピーカーを繋げるためのケーブル |
Micro USB Type-B to USB Type-A | SPRESENSEメインボードに対して電源供給を行うための変換ケーブル |
タンバリン | チャクラムの外周部分として百均のものを利用。軽量かつLEDテープを這わせやすい構造であれば代替品の使用も可 |
その他 | PE系発泡体、養生テープ、両面テープ、滑り止めなど |
仕様
sound chakramの主な処理の流れは次のようになります。
音が鳴るタイミングは、「音階が変化した瞬間」です。
音は鳴りっぱなしではなく、1秒程かけて音量がフェードアウトしていくように鳴ります。
また、音が鳴ったタイミングと同時にLEDが発光します。
こちらも同様に、1秒程かけて明るさがフェードアウトしていくように発光します。
通電時の角度を0度とし、右(左)へ何度傾いたかによって出力される音階が変わります。
0度から180度まで、30度毎に「ド→レ→ミ」のように音階が高くなります。
→例として、0度時点で「ド」が出力され、30度時点で「レ」、60度時点で「ミ」のように音階が変化します。
また、LEDの色は音階に対応しており、「ド=赤」「レ=オレンジ」「ミ=黄色」といったように、虹の色を順に発光します。
また、通電時の角度を0度とし、手前(奥)へ75度傾けると、75度傾いた時点の角度を0度としてキャリブレーションを行うことが出来ます。
キャリブレーションが行われた際には、LEDが白く発光します。
LEDが白く発光した後に音階が変化した場合には、LEDの色は元に戻ります。
sound chakramは現在「傾ける」以外のトリガーを実装しておらず、マルチタスクの難しい方やボタン等のトリガーを使うことが難しいユーザーにも演奏が簡単になっております。
※ プログラムコードは記事の最後に記載しております。
制作
初めに、Spresenseメインボードを拡張ボードにセットし、加速度センサーをSpresenseメインボードに装着します。
LEDテープのコネクタとSpresense拡張ボードを図のようにジャンパー線で繋ぎます。
赤のジャンパー線は5Vへ、緑のジャンパー線はD6へ、白のジャンパー線はGNDへ刺します。
SpresenseをPE系発泡体等で包み、図のような状態にします。
次に、チャクラムの形を作成します。
まず、タンバリンの音が鳴る部分を取り外します。
タンバリンの中心を通るように端から端までちょうど良いサイズの芯を通し、滑り止めなどを芯に巻き付けた上でテープなどを使って固定します。
チャクラムのグリップから垂直になるような位置に先程作成したSpresenseを固定します(PE系発泡体の穴はケーブルを刺す為のものです)。
LEDテープをタンバリンの外周に、空いた穴の中央を通るように沿わせて終点をテープで固定します。
タンバリンの外周部分に両面テープを貼り付け、両面テープの部分に任意の紙や素材を貼り付けて本体は完成となります。
スピーカーと電源に関しては演奏時にPE系発泡体の穴からコードを差し込みます。
動作デモ
-
LEDの点灯例
-
sound chakram 動作デモ
パートナーからのフィードバック
パートナーさん方に対してsound chakramのコンセプトを説明し、デモ動作を確認してもらったところ、次のようなご意見を頂きました。
- 肯定的なご意見
- 握る力をうまくコントロールできないため、ぎゅっと握ったままで使えるところが良い。
- 自分がやっていることと出る音(と光)が対応しているので演奏感がある。
- 操作が容易で良い。
ㅤ
- 改善に関するご意見
- ビープ音だけでなく、色々な音の種類を出せると良い。
- 握る部分のサイズや形を変更できると、より多くの人に使ってもらえそう。
- より軽量化できれば嬉しい。
※当記事の公開後、実際にパートナーさん方にsound chakramを使って演奏をしてもらい、より詳細なフィードバックやアドバイスを頂戴する予定です。
今後の展望
当記事ではここまで、soud chakramの名の通り「チャクラム」の形をした楽器として開発を行ってきましたが、音を出すために必要なのはチャクラムではなくSPRESENSEのメインボード(拡張ボード)本体とスピーカーであり、決してチャクラムの形を取り続ける必要はありません。そのため、「傾き」を用いることができる他の形への転換
も考慮すべき点になります。
例として、車椅子を普段から使っている人には、車輪にSPRESENSEのボードを取り付ける事によって、車輪の回転を「傾き」として検知して演奏することが可能です。
また、現在のプログラムは、音を発するタイミングを「音階の変わり目を検知した場合」にしており、ド→レ→ミ と異なる音階に進む場合は正しく発音が可能ですが、ド→ド→ド のように、同じ音階を続けて発音するパターンの組み込みが未実装
になります。
他にも、ビープ音以外の音を出力する仕組みや、オクターブ変化・半音階を出力するためのトリガーを調査中です。
さいごに
今回は、チャクラム型のゆる楽器、「sound chakram
」を制作しました。
製品として公開できるような完成度まで高めることはできず、プロトタイプの段階での公開となりましたが、少なくともパートナーさん方にとってよく感じて頂けるような製品が出来たのではないかと感じています。
様々な課題が存在するため、これらの課題を解決しつつ、しっかりと「ゆる楽器」足り得る楽器を探していきたいと思います。
プログラム
このプログラムを動作させるにあたり、LEDテープを利用するためのライブラリ「SpresenseNeoPixel-master」と、三軸加速度・ジャイロセンサーを利用するためのライブラリ「BMI160-Arduino-master」を事前にインストールする必要があります。
sound_chakram
#include <Wire.h>
#include <math.h>
#include <BMI160Gen.h>
#include <Audio.h>
#include <SpresenseNeoPixel.h>
#define BAUDRATE 115200
#define SENSE_RATE 200
#define GYRO_RANGE 250
#define LED_PIN 6 // LEDが接続されているピン番号
#define LED_COUNT 144 // 接続されているLEDの数
SpresenseNeoPixel<LED_PIN, LED_COUNT> neopixel;
// 色の設定
float hue = 0.0; // 色相(0.0〜1.0)
float brightness = 0.1; // 明るさ(0.0〜1.0)
float saturation = 1.0; // 彩度(0.0〜1.0)
int col[3]; // RGBの値を格納する配列
int lastPitchStep = -1; // 前回の色調の状態を追跡する変数
unsigned long fadeStartTime = 0; // フェードアウト開始時間
const unsigned long fadeDuration = 500; // フェードアウトの期間(ミリ秒)
bool fadingOut = false; // フェードアウト中かどうか
bool isBeeping = false; // ビープ音が再生中かどうかを示すフラグ
unsigned long beepStartTime = 0; // ビープ音開始時のタイムスタンプ
const unsigned long beepDuration = 1000; // ビープ音のフェードアウト期間(ミリ秒)
int lastToneIndex = -1;
int frequency;
bool isFlashing = false; // LEDが点滅中かどうかを示すフラグ
unsigned long flashStartTime = 0; // 点滅開始時のタイムスタンプ
const unsigned long flashDuration = 200; // 点滅の期間(ミリ秒)
float g_angle_roll = 0;
float g_angle_pitch = 0;
float g_angle_yaw = 0;
uint16_t adjust_usec;
AudioClass *theAudio;
float convertRawGyro(int gRaw) {
float lsb_omega = float(0x7FFF) / GYRO_RANGE;
return gRaw / lsb_omega; // deg/sec
}
void get_gyro_angles(float &roll, float &pitch, float &yaw) {
static unsigned long last_mills = 0;
int rawRoll, rawPitch, rawYaw;
BMI160.readGyro(rawRoll, rawPitch, rawYaw);
unsigned long cur_mills = millis();
unsigned long duration = cur_mills - last_mills;
last_mills = cur_mills;
float fduration = duration / 1000.0; // ms->s
float omega_roll = convertRawGyro(rawRoll);
float omega_pitch = convertRawGyro(rawPitch);
float omega_yaw = convertRawGyro(rawYaw);
g_angle_roll += omega_roll * fduration;
g_angle_pitch += omega_pitch * fduration;
g_angle_yaw += omega_yaw * fduration;
// rollの値が90以上または-90以下の場合、pitchとyawをリセット
if (g_angle_roll >= 65 || g_angle_roll <= -65) {
g_angle_pitch = 0.0;
g_angle_yaw = 0.0;
}
Serial.print("GYRO_roll:");
Serial.print(g_angle_roll);
Serial.print(",");
Serial.print("GYRO_pitch:");
Serial.print(g_angle_pitch);
Serial.print(",");
Serial.print("GYRO_yaw:");
Serial.print(g_angle_yaw);
Serial.println();
roll = g_angle_roll;
pitch = g_angle_pitch;
yaw = g_angle_yaw;
}
void setup() {
Serial.begin(BAUDRATE);
BMI160.begin(BMI160GenClass::I2C_MODE);
BMI160.setGyroRate(SENSE_RATE);
BMI160.setGyroRange(GYRO_RANGE);
BMI160.autoCalibrateGyroOffset();
adjust_usec = (1000 / SENSE_RATE - 2) * 1000;
theAudio = AudioClass::getInstance();
theAudio->begin();
theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, 0, 0);
neopixel.clear();
neopixel.framerate(40);
Serial.println("Start");
}
void loop() {
float roll, pitch, yaw;
get_gyro_angles(roll, pitch, yaw);
// 点滅処理
if (!isFlashing && (roll >= 65 || roll <= -65)) {
isFlashing = true;
flashStartTime = millis();
}
if (isFlashing) {
flash_white_leds();
return; // 点滅中は他の処理をスキップ
}
if (abs(pitch) > 180) pitch = 180;
int toneIndex = map(abs(pitch), 0, 180, 0, 6);
// 音階が変わった場合、ビープ音を再生
if (toneIndex != lastToneIndex) {
lastToneIndex = toneIndex;
play_tone(toneIndex); // ビープ音を再生
}
// ビープ音が鳴っている間、フェードアウト処理を行う
if (isBeeping) {
update_beep_fade_out();
}
int pitchStep = map(abs(pitch), 0, 180, 0, 6);
if (pitchStep != lastPitchStep) { // 色調が変わった場合
lastPitchStep = pitchStep; // 現在の色調を更新
set_led_color(pitch);
fadeStartTime = millis(); // フェードアウト開始時間を記録
fadingOut = true; // フェードアウト開始
} else if (fadingOut) {
update_fade_out(); // フェードアウト処理
}
usleep(adjust_usec);
}
void play_tone(int toneIndex) {
int tones[] = {262, 294, 330, 349, 392, 440, 494, 523}; // ドレミファソラシドの周波数
int frequency = tones[toneIndex]; // 周波数の選択
theAudio->setBeep(true, 0, frequency); // ビープ音を出力、音量は0(最大)
beepStartTime = millis(); // ビープ音開始時間を記録
isBeeping = true; // ビープ音開始
}
void set_led_color(float pitch) {
// 180度を7つの段階に分ける
int pitchStep = map(abs(pitch), 0, 180, 0, 6);
// 各段階に対応する色相を計算
// 赤(0.0)から紫(0.833)までの色相を使用
hue = pitchStep / 7.0;
// HSVからRGBに変換
hsv2rgb(hue, saturation, brightness, col);
// 全てのLEDに同じ色を設定
for (int i = 0; i < LED_COUNT; i++) {
neopixel.set(i, col[0], col[1], col[2]);
}
neopixel.show(); // LEDを更新
}
void flash_white_leds() {
unsigned long currentTime = millis();
if (currentTime - flashStartTime < flashDuration) {
// 点滅期間中は白色で点灯
float whiteHue = 0.0; // 白色の場合、色相は任意
float whiteSaturation = 0.0; // 白色の場合、彩度は0
float whiteBrightness = 0.05; // 明るさは0.1
int white[3];
hsv2rgb(whiteHue, whiteSaturation, whiteBrightness, white);
for (int i = 0; i < LED_COUNT; i++) {
neopixel.set(i, white[0], white[1], white[2]);
}
neopixel.show();
} else {
// 点滅期間終了後は元の色に戻す
isFlashing = false;
g_angle_roll = 0.0;
update_fade_out(); // フェードアウト処理を再開
}
}
void update_beep_fade_out() {
unsigned long currentTime = millis();
if (currentTime - beepStartTime < 1000) { // 1秒間でフェードアウト
float progress = (float)(currentTime - beepStartTime) / 1500.0;
int volume = -20 + progress * (-90 + 20); // -20から-90まで減少
theAudio->setBeep(true, volume, frequency); // 音量を更新
} else {
theAudio->setBeep(false, -90, 0); // ビープ音を停止
isBeeping = false; // ビープ音終了を示すフラグを下ろす
}
}
void update_fade_out() {
unsigned long currentTime = millis();
if (currentTime - fadeStartTime < fadeDuration) {
// フェードアウト中
float progress = (float)(currentTime - fadeStartTime) / fadeDuration;
float currentBrightness = brightness * (1.0 - progress);
hsv2rgb(hue, saturation, currentBrightness, col);
for (int i = 0; i < LED_COUNT; i++) {
neopixel.set(i, col[0], col[1], col[2]);
}
neopixel.show();
} else {
// フェードアウト完了
for (int i = 0; i < LED_COUNT; i++) {
neopixel.set(i, 0, 0, 0); // LEDを消灯
}
neopixel.show();
fadingOut = false; // フェードアウト終了
}
}
float fract(float x) { return x - int(x); }
float mix(float a, float b, float t) { return a + (b - a) * t; }
float step(float e, float x) { return x < e ? 0.0 : 1.0; }
int* hsv2rgb(float h, float s, float b, int* rgb) {
float rgbf[3];
rgbf[0] = b * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s);
rgbf[1] = b * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s);
rgbf[2] = b * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s);
rgb[0] = rgbf[0] * 255;
rgb[1] = rgbf[1] * 255;
rgb[2] = rgbf[2] * 255;
return rgb;
}
float* rgb2hsv(float r, float g, float b, float* hsv) {
float s = step(b, g);
float px = mix(b, g, s);
float py = mix(g, b, s);
float pz = mix(-1.0, 0.0, s);
float pw = mix(0.6666666, -0.3333333, s);
s = step(px, r);
float qx = mix(px, r, s);
float qz = mix(pw, pz, s);
float qw = mix(r, px, s);
float d = qx - min(qw, py);
hsv[0] = abs(qz + (qw - py) / (6.0 * d + 1e-10));
hsv[1] = d / (qx + 1e-10);
hsv[2] = qx;
return hsv;
}
プログラムを作成するにあたって「SPRESENSE と BMI160アドオンボードで傾きを測ってみた!(1)」と、「Spresense Arduino チュートリアル」を参考にしました。
※よりよいコードが出来た際には追記します。
-
Norihiro_Yamamoto
さんが
2024/01/30
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する