読めるのに書けないSDカード:StickS3にSDカード録音を実装するまで

前回、内蔵Flashに録音を保存するところまでは形になりました。ただ、内蔵Flashの容量には限りがあります。録音先をSDカードに広げられないか、というのが今回のテーマです。

TF HATと32GBのmicroSDカードを追加して、録音先を内蔵Flashからカード優先に切り替えることにしました。

M5StickC用TF(SD) HAT¥1,650Amazonで見る ▸

ここから、想定していなかった足踏みが始まります。

読めるのに、書けない

カードの種別や容量の読み取り、ルートディレクトリの一覧、既存ファイルの読み取り。ここまでは問題なく通りました。

ところが、新規ファイルの作成・書き込みだけが失敗します。画面とログには open write fail、write fail、0MB fail19 が出続けました。

カードを初期化し直しても、FAT32とexFATを両方試しても、カードを差し直しても同じでした。Mac側ではそのカードでフォルダの作成・削除もテキストファイルの作成・閲覧も普通にできています。読めるのに書けない。原因を「容量が0MB」「カードが壊れている」「フォーマットが違う」と決めつけるのは、まだ早すぎました。

errno=19は、思っていたエラーではなかった

手がかりはエラーコードの errno=19 でした。てっきりFatFsの「エラー番号19」のことだと思っていたのですが、これは勘違いでした。

この環境での errno=19 は ENODEV です。ESP-IDFのVFS(ファイルシステムを抽象化する層)には、FatFs側のFR_NOT_READYやFR_NOT_ENABLED、FR_NO_FILESYSTEMといった複数の異なるエラーを、まとめてENODEVに変換して返す経路があります。つまり、エラー19だけを見て「FATのエラー19だ」「フォーマットが悪い」と決めるのは、そもそも成立しない読み方でした。

参考にしていたTF HAT作者のサンプルコードも、カード認識やルート列挙といった読み取り系の検証が中心で、新規ファイルの作成・書き込みまでは確認されていませんでした。ArduinoのSDライブラリのSD.begin(-1, ...)と、CSピンを使わないHATの組み合わせに相性があるのでは、という疑いは持ちましたが、それだけを根本原因と断定できる材料はありませんでした。

ESP-IDFの標準ドライバに乗り換えたら、書けた

ArduinoのSDライブラリにこだわるのをやめて、最終ファームと同じESP-IDFのSDSPIドライバで、カードを壊さない範囲のテストを組みました。

ESP-IDF v5.5.1、SDSPI_SLOT_NO_CS、自動フォーマットは無効(format_if_mount_failed=false)という条件です。

結果は、SDHCの認識・容量取得・既存ファイルの読み取りに加えて、POSIX API(open・write・fsync・close・読み戻し)での新規ファイル作成に7回連続で成功、mkdirも成功しました。

途中でもう一つ発見がありました。ファイル構造体(FIL)をスタック上に置くとf_syncがCRCエラー(0x109)で失敗し、静的領域に置くと成功する、という挙動です。実装ではPOSIX APIを使っているのでこの問題自体は踏みませんが、低レイヤーのFatFsを直接触るときは覚えておいた方がよさそうです。

ここでの判断は、SDカードそのものやFAT形式が悪かったのではなく、Arduino側の書き込み経路とCSなしHATの組み合わせが問題候補で、ESP-IDF標準のSDSPIドライバなら実機で書き込みできる、というところまでです。Arduino側の根本原因は、今も特定できていません。

ファーム側への組み込み

SDカードは/sdcard/RECにマウントします。絶対に自動フォーマットはしません。

起動時にSDカードが使える状態なら、そのままSDを録音先にします。使えなければ内蔵Flashの/recordingsにフォールバック。この切り替えは起動時に一度だけ決めます。録音の途中でSD書き込みが失敗しても、別のストレージへ黙って切り替えることはしません。その録音は中断し、.partのまま残します。

Wi-Fi同期は、まずSDカード上の未送信録音を優先します。SD側に未送信のものが無いときだけ、SD導入前に内蔵Flashへ残っていた古い録音を一時的に読み込んで送信し、送り終えたらアンマウントする、という順番にしました。

ついでに見つかった小さなバグも直しました。SD保存時、BLEへ通知する録音番号が常に0になっていた問題です。ファイル名から番号を読み取るように直しています。

容量の綱引き

