K_Yokoyamaのアイコン画像
K_Yokoyama 2024年01月29日作成 (2024年01月31日更新)
製作品 製作品 閲覧数 351
K_Yokoyama 2024年01月29日作成 (2024年01月31日更新) 製作品 製作品 閲覧数 351

Spresenseと距離センサを使った「メロディペンダント」

Spresenseと距離センサを使った「メロディペンダント」

はじめに

皆さんは「ゆる楽器」という概念をご存知ですか?

この言葉は世界ゆるミュージック協会が提唱する、「ゆるい」と「楽器」を組み合わせた言葉で、誰でも簡単に演奏できる気軽で楽しい楽器のことです。

https://yurumusic.com/about/

特別な技術や練習がなくても、リズム感や音感がなくても誰でもすぐに簡単に音楽を楽しむことができるのがゆる楽器の特徴です。

今回はSpresenseと超音波距離センサを使ってゆる楽器の制作に取り組みました。

パートナーさんについて

今回のプロジェクトは2組のご家庭にパートナーとして協力してもらいながら開発を進めています。

どちらのご家庭にも小学生のお子さんがいらっしゃいます。二人の子供達と親御さんとも意見を交換しながら、誰でも楽しむことができるメディア遊び、すなわち「ゆる楽器」を作ることが本プロジェクトの最終的な目標となっています。

開発の初期段階では実際に対面でリサーチを行い、さまざまな道具を用意してどんな楽器や音に興味があるのか、どのような機能なら使いやすいと思ってもらえるのかを事前に考えた上でアイデアを考えました。開発の途中でもフィードバックをいただき、かなり制作の参考にさせていただいています。

コンセプト

今回の制作にあたってテルミンという楽器を参考にしました。これは両手を空間にかざして動かすだけで音を制御することができる電子楽器です。

https://ja.wikipedia.org/wiki/テルミン

この楽器の手を空間で動かすだけで不思議な音が鳴るという点が面白いと感じました。これをもっと簡単に場所を取らずに体験できたらと思い、手をかざすだけで音が鳴るという要素と場所を取らないという要素を中心に、次のようなコンセプトを考えました。

〜首からかけて手を体の前で動かすだけのテルミン風ペンダント型楽器〜
「メロディペンダント」

距離センサをペンダントにして首からかけることで、手を体の前で動かすだけで音が鳴る、そんな楽器を目指しました。

使用したもの

名称 概要
Spresenseメインボード メイン処理を行うマイコン、Arduino 互換あり
Spresense拡張ボード Spresenseメインボード用のアドオン、機能や接続を追加できる
Mic&LCD KIT for SPRESENSE LCDのみ使用、スイッチ 4個付き2.2インチ液晶基板、ili9341 ドライバ搭載
超音波距離センサhc-sr04 超音波の反射時間を利用して非接触で距離を測定することができる
可変抵抗半固定ボリューム 音のテンポ変更に使用、抵抗値10kΩ、抵抗誤差±10%
3Dプリンタ Purusa i3 MK3 スピーカーのケースの製作
PLA 3Dプリンタの素材、射出成形加工ができる熱可塑性プラスチック
レーザーカッター torotec speedy 100 Spresense等を格納する本体部分の制作
MDF(5mm、2.5mm) 本体部分の素材、木材と樹脂を混ぜた板材
USBミニスピーカー USB電源で0.35mmステレオミニプラグ、3Wスピーカーが2つ、百均で購入

仕組み

本作品の基本的な仕組みは以下の通りです。

基本の仕組み

首からぶら下げた超音波センサが前方向の距離を計測し、その前で手を前後させることで距離に対応した音が鳴ります。

音と距離の対応関係

音は立ち上がってからフェードアウトまでが、一定の秒数で繰り返し鳴ります。つまみを回すことでその速さを調節することができます。
本来音をシンセサイズする場合はAttack Time(アタックタイム)、Decay Time(ディケイタイム)、Sustain Level(サステインレベル)、Release Time(リリースタイム)のADSRの要素を考えるヒッツ用があります。(以下の画像参照)

音の要素

https://jp.yamaha.com/products/contents/music_production/guide_to_synth/003/index.html

しかし、検証の結果ビープ音が短くなると「プツッ」といったノイズ感のある音が発生しやすく、リリースタイムをある程度確保しないとフェードアウトしているかどうかが聞こえないような音になってしまいました。そこで今回はサステインレベルをなくし、以下のような設定で音を鳴らしています。

