Raspberry Pi Zero WHとADRSZDACでスタンドアロン音楽プレーヤーを作る(2026年10月4日更新)

Raspberry Piで音楽プレーヤーを作るならVolumio一択だと思います。

しかしRaspberry Pi Zero WHには結構きつい重さ。そこで、Bit Trade Oneで売っていたADRSZDACというハイレゾDAC拡張基盤を買い、スタンドアロンの音楽プレーヤーを作りました。

処理の重さを考え、USBストレージに曲を保存して、mpvで再生するという軽い気持ちで始めましたが、なかなかに難しく感じました。

また高音質化ということでADRPM1801Cも買い組み合わせています。

操作にはWeb UIを使わず、4個のタクトスイッチで各種操作します。機能としては、mpv起動、再生/停止、曲送り、曲戻し、シャッフルのON/OFFを物理ボタンで操作します。

結論

完成した構成は、Raspberry Pi Zero WHに「ADRSZDAC」「ADRPM1801C」、USB音楽ストレージ、4個のタクトスイッチを組み合わせたものです。

音楽ファイルはUSBメモリを/mnt/musicへマウントし、Pythonで書いたものが/music以下のサブディレクトリを含めて音楽ファイルを検索します。そしてプレイリストの/tmp/music.m3uを作成し、mpvで再生します。

ボタンは次のように割り当てました。

ボタンGPIO物理ピン操作
SW1GPIO 1711mpv 起動 / 終了
SW1GPIO 1711再生 / 停止
SW2GPIO 2713曲送り
SW3GPIO 2215曲戻し
SW4GPIO 2316シャッフル ON / OFF
GND―394個のスイッチ共通

mpvはそのままだと電源投入時から起動してしまいます。毎回電源を切るとなると、Raspberry Pi Zeroの省電力性が損なわれてしまいます。

そのため電源は常時いれたまま、systemdでmusic-controller.pyを起動して、SW1を長押ししたときにPythonからmpvを起動します。

つまり、【systemd → music-controller.py → SW1長押し → mpv起動】という構成です。

制作環境

今回の完成版では、OSをRaspberry Pi OS Lite 32bit / Trixieに変更しました。製作途中でBookwormからTrixieに変わってしまったため、再現性が失われると思い、はじめからつくりなおしました。

<ハードウェア構成>

  • Windows 11(PowerShell 5.1、主にsshで接続用)
  • Raspberry Pi Zero WH
  • Raspberry Pi OS Lite 32bit Trixie
  • ADRSZDAC(デジタルアナログコンバーター)
  • ADRPM1801C(外部システムクロックボード)
  • USBメモリ(400曲程度であれば16GBもあれば十分)
  • メスヘッダー40PIN
  • 6*6mmのタクトスイッチ4つ(4つ足でも足を折ったり切ったりすれば簡単に扱えます)
  • プロトタイプ基盤
  • 導線(ホームセンターで売っているものを使いました)

<ソフトウェア構成>

  • Raspberry Pi OS Lite 32bit(Debian GNU/Linux 13ベース)
  • Python3.13.5(Raspberry Pi OS標準搭載)
  • python3-gpiozero(GPIOボタン制御用)
  • python3-lgpio(gpiozeroからGPIOを操作するためのバックエンド。systemdではGPIOZERO_PIN_FACTORY=lgpioを指定しています)
  • mpv 0.40.0(音楽生成エンジン)

ADRSZDACはHiFiBerry DAC PLUS Proとして認識されます。

コントローラー用スクリプト

コントローラーに使用したスクリプトです。

#!/usr/bin/env python3
"""
Raspberry Pi Zero WHとADRSZDAC用GPIOコントローラー
====================================================================
"""
from __future__ import annotations

import json
import logging
import os
import re
import socket
import subprocess
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from signal import pause

from gpiozero import Button

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)
log = logging.getLogger("music_controller")

