Raspberry Piで超簡易オーディオプレイヤー 「松」2020

ディスプレイ搭載

RasPi Audio Player

「ボタンを押すと、楽曲ファイルが演奏され、もう一度ボタンを押すと演奏が止まる」という、誠に単純な機能を持つ装置

Raspberry pi 公式タッチパネルディスプレイを接続したGUIを備えたデバイスに搭載する超簡易オーディオプレイヤー

I2Cインタフェースによる温湿度気圧センサを搭載することにも配慮している。

2020年版では、iPhoneやiPad, Macの「ミュージック」アプリの音声を出力できるAirPlayにも対応

OSのインストール

インストールしたRaspbian OSのイメージ

2020-08-20-raspios-buster-armhf.img

ソフトウェアの設定は、共通のものを参照。

公式ディスプレイの方向を変更

変更するファイル /boot/config.txt

(追加内容)

 lcd_rotate=2


音楽再生アプリのための設定

PythonでVLCを制御するためのパッケージのインストール

 $ pip3 install python-vlc

I2C用Python moduleのインストール

 $ pip3 install smbus


必要なハードウェア

Raspberry Pi 3 model B/B+

Raspberry Pi 7インチ公式タッチディスプレイ(800x480px)

USBオーディオDAコンバーターキット REV.C [AKI.DAC-U2704 REV.C]

USBメモリ(またはUSBメモリアダプタとmicro SDHC card)

赤外線焦電センサ

押しボタンスイッチ

ピンソケット

回路図

Sound Cardとdevice numberの確認

$ aplay -l

これでUSB-DACのcard番号を調べる


USB-DACの設定

設定ファイル: /usr/share/alsa/alsa.conf

(修正前)

 defaults.ctl.card 0
 defaults.pcm.card 0

(修正後)

 defaults.ctl.card 1
 defaults.pcm.card 1


オーディオプレイヤーのソフトウェア

Python3を使って、実装する。

(ソースコード:ファイル名 tksimpleplayer.py)

#! /usr/bin/env python3
# coding:utf-8

# RapsberryPi Simple Music Player
#(準備)
# VLCのインストール install VLC MediaPlayer
# python VLCのインストール pip3 install python-vlc
# Tact Switch on GPIO 22
# LED on GPIO 27 Active Low
# Tact Switch ON -> PLAY (LED ON) -> Tact Switch ON -> STOP (LED OFF)
# Musicvplayer application software is VLC

import tkinter as tk
import RPi.GPIO as GPIO
import subprocess
import vlc
import pathlib
import random
import re

import urllib.parse

import time
import sys
import os