音の仕組み

それぞれの音に対応して棒人間のアニメーションがLCDに表示され、まるで棒人間が踊っているように見えます。無音状態だと頭と体だけですが、音が鳴ると手足が描画されます。

音とポーズの対応関係

制作

センサを格納するペンダント部分は3Dプリンタで制作しました。なるべくセンサが動かないようにサイズちょうどのケースにして、百均で売っている首掛けひもを取り付けられるように、穴を取り付けています。

ペンダント型センサーの作成

Spresenseや配線はMDFを箱状に組み立てられるように切り出して制作しました。モニタ用の穴とイヤホンジャック用の穴が側面に空いています。ボリュームスイッチは側面のブレッドボード上にあります。

本体

スピーカー接続状態

全体の回路図は以下の通りです。
プログラムコードは記事の最後にあります。

回路図

デモ動画

ここに動画が表示されます

パートナーさんからの意見

12月時点での進捗共有の際にはパートナーさん達から以下のような意見をいただきました。

【肯定的な意見】

  • 首からぶら下げるというアイデアがいい。
  • センサのケースがロボットのように見えて可愛い。

【アドバイス】

  • ペンダントにある程度の重さがあると嫌悪感につながる可能性がある。
  • 音だけでなく光など、別のフィードバックがあった方が楽しそう。(この時点ではLCDは未実装だった)

首から下げるというアイデアに共感していただいた一方で、体に密着するものになるので使いやすさや重さへの不安を感じていたという印象があります。

これらの意見は実際にかなり参考にさせていただきました。

工夫した点

センサをペンダントとして独立させたことで、実際に演奏をするときはペンダントを首からかけて手を体の前で前後させるだけで音を鳴らせます。複雑な手の動きが全く必要なく、感覚で演奏ができます。

また、パートナーさんの意見をもとに、LCDを用いた映像での出力を追加したことで自分の動きに対して音と映像でフィードバックを得ることができます。

今後の課題

センサ部分はSpresenseと有線で繋がっているので、本当の意味でどこでも使えるようにするにはセンサの無線化が望ましいです。ですがこれは同時にペンダントの重量が増える可能性が大きいのでセンサパーツの規格から考え直す必要があると考えています。

また映像によるフィードバックについては非常に簡単な棒人間表示にとどまっているので、「ペンダント部分がロボットに見えてかわいい」というパートナーさんの意見を参考に、ロボットが表示されるような映像を目指したいです。

さらに、シンセサイズをさらに詳細に行う、もしくはmp3の再生機能などを用いてもっと綺麗な音が鳴るようにしたいと思っています。

プログラムコード

イヤホンジャックへビープ音を出力するためのAudioライブラリに加え、LCDの描画を行うためのライブラリとして以下のサイトからAdafruit-ILI9341-spresense.zipとAdafruit-GFX-Library-spresense.zipをインクルードする必要があります。

https://github.com/kzhioki

Spresense用ソースコード