@dataclass(frozen=True)
class Config:
    music_dir: Path = Path("/mnt/music")
    playlist_path: Path = Path("/tmp/music.m3u")
    mpv_socket: str = "/tmp/mpv.sock"
    audio_device: str = "alsa/plughw:CARD=sndrpihifiberry,DEV=0"
    hold_time: float = 1.5
    bounce_time: float = 0.1
    socket_timeout: float = 1.0
    socket_wait_timeout: float = 3.0

CFG = Config()

AUDIO_EXTENSION_RE = re.compile(r"\.(mp3|m4a|flac|wav)$", re.IGNORECASE)

class MusicController:

    def __init__(self, cfg: Config) -> None:
        self.cfg = cfg
        self._process: subprocess.Popen | None = None
        self._lock = threading.Lock()
        self._sw1_long_handled = False
        self._shuffle_enabled = False

    def is_running(self) -> bool:
        if self._process is None:
            return False
        if self._process.poll() is None:
            return True
        self._process = None
        return False

    def _scan_playlist(self) -> list[Path]:
        tracks = [
            path
            for path in self.cfg.music_dir.rglob("*")
            if path.is_file() and AUDIO_EXTENSION_RE.search(path.name)
        ]
        tracks.sort()
        return tracks

    def _write_playlist(self) -> bool:
        if not self.cfg.music_dir.is_mount():
            log.warning("USBメモリがマウントされていません: %s", self.cfg.music_dir)
            return False

        tracks = self._scan_playlist()
        if not tracks:
            log.warning("音楽ファイルが見つかりません: %s", self.cfg.music_dir)
            return False

        self.cfg.playlist_path.write_text(
            "\n".join(str(t) for t in tracks) + "\n", encoding="utf-8"
        )
        log.info("プレイリストを作成しました: %d曲", len(tracks))
        return True

    def _wait_for_socket(self) -> bool:
        deadline = time.monotonic() + self.cfg.socket_wait_timeout
        while time.monotonic() < deadline:
            if os.path.exists(self.cfg.mpv_socket):
                return True
            if not self.is_running():
                return False
            time.sleep(0.05)
        return False

    def start(self) -> None:
        with self._lock:
            if self.is_running():
                log.info("mpv: 起動済み")
                return
            if not self._write_playlist():
                return

            Path(self.cfg.mpv_socket).unlink(missing_ok=True)
            self._shuffle_enabled = False

            log.info("mpv: 起動します")
            try:
                self._process = subprocess.Popen(
                    [
                        "mpv",
                        "--no-video",
                        f"--audio-device={self.cfg.audio_device}",
                        f"--input-ipc-server={self.cfg.mpv_socket}",
                        "--loop-playlist=inf",
                        f"--playlist={self.cfg.playlist_path}",
                    ]
                )
            except OSError as exc:
                log.error("mpvの起動に失敗しました: %s", exc)
                self._process = None
                return

            if self._wait_for_socket():
                log.info("mpv: READY")
            else:
                log.warning("mpv: IPCソケットの生成待ちがタイムアウトしました")

    def stop(self) -> None:
        with self._lock:
            if not self.is_running():
                log.info("mpv: 既に停止しています")
                return

            log.info("mpv: 停止します")
            self._process.terminate()
            try:
                self._process.wait(timeout=3)
            except subprocess.TimeoutExpired:
                log.warning("mpv: terminateがタイムアウトしたためkillします")
                self._process.kill()
                self._process.wait()

            self._process = None
            self._shuffle_enabled = False
            Path(self.cfg.mpv_socket).unlink(missing_ok=True)

    def _send(self, *command: str) -> bool:
        if not self.is_running():
            return False
        try:
            with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
                sock.settimeout(self.cfg.socket_timeout)
                sock.connect(self.cfg.mpv_socket)
                sock.sendall(
                    (json.dumps({"command": list(command)}) + "\n").encode("utf-8")
                )
                try:
                    sock.recv(4096)
                except socket.timeout:
                    pass
            return True
        except OSError as exc:
            log.warning("mpv IPCエラー: %s", exc)
            return False

    def sw1_held(self) -> None:
        self._sw1_long_handled = True
        if self.is_running():
            self.stop()
        else:
            self.start()

    def sw1_released(self) -> None:
        if not self._sw1_long_handled:
            log.info("SW1: PLAY / PAUSE")
            self._send("cycle", "pause")
        self._sw1_long_handled = False

    def sw2_pressed(self) -> None:
        if self._send("playlist-next"):
            log.info("SW2: NEXT")

    def sw3_pressed(self) -> None:
        if self._send("playlist-prev"):
            log.info("SW3: PREVIOUS")

    def sw4_pressed(self) -> None:
        if not self.is_running():
            log.info("SW4: mpv停止中のため無視します")
            return

        if not self._shuffle_enabled:
            if self._send("playlist-shuffle"):
                self._shuffle_enabled = True
                log.info("SW4: SHUFFLE ON")
        else:
            if self._send("playlist-unshuffle"):
                self._shuffle_enabled = False
                log.info("SW4: SHUFFLE OFF")