class SimplePlayer(tk.Frame):
    def __init__(self, master=None):

        self.background_color="#000020"
        self.musicdatapath = "/media/pi/0123-4567" # Path of Music DATA Folder, (with USB-ID)

        super().__init__(master)
        self.master.title("tkSimplePlayer")
        self.set_gpio()
        self.pack()
        self.create_widgets()
        self.status = "STOP"

        self.vlcmlp = vlc.MediaListPlayer() #VLC Media List Player
        self.vlcmlp.set_playback_mode(vlc.PlaybackMode.loop) #vlc playback mode をloop(play listの繰り返しに)

        # VLC Volume 100%
        self.VLC_volume = 100
        self.vlc_media_player=self.vlcmlp.get_media_player() # get media player instance from media list palyer
        self.vlc_media_player.audio_set_volume(self.VLC_volume) # volume set to media player

        # Play Listの生成
        self.vlcmlp.set_media_list( self.get_media_list() ) #set media list

    def stop_and_clean(self):
        #停止時の後始末
        self.led_off()
        if self.status == "PLAY" :
            self.vlcmlp.stop() #曲の停止
            print("--STOP--")
        GPIO.cleanup(22) # Cleanup GPIO 22
        GPIO.cleanup(27) # Cleanup GPIO 27

    def on_closing(self):
        self.stop_and_clean()
        root.destroy()

    def create_widgets(self):
        self.message_button = tk.Button(self, bg=self.background_color, fg="white", text="STOP", font=("",20), command=self.play_sound_tk_button)
        #self.message_button = tk.Button(self, text="STOP", font=("",20), command=self.play_sound_tk_button)
        self.message_button.pack(side=tk.LEFT, anchor=tk.W)

        # 曲のパス名ラベル
        self.mrl_label = tk.Label(self, text=" MRL is here. ", font=("",10), anchor='center',
            foreground='#ffffff', background=self.background_color,
            width=700)
        #self.mrl_label = tk.Label(self, text="MRL", font=("",8))
        self.mrl_label.pack(side=tk.LEFT, anchor=tk.E, fill=tk.X)


    def get_media_list(self):
        #メディアリストを更新し、返す。
        print("--LIST--")
        p_temp_media_list = vlc.MediaList() # MediaListを生成
        #メディアファイルのpathのリストを取得
        p_temp = pathlib.Path(self.musicdatapath) #USBメモリのパスを設定
        mrl_list = [] #mrl(楽曲ファイルのPATH)のリストの初期化
        extension_mrl_list = list(p_temp.glob('**/**/*.m[4p][3a]')) # 楽曲拡張子mp3,m4aのファイル名を取得
        mrl_list.extend(extension_mrl_list)

        length_mrl_list = len(mrl_list) #mrl_listの要素数を取得
        for i in range(length_mrl_list):
            mrl = random.choice(mrl_list)
            p_temp_media_list.add_media(str(mrl))#リストからランダムに取り出す
            #print(mrl) #DEBUG
        return p_temp_media_list


    def vlc_play(self):
        #DEBUG:print("DEBUG:status="+self.status)
        if self.status == "STOP" :
            self.led_on()

            self.status = "PLAY"
            print("--PLAY--")
            # Music media files in /media/pi directory
            self.vlcmlp.play() # 曲の再生
            self.message_button["text"] = "PLAY"

            #再生中にメディアリストを再構築する
            self.vlcmlp.set_media_list( self.get_media_list() ) #set media list
            print("--PLAYING--")

            # MRLを表示
            self.show_mrl()

        else:
            self.led_off()

            self.status = "STOP"
            self.vlcmlp.stop() #曲の停止

            self.message_button["text"] = "STOP"
            self.mrl_label['text'] = " "

            print("--STOP--")

    def show_mrl(self):
        # MRL (曲のファイルのパス名)を出力
        try :
            #MRLを取得
            mp = self.vlcmlp.get_media_player()
            m = mp.get_media()
            mrl_raw = m.get_mrl()
            #print(mrl_raw) # DEBUG:
            mrl_quote = urllib.parse.unquote(mrl_raw)# MRLのURLデコード (%20 -> spaceにするなど)
            mrl_with_ext = mrl_quote.lstrip("file://" + self.musicdatapath) # MRLの最初の共通部分削除

            # mrl = mrl_with_ext.replace(".mp3", '') # MRLの拡張子を削除(末尾でなくても削除する)
            # mrl = mrl.replace(".m4a", '') # MRLの拡張子を削除(末尾でなくても削除する)
            mrl =  re.sub("\.m[p4][3a]$", '', mrl_with_ext) # MRLの拡張子.mp3, .m4aを削除
            #print(mrl) # DEBUG:
        except:
            mrl = "MRL ERROR !"

        if self.vlcmlp.is_playing() :
            #曲の再生中だけ MRL更新
            len_mrl = len(mrl)
            if len_mrl < 32:
                #MRLの文字数が少ないときは、フォントを大きくする
                mrl_splitted = mrl.split('/') #/で分割
                mrl1stline = "" #1行目に全て表示
                for i in range(len(mrl_splitted)):
                    mrl1stline += mrl_splitted[i] + '  '

                self.mrl_label["font"] = ("",18) # labelのフォントサイズを大きめに
                mrl = mrl1stline

            else:
                #文字数が多いとき
                mrl_splitted = mrl.split('/') #/で分割
                if len(mrl_splitted) <= 2:
                    mrl1stline = mrl_splitted[0]
                    mrl2ndline = mrl_splitted[1]
                else:
                    mrl1stline = "" #1行目は、第1、第2要素
                    for i in range(2):
                        mrl1stline += mrl_splitted[i] + '   '
                    mrl2ndline = "" #2行目は第3要素以降
                    for i in range(len(mrl_splitted) - 2):
                        mrl2ndline += mrl_splitted[i + 2]
                if len(mrl1stline) < 75 and len(mrl2ndline) < 75:
                    self.mrl_label["font"] = ("",12) # labelのフォントサイズを中程度に
                else:
                    self.mrl_label["font"] = ("",9) # labelのフォントサイズを最小に

                mrl = mrl1stline + '\n' + mrl2ndline
            #Labelの内容をMRLで更新
            #print(mrl) #DEBUG:
            self.mrl_label["text"] = mrl

        self.after(1000, self.show_mrl) # 1sec. ごとにMRL表示



    def play_sound_gpio(self, gpiolist):
        ## DEBUG:print("DEBUG:" + self.status)
        print("SW: PRESSED")
        # ボタン長押しでreboot
        counter = 50; # 0.1秒間隔で、50回押され続けたら、長押しと判断
        while GPIO.input(22) == GPIO.LOW :
            time.sleep(0.1)
            counter -= 1
            if counter <= 0:
                os.system("sudo reboot")
        # ボタンが離されたら、音楽再生
        # VLC palyer control by GPIO
        self.vlc_play()


    def play_sound_tk_button(self):
        # clv palyer control by TKinter GUI
        print("DEBUG:play sound tk button")
        self.vlc_play()

    def set_gpio(self):
        GPIO.setmode(GPIO.BCM)  #GPIO Access by BCM
        GPIO.setup(27,GPIO.OUT)   #BCM PIN-27 as Output
        GPIO.setup(22,GPIO.IN, pull_up_down=GPIO.PUD_UP)
        #GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_OFF)   #BCM PIN-22 as Input
        GPIO.add_event_detect(22, GPIO.FALLING,bouncetime=500, callback=self.play_sound_gpio)
        #GPIO.add_event_detect(22, GPIO.RISING,bouncetime=200) #なぜか勝手に割り込みが入るのでダメ
        #GPIO.add_event_callback(22, self.play_sound_gpio) # Call back Trigger GPIO PIN-22
        self.led_off(); # LED OFF


    def led_on(self):
        GPIO.output(27, GPIO.HIGH)


    def led_off(self):
        GPIO.output(27, GPIO.LOW)


