siroitori0413のアイコン画像
siroitori0413 2024年01月29日作成 (2024年01月30日更新) © CC BY-SA 4+
製作品 製作品 閲覧数 223
siroitori0413 2024年01月29日作成 (2024年01月30日更新) © CC BY-SA 4+ 製作品 製作品 閲覧数 223

Spresenseでコーラス練習用録音マシン

Spresenseでコーラス練習用録音マシン

作品製作の目的と仕様検討

私は長年コーラスを趣味としています。
大人になってからはおかあさんコーラスの合唱団に所属していて毎年大会に出るなどわりと熱心に活動しています。

毎週の練習時にはスマホで録音をして後で聴き返して復習します。ボイスレコーダーを持参する方もいます。私もスマホ文化となる前はずっとボイスレコーダーでした。

そこで!
使いやすい録音デバイスが欲しい、「あ、Spresenseならそれ作れそう」と思い作ることにしました。

現状の録音の問題点

現状の練習時の録音の問題点は以下があげられます。

  • スマホ使うと、充電が心配
  • スマホは着信もあったりするのでなるべく使いたくない

→結論としてスマホじゃない録音専用デバイスがいいです。

また、録音専用デバイスを準備したところで、

  • 録音デバイスを全体の中心に置きたい

皆さん録音デバイスは自分の近くに置くのですが、そうすると自分の声や自分のパートばかり録音されて全体の声のバランスがわからないです。

キャプションを入力できます
※合唱団の人数はもうちょっと多いです

本当は全体の中心の位置(先生の近く)に録音デバイスを置きたいですが、そうすると自分から遠いので録音のON/OFFがさっと行えませんし行ったり来たりしていると練習の迷惑になります。
少し離れた場所から操作できるものをと考えました。

  • 録音したものを聴き返すとき、聴きたい部分だけを抽出したい

他のパートの歌唱指導部分は録音を聴いて復習するときに飛ばしたいのですが早送りしすぎたり戻したりしすぎるので良い部分で切り取った録音が欲しいです。聴き返したい部分をあとで切り取って編集を行ったりもするのですがそれもなかなか手間がかかります。
リアルタイムでほしい部分のタイミングを記録出来てあとでサクッと必要箇所の音声ファイルだけ取り出せるものができたらと考えました。

あとボイスレコーダーを使用されているご年配のお姉さま方は、誤って再生ボタンを押して録音が大きな音で流れ出してテンパってしまいなかなか止められずあたふたする、というのもあるあるの光景です。
あえて録音デバイスに再生機能をつけないようにしようと思いました。

今回の録音マシンの仕様

  • 録音は録り逃しのないよう練習中全部録音(Spresenseで省エネなので長時間録音もいけそう)
  • あとで復習に必要な部分のピックアップタイミング(開始・終了)を記録できるようにする
  • 録音を取り込むときに必要な部分・不必要な部分に切り分けて取得できるようにする(※以降必要な部分をピックアップと記載します)
  • 上記ピックアップの記録は離れた場所からリモコン操作できるようにする
  • あえて本デバイス単体での再生はできないようにする

録音イメージ

試行錯誤

録音できる装置を作る

Spresenseの本(「SPRESENSEではじめるローパワーエッジAI」)とネット情報を参考に、まずはブレッドボードで単純に録音ができる装置を作りました。

録音できる装置をブレッドボードで作成

手持ちのオーディオジャックの部品での接続についてどう配線したらよいか悩みましたが、データシートとにらめっこして配線できました。

その後、録音開始のためのタクトスイッチと、ピックアップのタイミングを記録できる用のタクトスイッチをつけました。

タクトスイッチの追加

Spresense配線図

ここまでできたら、モバイルバッテリーで長い時間の通常録音ができるか歌の練習に持って行って確かめてみました。

しっかり録音できていました。
さすがソニーさんのマイコンとあって音質もとても良いと思いました。

次にブレッドボードはユニバーサル基板(両面スルーホールタイプ)に置き換えました。
また、ピックアップON/OFF状態がわかるようにLEDを付けました。(ON時に点灯)

はんだづけ準備
ユニバーサル基板に両面はんだ付け
リューターでカット
裏面
Spresenseの拡張ボードとドッキング

20240117コーラスの練習音声録音