def main() -> None:
    controller = MusicController(CFG)

    sw1 = Button(17, hold_time=CFG.hold_time, hold_repeat=False, bounce_time=CFG.bounce_time)
    sw2 = Button(27, bounce_time=CFG.bounce_time)
    sw3 = Button(22, bounce_time=CFG.bounce_time)
    sw4 = Button(23, bounce_time=CFG.bounce_time)

    sw1.when_held = controller.sw1_held
    sw1.when_released = controller.sw1_released
    sw2.when_pressed = controller.sw2_pressed
    sw3.when_pressed = controller.sw3_pressed
    sw4.when_pressed = controller.sw4_pressed

    log.info("Music Controller 起動完了")
    log.info("SW1 GPIO17 : 長押し=mpv起動/停止, 短押し=再生/一時停止")
    log.info("SW2 GPIO27 : 次の曲")
    log.info("SW3 GPIO22 : 前の曲")
    log.info("SW4 GPIO23 : シャッフル ON/OFF")
    log.info("GND        : 39番ピン")

    try:
        pause()
    except KeyboardInterrupt:
        log.info("終了します")
        controller.stop()

if __name__ == "__main__":
    main()

サービスの内容は次の通りです。

[Unit]
Description=Raspberry Pi Music Controller
After=local-fs.target mnt-music.automount
Wants=mnt-music.automount

[Service]
Type=simple
User=ユーザー名
Environment=LG_WD=/tmp
Environment=GPIOZERO_PIN_FACTORY=lgpio
ExecStart=/usr/bin/python3 /home/ユーザー名/music_controller.py
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

くじけそうだった点

今回、特に時間がかかったのがADRSZDACに搭載されているスイッチの扱いです。結論から言えば元からついているボタンをPythonで扱うことはできませんでした。

あまり状態のよくないRaspberry Pi Zeroだったので、GPIOそのものの故障を疑いました。ジャンパピンでのGPIO単体のテストでは正常でした。問題の切り分けの結果ADRSZDAC側に問題がありそうな感じでした。

公式ページに書かれていた/boot/firmware/config.txtの設定通りに変更しましたが、やはり動きません。結局Pythonで制御するためには、config.txtの内容は以下の通りで安定することになりました。

dtparam=i2s=on
dtoverlay=vc4-kms-v3d
dtoverlay=hifiberry-dacplus-pro,24db_digital_gain
dtoverlay=dwc2,dr_mode=host
dtoverlay=nospi10

※dtoverlay=hifiberry-dacplus-pro,24db_digital_gainはADRPM1801C(別売り外部システムクロックボード)の設定です。
※ADRSDACだけの場合は、dtoverlay=hifiberry-dacplus-std,24db_digital_gainです。

なぜVolumioやMoOdeを使わないのか

今回、VolumioやMoOdeを採用しませんでした。理由はRaspberry Pi Zero WHの性能(シングルコアCPU、RAM512MB)の力不足です。

Raspberry Pi Imagerで書き込める高機能オーディオ専用OSは、システムが重く、Raspberry Pi Zero WHで快適に動作させることは難しいです。

様々考えましたが、Raspberry Pi OS LiteとmpvとPython制御によるタクトスイッチが一番ストレスなく動かせました。

実際に使ったコマンド

