본문 바로가기

Programming/Python

[Python] Raspberry Pi와 PiFace CAD를 이용한 MP3/라디오 플레이어 만들기

(* Raspberry Pi의 로고)


 기존에 PXA270과 같은 개발 보드류는 무겁고 큰데다, 가격이 비싸 접근성이 좋지 않은 편이었고 그렇기에 개발 보드는 마치 소수의 (이를테면 임베디드 계열처럼) 관련 업종 엔지니어들에게만 허락된 영역처럼 느껴졌었다. 그런데 2012년, 영국의 라즈베리 파이 재단[각주:1]이 학교와 개발도상국에서 기초 컴퓨터 과학 교육을 증진시키기 위해 본인들의 이름을 본딴 라즈베리 파이(Raspberry Pi)라는 초소형 보드/PC를 개발 및 출시하면서 상황은 완전히 바뀌었다.


 5만원도 하지 않는 (비교적) 저렴한 모델 가격과 리눅스를 이용하며, 자체적인 OS를 갖추고 있던 라즈베리 파이의 환경 덕에 접근성이 높아졌고, 교육용으로 성공했을 뿐만 아니라, 이를 이용한 기발하고 창의적인 활용/응용법이 우후죽순 등장하기 시작했다. 해를 거듭해 새 모델은 물론이거니와 재단이나 서드 파티 회사에서 출시하는 라즈베리 파이 호환 주변기기(액세서리)가 여럿 출시되면서 라즈베리 파이의 가능성과 확장성, 그리고 '유연성'(flexibility)은 더욱 늘어나고 있다.


 지금까지 간략하게 라즈베리 파이를 설명해보았다. 흥미가 돋는다고? 그렇다면 우리도 라즈베리 파이를 이용해 당장 무언가를 창조해보면 되지 않을까. 요즘 대대적으로 보급된 스마트폰은 이미 음악 플레이어와 라디오 재생 기능을 기본적으로 포함하고 있지만, 간혹 MP3 플레이어를 사서 쓰던 때가 지금 생각하면 불편하더라도, 가끔은 그리울 때가 있다. 그런 감성을 가지고 Python으로 프로그래밍한 MP3/라디오 플레이어를 만들어보자. 플레이어에 디스플레이가 빠지면 섭하니(그래서 나는 애플에서 출시했던 '아이팟 셔플'의 감성을 이해할 수 없다) OpenSX라는 회사에서 출시한 파이페이스(PiFace CAD)라는 주변기기 또한 이용해볼 것이다.


…물론 이렇게 거창한 걸 만들 건 아니고...