####################################################
root = tk.Tk()
app = SimplePlayer(master=root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.configure(background=app.background_color)
app.configure(background=app.background_color)
root.geometry("800x50+0+275") # Window geomety : X-axis offset 0, Y-axis offset +350
app.mainloop()

オーディオプレイヤーのソースコードの保存

ソースコードを(Python3)をtksimpleplayer.pyという名前で保存

場所は/home/pi/tksimpleplayer/

ソースコードを実行可能に設定する。

$ chmod +x /home/pi/tksimpleplayer/tksimpleplayer.py

OSの起動時に自動的に起動するように設定する。

編集するファイル: /home/pi/.config/lxsession/LXDE-pi/autostart

最後に下記のエントリを追加

@/home/pi/tksimpleplayer/tksimpleplayer.py

人感センサでディスプレイのバックライトをON/OFFするスクリプト

ファイル名 /home/pi/ctrl_backlight/sensor_backlight_on_off.py

#!/usr/bin/python
# coding: utf-8
#
# GPIOピン18番に焦電センサ(TOP3224)を接続。動きを検出すると1が数秒間出力される
# バックライトが60秒後に消灯する。
# 焦電センサが動きを検出すると、バックライトは点灯する。
#
 
# GPIOモジュール, timeモジュール, osモジュールをインポートする
import RPi.GPIO as GPIO
import time
import os
 
# GPIO指定をGPIO番号で行う
GPIO.setmode(GPIO.BCM)
 
# GPIO18ピンを入力モードに設定
GPIO.setup(18, GPIO.IN)

#Display Power Management System を OFF にする
#print "xset dpms force off"
#os.system('xset dpms force off')

while True: 
    # GPIO18ピンの入力状態を表示する
    gpio18 = GPIO.input(18)
    #debug:
    #print (gpio18)
        
    if gpio18 == 1 :
        #焦電センサが動きを検出したら、バックライト一定時間点灯するようにDPMSをONにする
        print( "xset dpms force on")
        os.system('xset dpms force on')
        #600sec.でsleep, standby, off
        #os.system('xset dpms 600 600 600')
        #60sec.でsleep, standby, off
        print( "xset dpms 60 60 60 ")
        os.system('xset dpms 60 60 60')
        #59sec待つ
        time.sleep(59)
    

    # 1sec. sleep状態になり、他の仕事にお任せする
    else :
        time.sleep(1)
    
# GPIOピンをリセット
GPIO.cleanup()

OSの起動時に自動的に起動するように設定する。

編集するファイル: /home/pi/.config/lxsession/LXDE-pi/autostart

最後に下記のエントリを追加

@/home/pi/ctrl_backlight/sensor_backlight_on_off.py


AirPlay対応の設定

iPhoneやiPad, Macの「ミュージック」アプリの音声を出力できるAirPlayに対応させる設定

Shairport-syncのインストール

必要なライブラリおよびgitのインストール

 $ sudo apt install git autoconf libdaemon-dev libpopt-dev libconfig-dev libasound2-dev libpulse-dev \ libavahi-client-dev libssl-dev libsoxr-dev


shairport-syn のファイル取得とビルド、インストール

 $ git clone https://github.com/mikebrady/shairport-sync.git
 $ cd shairport-sync
 $ autoreconf -i -f
 $ ./configure --sysconfdir=/etc --with-alsa --with-pa --with-avahi --with-ssl=openssl \
    --with-metadata --with-soxr --with-systemd
 $ make && sudo make install


shairport-sync設定ファイルの編集

コマンド aplay -l で 確認したカード番号をもとにファイル/etc/shairport-sync.confを編集する

$ sudo vi /etc/shairport-sync.conf

(編集する部分)

 general =
 {
   name = "AirPlay%H";    //AirPlayサーバの名前(任意)
  }

(USB-DACを出力として使うための変更部分)

(変更前)

 alsa = {
   output_device = “default”;

( HDMI使用時にUSB-DACを出力として使うための変更部分(DACのcardが 2 の場合))

  alsa =  {
   output_device = “hw:2,0”;

(HDMI使用しないときにUSB-DACを出力として使うための変更部分(DACのcardが 1の場合))

 alsa =  {
   output_device = “hw:1,0”;


Shairport起動

 $ sudo systemctl start shairport-sync


OS 起動時にShairportが起動するよう設定する。

 $ sudo systemctl enable shairport-sync