ここからは実際に使ったコマンドについて触れていきたいとおもいます。

STEP 1:OSの更新

いつも通りの3つのコマンドです。

sudo apt update
sudo apt full-upgrade -y
sudo reboot

再起動後に作業を始めます。ただRaspberry Pi Zero WHはとても再起動が遅いので根気よく待ちましょう。

STEP 2:必要パッケージのインストール

必要なパッケージをインストールします。Python3はTrixieになってから標準搭載になったので、素のままで行けました。

sudo apt install -y mpv
sudo apt install -y python3-gpiozero
sudo apt install -y python3-lgpio
sudo apt install -y netcat-openbsd

STEP 3:DACの設定をする

ADRSDAC(DAC)を有効化します。

sudo nano /boot/firmware/config.txt
dtparam=i2s=on
dtoverlay=vc4-kms-v3d
dtoverlay=hifiberry-dacplus-pro,24db_digital_gain
dtoverlay=dwc2,dr_mode=host
dtoverlay=nospi10

今回はADRPM1801Cを乗せているので、dtoverlay=hifiberry-dacplus-pro,24db_digital_gainです。

そして再起動して反映させます。

sudo reboot

再起動を待っている間にUSBメモリに曲を入れておきましょう。

STEP 4:USBメモリを自動マウントできるようにします

再起動したらUSBメモリをfstabに登録します。必須ではありませんが、USBメモリは挿したり抜いたりすると認識されない場合があります。それは名前がsda1やsda2など変わるためです。名前を固定するためにUUIDをfstabに記入します。

まずはUUIDの確認をします。

blkid

すると、このようにUUIDが表示されるのでメモを取るかコピペしておいてください。

/dev/sda1: UUID="USBメモリのUUID" TYPE="exfat"

UUIDが分かったらfstabへ登録します。

sudo nano /etc/fstab

おそらく既に2行登録されていると思いますが、3行目に次の一行を追加してください。こちらは” “は不要です。

UUID=USBメモリのUUID /mnt/music exfat defaults,nofail,x-systemd.automount,x-systemd.device-timeout=10 0 0

fstabをいじったら必ずdaemon-reloadを実行してください。

sudo systemctl daemon-reload

自動マウントを行うように次のように打ち込みます。

sudo systemctl start mnt-music.automount

そして確認を行います。

  • 曲入りUSBメモリを抜きます
  • 2~3秒ほど待つ
  • 曲入りUSBメモリを再度挿します
ls /mnt/music

これで曲名やサブフォルダ名を見ることができたらUSBメモリの自動マウントは完成です。

STEP 5:mpvを確認

mpvを確認

ここからはPowerShell 5.1を2窓展開したほうが楽になります。

まずはmpv単体で動かしてみます。

mpv --no-video \
--audio-device=alsa/plughw:CARD=sndrpihifiberry,DEV=0 \
"/mnt/music/"

–audio-device=alsa/plughw:CARD=sndrpihifiberry,DEV=0この部分はADRSZDACから音を出すために指定しています。

STEP 6:曲をプレイリスト化してmpvで鳴らす

最終的に組むPythonでは、1曲ずつファイル名を指定するのではなくプレイリストをつくります。

/tmp/music.m3u

このようなプレイリストを作り、それをmpvへ渡します。

mpv --no-video \
--audio-device=alsa/plughw:CARD=sndrpihifiberry,DEV=0 \
--playlist=/tmp/music.m3u

これで曲が鳴ればひとまず完成です。

STEP 7:コントローラーの作成

記事の前半に書きましたが各GPIOにタクトスイッチから伸ばした導線をハンダ付けしていきます。私はハンダ付けがとても苦手なので、一つメスヘッダーをダメにしました。最終的には次のようになります。

  • GPIO17 → SW1 1.5秒長押し→ mpv起動 / mpv停止
  • GPIO17 → SW1 → 再生 / 停止
  • GPIO27 → SW2 → 曲送り
  • GPIO22 → SW3 → 曲戻し
  • GPIO23 → SW4 → シャッフルON / OFF

