用Python为树莓派制作一个GPIO控制的音乐播放器

Posted by 橙叶 on Thu, Aug 9, 2018

话说最近真是干啥啥不行。

先说背景

手机被收了,只能用树莓派听歌,不过我的派并没有配屏幕……所以最初的方法是:打开电脑->连上VNC->打开树莓派的浏览器访问网易云,虽然能听,但是切歌、调音量都得通过电脑调,很麻烦,而且开着Chromium树莓派发热恐怖。

手头上正好有红外接收头+遥控器,于是乎萌生了用遥控器控制树莓派的方法。先网上搜了一下有没有现成的轮子,结果“哇,LIRC一步到位”“哇,XBMC专业播放器”,然后我被各种坑,被网上的那些半残教程坑、被软件坑、被遥控器坑……经过一番艰苦奋斗,终于调好了,然而当我把调好的设备转移到书房的时候,红外接收头烧了……烧了,散发出一种刻骨铭心的糊味,熏出了我淡淡的忧伤。

悲愤的我重新刷回了raspbian,打开文本编辑器,自己来。

要不说我干啥啥不行呢,熬夜捣鼓出了100来行Python,还是在有pygame这个轮轴的前提下,实现了暂停和继续播放、音量调整,使用GPIO+微动开关控制。第二天上午加上了切歌功能。

OK,正文开始。

首先是接线,线一大坨,不过很简单,就是接五个按键。如图(不含LCD):

(本来自己摸索着连了一大坨很有成就感,画出来一看真TM简单)

以下是成品图,最终我添加了一块LCD1602,用来显示歌曲序号和音量:

然后是代码,用pygame库来实现音乐的播放和控制,多线程同时监听5个按键和控制LCD,整体逻辑很简单,说实在的还是有许多不好把控的细节的,比如……额……控制上下曲的时候防止数组溢出、确保LCD的文字正好占满全屏,以及一些反应播放状态变量方便增加新功能。

P.S.:关于监测GPIO口状态的方法,我用的是GPIO.event.detected(),这个会在后台持续监听,其实我已经用while轮询了,就没必要用这个,一般的GPIO.input()就可以了,懒得改了。

添加音乐需要先执行scan.py生成目录。