2024/1/17 この装置で再度練習音声を録音したところ、キーンというノイズがのっていました・・。またブツブツという音ものることがありました。
→その後いろいろ試してみたところ、マイクのジャックとプラグの接触が原因ということが判明。ノイズがのらない位置にプラグを回しておくことで回避することができました。

離れた場所からの制御の実装(赤外線)

離れた場所からピックアップON/OFFのタイミングを記録したいと考えました。

  1. WiFiでルーターを経由して操作
     →モバイルルータなど接続環境が必要。
  2. WiFiでアクセスポイントとして操作
     →スマホなどからブラウザ経由で操作できるようになるがその間スマホがネットにつながらない。別途専用のリモコンデバイスを作成するという手段も。
  3. Bluetoothで操作
     →Bluetoothを使えないといけないが、Spresense用のボードなどを持っていない。
  4. 赤外線リモコンで操作

など考えられましたが、上に書いた理由から2と4でどちらにするか悩みましたがネットにSpresenseで赤外線送受信されている情報があり比較的簡単に実装できそうだったのと、手持ちの電子工作セットに赤外線受信部品(赤外線センサー)とリモコンがあったのでそちらにしました。

▼参考にした情報▼
SPRESENSEアプリケーションをリモコンで動かす方法
https://qiita.com/baggio/items/773d8af926a931ae140f

上記参考情報を参考にSpresenseで赤外線受信するコードを簡単に書き、まずはブレッドボードで確かめてみました。

なお赤外線センサーは、手持ちのものは VS1838B という型番だったのでこれで検索してデータシートを確認したところピン配置が情報のものとは異なっていたので、合うピンに読みかえています。

VS1838B のデータシート
https://html.alldatasheet.jp/html-pdf/1132465/ETC2/VS1838B/111/1/VS1838B.html

赤外線受信をブレッドボードで確認

そして現在の基板に追加で実装しました。オレンジ色のタクトスイッチの左側に見えるのが赤外線センサーです。

赤外線センサーを基板に追加

配線図(赤外線センサー追加)
(最初のブレッドボードでの回路図からピックアップボタンのピン番号(緑色)が変更になっています)

MP3の分割処理

全体の録音ファイルをMP3として保存しているのですが、ピックアップ状態は、録音開始から何秒のタイミングで"ON" "OFF"となったのかという情報をファイルに書き出すようにしました。

・Sound_yyyyMMdd_hhmmss.mp3
・Sound_yyyyMMdd_hhmmss.mp3.txt ‥ピックアップファイルと呼びます

音声ファイルとピックアップファイル

ピックアップファイルは、開始・終了の順でスペース区切りで記録します。
上記の例ですと、開始から3.621秒~7.872秒の間が切り出したいmp3ファイルとなります。
なお、録音ファイルに複数ピックアップ状態がある場合は、
「3.621 7.821 10.051 20.250 …」
のように続けて入っていきます。

この2つのファイルからピックアップ状態の時のみを切り出す処理について、これも本当はSpresenseで行えたらよいのですが、PC上で分割したほうが何倍も速いと思ったためシェルで作成しました。

運用としては、録音&ピックアップ状態記録後出力された2つのファイルをPCに持ってきてから分割シェルを起動して分割します。
ソースコードは後述します。

MP3の分割処理を行うシェル

完成した作品

製作に使用した部品

環境構築

・SDカードにDSPファイルのインストール
・PCへffmpegのセットアップ
・シェル実行環境のセットアップ(Windows)
が必要になります。

SDカードにDSPファイルのインストール

SpresenseにMP3で録音するためにDPSファイルのインストールが必要になります。
詳しくはここでは書きませんが、mp3_enc_installer でインストールできます。

PCへffmpegのセットアップ

PCにffmpegをセットアップします。
私はWindowsマシンを使っておりWindowsにセットアップしています。

シェル実行環境のセットアップ(Windows)

Windowsマシンの場合デフォルト状態ではシェルが起動できないので、Gitをインストールして実行できるようにします。
https://git-scm.com/

ソースコード

SpresenseのArduinoコードはメインコアとサブコア1で構成しています。

SpresenseのArduinoコード(メインコア)

メインコアでは赤外線受信以外の処理をしています。

SpresenseのArduinoコード(メインコア)