ダメにしてしまったメスヘッダー。素直にフラックスを使った方がよかったです。戒めのため、このメスヘッダーを公開しておきます。

さて、結論の部分にも同じものを載せていますが、もう一度載せます。

#!/usr/bin/env python3
"""
====================================================================
Raspberry Pi Zero WHとADRSZDAC/ADRPM1801C用音楽コントローラー
====================================================================
"""
from __future__ import annotations

import json
import logging
import os
import re
import socket
import subprocess
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from signal import pause

from gpiozero import Button

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)
log = logging.getLogger("music_controller")


@dataclass(frozen=True)
class Config:
    music_dir: Path = Path("/mnt/music")
    playlist_path: Path = Path("/tmp/music.m3u")
    mpv_socket: str = "/tmp/mpv.sock"
    audio_device: str = "alsa/plughw:CARD=sndrpihifiberry,DEV=0"
    hold_time: float = 1.5
    bounce_time: float = 0.1
    socket_timeout: float = 1.0
    socket_wait_timeout: float = 3.0


CFG = Config()


AUDIO_EXTENSION_RE = re.compile(r"\.(mp3|m4a|flac|wav)$", re.IGNORECASE)


class MusicController:

    def __init__(self, cfg: Config) -> None:
        self.cfg = cfg
        self._process: subprocess.Popen | None = None
        self._lock = threading.Lock()
        self._sw1_long_handled = False
        self._shuffle_enabled = False


    def is_running(self) -> bool:
        if self._process is None:
            return False
        if self._process.poll() is None:
            return True
        self._process = None
        return False

    def _scan_playlist(self) -> list[Path]:
        tracks = [
            path
            for path in self.cfg.music_dir.rglob("*")
            if path.is_file() and AUDIO_EXTENSION_RE.search(path.name)
        ]
        tracks.sort()
        return tracks

    def _write_playlist(self) -> bool:
        if not self.cfg.music_dir.is_mount():
            log.warning("USBメモリがマウントされていません: %s", self.cfg.music_dir)
            return False

        tracks = self._scan_playlist()
        if not tracks:
            log.warning("音楽ファイルが見つかりません: %s", self.cfg.music_dir)
            return False

        self.cfg.playlist_path.write_text(
            "\n".join(str(t) for t in tracks) + "\n", encoding="utf-8"
        )
        log.info("プレイリストを作成しました: %d曲", len(tracks))
        return True

    def _wait_for_socket(self) -> bool:
        deadline = time.monotonic() + self.cfg.socket_wait_timeout
        while time.monotonic() < deadline:
            if os.path.exists(self.cfg.mpv_socket):
                return True
            if not self.is_running():
                return False
            time.sleep(0.05)
        return False

    def start(self) -> None:
        with self._lock:
            if self.is_running():
                log.info("mpv: 起動済み")
                return
            if not self._write_playlist():
                return

            Path(self.cfg.mpv_socket).unlink(missing_ok=True)
            self._shuffle_enabled = False

            log.info("mpv: 起動します")
            try:
                self._process = subprocess.Popen(
                    [
                        "mpv",
                        "--no-video",
                        f"--audio-device={self.cfg.audio_device}",
                        f"--input-ipc-server={self.cfg.mpv_socket}",
                        "--loop-playlist=inf",
                        f"--playlist={self.cfg.playlist_path}",
                    ]
                )
            except OSError as exc:
                log.error("mpvの起動に失敗しました: %s", exc)
                self._process = None
                return

            if self._wait_for_socket():
                log.info("mpv: READY")
            else:
                log.warning("mpv: IPCソケットの生成待ちがタイムアウトしました")

    def stop(self) -> None:
        with self._lock:
            if not self.is_running():
                log.info("mpv: 既に停止しています")
                return

            log.info("mpv: 停止します")
            self._process.terminate()
            try:
                self._process.wait(timeout=3)
            except subprocess.TimeoutExpired:
                log.warning("mpv: terminateがタイムアウトしたためkillします")
                self._process.kill()
                self._process.wait()

            self._process = None
            self._shuffle_enabled = False
            Path(self.cfg.mpv_socket).unlink(missing_ok=True)

    def _send(self, *command: str) -> bool:
        if not self.is_running():
            return False
        try:
            with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
                sock.settimeout(self.cfg.socket_timeout)
                sock.connect(self.cfg.mpv_socket)
                sock.sendall(
                    (json.dumps({"command": list(command)}) + "\n").encode("utf-8")
                )

                try:
                    sock.recv(4096)
                except socket.timeout:
                    pass
            return True
        except OSError as exc:
            log.warning("mpv IPCエラー: %s", exc)
            return False


    def sw1_held(self) -> None:
        self._sw1_long_handled = True
        if self.is_running():
            self.stop()
        else:
            self.start()

    def sw1_released(self) -> None:
        if not self._sw1_long_handled:
            log.info("SW1: PLAY / PAUSE")
            self._send("cycle", "pause")
        self._sw1_long_handled = False


    def sw2_pressed(self) -> None:
        if self._send("playlist-next"):
            log.info("SW2: NEXT")

    def sw3_pressed(self) -> None:
        if self._send("playlist-prev"):
            log.info("SW3: PREVIOUS")


    def sw4_pressed(self) -> None:
        if not self.is_running():
            log.info("SW4: mpv停止中のため無視します")
            return

        if not self._shuffle_enabled:
            if self._send("playlist-shuffle"):
                self._shuffle_enabled = True
                log.info("SW4: SHUFFLE ON")
        else:
            if self._send("playlist-unshuffle"):
                self._shuffle_enabled = False
                log.info("SW4: SHUFFLE OFF")


