kasys が 2026年01月27日16時12分02秒 に編集
初版
タイトルの変更
Bubble Player【SPRESENSEで作る本格デジタルオーディオプレイヤー】
タグの変更
SPRESENSE
mp3
PlatformIO
SSD1309
MPU6050
OLEDディスプレイ
Arduino
メイン画像の変更
記事種類の変更
製作品
ライセンスの変更
(GPL-3.0+) GNU General Public License, version 3
本文の変更
## 概要・製作動機 SPRESENSEを用いて、携帯型デジタルオーディオプレーヤー **BubblePlayer** を制作しました。PC側で音源の準備(エンコード)とライブラリDB生成を行い、SPRESENSE側では無駄なファイルスキャンを避けて軽快にブラウズ・再生できるように設計し、 **「日常で使えるデジタルオーディオプレーヤー」** としての使い心地を目指しました。 制作のきっかけは、昨年Xのタイムラインで過去に販売されていたいわゆる [ 香水瓶ウォークマン ](https://www.sony.jp/CorporateCruise/Press/200503/05-0308/?srsltid=AfmBOoozU3XK9Uo9dV-RNWj6OtMwgvoEL5vG9yE33x4h4HGrKTW0D1GA) を見かけ、その透明感あるデザインと“泡”が浮かぶような演出に強く惹かれたことです。そこで、この雰囲気をオマージュしつつ、ちゃんと使えるデジタルオーディオプレーヤー(DAP)をSPRESENSEで作ってみたいと考え、関連パーツを揃えて開発を開始しました。 本作品では、香水瓶ウォークマンの雰囲気(透明筐体・OLED表示・泡のスクリーンセーバー)を取り入れながら、 **(1) 曲数が増えても快適に動くデータ設計** と、**(2) 持ち歩き時に直感的に操作できるUI/操作系** を両立することを目標にしました。加えて、組込み機器としての合理性(軽量なデータ構造、低負荷な描画、少ない部品点数)も重視し、SPRESENSEを使った **“本格DAP”** として成立させることを狙っています。 ## 特徴 1) 曲数が増えても快適なDB設計 PC上でエンコードとDB生成を行うため、SPRESENSE側での全ファイルスキャンを避けられます。ID3タグなどのメタ情報もDB側に取り込み、タイトル/アーティスト等の表示に利用できます。実際に1500曲以上を入れても問題なく動作しています。 2) 多様な再生モード  プレイリスト機能に対応し、アーティスト/アルバム/年などでの再生にも対応しています。 3) PCで完結する書き込みツール(GUI)  Micro SDに書き込むためのデータ作成を、GUIベースのPythonツールで行えます。ライブラリの更新や管理を簡単にしました。 4) OLED表示(再生情報・バッテリー)  再生画面でタイトル/アーティスト/再生時間等を表示できます。バッテリー残量表示も搭載しています。 5) 泡スクリーンセーバー(加速度追従)  焼き付き防止のスクリーンセーバーとして泡アニメーションを実装しました。加速度センサで重力方向を推定し、泡が常に“上方向”に流れるため、持ち方で見え方が変化します。そのため、アニメーションの変化を楽しむことができます。 6) ジェスチャ操作(シェイク) 加速度センサを用いて、シェイクによる操作(例:2回シェイクで再生/一時停止)を実現しました。 7) 回転スイッチによる直感操作 上部の回転機構で前後曲などを操作できます。過去の「香水瓶ウォークマン」の操作をヒントに設計しました。 8) 大容量バッテリー+USB Type-C充電 1800mAhバッテリーを搭載し、USB Type-Cで充電できます。 ## システム構成 ### 概要  本機は **Spresense(メインボード+拡張ボード)** を中核に、表示・入力・電源を追加して携帯型プレーヤーとして成立させています。主な構成要素は **OLEDディスプレイ、6軸センサ(MPU-6050)、操作ボタン、リチウムイオン電池(3.7V 1800mAh)、充電・電源制御(LiPo Amigo Pro)** です。 電源は1800mAhのリチウムイオン電池を採用し、**LiPo Amigo Pro** によって **USB Type-Cでの充電、充電しながらの動作、電源スイッチによるON/OFF** を実現しています(携帯機としての運用性を重視しました)。 表示(OLED)とセンサ(MPU-6050)は **I2C(SDA/SCL)** 接続のため、同一バスに接続して配線を簡素化しています。操作ボタンはGPIO入力として接続し、まとめて **Arduinoシールド用ユニバーサル基板** 上に実装・配線しました。  音源とDBは **micro SDカード(32GB)** に格納し、SPRESENSE拡張ボードに接続しています。 ### 部品表 | 区分 | 部品名 | 型番 / 仕様 | 数量 | 備考(用途・補足) | | --- | ---------------- | ---------------------------: | -: | ------------------------------------------ | | メイン | SPRESENSE メインボード | SPRESENSE メインボード | 1 | 制御・再生処理 | | メイン | SPRESENSE 拡張ボード | SPRESENSE 拡張ボード | 1 | 3.3Vモードで使用 | | 表示 | OLEDディスプレイモジュール | 2.4インチ 128x64 I2C接続 SSD1309| 1 | 再生情報表示・スクリーンセーバー | | センサ | 6軸IMU(加速度/ジャイロ) | MPU-6050 モジュール(I2C) | 1 | シェイク操作・泡アニメの上方向推定 | | 入力 | タクトスイッチ | 汎用品(高さ2種類) | 6 | 操作ボタン。**うち2個を回転機構の左右入力として使用** | | 記憶 | microSDカード | 32GB | 1 | 楽曲+DB+プレイリスト格納 | | 電源 | LiPo充電・電源制御ボード | LiPo Amigo Pro(USB Type-C充電) | 1 | 充電・電源ON/OFF・電源系統の整理 | | 電源 | LiPoバッテリー | 3.7V / 1800mAh | 1 | 携帯用途の連続再生 | | 電源 | 電源スイッチ | 汎用スライドスイッチ | 1 | タクトスイッチでも可能だが、誤動作防止のためにスライドスイッチ採用 | | 配線 | ユニバーサル基板 | Arduinoシールド用ユニバーサル基板 | 1 | OLED/IMU/ボタン配線を集約 | | 配線 | ピンヘッダ/ソケット | 2.54mm ピッチ | 適量 |配線用| | 配線 | ジャンパ線 | 単線 | 適量 | 配線用 | | 配線 | ステレオミニジャック | 汎用品 | 1 | 延長配線用 | | 配線 | ステレオミニプラグ | 汎用品 | 1 | 延長配線用 | | 機構 | 3Dプリント筐体 | 4部品構成 | 1式 | Blender設計、接着剤不要の設計 、回転スイッチ機構兼用 | | 固定 | 基板固定材 | 両面テープ/フォーム材 等 | 適量 | **ネジ無し固定**のため | | 周辺 | USBケーブル | USB Type-C | 1 | 充電用 | | 周辺 | イヤホン | 3.5mm | 1 | デモ用 | ### 筐体    筐体はBlenderで設計し、3Dプリンタで出力しました。図のように **4つの部品**で構成され、接着剤を使わずに確実に組み付けできるように設計しています。特に、**上部の回転スイッチ機構が操作部であると同時に、筐体部品の結合(固定)を兼ねる**構造になっている点が特徴です。**OLED表示部は透明素材を使用**することで、筐体の堅牢性と表示を両立しています。  また、筐体には以下の図のようにパーツが配置されます。  ## ソフトウェア関連の設計内容 ### SPRESENSEにおける再生可能音源とソフトウェア SPRESNSEでは基本的にMP3とWAVファイルの再生しかサポートしていません。しかし、一般に普及している音源データとして最近はm4aやflacが主流となっているため変換する必要があります。また、SPRESENSEではMP3ファイルに含まれるID3メタデータ(タイトルやアーティスト名、アルバムなどの情報)を読めないという問題があります。これらは、デジタルオーディオプレーヤー(DAP)を作るうえでは致命的な問題となり得ます。 公式の[MP3プレイヤーサンプル](https://github.com/sonydevworld/spresense-arduino-compatible/blob/master/Arduino15/packages/SPRESENSE/hardware/spresense/1.0.0/libraries/Audio/examples/application/player_playlist/player_playlist.ino)では、プレイリストをCSVとして扱っています(TRACK_DB.CSV)。この場合、手書きでCSVを作るため、1000曲を超える規模のDAPとして運用することを考えると現実的な方法ではありません。また、SPRESENSEにファイルスキャンをさせる方法も考えられますが、これはCPU性能の問題から規模が大きくなると時間がかかりすぎます。 そこで、今回はあらかじめPCにソフトウェアを用意し、m4aやflacなどの非対応ファイルからのエンコード、メタデータの抽出、ファイルデータベースの作成、メタデータデータベースの作成などを行うことにしました。ソフトウェアはPythonで構築し、GUIはTkinter、エンコードやメタデータの抽出はffmpegを使用しています。DB構造やバイナリ形式、PythonコードはChatGPTと共に作成しました。また、自由に作れるプレイリストなども仕様として定義し、GUI上から作成できるようにしました。  ### SPRESENSE搭載プログラム SPRESENSE側はplatform.ioでAruduinoIDE環境としてプログラムしています。使っている外部ライブラリはGUIライブラリのU8g2のみです。起動後は次のような再生方法選択画面が出ます。再生方法には、プレイリスト再生、全楽曲再生、アルバム再生、アーティスト再生、同一年再生があります。  また、再生画面は以下のようになっています。タイトル、アーティスト、アルバム、再生時間、音量、バッテリーインジケータなどが表示されます。タイトル表示が収まりきらないときは自動スクロールで表示されるため、しっかりとタイトルを確認することが可能です。操作は、本体ボタンを押すか回転スイッチ、シェイク動作で可能です。本体ボタンでは、楽曲の一時停止/再生を操作したり、次の曲、前の曲の操作、音量の操作が可能です。回転スイッチは楽曲の変更、シェイク動作は2往復で楽曲の一時停止/再生、3往復で次の曲となっています。  OLEDの焼付き防止として以下のようなスクリーンセーバーも搭載しています。泡のアニメーションが常に地面に対して上方向に流れます。これは加速度センサーを使用して上方向を検出しています。泡に加えてタイトルなどを表示するモードも用意してあります。    ## おわりに ## ソースコード ### データ設計 ### SDカードの要求するディレクトリ構造 SDカード内にはPythonスクリプトで生成されたファイルを以下のように配置する必要があります。また、デコーダファイルも同様に配置する必要があります。 ```markdown:SDカード内の想定される構造 (SD root) ├─ BIN/ │ ├─ MP3DEC │ └─ WAVDEC ├─ MUSIC/ │ └─ ... (変換済みMP3) ├─ DB/ │ ├─ library.bin (ファイル情報DBのバイナリ) │ └─ playlists.bin (すべてのプレイリスト情報を記録したバイナリ) └─ PLAYLISTS/ └─ *.plb (プレイリスト単体のバイナリファイル) ``` ### DB仕様書 ```markdown:データベース仕様書 # Spresense 音楽プレーヤー向け SDカード仕様(SPDB v2 + PLBプレイリスト) 本仕様は、PC側ビルダー(SD MP3 Builder GUI)で生成した **音源ファイル** と **メタデータDB(SPDB v2)**、および **プレイリスト(.plb)** を、Spresense(MCU)側で低負荷に参照・再生するための **SDカード格納方式とバイナリフォーマット** を定義する。 * 対象規模: 最大 20,000 曲程度 * 表示: モノクロOLED(アルバム画像等は非対象) * メタデータ: 日本語を含むUTF-8 --- ## 1. 用語 * **TrackID**: `library.bin` 内の Track レコード番号(0-based)。最大 65,535 まで。 * **ArtistID / AlbumID**: `library.bin` 内の Artist/Album レコード番号(0-based)。最大 65,535 まで。 * **OutRelPath**: SDカードルートからの相対パス(例: `MUSIC/Artist/Album/001.mp3`)。区切りは `/`。 --- ## 2. SDカード ディレクトリ構成(推奨) SDカードの **ルート直下** に以下を配置する。 /MUSIC/ ...(再生対象のMP3ファイル) /DB/ library.bin (SPDB v2 メタデータDB) playlists.bin (PLM v1: プレイリスト名メタデータ) glyphset.txt (任意: フォント最小化用、UTF-8文字集合) playlists.json (任意: PC側GUIの状態保存。MCU側は不要) /PLAYLISTS/ <name>.plb (推奨: TrackIDベースのプレイリスト) <name>.m3u8 (任意: デバッグ/人間向け) ### 2.1 音源ファイル(/MUSIC) * 出力は **MP3** を基本とする(WAV等を残す運用も可能だが、MCU側プレーヤー実装が対応していること)。 * MCU負荷軽減のため、PC側で **ID3等のメタデータを削除** しておく(MCU側は `library.bin` を参照)。 * ファイルパスは `OutRelPath` と一致すること。 --- ## 3. メタデータDB: SPDB v2(/DB/library.bin) `library.bin` は以下の順で構成される。 1. Header(固定長) 2. ArtistRec 配列(固定長) 3. AlbumRec 配列(固定長) 4. TrackRec 配列(固定長) 5. Artist→Album リンク配列(u16配列) 6. Album→Track リンク配列(u16配列) 7. String Pool(UTF-8連結バイト列) 8. CRC32(任意、flagsで有効化) ### 3.1 エンディアン * **Little Endian** ### 3.2 ヘッダ(Header) * **サイズ:** 92 bytes * **フィールド(順序):** | 項目 | 型 | 説明 | | ------------ | ------- | ------------------------------------------- | | magic | char[4] | 常に `"SPDB"` | | version | u16 | 常に `2` | | header_size | u16 | 常に `92` | | flags | u32 | bit0=CRC32付与(推奨) | | build_epoch | u32 | 生成時刻(UNIX epoch) | | db_size | u32 | ファイル全体サイズ(CRC32含む場合は末尾+4) | | artist_count | u16 | ArtistRec数 | | album_count | u16 | AlbumRec数 | | track_count | u16 | TrackRec数 | | reserved0 | u16 | 予約(0) | | dword[0] | u32 | off_artists(ArtistRec先頭オフセット) | | dword[1] | u32 | off_albums(AlbumRec先頭オフセット) | | dword[2] | u32 | off_tracks(TrackRec先頭オフセット) | | dword[3] | u32 | off_artist_album_index(Artist→Albumリンク先頭) | | dword[4] | u32 | off_album_track_index(Album→Trackリンク先頭) | | dword[5] | u32 | off_string_pool(String Pool先頭) | | dword[6] | u32 | total_artist_album_links(Artist→Albumリンク総数) | | dword[7] | u32 | total_album_track_links(Album→Trackリンク総数) | | dword[8..15] | u32 | 予約(0) | #### 3.2.1 CRC32 * `flags & 0x1 != 0` の場合、ファイル末尾に `u32 crc32` が付与される。 * CRC32は **「先頭から crc32フィールド直前まで」** のバイト列に対して計算する(一般的な運用)。 ### 3.3 レコード定義(固定長) #### 3.3.1 ArtistRec(16 bytes) | 項目 | 型 | 説明 | | ---------------- | --- | --------------------------- | | name_off | u32 | String Pool内オフセット | | name_len | u16 | バイト長(UTF-8) | | album_link_count | u16 | このアーティストに属するアルバム数 | | album_link_start | u32 | Artist→Albumリンク配列内の開始インデックス | | reserved | u32 | 予約(0) | #### 3.3.2 AlbumRec(24 bytes) | 項目 | 型 | 説明 | | ---------------- | --- | -------------------------- | | name_off | u32 | String Pool内オフセット | | name_len | u16 | バイト長(UTF-8) | | artist_id | u16 | ArtistID | | year | u16 | アルバム年(YYYY、未知は0) | | track_link_count | u16 | このアルバムに属する曲数 | | track_link_start | u32 | Album→Trackリンク配列内の開始インデックス | | reserved0 | u32 | 予約(0) | | reserved1 | u32 | 予約(0) | #### 3.3.3 TrackRec(32 bytes) | 項目 | 型 | 説明 | | ------------- | --- | ---------------------------------- | | title_off | u32 | String Pool内オフセット | | title_len | u16 | バイト長(UTF-8) | | album_id | u16 | AlbumID | | artist_id | u16 | ArtistID | | track_no | u16 | トラック番号(未知は0) | | disc_no | u16 | ディスク番号(未知は0) | | duration_ms | u32 | ミリ秒 | | path_off | u32 | String Pool内オフセット(OutRelPath) | | path_len | u16 | バイト長(UTF-8) | | codec | u8 | コーデックコード(後述) | | flags | u8 | 予約/フラグ(後述) | | reserved_u16 | u16 | **SPDB v2: track_year(YYYY、未知は0)** | | reserved2_u32 | u32 | 予約(0) | ### 3.4 リンク配列 #### 3.4.1 Artist→Albumリンク配列 * 型: `u16[]`(AlbumIDの配列) * 長さ: `total_artist_album_links` * 各ArtistRecは `album_link_start` と `album_link_count` でスライスを参照する。 #### 3.4.2 Album→Trackリンク配列 * 型: `u16[]`(TrackIDの配列) * 長さ: `total_album_track_links` * 各AlbumRecは `track_link_start` と `track_link_count` でスライスを参照する。 ### 3.5 String Pool * UTF-8バイト列を **連結しただけ** の領域。 * 文字列はNUL終端されない。 * 参照は `(off, len)` で行う。 ### 3.6 codec コード(TrackRec.codec) PC側ビルダーが付与する代表値。 | 値 | 意味 | | -: | ------------------ | | 0 | 不明 | | 1 | MP3 | | 2 | WAV(PCM系含む) | | 3 | FLAC | | 4 | M4A/MP4(AAC/ALAC等) | | 5 | OGG | | 6 | OPUS | | 7 | AAC(拡張子aac等) | ※ MCU側プレーヤーがMP3のみ対応の場合でも、DBの表示やフィルタに利用できる。 --- ## 4. プレイリスト方式(PLM v1 + PLB v1) Spresense側で日本語ファイル名が扱いにくい場合に備え、 **「表示名(UTF-8)とPLB参照」を分離**する。 プレイリスト名は **PLMメタデータ**から取得し、実体のPLBはASCII名で参照する。 ### 4.1 プレイリストメタデータ(PLM v1) * 配置: `/DB/playlists.bin` * エンディアン: Little Endian * 目的: 日本語の表示名をUTF-8で保持し、PLBファイル名に依存しない #### 4.1.1 ヘッダ(32 bytes) | 項目 | 型 | 説明 | | ------------- | ------- | ---- | | magic | char[4] | `"PLM1"` | | version | u16 | `1` | | header_size | u16 | `32` | | flags | u32 | 予約(0) | | count | u32 | プレイリスト数 | | off_items | u32 | Item配列の先頭オフセット | | off_string_pool | u32 | String Pool先頭 | | string_size | u32 | String Poolサイズ | | reserved | u32 | 予約(0) | #### 4.1.2 Item(固定長 20 bytes) | 項目 | 型 | 説明 | | --------- | --- | ---- | | name_off | u32 | String Pool内オフセット(表示名) | | name_len | u16 | バイト長(UTF-8) | | plb_off | u32 | String Pool内オフセット(PLBファイル名/相対パス) | | plb_len | u16 | バイト長 | | track_count | u32 | 曲数(PLBと同値) | | reserved | u32 | 予約(0) | #### 4.1.3 String Pool * UTF-8バイト列を連結 * NUL終端なし((off,len)参照) #### 4.1.4 PLB参照ルール * `plb` は **ASCIIファイル名** を推奨(例: `pl_1a2b3c4d5e6f.plb`) * `plb` に `/` が含まれない場合、MCU側は `PLAYLISTS/` を自動で前置して参照する ### 4.2 プレイリスト本体(PLB v1) 従来の「プレイリスト内にパスを持ち、再生時にパス→検索」方式は、検索コストとI/Oが増える。 本仕様では **プレイリストは TrackID の列**として扱い、MCU側は TrackID から直接 TrackRec を参照する。 #### 4.2.1 ファイル配置 * `/PLAYLISTS/<plb_id>.plb` #### 4.2.2 フォーマット(PLB v1) * エンディアン: Little Endian * 目的: 低負荷・高速パース(逐次読み出し可) | 項目 | 型 | 説明 | | --------- | ---------- | -------------- | | magic | char[4] | `"PLB1"` | | version | u16 | `1` | | flags | u16 | 予約(0) | | count | u32 | TrackID数 | | track_ids | u16[count] | TrackIDの配列 | | crc32(任意) | u32 | 将来拡張(現行は未使用推奨) | * TrackIDは `library.bin` の TrackRec のインデックス(0-based)。 * `count` が大きい場合でも、MCU側は `u16` を逐次読み出すだけでよい。 ### 4.3 参照整合性 * TrackIDは、同一SDカード上の `library.bin` と一致していること。 * ビルドし直した場合に TrackID の並びが変わる運用では、PLM/PLBを同時に再生成する。 --- ## 5. 参考: M3U8(任意) `/PLAYLISTS/<name>.m3u8` は人間向け・デバッグ向け。 * UTF-8 * 先頭行: `#EXTM3U` * 以降、1行1曲で `OutRelPath` を記載(例: `MUSIC/.../xxx.mp3`) MCU側は原則 `.plb` を使用する。 --- ## 6. Spresense側ライブラリ(読み込みAPI)要求仕様(更新版) ### 6.1 目標 * `library.bin` を **部分読み(ランダムアクセス)** で参照 * 文字列は必要時のみデコード(UTF-8) * プレイリスト一覧は `playlists.bin`(PLM)を参照し、曲取得は `.plb` の TrackIDで行う ### 6.2 推奨API(C/C++) #### 6.2.1 DB * `bool spdb_open(const char* path, SpdbHandle* out);` * `void spdb_close(SpdbHandle* h);` * `uint16_t spdb_artist_count(const SpdbHandle* h);` * `uint16_t spdb_album_count(const SpdbHandle* h);` * `uint16_t spdb_track_count(const SpdbHandle* h);` * `bool spdb_read_artist(const SpdbHandle* h, uint16_t artist_id, ArtistRec* out);` * `bool spdb_read_album(const SpdbHandle* h, uint16_t album_id, AlbumRec* out);` * `bool spdb_read_track(const SpdbHandle* h, uint16_t track_id, TrackRec* out);` * `bool spdb_read_string(const SpdbHandle* h, uint32_t off, uint16_t len, char* buf, size_t buf_size);` * bufはNUL終端する * `bool spdb_artist_albums(const SpdbHandle* h, uint16_t artist_id, uint16_t* out_ids, uint16_t max, uint16_t* out_count);` * `bool spdb_album_tracks(const SpdbHandle* h, uint16_t album_id, uint16_t* out_ids, uint16_t max, uint16_t* out_count);` #### 6.2.2 Playlist Metadata(PLM) * `bool plm_open(const char* path, PlmHandle* out);` * `void plm_close(PlmHandle* h);` * `bool plm_read_header(PlmHandle* h, PlmHeader* out);` // magic, version, count * `bool plm_read_item(PlmHandle* h, uint32_t index, PlmItem* out);` * `bool plm_read_string(const PlmHandle* h, uint32_t off, uint16_t len, char* buf, size_t buf_size);` #### 6.2.3 Playlist(PLB) * `bool plb_open(const char* path, PlbHandle* out);` * `void plb_close(PlbHandle* h);` * `bool plb_read_header(PlbHandle* h, PlbHeader* out);` // magic, version, count * `bool plb_read_next(PlbHandle* h, uint16_t* out_track_id);` // 逐次 * `bool plb_seek(PlbHandle* h, uint32_t index);` // 任意(未実装でも可) ### 6.3 MCU側の再生フロー(推奨) 1. 起動時: `spdb_open("/DB/library.bin")` 2. 画面遷移 * Artist一覧: ArtistRecを順に読み、必要なnameのみString Poolから取得 * Album一覧: Artist→Albumリンクを読み、AlbumRecのname/yearを取得 * Track一覧: Album→Trackリンクを読み、TrackRecのtitle/track_no/disc_no/track_year を取得 3. 再生 * TrackRecから `OutRelPath` を読み出し、`/`区切りパスとしてSDから音源ファイルをopen 4. プレイリスト再生 * `playlists.bin` から表示名とPLB参照を取得 * `.plb` から TrackID を逐次読み * TrackID→TrackRec→OutRelPath→open ### 6.4 エラー処理 * magic/version/header_size が不一致なら即エラー * オフセット/サイズが `db_size` を超える場合は破損扱い * TrackIDが `track_count` 以上ならスキップ(プレイリスト破損) * UTF-8デコード失敗時は代替文字(`?` 等)で表示可能 --- ```