이런 걸 만들 계획이다



 준비물은 다음과 같다. 모두 국내 쇼핑몰에서 쉽게 구할 수 있는 제품이다:


    • 라즈베리 파이 3 (홈페이지: https://www.raspberrypi.org/)
    • 파이페이스 컨트롤 & 디스플레이 (홈페이지: http://www.piface.org.uk/)
    • 전원용 미니 5핀 케이블 혹은 충전기
    • 최소 8기가 이상의 SD카드 및 SD카드 리더기(SD카드 슬롯이 따로 탑재된 컴퓨터라면 상관없다)
    • 사운드 출력을 확인할 스피커/이어폰
    • 공유기와 라즈베리 파이에 사용할 랜선




1. 라즈베리 파이 세팅하기


 ① 라즈베리 파이를 사용하기에 앞서, 우선 라즈베리 파이에 사용할 OS인 라즈비언(Raspbian)을 설치해줘야한다. 우선 라즈베리 파이 공식 홈페이지에서 "Raspbian Jessie with PIXEL"를 다운받는다.


 링크: https://www.raspberrypi.org/downloads/raspbian/




 ② OS를 설치하기 전에 SD카드를 새로 포맷해줘야 한다. SDFormatter를 설치한 후 이를 이용해보자.


 링크: https://www.sdcard.org/downloads/formatter_4/eula_windows/SDFormatterv4.zip


 SDFormatter를 실행했다면, 우선 Option 버튼을 눌러 FORMAT SIZE ADJUSTMENT 세팅을 ON으로 바꾼 뒤에 포맷을 진행하도록 하자. 시간이 조금 걸린다.





 ③ 이제 SD카드도 준비를 완료했으니 OS를 입히기만 하면 되는데, 이를 위해 Win32 Disk Imager라는 프로그램을 사용할 것이다. 다음 링크를 들어가 프로그램을 설치하자.


 링크: https://sourceforge.net/projects/win32diskimager/files/latest/download (링크 접속 후 5초 정도 기다리면 된다)


 다운받은 라즈비안 압축파일을 풀어 나온 라즈비안 img 파일을 불러온 뒤, SD카드에 Write해준다. 'Confirm overwrite'라며 덮어쓰기를 할거냐는 경고창이 뜨는 경우도 있는데, 가볍게 Yes를 눌러주면 된다. 역시 시간이 조금 걸린다.





 ④ 이미지 쓰기가 끝났다면 라즈베리 파이에 SD카드를 꽂고 5핀 케이블로 전원을 연결한다. 그리고나서 PC와 네트워크를 공유하는 공유기와 라즈베리 파이를 랜선으로 연결해준다.


 연결을 완료했다면 다음과 같이 전원과 랜 부분에 정상적으로 불빛이 나오는지 확인해보자: (불빛이 나오지 않는다면 전원 케이블 혹은 랜선에 문제가 있는 게 아닌지 의심해봐야 한다)




 정상적으로 불빛이 나온다면, 이제 라즈베리 파이에 원격으로 접속하기 위해 IP 주소를 알아내야 한다. 공유기 대부분의 설정 웹 페이지로 쓰이는 http://192.168.0.1에 접속한 후 고급 설정 -> 네트워크 관리 -> 내부 네트워크 정보에서 라즈베리 파이의 IP 주소를 확인해보자.






 ⑤ 라즈베리 파이의 IP 주소를 알아냈다. 하지만 초기 설정 때문에 지금 바로 원격으로 접속할 수는 없다. 그래서 우선 라즈베리 파이를 SSH[각주:2]로 접속한 후 VLC[각주:3] 옵션을 활성화시켜야 한다. 윈도우용 SSH 클라이언트인 PuTTY를 설치해보자.


 링크: http://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html


사용하는 컴퓨터의 운영체제 비트에 맞게 다운받아 설치해주자


 설치를 완료했다면 PuTTY를 실행한다. Host Name(or IP Address) 란에 라즈베리 파이의 IP 주소를 적고, SSH로 선택이 되어있는지 확인한 뒤 Open를 눌러준다. PuTTY Security Alert가 떠도 가볍게 Yes를 눌러주자.





 SSH로 무사히 접속하게 되면 커맨드 창이 뜬다. 먼저 ID와 비밀번호를 입력받는데, 라즈비안의 기본 ID/비밀번호는 각각 pi와 raspberry이므로 이를 입력해주도록 하자. 비밀번호는 입력은 받지만 화면에 뜨지는 않으니 되도록 틀리지 않게 입력해주자.



 주황색으로 박스친 부분은 비밀번호가 너무 기본적이어서(즉, 모두가 초기 비밀번호임을 알고 있기 때문에) 보안상 위험이 매우 크니 비밀번호를 바꿔달라는 경고 메시지이다. passwd 라는 명령어를 입력하면 비밀번호를 바꿀 수 있으니 꼭 바꿔주도록 하자.





 ⑥ 이제 원격 데스크탑 기능을 활성화할 차례이다. 라즈베리 파이의 설정을 변경하기 위해 다음 명령어를 입력해주자.


$ sudo raspi-config


 라즈베리 파이의 설정 툴에 들어가게 된다. 5 Interfacing Options -> P3 VNC 순으로 들어간 뒤 <Yes>를 선택한다.  








 ⑥ 드디어 원격 데스크탑 기능이 활성화되었다. 마지막으로 원격 접속을 위한 프로그램을 다운받으면 된다. 다음 링크에서 VNC Viewer를 다운받는다.


 링크: https://www.realvnc.com/download/viewer/




 VNC Viewer를 실행한 후, 라즈베리 파이의 IP 주소를 입력하고 엔터를 친다. ID와 비밀번호를 입력하고 OK를 누르면 원격 접속이 완료된다. 처음 접속하는 경우 경고창이 뜨는데 가볍게 Yes를 눌러주자.







2. PiFace CAD 및 LIRC 리모컨 세팅하기


 앞에서 라즈베리 파이의 주변기기 정도로만 언급했었기 때문에 우선 파이페이스 CAD(PiFace Control and Display)에 대해서 부연 설명을 해보자면, 16x2 LCD 디스플레이가 부착되어있어 문자 출력이 가능하고 8가지의 버튼과 함께 적외선 리시버가 부착되어있어 리모콘으로 컨트롤이 가능하기에 여러모로 활용성이 높다고 볼 수 있겠다.


이런 게 가능하다는 것이다



 ① 우선 PiFace CAD를 라즈베리 파이에 조립해보자. 구조상 어쩔 수 없이 조금은 붕 뜨게 되어있으므로 무리하게 끝까지 핀을 꽂지 않도록 하자. 둘 다 손상될 위험이 크기 때문.




 근데 이렇게 꽂기만 한다고 바로 되는 게 아니다. 직렬 주변기기 인터페이스 버스, 즉 SPI로 통신이 가능하도록 설정을 따로 활성화해야한다. 터미널을 실행하고( >_ 아이콘) sudo raspi-config를 입력해 아까 SSH로 들어갔었던 라즈베리 파이 설정 툴로 다시 들어가보자. 




 마찬가지로 5 Interfacing Options -> P4 SPI 순으로 들어간 뒤 <Yes>를 선택한다. (아까 SSH에서 설정할 때 같이 하면 더 좋은 게 맞지만, 일단 하나의 과정으로써 설명하고자 잠시 번거로움을 감수한 것이므로 이 글을 읽었지만 아직 시도를 하지 않은 사람이라면 SSH 설정 단계에서 VNC와 SPI를 같이 설정해줘도 무방하다. 아니, 훨씬 편할 것이다.)







 ② 이제 PiFace CAD를 정상적으로 활용할 수 있게 됐다. 파이썬에서 PiFace CAD를 프로그래밍하기 위해 라이브러리를 받아야한다. 터미널에서 다음 명령어를 차례차례 입력해보자. 사용자 환경에 따라 다르겠지만, 여튼 조금 오래 걸린다:


$ sudo rm -rf /var/lib/chksshpwd/

$ sudo apt-get install -y libpam-chksshpwd

※ 혹시나 있을 libpam-chksshpwd:armhf 오류의 발생에 대비해 우선적으로 입력해준다.


$ sudo apt-get update

$ sudo apt-get upgrade

$ sudo apt-get install python3-pifacecad


 모두 설치가 무사히 완료됐다면 테스트를 해본다. 터미널에서 다음 명령어를 입력해보자:


$ python3 /usr/share/doc/python3-pifacecad/examples/sysinfo.py


※ 명령어 python3 (파이썬 소스 파일) 은 Python 버전 3으로 해당 파이썬 소스 파일을 컴파일하는 기능이다.

ex) python3 helloworld.py


 PiFace CAD의 결과를 확인해본다. 다음과 같이 IP 주소, CPU 사용량 등 시스템 정보를 출력할 것이다:






 ③ PiFace CAD 세팅은 전부 완료했다. PiFace CAD의 적외선 센서를 이용하기 위해서는 리눅스 환경에서 리모컨의 적외선 신호를 받고 디코딩하는 기능을 하는 LIRC(Linux Infrared Remote Control)를 설치하고, 자신이 사용하는 리모컨의 정보를 별도로 등록해줘야 한다. 조금 번거롭지만 간단하지 너무 걱정하지 말자.


 우선 PiFace CAD의 LIRC 환경을 설정하는 쉘 스크립트를 실행시켜보자. 다음 명령어를 차례대로 터미널에서 입력한다:


$ sudo wget https://raw.github.com/piface/pifacecad/master/bin/setup_pifacecad_lirc.sh

$ sudo chmod +x setup_pifacecad_lirc.sh

$ sudo ./setup_pifacecad_lirc.sh


※ sudo ./setup_pifacecad_lirc.sh 실행시 설치를 계속할 것인지 묻는다. 무조건 Yes (1번 입력)를 해주자.


 쉘 스크립트 실행이 완료되었다면 다음 명령어를 입력해 LIRC를 설치한다. 설치 후엔 재부팅을 해주도록 한다:


$ sudo apt-get install lirc

$ sudo reboot





 ④ LIRC를 설치했으니, 이제 내가 사용하는 리모컨과 각 버튼을 등록해야 한다. 다음 명령어를 입력하자:


$ sudo irrecord -f -d /dev/lirc0 /etc/lirc/lircd.conf



 리모컨과 각 버튼을 등록하는 irrecord가 실행된다. 엔터를 두 번 치면 리모컨과 버튼별 적외선 정보의 레코딩을 시작한다.



 갑자기 아무것도 뜨지 않는다고 당황하지 말자. 말 그대로 "start recording"에 들어간 상태이기 때문이다. 이 때, 리모컨에서 아무 버튼이나 계속해서 눌러준다. (레코딩 시작 이후 10초간 반응이 없으면 irrecord가 종료된다!) 버튼을 누를 때마다 그걸 인식했을 경우 점이 하나가 찍힌다. 점이 한 줄의 끝까지 찍힐 때까지 여러 개의 버튼을 여러 번 눌러주자. 그러면 어느 순간 "Found gap"이 뜨며 리모컨 자체가 등록됐음을 알려준다.




 그 다음부터는 버튼 별 등록 과정이다. 만약에 내가 리모컨에서 0번 버튼을 등록하고 싶다면 이름을 KEY_0이라 적고, "Now hold down button ~"이라는 문구가 뜨면 바로 0번 버튼을 눌러주면 된다. "Got it."이 뜬 순간 버튼 등록이 완료된 것이다. 자신이 필요하다고 생각되는 만큼 버튼을 등록하고, 이정도면 적당하겠다 싶으면 엔터를 눌러 irrecord를 종료하도록 한다. 우리가 만들 MP3/라디오 플레이어는 8개 버튼이 적당하다. 


 ※ 참고로 버튼의 이름은 irrecord에서 정해진 버튼 이름 중에서만 설정이 가능한데, 그 리스트는 터미널에서 다음 명령어를 쳐서 확인해볼 수 있다:


$ irrecord --list-namespace





 마지막으로 버튼이 제대로 등록됐는지 확인해보자. 터미널에 irw를 입력하고 버튼을 마구 눌러본다. 누른 버튼의 이름이 뜨면 등록이 제대로 된 것이다.


$ irw


※ 리눅스에서 어떤 프로그램을 실행하는 도중 빠져나가고자 할 때(=프로세스를 종료하고자 할 때)는 Ctrl+C를 누르면 된다.






 ⑤ 리모컨과 버튼 등록까지 완료했다. 근데 마지막으로 딱 한 단계가 더 남았다. 파이썬에서 프로그램에 사용하기 위해 리모컨/버튼 설정을 읽어오기 위한 lircrc 스크립트를 작성해야 한다. 스크립트의 확장자는 .lircrc이며 (그러므로 파일의 풀 네임은 예를 들어 radio.lircrc 처럼 된다) 스크립트의 구조는 다음과 같이 아주 간단하다. 


begin

button = KEY_0

config = 0

prog = radio

end


begin

button = KEY_1

prog = radio

config = 1

end


한 버튼 당 무조건 begin으로 시작해 end로 끝나야 하며button에는 irrecord로 등록한 버튼의 이름을 입력한다. config는 버튼을 누를 경우 호출/출력될 커맨드를 입력한다. (예를 들어 0번을 눌렀을 때 "You Pressed 0"가 나오도록 하고 싶다면 echo "You Pressed 0"를 적어준다)


prog에는 스크립트에 등록한 버튼들이 작동하게 될 프로그램의 이름을 임의로 적어준다. 우리가 만들 게 라디오/MP3 플레이어이므로 'radio'를 적어주었다.


※ button, config, prog의 순서는 상관없다.


 터미널에서 다음 명령어를 입력해 lircrc 파일을 생성해준다. 위 구조에 맞춰 lircrc 스크립트를 작성해준다.


$ sudo nano ~/.lircrc








3. MPD/MPC 세팅하기


 ① 이제 음악/라디오를 재생하기 위한 플레이어를 설치해야한다. 우리는 리눅스에서 음악 파일을 재생, 관리해주는 서비스인 MPD(Music Player Daemon)와, 그 MPD를 명령어로 쉽게 컨트롤할 수 있는 클라이언트인 MPC를 이용할 것이다. 다음 명령어를 입력해주자:


$ sudo apt-get install mpd mpc

$ sudo apt-get install mpg321 lame


※ mpg321/lame은 mp3 코덱이다.




 ② MPD/MPC 설치가 완료됐다면, 우리가 만들 라디오/MP3 플레이어에 온전하게 쓰기 위해 약간의 설정이 필요하다. 다음 명령어를 입력해 MPD 설정에 들어가자:


$ sudo nano /etc/mpd.conf


 이제 몇몇 부분의 설정을 다음과 같이 바꿔준다. 우리가 만들 MP3/라디오 플레이어가 당장은 본체에 내장된 파일만 재생하지만, 혹시나 이 글을 읽는 분이 클라우드나 FTP 서버를 활용할 수도 있다고 가정하고 네트워크 설정 또한 건드려보기로 했다. 


music_directory        "(본인이 사용하고자 하는 음악 디렉토리)"


bind_to_address        "any"

port                        "6600"

auto_update            "yes"


# An example of an ALSA output:

#

audio_output {

type             "alsa"

name           "my ALSA Device"

device          "hw:0,0"

mixer_type    "software"

}


# An example of an httpd output (built-in HTTP streaming server):

#

audio_output {

type             "httpd"

name            "My HTTP Sream"

encoder        "lame"

port             "8881"

bitrate          "128"

format          "44100:16:1"

}


※ bitrate나 format은 주석 처리 그대로 놔두거나 본인의 입맛에 맞게 설정해도 된다.



 설정을 완료했다면 재부팅을 해주자.


$ sudo reboot




4. 프로그램 코딩하기


 드디어 기나긴 세팅 과정을 마무리했으니, 본격적으로 프로그램을 코딩해보자. 라즈비안은 파이썬 프로그래밍 전용 파이썬 2/3 컴파일러가 깔려있기도 하고, 입맛대로 nano라던지 다른 에디터를 사용해도 좋겠다. 대신, 우리가 만들 프로그램은 파이썬 3로 작성할 것이다.


  여러분들이 심심할 때마다 둘러봐야할 매뉴얼들:


 ① 프로그램에 필요한 모듈/라이브러리를 불러온다.


1
2
3
4
5
import pifacecad
import os
import subprocess
from time import sleep
import datetime
cs

PiFace CAD를 이용하기 위해선 필수적으로 pifacecad 모듈의 import가 필요하며, os는 환경 변수나 디렉터리, 파일 등의 OS 자원을 제어할 수 있게 해주는 모듈로, mpc의 원활한 활용을 위해 사용한다. 


subprocess는 파이썬에서 쉘 명령을 실행할 수 있게 해주는 라이브리러로 mpc 제어창으로부터 현재 볼륨이나 플레이리스트를 만들 때 사용할 것이다. 그리고 정해진 시간을 딜레이 시켜주는 sleep 함수를 위해 sleep을 불러오고, 현재 시간을 출력하기 위해 datetime을 import하였다.



 ② 버튼/리모컨 조작으로 인한 인터럽트를 감지하는 Event Listener/IR Listener를 초기화해준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cad = pifacecad.PiFaceCAD()
 
swlistener = pifacecad.SwitchEventListener()
for i in range(8):
    swlistener.register(i, pifacecad.IODIR_FALLING_EDGE, radio_preset_switch)
    
irlistener = pifacecad.IREventListener(prog="radio", lircrc="~/.lircrc")
for k in range(8):
    irlistener.register(str(k), radio_preset_ir)
    
swlistener.activate()
irlistener.activate()
 
cad.lcd.backlight_on()
cad.lcd.cursor_off()
cad.lcd.blink_off()

cs


먼저 PiFace CAD를 cad = pifacecad.PiFaceCAD()로 초기화해준다


PiFace CAD에 내장된 버튼(스위치)이 눌릴 때, 리모컨 버튼이 눌릴 때 인터럽트를 줄 수 있도록 각각 SwitchEventListener와 IREventListener를 할당한다. IREventListener는 두 인자 prog와 lircrc를 받게 되는데, prog는 앞서 lircrc 스크립트를 짤 때 prog에 적었던 이름과 통일을 시켜야 하며, lircrc는 프로그램에 사용하기 위해 앞서 작성했던 lircrc의 위치를 적는다.


또한 앞서 얘기했듯이 우리는 이 프로그램에 8개의 버튼을 사용할 예정이므로 for문을 이용한 Listener 등록 범위는 8로 정한다. swlistener는 버튼이 눌릴 때(pifacecad.IODIR_FALLING_EDGE) 이벤트가 발생하는 것으로 설정, 이벤트 발생 시 radio_preset_switch라는 함수를 호출하고, irlistener는 리모컨 버튼이 눌릴 때 이벤트를 발생시켜 radio_preset_ir 함수를 호출하도록 한다.


activate()로 두 Event Listener를 활성화시켜주자. CAD의 LCD를 건드리는 함수들은 버그/에러의 발생 가능성을 없애기 위해 Event Listener를 활성화시킨 후에 작동하도록 Event Listener 아래에 배치한다


1
2
3
4
5
def radio_preset_switch(event):
    check_buttonir(event.pin_num)
    
def radio_preset_ir(event):
    check_buttonir(int(event.ir_code))
cs


radio_preset_switch와 radio_preset_ir 함수의 모습이다. 버튼과 리모컨으로 Event가 발생했을 때, 그에 맞는 기능을 수행하는 함수로 핀넘버(=눌린 PiFace CAD 버튼의 인덱스 번호)와 IR 코드(=리모콘 버튼의 인덱스 번호)를 할당한다.


앞서 lircrc 스크립트의 config를 모두 숫자로 한 이유가 여기에 있는데, 리모컨 인터럽트의 커맨드를 숫자로 해주면 Switch에 의한 인터럽트(=숫자를 할당하기 때문)를 받아 기능하는 함수인 check_buttonir을 리모콘에 의한 인터럽트로도 같이 공유할 수 있기 때문이다.


만약에 lircrc 스크립트의 config를 play, stop과 같은 문자열을 사용했다면, IR의 인터럽트를 받아 기능하는 함수를 따로 만들어주면 된다. 


 

 ③ 버튼/리모컨 조작으로 인한 인터럽트를 감지해, 해당하는 Event의 기능을 수행하는 함수:



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
player_type =  # (1: MP3 / 0: radio)
playlist_pos = # position in playlist
 
status = "paused"
flag_buttonir = # flag of event occured when some of button or remote pressed
flag_mute = # mute status or not (0: unmute / 1: mute)
flag_toggle = # pause status or not (0: play / 1: pause)
flag_stop = # stop status or not (0: play / 1: stop)
flag_changetype = # for displaying player type change
 
current_volume = 0
pos_shift = 0
cnt = 0
 
def check_buttonir(num):
    global playlist_pos
    global player_type
    global status
    global flag_buttonir
    global flag_toggle
    global flag_stop
    global flag_mute
    global flag_changetype
    global pos_shift
    global cnt
 
    pos_shift = 0
    cnt = 0

cs


check_buttonir은 8개의 버튼 중 하나가 눌릴 때마다 각자의 기능(재생/일시정지, 정지, 이전 곡, 다음 곡, 모드 스위칭, 음소거, 볼륨 감소, 볼륨 증가)을 수행해야 하기 때문에 플래그 등으로 사용될 여러 전역변수를 필요로 한다. 


player_type: 현재 플레이어 모드가 MP3 플레이어인지, 라디오 플레이어인지를 판단하기 위해 사용한다.

playlist_pos: 생성된 MP3 혹은 라디오 플레이리스트에서 현재 재생되는 곡이 몇 번째인지 position을 저장한다.


status: 현재 상태("playing", "paused", "stopped")를 나타낸다.

flag_buttonir: LCD 화면을 새로고침(refresh)할 필요가 있는 기능이 호출됐을 때 사용한다.

flag_mute: 음소거 상태인지 아닌지를 판단하기 위해 사용한다.

flag_toggle: 재생과 일시정지를 하나의 버튼으로 토글(toggle)할 것이기 때문에, 현재 '재생중'인지 '일시정지'인지를 판단하기 위해 사용한다.

flag_stop: 정지 상태인지 아닌지를 판단하기 위해 사용한다.

flag_changetype: 플레이어 모드를 바꿀때, 바뀐 모드가 무엇인지 출력해주기 위해 사용한다.


current_volume: 현재 볼륨을 저장한다.

pos_shift, cnt: LCD 제목 흐름 기능에 사용될 변수.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    if (num == 0):
        os.system('mpc toggle')
 
        if(flag_toggle == or flag_stop == 1):
            flag_stop = 0
            flag_toggle = #play
            status = "playing"
        else:
            flag_toggle = #pause
            status = "paused"
            
    elif (num == 1):
        os.system('mpc stop')
        flag_stop = 1
        status = "stopped"
        
    elif (num == and status == "playing" or status == "paused"):
        if ((playlist_pos -1>= 0):
            playlist_pos = playlist_pos - 1
            os.system('mpc prev')
        elif (playlist_pos == 0):
            if (player_type == 1):
                playlist_pos = len(music) -1
                os.system('mpc play %d' % len(music))
            else:
                playlist_pos = len(radioChannel) -1
                os.system('mpc play %d' % len(radioChannel))
        flag_toggle = 0
        flag_buttonir = 1
            
    elif (num == 3):
        if (player_type == and (playlist_pos +1<= len(music) and status == "playing" or status == "paused"):
            if ((playlist_pos +1== len(music)):
                playlist_pos = 0
                os.system('mpc play 1')
            else:
                playlist_pos = playlist_pos + 1
                os.system('mpc next')
            flag_toggle = 0
            flag_buttonir = 1
        elif (player_type == and (playlist_pos +1<= len(radioChannel) and status == "playing" or status == "paused"):
            if ((playlist_pos +1== len(radioChannel)):
                playlist_pos = 0
                os.system('mpc play 1')
            else:
                playlist_pos = playlist_pos + 1
                os.system('mpc next')
            flag_toggle = 0
            flag_buttonir = 1

cs


(인덱스 값으로) 0번부터 3번 버튼이 눌렸을 때 기능을 수행하는 부분이다. 0번은 음악의 재생/일시정지 토글 기능을 한다. 1번은 정지 기능을 한다. 2번, 3번 버튼은 이전 곡/다음 곡을 재생하는 기능을 한다.


※1 mpc는 정지된 상태에서 이전 곡/다음 곡 기능을 수행하지 않는다. 그렇기 때문에 status == "playing" or "pause"를 붙여 재생/일시정지 상태일 때에만 작동하도록 예외 처리가 필요하다.


※2 현재 재생하고 있는 곡의 포지션(playlist_pos)이 플레이리스트의 마지막일 경우, 다음 곡 기능을 수행할 때 첫 번째 곡으로 돌아간다. 반대로 playlist_pos가 플레이리스트의 제일 처음일 경우, 이전 곡 기능을 수행할 때 플레이리스트의 마지막 곡으로 돌아간다. 은근히 이런 프로그램을 짤 때 간과되는 기능인데, 요즘 스마트폰은 물론이거니와 이미 오래전부터 MP3 플레이어는 이런 순환 재생을 지원했기 때문에 넣어주었다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    elif (num == 4):
        if player_type:
            player_type = 0
            flag_buttonir = 1
            os.system('mpc stop')
            os.system('mpc clear')
            os.system('mpc load Radio')
            flag_changetype = 1
        else:
            player_type = 1
            flag_buttonir = 1
            os.system('mpc stop')
            os.system('mpc clear')
            os.system('mpc load MP3')
            flag_changetype = 1
        playlist_pos = 0
        flag_toggle = 1
            
    elif (num == 5):
        if(flag_mute == 0):
            os.system('mpc volume 0')
            get_volume()
            flag_mute = 1
        else:
            os.system('mpc volume \+%s' % str(current_volume))
            get_volume()
            flag_mute = 0
        flag_buttonir = 1
        
    elif (num == 6):
        os.system('mpc volume -5')
        get_volume()
        flag_mute = 0
        
    elif (num == 7):
        os.system('mpc volume \+5')
        get_volume()
        flag_mute = 0

cs


(인덱스 값으로) 4번부터 7번 버튼이 눌렸을 때 기능을 수행하는 부분이다. 4번은 플레이어 모드를 바꾸는 기능으로, 각 모드에 맞는 플레이리스트를 불러온다. 5번은 음소거/음소거 해제 기능이다. 음소거를 해제할 때는 음소거 하기 전 저장됐던 현재 볼륨 값으로 다시 돌아간다. 6/7번은 볼륨을 5씩 감소/증가시킨다.



 ④ MP3/라디오 플레이리스트를 만드는 함수와 현재 볼륨을 저장하는 함수:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def get_volume():
    global current_volume
    global display_volume
    volume = subprocess.check_output("mpc status | grep volume", shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
    volume = volume[7:volume.find("%")]
    display_volume = volume
    
    if(volume.replace(' '''!= '0'): # save current volume but not at volume 0
        current_volume = int(volume.replace(' ''')) # remove blank (ex: '  5' -> 5)
    
def create_playlist(_type):
    os.system('mpc clear')
 
    if(_type == 0):
        os.system('mpc rm MP3'#remove playlist to prevent file/url add error
        for songname in music:
            subprocess.check_output("mpc add " + songname, shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
        os.system('mpc save MP3')
    elif(_type == 1):
        os.system('mpc rm Radio'# same as MP3
        for channel in radioChannel:
            subprocess.check_output("mpc add " + channel, shell=True, stderr=subprocess.STDOUT, universal_newlines=True)
        os.system('mpc save Radio')
cs



mpc status의 출력 메시지에서 volume이 나오는 부분을 찾고, 그 줄의 8번째 위치의 글자부터 '%'가 나오기 전까지의 문자열을 volume에 저장한다. 여기서 예를 들어 볼륨이 85라면 volume에는 셋째 자리에 빈칸이 포함된 ' 85'가 저장되는데, 정수값으로 저장해주기 위해 빈칸(" ")을 없애고 int함수로 정수화시켜 current_volume에 저장한다. LCD에는 빈칸이 포함된 볼륨값을 문자열로 출력해주기 위해 정수화 과정 없이 그대로 display_volume에 할당시켜준다.


플레이리스트 제작은 MP3용과 라디오용을 따로 만들어주지 않기 위해 조건문을 사용했다. music 리스트의 길이(= MP3곡의 갯수)나 radioChannel 리스트의 길이(=라디오 채널의 갯수)만큼 MP3 혹은 라디오 플레이리스트를 만든다. music이나 radioChannel 리스트의 값이 변경됐을 때, 이를 적용시켜주기 위해 새로 만들기 전 기존에 제작된 플레이리스트를 삭제한다.



 ⑤ LCD에 출력할 플레이어 화면 중 스피커, 재생, 정지, 일시정지 아이콘을 할당하자:



1
2
3
4
5
6
7
8
9
speaker = pifacecad.LCDBitmap([0b00001,0b00011,0b01111,0b01111,0b01111,0b00011,0b00001,0])
play = pifacecad.LCDBitmap([0,0b01000,0b01100,0b01110,0b01100,0b01000,0,0])
stop = pifacecad.LCDBitmap([0,0b11111,0b11111,0b11111,0b11111,0b11111,0,0])
pause = pifacecad.LCDBitmap([0,0b11011,0b11011,0b11011,0b11011,0b11011,0,0])
 
cad.lcd.store_custom_bitmap(0, speaker)
cad.lcd.store_custom_bitmap(1, play)
cad.lcd.store_custom_bitmap(2, stop)
cad.lcd.store_custom_bitmap(3, pause) # can store only 8 custom bitmaps
cs


앞서 언급했듯이 PiFace CAD는 16x2의 블럭으로 구성되어있는데, 한 블럭은 5x8 픽셀로 구성된다. 예를 들어 스피커 아이콘의 비트맵은 이러하다:



바로 감이 오지 않는가? LCDBitmap에 할당하고자 하는 리스트의 인덱스는 블럭의 각 행이 되고, 리스트에 들어가는 값에서 이진법으로 어느 자릿수에 1값이 들어가면 칸이 채워진 것으로 본다. 그러니까 리스트의 첫 번째 인덱스의 값이 0b00001이라면 첫 번째 행의 맨 오른쪽 픽셀만 찍히는 것이다. 그래서 나는 비트맵을 입력할 때 되도록이면 2진수(0b)로 입력하기를 권한다. 일단 시각적으로 10진수나 8진수, 16진수에 비해서 훨씬 눈에 잘 들어오기 때문에.


커스텀 비트맵은 최대 8개까지 저장 가능하다.



  마지막으로 LCD에 출력할 플레이어 화면을 담당하는 while문이다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
while True:
    if (flag_buttonir == 1):
        cad.lcd.clear()
        cad.lcd.cursor_off()
        flag_buttonir = 0
 
    #display current time
    now = datetime.datetime.now()
    cad.lcd.set_cursor(01)
    nowTime = now.strftime("%H:%M")
    cad.lcd.write(nowTime)
 
    #display volume
    cad.lcd.set_cursor(61)
    cad.lcd.write_custom_bitmap(0)
    cad.lcd.set_cursor(71)
    cad.lcd.write(display_volume)
 
    # 'initializing screen'
    if(initial_screen == 0):
        cad.lcd.set_cursor(00)
        cad.lcd.write("Initializing...")
        sleep(2)
        check_buttonir(0)
        initial_screen = 1
 
    #display play/pause
    if(flag_toggle == 0):
        cad.lcd.set_cursor(00)
        cad.lcd.write_custom_bitmap(1)
        cad.lcd.write(" ")
    elif(flag_toggle == 1):
        cad.lcd.set_cursor(00)
        cad.lcd.write_custom_bitmap(3)
        cad.lcd.write(" ")
 
    #display stop
    if(flag_stop == 1):
        cad.lcd.set_cursor(00)
        cad.lcd.write_custom_bitmap(2)
        cad.lcd.write(" ")
 
    #display playlist (ex: 10/11)
    cad.lcd.set_cursor(111)
    cad.lcd.write("%2d" % int(playlist_pos+1))
    cad.lcd.set_cursor(131)
    cad.lcd.write("/")
    cad.lcd.set_cursor(141)
    if(player_type == 1):
        cad.lcd.write("%d" % len(musicName))
    else:
        cad.lcd.write("%d" % len(radioName))
cs


LCD 화면을 새로고침(refresh)할 필요가 있는 기능이 호출됐을 때(다음 곡, 이전 곡, 모드 변경 등) 화면을 clear하고 커서를 꺼준다. (커서는 항시 꺼주는 게 LCD를 보기에 아주 깔끔하다)


위에서부터 순서대로 현재 시간 출력, 현재 볼륨 출력, 프로그램을 켰을 때 초기화를 하는 (척하는) 기능, 현재 재생 상태를 아이콘으로 출력, 현재 재생하는 곡의 포지션을 출력하는 기능을 한다.



cad.lcd.set_cursor 함수는 LCD 출력을 위한 기준점 블럭의 행과 열의 인덱스 값을 두 인자로 받는다. 예를 들어 두 번째 열의 3번째 칸에 숫자 1을 써주고 싶다면 다음과 같은 소스를 쓰면 된다:


1
2
cad.lcd.set_cursor(21
cad.lcd.write("1")
cs


뭔가 거창해보이지만 사실 플레이어 모드가 바뀔 때마다 약 1초 정도 바뀐 모드가 무엇인지를 출력하고, 곡/라디오 채널 이름이 14글자 이상일 경우 좌우로 흐르는 기능을 하는 소소한 부분이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    #display player type -> music/radio name
    if(player_type == 1):
        if(flag_changetype == 1):
            check_buttonir(0)
            cad.lcd.set_cursor(00)
            cad.lcd.write(clear)
            cad.lcd.set_cursor(00)
            cad.lcd.write(">MP3 Player")
            sleep(1)
            flag_changetype = 0
            
        # if song title's length is more than 14, it flows back and forth
        if((len(musicName[playlist_pos])) > 14):
            cad.lcd.set_cursor(10)
            cad.lcd.write(clear)
            cad.lcd.set_cursor(20)
            cad.lcd.write(musicName[playlist_pos][pos_shift:])
            if( pos_shift == len(musicName[playlist_pos]) - 14):
                pos_shift = pos_shift - 1
            elif( pos_shift < len(musicName[playlist_pos]) and pos_shift < cnt):
                  pos_shift = pos_shift - 1
                  if ( pos_shift == ):
                      cnt = 0
            else:
                pos_shift = pos_shift + 1
                cnt = cnt + 1
        else:
            cad.lcd.set_cursor(10)
            cad.lcd.write(clear)
            cad.lcd.set_cursor(20)
            cad.lcd.write(musicName[playlist_pos])
            
        sleep(0.35)
 
    else:
        if(flag_changetype == 1):
            check_buttonir(0)
            cad.lcd.set_cursor(00)
            cad.lcd.write(clear)
            cad.lcd.set_cursor(00)
            cad.lcd.write(">Internet Radio")
            sleep(1)
            flag_changetype = 0
 
        # if name of radio station's length is more than 14, it flows back and forth
        if((len(radioName[playlist_pos])) > 14):
            cad.lcd.set_cursor(10)
            cad.lcd.write(clear)
            cad.lcd.set_cursor(20)
            cad.lcd.write(radioName[playlist_pos][pos_shift:])
            if( pos_shift == len(radioName[playlist_pos]) - 14):
                pos_shift = pos_shift - 1
            elif( pos_shift < len(radioName[playlist_pos]) and pos_shift < cnt):
                  pos_shift = pos_shift - 1
                  if ( pos_shift == ):
                      cnt = 0
            else:
                pos_shift = pos_shift + 1
                cnt = cnt + 1
        else:
            cad.lcd.set_cursor(10)
            cad.lcd.write(clear)
            cad.lcd.set_cursor(20)
            cad.lcd.write(radioName[playlist_pos])
 
        sleep(0.35)
 
    sleep(0.1)

cs


 ⑥ …그러므로 완성된 소스는 이렇게 된다:



 ⑦ 최종 소스를 컴파일하기 전에 ir.py 안의 내용을 일부 수정해야 한다. 다음 명령어를 입력한다:


>>> sudo nano /usr/lib/python3/dist-packages/pifcaecad/ir.py


 소스 중 self.event_queue = multiprocessing.queues.SimpleQueue()라는 라인을 찾을 수 있을 것이다. 중간의 .queues를 지워 multiprocessing.SimpleQueue()가 되도록 만들어준다.






5. 작동 영상 및 마무리


 백문이 불여일견이라고 소스가 어떻게 돌아가는지 실제 작동 영상으로 확인해보자:



 16x2의 LCD 화면이라 제약이 많지만, 좌우로 움직이는 제목, 현재 재생 상태를 나타내는 아이콘, 현재 시간, 볼륨, 플레이리스트 포지션까지... 그래도 한 화면 안에 최대한 MP3나 라디오 플레이어로부터 필요한 재생 정보들을 출력할 수 있도록 인터페이스 구성을 해본 것 같다. 옆으로 한 네 칸만 넓었으면 플레이타임 같은 거도 넣어봤을텐데 할 수 없지.


 이번 프로그램을 제작하면서 가장 중점에 두었던 것은 '라즈베리 파이와 파이페이스를 이용하기 때문에 한계가 있을 수 밖에 없지만, 그 한계 안에서도 우리가 알고 있는 MP3/라디오 플레이어의 기능에 최대한 가깝게 구현하자'였고, 그렇기 때문에 예외 처리를 더 한다던가 기능을 더 추가해보려고 많이 욕심을 냈었다. 결과물은 약간 신통찮지만, 나름대로는 약간 만족스럽기도 하다. 이런 시행착오와 한계를 알고 그 안에 머무는 (무언가 역설적인) '여유'를 겪다보면, 라즈베리 파이와 같은 개발 보드와 더 친해질 수 있지 않을까 싶기도 하고.


 나는 일단 쉬운 코딩을 위해 음악파일과 제목, 라디오 채널 스트리밍 주소와 채널 이름을 모두 리스트 자료형으로 생성했었지만, 텍스트 파일에서 파싱을 한다던지, 폴더채로 읽는다던지와 같은 응용도 가능할 것이다. 중간중간 매뉴얼을 링크한 이유는 그런 부분에 있다. 그 밖에 mpc 말고도 pygame mixer나 mplayer를 이용해보는 방법도 있겠다. 이 프로젝트를 따라해보면서 흥미가 생겼다면, 여러 번 바꿔보고 다른 방식으로 활용해보면서 '나의 것'을 만들어보자.


 ※ 버그를 발견했거나 궁금한 점이 있으신 분들은 댓글로 남겨주시면 감사하겠습니다.





  1. 재밌게도 영국에서는 1980년대에 전국민 컴퓨터 소양 교육 프로젝트의 일환으로 BBC 마이크로(BBC Micro)가 출시된 전적이 있었다. 다만 BBC 마이크로는 그 당시 나왔던 다른 컴퓨터 기종(ZX 스펙트럼, 코모도어 64)에 비해 비쌌다는 게 문제였고, BBC 마이크로 사용자들은 소위 "posh"(상류층)라는 별칭이 붙기도 했었다고. [본문으로]
  2. 시큐어 셸; Secure SHell. 네트워크 상의 다른 컴퓨터에 로그인하거나 원격 시스템에서 명령을 실행하고 다른 시스템으로 파일을 복사할 수 있도록 해 주는 응용 프로그램 또는 그 프로토콜을 말한다. [본문으로]
  3. 가상 네트워크 컴퓨팅; Virtual Network Computing. 컴퓨터 환경에서 원격으로 다른 컴퓨터를 제어하는 그래픽 데스크톱 공유 시스템을 말한다. [본문으로]