def main() -> None:
    controller = MusicController(CFG)

    sw1 = Button(17, hold_time=CFG.hold_time, hold_repeat=False, bounce_time=CFG.bounce_time)
    sw2 = Button(27, bounce_time=CFG.bounce_time)
    sw3 = Button(22, bounce_time=CFG.bounce_time)
    sw4 = Button(23, bounce_time=CFG.bounce_time)

    sw1.when_held = controller.sw1_held
    sw1.when_released = controller.sw1_released
    sw2.when_pressed = controller.sw2_pressed
    sw3.when_pressed = controller.sw3_pressed
    sw4.when_pressed = controller.sw4_pressed

    log.info("Music Controller 起動完了")
    log.info("SW1 GPIO17 : 長押し=mpv起動/停止, 短押し=再生/一時停止")
    log.info("SW2 GPIO27 : 次の曲")
    log.info("SW3 GPIO22 : 前の曲")
    log.info("SW4 GPIO23 : シャッフル ON/OFF")
    log.info("GND        : 39番ピン")

    try:
        pause()
    except KeyboardInterrupt:
        log.info("終了します")
        controller.stop()


if __name__ == "__main__":
    main()

これがコントローラー本体になります。長い構文になってしまったので、一応チェックしたいと思います。

python3 -m py_compile ~/music_controller.py

STEP 8:サービスファイルを作成します。

紆余曲折しましたが、とにかく、最終的に動作したサービスファイルを作ります。

/etc/systemd/system/music-controller.service
[Unit]
Description=Raspberry Pi Music Controller
After=local-fs.target mnt-music.automount
Wants=mnt-music.automount

[Service]
Type=simple
User=p

Environment=LG_WD=/tmp
Environment=GPIOZERO_PIN_FACTORY=lgpio

ExecStart=/usr/bin/python3 /home/p/music_controller.py

Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

ここが結構重要で、Environment=GPIOZERO_PIN_FACTORY=lgpioとgpiozeroのGPIOバックエンドとしてlgpioを明示しないと動きませんでした。

Environment=LG_WD=/tmpこれも重要です。systemdから起動した場合のlgpioの通知ファイルで.lgd-nfy-Nを作成するために必要でした。これが無い状態だと、FileNotFoundError: .lgd-nfy-3が発生して、結構沼にはまりました。

STEP 9:systemdに登録します

sudo systemctl daemon-reload
sudo systemctl enable music-controller.service
sudo systemctl start music-controller.service

エラーがでてないか確認をします。Active: active (running)が表示されることを確認します。Active: activationとなっていた場合しばらくしてからもう一度ためしてみてください。