main.py:

  1. #coding=utf-8  
  2. import RPi.GPIO as GPIO,time,pygame,threading,os,json  
  3. #import LCD1602 Module  
  4. from lcd_i2c import *  
  5. lcd_string("Loading...",LCD_LINE_1)  
  6. #GPIO setup  
  7. #part1  
  8. GPIO.setmode(GPIO.BCM)  
  9. #Pin2use [volumeup,volumedown,play/pause,next,previous]  
  10. pinList= [17,27,18,22,23]  
  11. pinVolume = pinList[0:2]  
  12. GPIO.setup(pinList,GPIO.IN)  
  13. for pinSingle in pinList:  
  14.     GPIO.add_event_detect(pinSingle, GPIO.RISING)  
  15. pinPlay = pinList[2]  
  16. pinNext = pinList[3]  
  17. pinPrev = pinList[4]  
  18. #Default volume value(0.0~1.0)  
  19. volume = 0.50000000  
  20. statelist = [0,0]  
  21.   
  22. #Music Playing  
  23. with open('index.list','r') as index:  
  24.     filelist = json.loads(index.read())  
  25.     print(filelist)  
  26. #filelist =['菅原纱由理(THE\xa0SxPLAY) - Guardian.mp3', '不才 - 一身诗意千寻瀑.mp3', 'Riin - Alice good night.mp3', '林和夜 - 相聚万年树.mp3', '苏尚卿 - 雪年轮(颜如玉ver).mp3', '菅原纱由理(THE\xa0SxPLAY) - キミが残した世界で.mp3', '匀子 - 铃舟.mp3', '花粥 - 一腔诗意喂了狗.mp3', 'Minnutes - I Can.mp3', '绯村柯北,灰老板 - 东流.mp3', 'Cranky - La Promesse.mp3', '关晓彤 - 音梦.mp3', '刘惜君 - 我很快乐.mp3', '张恋歌 - 不忘 (完整版).mp3', '呦猫UNEKO - 梦回还.mp3', '冥月,Mario - 若当来世.mp3', '林和夜 - 此彼绘卷.mp3']  
  27. lens = len(filelist)  
  28. filePlaying = 0  
  29. pygame.mixer.init()  
  30. track = pygame.mixer.music.load(filelist[filePlaying])  
  31. pygame.mixer.music.set_volume(volume)  
  32. pygame.mixer.music.play()  
  33. pause=0  
  34. #main function  
  35. def playpause():  
  36.     global pause  
  37.     while True:  
  38.         while GPIO.event_detected(pinPlay):  
  39.             while GPIO.event_detected(pinPlay) == 0:  
  40.                 if pause == 0:  
  41.                     print('pause music')  
  42.                     pygame.mixer.music.pause()  
  43.                     pause=1  
  44.                 else:  
  45.                     print('resume music')  
  46.                     pygame.mixer.music.unpause()  
  47.                     pause=0  
  48.                 break  
  49.             time.sleep(1)  
  50.         time.sleep(1)  
  51.   
  52.   
  53. def volume():  
  54.             #volume  
  55.         global volume  
  56.         volume = 0.5  
  57.         while True:  
  58.             i=0  
  59.             for pinSingle in pinVolume:  
  60.                 statelist[i] = GPIO.event_detected(pinSingle)  
  61.                 i=i+1  
  62.         #    print('Volume keys state',statelist)  
  63.             if statelist[0]==True:  
  64.                 print('volume up')  
  65.                 volume = volume + 0.1  
  66.                 if volume > 1.0:  
  67.                     volume = 1.0  
  68.                 print('volume value:',volume)  
  69.             if statelist[1]==True:  
  70.                 print('volume down')  
  71.                 volume = volume - 0.1  
  72.                 if volume < 0.0:  
  73.                     volume = 0.0  
  74.                 print('volume value:',str(round(volume*10)))  
  75.             pygame.mixer.music.set_volume(volume)  
  76.             time.sleep(0.5)  
  77.   
  78. #自动切歌 手动切歌  
  79. def switch():  
  80.     global filePlaying,track,pause  
  81.     while True:  
  82.         filePlayingchk = filePlaying  
  83.         if pygame.mixer.music.get_busy() == 0 :  
  84.             filePlaying = filePlaying + 1  
  85.         else:  
  86.             if GPIO.event_detected(pinNext) == 1  :  
  87.                 while GPIO.event_detected(pinNext) == 0:  
  88.                     filePlaying = filePlaying + 1  
  89.                     break  
  90.             if GPIO.event_detected(pinPrev) == 1  :  
  91.                 while GPIO.event_detected(pinPrev) == 0:  
  92.                     filePlaying = filePlaying - 1  
  93.                     break  
  94.         if filePlaying < 0:  
  95.             filePlaying = lens -1  
  96.         elif filePlaying > lens -1:  
  97.             filePlaying = 0  
  98.         if filePlayingchk != filePlaying :  
  99.             track = pygame.mixer.music.load(filelist[filePlaying])  
  100.             pygame.mixer.music.play()  
  101.             pause = 0  
  102.             print(filePlaying + 1,filelist[filePlaying])  
  103.         time.sleep(0.5)  
  104.   
  105. #LCD1602 Module  
  106. def displayContent():  
  107.   # Main program block  
  108.   global filePlaying,pause,volume,lens  
  109.   # Initialise display  
  110.   lcd_init()  
  111.   
  112.   while True:  
  113.   
  114.     # Send some test  
  115.     figure = len(str(lens)) + len(str(filePlaying + 1))  
  116.     if pause == 0:  
  117.         playState = "Playing"  
  118.         dots = 8 - figure  
  119.         while dots > 0:  
  120.             playState = playState + "."  
  121.             dots = dots -1  
  122.     else:  
  123.         playState = "Paused"  
  124.         dots = 10 - figure  
  125.         while dots > 1:  
  126.             playState = playState + "."  
  127.             dots = dots -1  
  128.     if int(round(volume,1)*10) == 10:  
  129.         dots2 = "....."  
  130.     else:  
  131.         dots2 = "......"  
  132.     lcd_string(playState + str(filePlaying + 1) + "/" + str(lens) ,LCD_LINE_1)  
  133.     lcd_string("Volume" + dots2 + str(round(volume*10)) + "/" + "10",LCD_LINE_2)  
  134.     time.sleep(1)  
  135.   
  136. def displayModule():  
  137.     while True:  
  138.         if __name__ == '__main__':  
  139.             try:  
  140.                 displayContent()  
  141.             except KeyboardInterrupt:  
  142.                 pass  
  143.             finally:  
  144.                 lcd_byte(0x01, LCD_CMD)  
  145.   
  146.   
  147. #Muti-threads  
  148. threads=[]  
  149. t1 = threading.Thread(target=playpause)  
  150. threads.append(t1)  
  151. t2 = threading.Thread(target=volume)  
  152. threads.append(t2)  
  153. t3 = threading.Thread(target=switch)  
  154. threads.append(t3)  
  155. t4 = threading.Thread(target=displayModule)  
  156. threads.append(t4)  
  157.   
  158. print('Wait......')  
  159. print(filePlaying + 1,filelist[filePlaying])  
  160. if __name__ == '__main__':  
  161.     for t in threads:  
  162.         t.setDaemon(True)  
  163.         t.start()  
  164.     t.join()  