#include <Audio.h> #include "SPI.h" #include "Adafruit_GFX.h" #include "Adafruit_ILI9341.h" #define trigPin 2 #define echoPin 3 #define TFT_DC 9 #define TFT_CS 10 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); AudioClass *theAudio; int ptone = 1; void playToneWithFade(int tone, double fadeOutTime) { double volume = -70; //初期音量 double stepTime = fadeOutTime / 100; //音量を下げるステップの時間 theAudio->setBeep(1, volume, tone); for (int i = 0; i < 10; i++) { //音を立ち上げる volume += 6; theAudio->setBeep(1, volume, tone); delay(stepTime); } for (int i = 0; i < 10; i++) { //最大音量から少し下げる volume -= 2; theAudio->setBeep(1, volume, tone); delay(stepTime); } for (int i = 0; i < 80; i++) { //だんだんフェードアウトする volume -= 0.5; theAudio->setBeep(1, volume, tone); delay(stepTime); } theAudio->setBeep(0, volume, tone); //音を止める } void setup() { Serial.begin (9600); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); theAudio = AudioClass::getInstance(); theAudio->begin(); puts("initialization Audio Library"); theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, 0, 0); tft.begin(); } void loop() { long duration, distance; int tone = 0; int flag = 0; //音を変更するかどうかの確認のフラグ digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); duration = pulseIn(echoPin, HIGH); //距離を計測 distance = duration / 58; //cmに変換 int val; val = analogRead(0); //可変抵抗の値をとる if(val < 100) { //テンポ小さすぎると音が出ないので100以上に調節 val = 100; } double time = val * 2; //秒に変換(概算) int centerX = tft.width() / 2; int centerY = tft.height() / 2; int radius = min(tft.width(), tft.height()) / 5; // 頭の半径 int armparts = radius / 1.5; //腕の長さの基準 int legparts = radius / 1.3; //足の長さの基準 tft.drawCircle(centerX, centerY + radius * 2, radius, ILI9341_WHITE); //頭を描画 tft.drawLine(centerX, centerY + radius, centerX, centerY - radius, ILI9341_WHITE); //体を描画 if (distance <= 8 && ptone != tone) { //ドの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX - armparts * 2, centerY + armparts * 2, ILI9341_WHITE); //右腕 tft.drawLine(centerX, centerY + armparts / 2, centerX + armparts * 2, centerY + armparts * 2, ILI9341_WHITE); //左腕 tft.drawLine(centerX, centerY - radius, centerX - legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //右足 tft.drawLine(centerX, centerY - radius, centerX + legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //左足 tone = 262;//Do flag = 1; } else if(distance <= 12 && ptone != tone) { //レの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX + radius, (centerY + armparts / 2) + radius, ILI9341_WHITE); //右腕1 tft.drawLine(centerX + radius, (centerY + armparts / 2) + radius, centerX + radius + 20, (centerY + armparts / 2) + radius * 2, ILI9341_WHITE); //右腕2 tft.drawLine(centerX, centerY + armparts / 2, centerX - radius, (centerY + armparts / 2) - radius, ILI9341_WHITE); //左腕1 tft.drawLine(centerX - radius, (centerY + armparts / 2) - radius, centerX - radius - 20, (centerY + armparts / 2) - radius * 2, ILI9341_WHITE); //左腕2 tft.drawLine(centerX, centerY - radius, centerX - legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //右足 tft.drawLine(centerX, centerY - radius, centerX + legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //左足 tone = 294;//Re flag = 1; } else if(distance <= 16 && ptone != tone) { //ミの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX - radius, (centerY + armparts / 2) + radius, ILI9341_WHITE); //右腕1 tft.drawLine(centerX - radius, (centerY + armparts / 2) + radius, centerX - radius - 20, (centerY + armparts / 2) + radius * 2, ILI9341_WHITE); //右腕2 tft.drawLine(centerX, centerY + armparts / 2, centerX + radius, (centerY + armparts / 2) - radius, ILI9341_WHITE); //左腕1 tft.drawLine(centerX + radius, (centerY + armparts / 2) - radius, centerX + radius + 20, (centerY + armparts / 2) - radius * 2, ILI9341_WHITE); //左腕2 tft.drawLine(centerX, centerY - radius, centerX - legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //右足 tft.drawLine(centerX, centerY - radius, centerX + legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //左足 tone = 330;//Mi flag = 1; } else if(distance <= 20 && ptone != tone) { //ファの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX + radius, (centerY + armparts / 2) + radius, ILI9341_WHITE); //右腕1 tft.drawLine(centerX + radius, (centerY + armparts / 2) + radius, centerX + radius + 20, (centerY + armparts / 2) + radius * 2, ILI9341_WHITE); //右腕2 tft.drawLine(centerX, centerY + armparts / 2, centerX - radius, (centerY + armparts / 2) - radius, ILI9341_WHITE); //左腕1 tft.drawLine(centerX - radius, (centerY + armparts / 2) - radius, centerX - radius - 20, (centerY + armparts / 2) - radius * 2, ILI9341_WHITE); //左腕2 tft.drawLine(centerX, centerY - radius, centerX + radius, centerY + radius - 15, ILI9341_WHITE); //右足1 tft.drawLine(centerX + radius, centerY + radius - 15, centerX + radius, centerY - radius, ILI9341_WHITE); //右足2 tft.drawLine(centerX, centerY - radius, centerX - legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //右足 tone = 349;//Fa flag = 1; } else if(distance <= 24 && ptone != tone) { //ソの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX - radius, (centerY + armparts / 2) + radius, ILI9341_WHITE); //右腕1 tft.drawLine(centerX - radius, (centerY + armparts / 2) + radius, centerX - radius - 20, (centerY + armparts / 2) + radius * 2, ILI9341_WHITE); //右腕2 tft.drawLine(centerX, centerY + armparts / 2, centerX + radius, (centerY + armparts / 2) - radius, ILI9341_WHITE); //左腕1 tft.drawLine(centerX + radius, (centerY + armparts / 2) - radius, centerX + radius + 20, (centerY + armparts / 2) - radius * 2, ILI9341_WHITE); //左腕2 tft.drawLine(centerX, centerY - radius, centerX - radius, centerY + radius - 15, ILI9341_WHITE); //右足1 tft.drawLine(centerX - radius, centerY + radius - 15, centerX - radius, centerY - radius, ILI9341_WHITE); //右足2 tft.drawLine(centerX, centerY - radius, centerX + legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //左足 tone = 392;//So flag = 1; } else if(distance <= 28 && ptone != tone) { //ラの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX + radius, (centerY + armparts / 2) + radius, ILI9341_WHITE); //右腕1 tft.drawLine(centerX + radius, (centerY + armparts / 2) + radius, centerX + radius + 20, (centerY + armparts / 2) + radius * 2, ILI9341_WHITE); //右腕2 tft.drawLine(centerX, centerY + armparts / 2, centerX + radius, centerY + armparts / 2, ILI9341_WHITE); //左腕1 tft.drawLine(centerX + radius, centerY + armparts / 2, centerX + radius * 2, centerY + armparts + 15, ILI9341_WHITE); //左腕2 tft.drawLine(centerX, centerY - radius, centerX + radius, centerY - radius - 15, ILI9341_WHITE); //右足1 tft.drawLine(centerX + radius, centerY - radius - 15, centerX + radius - 40, centerY - radius - 50, ILI9341_WHITE); //右足2 tft.drawLine(centerX, centerY - radius, centerX - legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //左足 tone = 440;//Ra flag = 1; } else if(distance <= 32 && ptone != tone) { //シの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX - radius, (centerY + armparts / 2) + radius, ILI9341_WHITE); //右腕1 tft.drawLine(centerX - radius, (centerY + armparts / 2) + radius, centerX - radius - 20, (centerY + armparts / 2) + radius * 2, ILI9341_WHITE); //右腕2 tft.drawLine(centerX, centerY + armparts / 2, centerX - radius, centerY + armparts / 2, ILI9341_WHITE); //左腕1 tft.drawLine(centerX - radius, centerY + armparts / 2, centerX - radius * 2, centerY + armparts + 15, ILI9341_WHITE); //左腕2 tft.drawLine(centerX, centerY - radius, centerX - radius, centerY - radius - 15, ILI9341_WHITE); //右足1 tft.drawLine(centerX - radius, centerY - radius - 15, centerX - radius + 40, centerY - radius - 50, ILI9341_WHITE); //右足2 tft.drawLine(centerX, centerY - radius, centerX + legparts * 2, centerY - radius - legparts * 2, ILI9341_WHITE); //左足 tone = 494;//Si flag = 1; } else if(distance <= 36 && ptone != tone) { //高い方のドの音を鳴らす場合の処理 tft.drawLine(centerX, centerY + armparts / 2, centerX - armparts * 2, centerY + armparts * 2, ILI9341_WHITE); //右腕1 tft.drawLine(centerX, centerY + armparts / 2, centerX + armparts * 2, centerY + armparts * 2, ILI9341_WHITE); //左腕1 tft.drawLine(centerX, centerY - radius, centerX - legparts * 2.2, centerY - radius - 10, ILI9341_WHITE); //右足 tft.drawLine(centerX, centerY - radius, centerX + legparts * 2.2, centerY - radius - 10, ILI9341_WHITE); //左足 tone = 523;//Do2 flag = 1; } if(distance > 36) { //計測距離の表示 Serial.println("Out of range"); } else { Serial.print(distance); Serial.println(" cm"); } if(flag == 1) { //音の再生と画面のリセット playToneWithFade(tone, time); //音を鳴らす ptone = tone; tft.fillScreen(ILI9341_BLACK); //画面を黒でリセット flag == 0; } }
ログインしてコメントを投稿する