systemctl status music-controller.service --no-pager

STEP 10:再起動後の確認

まずは再起動をかけます。

sudo reboot

再起動後もチェックを行い、正常稼働しているか確認します。

systemctl status music-controller.service --no-pager

さらに実際のコントローラーできちんと動作するかを確認します。ボタンをおして挙動がおかしい場合はPythonのGPIOの番号が間違っている可能性が高いです。

すべてが確認出来たら完成です。

ごちゃごちゃしていますが、奥にヘッドホン、青がコントローラー、緑に光っているのはDAC、その下にRaspberry Pi Zero WH、白いのは音楽が入っているUSBメモリです。

よくある質問(FAQ)

Q
GUIは必要ですか?
A

今回の構成では必要ないです。Raspberry Pi OS Lite 32bit上でGPIOとmpvを使用しているため、Web UIもGUIも使っていません

Q
音楽ファイルはどこに置きますか?
A

今回は楽をするためにUSBメモリに音楽ファイルを全て入れています。USBメモリを/mnt/musicへマウントします。.mp3、.m4a、.flac、.wavを検索し、サブディレクトリも対象にしています。

Q
mpvは常に起動していますか?
A

いいえ。systemdが起動するのはmusic_controller.pyだけです。PythonのプログラムがGPIO入力を待機し、SW1を長押ししたときにmpvを起動/停止させます。

いいえ。systemdが起動するのはmusic_controller.pyです。

PythonプログラムがGPIO入力を待機し、SW1を長押ししたときにmpvを起動します。初期段階では勝手に鳴り始めるのが不便だったため、このような設計になりました。

Q
ボタンは何個必要ですか?
A

目的に合わせて4つにしました。ボタンの短押し、長押しなどで制御できるため、数*2以上の制御ができます。アンプに繋ぐ予定だったため、音量ボタンは付けませんでしたが、増やすことで音量の増減もできると思います。

Q
Raspberry Piを再起動しても使えますか?
A

systemdへ登録しているため、再起動後もmusic-controller.pyが自動起動します。今回の検証では再起動後の動作も確認しています。

まとめ

今回作成したのは、Raspberry Pi Zero WH、ADRSZDAC、ADRPM1801C、USBストレージ、4個の物理ボタンを組み合わせたシンプルかつ軽量な音楽プレーヤーです。

Raspberry Pi ZeroではWeb UIの動作は重いです。そこでRaspberry Pi OS Lite上でmpvとPythonを動かし、物理ボタンだけで音楽を操作できるようにしました。

音楽ファイルはUSBメモリに取り込み/mnt/musicへマウントします。自動マウントさせることにより、音楽ファイルからプレイリストを自動生成し、mpvが再生します。

最初はDAC側のボタン入力でつまずきましたが、外部タクトスイッチを使い、DAC設定とsystemdのGPIO環境を整えることで、最終的に再起動後も動作するスタンドアロンプレーヤーとして完成しました。

公式リファレンス

関連記事

Raspberry Pi、Linux、DAC、音楽再生に関する記事を関連リンクとして追加します。

更新履歴

  • 2026年10月4日 更新
  • 2026年9月30日 初投稿
P
P

【この記事を書いた人】

黒瓜 勉(Lonely Lab運営・執筆)

PC-9821時代からPCやインターネットに触れてきた、いわゆる「インターネット老人」です。英文学博士課程前期で培った英文一次資料の読解力を生かし、PC-98、Windows 3.1から、現在のWindowsやRaspberry Piまで、世代を問わず実機で検証しています。

「今あるものを、もっと長く楽しむ」をテーマに、公式仕様や一次情報を確認しながら、実際に試した結果を記録しています。特に、安全に使えること、まだ使える可能性があること、同じ条件なら再現できることを大切にしています。

本記事も実機環境で検証した内容をもとに執筆しています。
動作環境やソフトウェアのバージョンによって、結果が異なる場合があります。

詳しいプロフィール・運営方針は、運営者情報をご覧ください。

運営者情報:https://lonely1.jp/helloworld/