/* * my_recorder_main.ino */ // メインコア用 #ifdef SUBCORE #error "Core selection is wrong!!" #endif #include <MP.h> #include <Audio.h> #include <SDHCI.h> #include <RTC.h> /* 録音ボタン */ #define REC_BUTTON (8) /* ピックアップボタンとLED */ #define PICKUP_BUTTON (9) #define PICKUP_LED (10) /* 録音状態に関するフラグ */ bool bIsRecording = false; bool bWasRecording = false; //以前の状態 /* ピックアップ状態に関するフラグ */ bool bPickUpButtonPressed = false; bool bWasPickUpButtonPressed = false; SDClass SD; File myFile; // 録音音声ファイル AudioClass *theAudio = AudioClass::getInstance(); File puFile; // ピックアップファイル /* 録音時間(ミリ秒)ピックアップのタイミングのために記録 */ uint32_t recStartTime = 0; void setup() { Serial.begin(115200); while (!Serial); // LEDピン定義 // ・録音関係 pinMode(LED0, OUTPUT); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); pinMode(LED3, OUTPUT); pinMode(REC_BUTTON, INPUT_PULLUP); // ・ピックアップ関係 pinMode(PICKUP_BUTTON, INPUT_PULLUP); pinMode(PICKUP_LED, OUTPUT); digitalWrite(PICKUP_LED, LOW); //TODO 時刻合わせ(現状コンパイル時刻) // Initialize RTC at first RTC.begin(); // Set the temporary RTC time RtcTime compiledDateTime(__DATE__, __TIME__); RTC.setTime(compiledDateTime); RtcTime now = RTC.getTime(); Serial.print(now.year()); Serial.print("/"); Serial.print(now.month()); Serial.print("/"); Serial.print(now.day()); Serial.print(" "); Serial.print(now.hour()); Serial.print(":"); Serial.print(now.minute()); Serial.print(":"); Serial.println(now.second()); // サブコアの起動 for (int subid = 1; subid <= 1; subid++) { int ret = MP.begin(subid); if (ret < 0) { MPLog("MP.begin(%d) error = %d\n", subid, ret); // エラー dispError(); } } //データ受信をポーリングモードで監視 MP.RecvTimeout(MP_RECV_POLLING); while (!SD.begin()){ MPLog("Insert SD card.\n"); blikPickUpLed(); } } // 外から見てエラー状態と分かるように表示 void dispError(){ // ピックアップLEDを点滅させる while(1){ blikPickUpLed(); } } // ピックアップLEDを点滅させる void blikPickUpLed(){ digitalWrite(PICKUP_LED, HIGH); delay(30); digitalWrite(PICKUP_LED, LOW); delay(30); } void changeRecLed(int lastLedId){ // 録音開始/終了時にSpresense本体のLEDを点灯/消灯する if (lastLedId == 0){ // 引数0のときはLED0のみ点灯/消灯する digitalWrite(LED0, (bIsRecording ? HIGH : LOW)); }else if (lastLedId == 3){ // 引数3のときはLED1~3を点灯/消灯する digitalWrite(LED1, (bIsRecording ? HIGH : LOW)); delay(100); digitalWrite(LED2, (bIsRecording ? HIGH : LOW)); delay(100); digitalWrite(LED3, (bIsRecording ? HIGH : LOW)); } } void startRec(){ MPLog("Start Recording Start\n"); //LED状態を変更 changeRecLed(0); theAudio->begin(); theAudio->setRecorderMode(AS_SETRECDR_STS_INPUTDEVICE_MIC, 21); //48kHzのMP3 theAudio->initRecorder(AS_CODECTYPE_MP3, "/mnt/sd0/BIN" , AS_SAMPLINGRATE_48000, AS_CHANNEL_MONO); RtcTime now = RTC.getTime(); char fileName[25]; sprintf(fileName, "Sound_%04d%02d%02d_%02d%02d%02d.mp3", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()); MPLog("%s\n", fileName); myFile = SD.open(fileName, FILE_WRITE); if (!myFile) { MPLog("File open error\n"); // 録音エラー処理 dispError(); } theAudio->startRecorder(); recStartTime = millis(); // ピックアップファイルも同時に作成 // ピックアップファイル準備 … mp3ファイルの後に.txtをつけたファイル名にする char pufileName[29]; sprintf(pufileName, "%s%s", fileName, ".txt"); MPLog("%s\n", pufileName); puFile = SD.open(pufileName, FILE_WRITE); // ピックアップボタン初期状態 bPickUpButtonPressed = false; //LED状態を変更 changeRecLed(3); MPLog("Start Recording End\n"); } void endRec(){ // 録音LED状態を変更 changeRecLed(0); theAudio->stopRecorder(); theAudio->closeOutputFile(myFile); theAudio->setReadyMode(); theAudio->end(); //while(1); // ピックアップファイルの終了操作 if (bPickUpButtonPressed){ //ピックアップがTrueのまま録音終了された場合 // ピックアップステータスはFalseにする bPickUpButtonPressed = false; // ステータス変更処理 changePickUpStatus(); } // ピックアップファイルを閉じる puFile.close(); //LED状態を変更 changeRecLed(3); } void changePickUpStatus(){ MPLog("changePickUpStatus Start\n"); // ピックアップステータス変わった時に処理 digitalWrite(PICKUP_LED, (bPickUpButtonPressed ? HIGH : LOW)); // ピックアップファイル入出力 puFile char timeWord[9]; //xxxx.xxxx // 経過時間を秒で記録する float val = (float)(millis() - recStartTime) / 1000.0; sprintf(timeWord, "%-1.4f ", val); MPLog("PICKUP CONTENTS WRITE : %s\n", timeWord); puFile.print(timeWord); MPLog("changePickUpStatus End \n"); } // 現在のボタンの状態をチェック void updateButtonPinStatus(int pin, bool* status) { bool current = !(bool)digitalRead(pin); // MPLog("before: %s, current: %s\n", *status ? "t" : "f", current ? "t" : "f"); if (current) { *status = !*status; while (!(bool)digitalRead(pin)) {} delay(100); } } void loop() { bool isErr = false; updateButtonPinStatus(REC_BUTTON, &bIsRecording); updateButtonPinStatus(PICKUP_BUTTON, &bPickUpButtonPressed); if (bIsRecording){ if (!bWasRecording){ // 前回までスタートしてなかったけどスタートした時は録音開始 startRec(); // ピックアップ前回値も初期化 bWasPickUpButtonPressed = false; } // MPLog("Recording.."); err_t err = theAudio->readFrames(myFile); // エラーが発生していた時は次回のループで録音終了にする if (err != AUDIOLIB_ECODE_OK){ isErr = true; } // サブコア1からの受信データを判定 int8_t rcvid; // メインコアの送信ID uint32_t data; // 受信データ int ret = MP.Recv(&rcvid, &data, 1); if (ret > 0){ // データが届いていた if (rcvid == 10){ MPLog("IR Received! rcvid=%d", rcvid); // ピックアップステータスを変更 bPickUpButtonPressed = !bPickUpButtonPressed; } } // ピックアップ判定 if (bPickUpButtonPressed != bWasPickUpButtonPressed){ // ピックアップステータス変わった時に処理 changePickUpStatus(); bWasPickUpButtonPressed = bPickUpButtonPressed; } }else if (!bIsRecording && bWasRecording){ // 前回録音中だったけど終了したときは録音終了 MPLog("End Record 1\n"); err_t err = theAudio->readFrames(myFile); MPLog("End Record 2\n"); endRec(); } // 現在の状態を前回の状態値として保持 bWasRecording = bIsRecording; // エラーが発生していた時は次回のループで録音終了にするため // 疑似的に録音終了ボタン押下とする if (isErr){ MPLog("!!Error Raised!!\n"); bIsRecording = !bIsRecording; } // usleepさせることで割り込み処理を受け付ける usleep(40000); }