scan.py:

  1. #coding=utf-8  
  2. import os,json  
  3.   
  4. #指定扫描目录,生成index.list  
  5. path = '/home/pi/Music/'  
  6.   
  7. dirs = os.listdir(path)  
  8. support = ['.mp3','.ogg']  
  9. filelist = []  
  10. for file in dirs:  
  11.     filename = os.path.splitext(file)  
  12.     if filename[-1] in support:  
  13.         filelist.append(path + file)  
  14.         print(file)  
  15. with open('index.list', 'w') as index:  
  16.     index.write(json.dumps(filelist))  

这两个文件还可以再整合,另外这样还是需要在SSH上手动运行的,可以添加一个开机启动服务。

LCD1602的操作库,会在main.py里导入:

  1. #!/usr/bin/python  
  2. #coding=utf-8  
  3. #--------------------------------------  
  4. #    ___  ___  _ ____  
  5. #   / _ \/ _ \(_) __/__  __ __  
  6. #  / , _/ ___/ /\ \/ _ \/ // /  
  7. # /_/|_/_/  /_/___/ .__/\_, /  
  8. #                /_/   /___/  
  9. #  
  10. #  lcd_i2c.py  
  11. #  LCD test script using I2C backpack.  
  12. #  Supports 16x2 and 20x4 screens.  
  13. #  
  14. # Author : Matt Hawkins  
  15. # Date   : 20/09/2015  
  16. #  
  17. # http://www.raspberrypi-spy.co.uk/  
  18. #  
  19. # Copyright 2015 Matt Hawkins  
  20. #  
  21. # This program is free software: you can redistribute it and/or modify  
  22. # it under the terms of the GNU General Public License as published by  
  23. # the Free Software Foundation, either version 3 of the License, or  
  24. # (at your option) any later version.  
  25. #  
  26. # This program is distributed in the hope that it will be useful,  
  27. # but WITHOUT ANY WARRANTY; without even the implied warranty of  
  28. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
  29. # GNU General Public License for more details.  
  30. #  
  31. # You should have received a copy of the GNU General Public License  
  32. # along with this program.  If not, see <http://www.gnu.org/licenses/>.  
  33. #  
  34. #--------------------------------------  
  35. import smbus  
  36. import time  
  37.   
  38. # Define some device parameters  
  39. I2C_ADDR  = 0x3f # I2C device address  
  40. LCD_WIDTH = 16   # Maximum characters per line  
  41.   
  42. # Define some device constants  
  43. LCD_CHR = 1 # Mode - Sending data  
  44. LCD_CMD = 0 # Mode - Sending command  
  45.   
  46. LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line  
  47. LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line  
  48. LCD_LINE_3 = 0x94 # LCD RAM address for the 3rd line  
  49. LCD_LINE_4 = 0xD4 # LCD RAM address for the 4th line  
  50.   
  51. LCD_BACKLIGHT  = 0x08  # On  
  52. #LCD_BACKLIGHT = 0x00  # Off  
  53.   
  54. ENABLE = 0b00000100 # Enable bit  
  55.   
  56. # Timing constants  
  57. E_PULSE = 0.0005  
  58. E_DELAY = 0.0005  
  59.   
  60. #Open I2C interface  
  61. #bus = smbus.SMBus(0)  # Rev 1 Pi uses 0  
  62. bus = smbus.SMBus(1) # Rev 2 Pi uses 1  
  63.   
  64. def lcd_init():  
  65.   # Initialise display  
  66.   lcd_byte(0x33,LCD_CMD) # 110011 Initialise  
  67.   lcd_byte(0x32,LCD_CMD) # 110010 Initialise  
  68.   lcd_byte(0x06,LCD_CMD) # 000110 Cursor move direction  
  69.   lcd_byte(0x0C,LCD_CMD) # 001100 Display On,Cursor Off, Blink Off  
  70.   lcd_byte(0x28,LCD_CMD) # 101000 Data length, number of lines, font size  
  71.   lcd_byte(0x01,LCD_CMD) # 000001 Clear display  
  72.   time.sleep(E_DELAY)  
  73.   
  74. def lcd_byte(bits, mode):  
  75.   # Send byte to data pins  
  76.   # bits = the data  
  77.   # mode = 1 for data  
  78.   #        0 for command  
  79.   
  80.   bits_high = mode | (bits & 0xF0) | LCD_BACKLIGHT  
  81.   bits_low = mode | ((bits<<4) & 0xF0) | LCD_BACKLIGHT  
  82.   
  83.   # High bits  
  84.   bus.write_byte(I2C_ADDR, bits_high)  
  85.   lcd_toggle_enable(bits_high)  
  86.   
  87.   # Low bits  
  88.   bus.write_byte(I2C_ADDR, bits_low)  
  89.   lcd_toggle_enable(bits_low)  
  90.   
  91. def lcd_toggle_enable(bits):  
  92.   # Toggle enable  
  93.   time.sleep(E_DELAY)  
  94.   bus.write_byte(I2C_ADDR, (bits | ENABLE))  
  95.   time.sleep(E_PULSE)  
  96.   bus.write_byte(I2C_ADDR,(bits & ~ENABLE))  
  97.   time.sleep(E_DELAY)  
  98.   
  99. def lcd_string(message,line):  
  100.   # Send string to display  
  101.   
  102.   message = message.ljust(LCD_WIDTH," ")  
  103.   
  104.   lcd_byte(line, LCD_CMD)  
  105.   
  106.   for i in range(LCD_WIDTH):  
  107.     lcd_byte(ord(message[i]),LCD_CHR)  
  108.   
  109. def main():  
  110.   # Main program block  
  111.   
  112.   # Initialise display  
  113.   lcd_init()  
  114.   
  115.   while True:  
  116.   
  117.     # Send some test  
  118.     lcd_string("RP嗨L          <",LCD_LINE_1)  
  119.     lcd_string("I2C LCD        <",LCD_LINE_2)  
  120.   
  121.     time.sleep(3)  
  122.   
  123.     # Send some more text  
  124.     lcd_string(">         RPiSpy",LCD_LINE_1)  
  125.     lcd_string(">        I2C LCD",LCD_LINE_2)  
  126.   
  127.     time.sleep(3)  
  128.   
  129. if __name__ == '__main__':  
  130.   
  131.   try:  
  132.     main()  
  133.   except KeyboardInterrupt:  
  134.     pass  
  135.   finally:  
  136.     lcd_byte(0x01, LCD_CMD)  

Python2.7和3都可以用,推荐使用3,可以在SSH里显示中文歌名。

哇我捣鼓了好久的东西这一会儿就说完了,不得劲啊。毕竟这些东西在树莓派上用Python实现比较省力,不涉及底层。如果做到单片机上就比较牛逼了(不使用现成的模块),不多说了,最近在研究电子电路。



comments powered by Disqus