StickS3のFlashは8MBですが、全部を録音に使えるわけではありません。ブートローダー・NVS・OTA領域を差し引く必要があります。

OTA領域を広げるため、内蔵録音領域を縮小しました。OTAスロットは各2.00MBから2.69MBへ、内蔵ストレージは3.94MBから2.56MBへ。20kbpsのOpus録音で、内蔵ストレージはおよそ17分ぶんです。容量に余裕を持たせるため、通常時の録音先はSDカードを優先する構成にしています。

30分を超えたら、無音のところで切る

長時間録音を1ファイルにし続けると、扱いづらくなります。録音が30分を超えたあと、無音を検出したタイミングでファイルを確定し、次のファイルに移る仕様を足しました。

約0.6秒の無音を区切りの目安にし、35分まで無音が見つからなければ強制的に区切ります。60ms単位のRMS(音量の指標)を追いかけて、雑音の基準値と比較する形で無音を判定します。雑音基準の上昇の追従の速さは0.001に調整しました。最初0.004にしていたときは、小さな声の途中で区切ってしまうことがありました。

24分の実録音データを使ったシミュレーションでは、2分ごとの区切りを10回試して、強制区切りは0回。すべて無音のタイミングで区切れています。ただ、実際に30分を超える録音を実機で流したときの音質や、区切り目の欠落の有無、Macで別々のノートとして扱われるかどうかは、まだ確認できていません。

ラディに、SD録音の色を足す

作業台(自作のダッシュボード)のマスコット「ラディ」の状態表示を、StickS3の小さな画面にも持ってきました。

待機はオレンジ、内蔵Flash録音中は緑に音符、SDカード録音中はピンクに音符、Wi-Fi接続後のアップロード中は水色にキラキラ、という対応にしています。水色への切り替えは、同期ボタンを押した時点ではなく、Wi-Fi接続が実際に成立した通知を受けてから行うようにしました。Wi-Fiを探している間や同期に失敗したときは、オレンジの同期画面のままにしています。

画像は112×112ピクセルの透明BGRAに変換し、LVGLのARGB8888形式で埋め込みました。1枚あたり約50KB。ピンクと水色を足しても、容量の上限には収まっています。

Mac側でも、地味なバグが見つかった

9月23日の22時以降、文字起こしが進まなくなり、92件中26件が未処理のまま残っていた時期がありました。

原因は、受信サーバーをlaunchctl submitで起動したときに、PATHが/usr/bin:/bin:/usr/sbin:/sbinに絞られてしまい、Homebrew配下にあるffmpegとwhisper-cliが見つからなくなっていたことでした。エラーも/dev/nullに捨てられていたので、失敗していること自体に気づきにくい状態でした。

LaunchAgentに切り替えて、Homebrewを含むPATH、RunAtLoad、KeepAlive、ログ出力をきちんと設定し直しました。ついでに、無音や1秒未満の録音を毎回の再起動のたびに処理し続けてしまうバグも直しています。空の文字起こし結果を例外として扱っていたのが原因で、NoSpeechErrorとして区別し、mynotebook_status: "no_speech"のまま完了扱いにして、ノートは作らないようにしました。修正後は103件を処理し、うちノートになったのが94件、話し声なし判定が9件です。

実機で確認できたこと、まだできていないこと

確認できているのは、8秒の録音がSDカードのREC/に.oggとして保存されたこと、.partから.oggへの確定処理、Wi-Fi同期でSD上の録音をアップロードできたこと、Macでの文字起こしからノート登録までが通ったこと、待機時のオレンジとSD録音中のピンクが実機画面で確認できたことです。

まだ確認できていないのは、Wi-Fi接続後に実際に水色へ変わるか、SDに未送信録音が無い状態で内蔵Flashの古い録音がちゃんと同期されるか、SDカードを外した状態で起動して内蔵Flash録音にフォールバックし緑のラディが出るか、30分超の録音が無音地点で綺麗に分割されるか、分割後の録音がMac側で別々のノートになるか、といったあたりです。

—

ここまでの内容は、GitHubのmainにPR経由で統合済みの状態を書いています。ただ、この作業のすぐあとに、ラディのアイコン周りをローカルでいじり直した作業が別にあり、そちらはまだコミットもされていなければ、実機書き込みも成功していません。GitHubに残っている記録と、直近の手元の試行錯誤は、別のものとして扱うようにしています。混ざると、あとで自分が読み返したときに困るので。

コメント

タイトルとURLをコピーしました