SpresenseのArduinoコード(サブコア)

サブコア1では赤外線受信の処理をしています。
ピックアップのON/OFFはタクトスイッチを押す以外に赤外線で送ることを可能にしていますが、サブコア1ではその赤外線を受信したときにメインコアに通知するという役割をしています。

IRRemoteはソースコードのヘッダにも書いている通りこちらを使用していますが、私の環境ではすでにArduino用のIRremoteのライブラリが入っておりインストールできませんでした。そのためいったん元のIRremoteのライブラリは削除してから入れました。

SpresenseのArduinoコード(サブコア1)

/* * my_recorder_sub.ino * ・使用ライブラリ * https://github.com/baggio63446333/Spresense-IRremote */ #if (SUBCORE != 1) #error "Core selection is wrong!!" #endif #include <IRremote.h> #include <MP.h> #include "common.h" #define RECV_PIN 11 #define PICKUP_IR_CODE XXXXXXXX // 対象とする赤外線コードを定義する IRrecv irrecv(RECV_PIN); decode_results results; void setup() { MP.begin(); irrecv.enableIRIn(); } void loop() { if (irrecv.decode(&results)){ // 対象のコードだった場合に送信する // dumpInfo(&results); // Output the results if (results.value == PICKUP_IR_CODE){ MPLog("%d\n", results.value); MP.Send(MSGID_IR, results.value); // Send IR value to MainCore } irrecv.resume(); } delay(100); }

録音ファイル(MP3)ピックアップ分割処理のシェル

ピックアップ指定した個所をあるだけ抜き出して"ON"のフォルダにmp3を作成(抜き出し)します。
また、抜き出されなかった部分に対しても"OFF"のフォルダに作成します。

cut_mp3.sh

#!/bin/bash for input_file in $(ls *.mp3); do timestamps=(0 $(cat $input_file.txt)) # Get the duration of the input file duration=$(/d/ffmpeg/bin/ffprobe -i "$input_file" -show_entries format=duration -v quiet -of csv="p=0") # Iterate through the timestamps and cut the file accordingly for ((i=0; i<${#timestamps[@]}; i++)); do start_time=${timestamps[i]} end_time=${timestamps[i+1]:-$duration} # Use end of file if last timestamp if [ $(( $i & 1 )) -eq 0 ]; then folder="OFF" else folder="ON" fi echo Will trim audio from $start_time to $end_time and save to $folder output_file="${folder}/${input_file}_${i}.mp3" /d/ffmpeg/bin/ffmpeg -loglevel error -i "$input_file" -ss "$start_time" -to "$end_time" -c copy -y "$output_file" done done

上記のうち、mmpegはそれぞれの環境に合わせてパスを指定してください。
・/d/ffmpeg/bin/ffprobe
・/d/ffmpeg/bin/ffmpeg
の箇所です。

また、このシェルを実行するためにWindows用にバッチファイルも作成しました。

cut_mp3_sh.bat

@echo off "D:\Program Files\Git\bin\sh.exe" --login -i -c "./cut_mp3.sh %1"

Windowsからはこのcut_mp3_sh.batをたたくことで、同じ階層にあるXXX.mp3とXXX.mp3.txtのセットに対してピックアップ分割処理を行います。

使い方

使い方として一連の流れを動画にまとめてみました。

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

残っている課題

メインコアの時刻合わせ

時刻合わせについてはSpresenseのサンプルスケッチを元にいくつかのやり方がありましたが、現状コンパイル時刻で合わせるようにしています。

せっかくSpresenseなので起動したときにGPSで合わせたいところですが、前回の作品開発時GPS情報を取得した際につないで結構待たないといけなかったので、今回のものには適さないかなと判断しやっていません。

電源は基本的にモバイルバッテリーを使用しますがバッテリーを接続しなおすと(再起動すると)時刻が前回のコンパイル時刻に戻ってしまいます、、、。
その部分について今後考慮が必要です。

マイク

無指向性だったのでその点はよかったのですが、ピンマイクのためあまり遠くの音は拾えず音が小さくなってしまっており、マイクを別のものに変えることも検討していけたらと思います。
こういう用途に合うマイク(オーディオプラグ形式のもの)があれば教えていただきたいです。

WiFi対応

メインボードに下記WiFiアドオンボードを載せているのですが実は現状機能していません!
▼ Spresense用Wi-Fi add-onボード
https://www.switch-science.com/products/4042
写真はどれもアドオンボードが写っていますのでWiFi対応していると思われていたらすみません。
本当は、SDカードを取り出さずにデータをWiFiで送信できるようにと考えていたのですが、そこまで実装が間に合いませんでした。

さいごに

当面はこのケースで運用してみようと思います。半透明なので赤外線も届きます。

箱に入れました

息子のmemetan氏にいろいろ相談して一部手伝ってもくれました。シェルの部分は気づいたら全部書いてくれました。
おかげで完成できました。どうもありがとうございました!

siroitori0413のアイコン画像
https://siroitori.hatenablog.com/
ログインしてコメントを投稿する