最終更新日:181113 原本2016-10-15 

Kivyで、シンプルなミュージックプレイヤー②


Bitbucketにソースをおきました。
https://bitbucket.org/toritoritorina/kivy-simple-music

Kivyで、シンプルなミュージックプレイヤー①
https://torina.top/detail/302/

の続きです。

音量のアップ、マイナスとシークバーに対応してみました。


再生時間などが表示され、バーも動きますね。シークバークリックで、その位置から再生になります。


+、-で音量が変化します。


main.py
増えた関数があるのと、全体的にかわりました。
  1.  
  2. import os
  3. from kivy.app import App
  4. from kivy.core.audio import SoundLoader
  5. from kivy.clock import Clock
  6. from kivy.properties import ObjectProperty
  7. from kivy.uix.boxlayout import BoxLayout
  8. from kivy.uix.popup import Popup
  9.  
  10.  
  11. def get_time_string(now, end):
  12. """ '0:15/1:19' のような再生時間の文字列表現を返す
  13.  
  14. 引数:
  15. now: 現在の再生時間を、秒単位で指定。floatでもOK
  16. end: 終了時の再生時間を、秒単位で指定。floatでもOK
  17. """
  18.  
  19. now_m, now_s = map(int, divmod(now, 60))
  20. now_string = "{0:d}:{1:02d}".format(now_m, now_s)
  21.  
  22. end_m, end_s = map(int, divmod(end, 60))
  23. end_string = "{0:d}:{1:02d}".format(end_m, end_s)
  24.  
  25. return "{0}/{1}".format(now_string, end_string)
  26.  
  27.  
  28.  
  29. class PopupChooseFile(BoxLayout):
  30.  
  31. # 現在のカレントディレクトリ。FileChooserIconViewのpathに渡す
  32. current_dir = os.path.dirname(os.path.abspath(__file__))
  33.  
  34. # MusicPlayerクラス内で参照するための設定
  35. select = ObjectProperty(None)
  36. cancel = ObjectProperty(None)
  37.  
  38.  
  39. class MusicPlayer(BoxLayout):
  40.  
  41. audio_button = ObjectProperty(None) # >や||のボタンへのアクセス
  42. status = ObjectProperty(None) # 画面中央のお知らせとなるテキスト部分
  43. volume_value = ObjectProperty(None) # 音量の値
  44. bar = ObjectProperty(None) # 再生位置
  45. time_text = ObjectProperty(None) # 再生時間のテキスト
  46. is_playing = False # 再生中か否か。一時停止時はTrue
  47. sound = None
  48.  
  49. def choose(self):
  50. """Choose File押下時に呼び出され、ポップアップでファイル選択させる"""
  51.  
  52. content = PopupChooseFile(select=self.select, cancel=self.cancel)
  53. self.popup = Popup(title="Select MP3", content=content)
  54. self.popup.open()
  55.  
  56. def play_or_stop(self):
  57. """||や>押下時。再生中なら一時停止、停止中なら再生する"""
  58.  
  59. if not self.sound:
  60. self.status.text = 'Please Select MP3'
  61.  
  62. else:
  63. # 再生中ならポーズ処理
  64. if self.sound.state == "play":
  65. self._pause()
  66.  
  67. # 一時停止中なら再スタート
  68. elif self.sound.state == "stop":
  69. self._restart()
  70.  
  71. def volume_plus(self):
  72. """ボリュームのアップ"""
  73.  
  74. if not self.sound:
  75. self.status.text = 'Please Select MP3'
  76.  
  77. else:
  78. self.sound.volume += 0.1
  79. if self.sound.volume > 1:
  80. self.sound.volume = 1
  81. value = int(self.sound.volume*10)
  82. self.volume_value.text = str(value)
  83.  
  84. def volume_minus(self):
  85. """ボリュームのダウン"""
  86.  
  87. if not self.sound:
  88. self.status.text = 'Please Select MP3'
  89.  
  90. else:
  91. self.sound.volume -= 0.1
  92. if self.sound.volume < 0:
  93. self.sound.volume = 0
  94. value = int(self.sound.volume*10)
  95. self.volume_value.text = str(value)
  96.  
  97. def cancel(self):
  98. """ファイル選択画面でキャンセル"""
  99.  
  100. self.popup.dismiss()
  101.  
  102. def select(self, path):
  103. """ファイル選択画面で、ファイル選択時"""
  104.  
  105. if self.sound:
  106. self._stop()
  107.  
  108. self.sound = SoundLoader.load(path)
  109. self.sound_name = os.path.basename(path)
  110.  
  111. # 再生を試みて、できない(mp3じゃない)ならexcept。MP3にしろとメッセージ
  112. try:
  113. self._start()
  114. except AttributeError:
  115. self.status.text = 'Should MP3'
  116. finally:
  117. self.popup.dismiss()
  118.  
  119. def click_seek(self, position):
  120. """シークバークリックでの、音楽移動処理
  121.  
  122. 実際はスライダーの値が変わるたびに呼ばれるため、シークバークリック時と、
  123. 0.1秒毎(_timerでの値増減後)にもこの関数が呼ばれます。
  124. ユーザがシークバークリックした際と区別するため、以下の条件式で判定
  125. position != self.one_before+0.1
  126. 以前から0.1しか動いていない=タイマーでの移動分で、ユーザのシーククリックではないと判断し何もしない
  127.  
  128. 引数:
  129. position: クリックされたシークバーの位置
  130. """
  131.  
  132. # mp3をロードしてない、もしくは既に_stop後
  133. if not self.sound:
  134. self.status.text = 'Please Select MP3'
  135. self.bar.value = 0
  136.  
  137. # ユーザがシークバークリックをした場合
  138. elif position != self.one_before+0.1:
  139. self._pause()
  140. self._restart(position)
  141.  
  142. def _timer(self, dt):
  143. """バーと再生時間テキストの更新"""
  144.  
  145. # 既に_stopされていた場合
  146. if not self.sound:
  147. return False
  148.  
  149. # バーが最大値を超えたらストップで、タイマー解除
  150. elif self.bar.value >= self.bar.max:
  151. self._stop()
  152. return False
  153.  
  154. else:
  155. self.one_before = self.bar.value
  156. self.bar.value += 0.1
  157. self.time_text.text = get_time_string(
  158. self.bar.value, self.sound.length)
  159.  
  160. def _restart(self, position=None):
  161. """再スタート"""
  162.  
  163. self.sound.play()
  164. Clock.schedule_interval(self._timer, 0.1)
  165.  
  166. # 再生位置の指定があればそこから、なければpause時の位置から再生
  167. restart_position = position if position else self.pause_position
  168. self.sound.seek(restart_position) # 再生位置の復元
  169.  
  170. self.audio_button.text = "||"
  171. self.status.text = 'Playing {}'.format(self.sound_name)
  172.  
  173. def _pause(self):
  174. """一時停止"""
  175.  
  176. self.sound.stop()
  177. Clock.unschedule(self._timer)
  178.  
  179. self.pause_position = self.sound.get_pos()
  180.  
  181. self.audio_button.text = ">"
  182. self.status.text = 'Stop {}'.format(self.sound_name)
  183.  
  184. def _start(self):
  185. """スタート処理。mp3ファイル選択後に呼ばれる"""
  186.  
  187. self.sound.play()
  188. Clock.schedule_interval(self._timer, 0.1)
  189. self.is_playing = True
  190.  
  191. self.audio_button.text = "||"
  192. self.status.text = 'Playing {}'.format(self.sound_name)
  193.  
  194. self.bar.max = self.sound.length
  195.  
  196. def _stop(self):
  197. """停止"""
  198.  
  199. self.sound.stop()
  200. self.sound = None
  201. Clock.unschedule(self._timer)
  202. self.is_playing = False
  203.  
  204. self.audio_button.text = ">"
  205. self.status.text = 'Stop {}'.format(self.sound_name)
  206.  
  207. self.bar.value = 0
  208. self.time_text.text = '0:00/0:00'
  209.  
  210.  
  211. class Music(App):
  212. icon = "ico.png"
  213.  
  214. def build(self):
  215. return MusicPlayer()
  216.  
  217.  
  218. if __name__ == "__main__":
  219. Music().run()



music.kv
  1.  
  2. <MusicPlayer>:
  3. status: status_text
  4. audio_button: audio_button
  5. volume_value: volume_value
  6. bar: bar
  7. time_text: time_text
  8.  
  9. orientation: 'vertical'
  10. padding: 20
  11.  
  12. Label:
  13. size_hint: 1, .8
  14. id: status_text
  15. text: ""
  16. center: root.center
  17. color: 1, 0, 0, 1
  18.  
  19. BoxLayout:
  20. size_hint: 1, .1
  21. orientation: 'horizontal'
  22.  
  23. Label:
  24. id: time_text
  25. size_hint: .1, 1
  26. text: '0:00/0:00'
  27. background_color: 0, .2, 1, 1
  28.  
  29. Slider:
  30. id: bar
  31. size_hint: .9, 1
  32. max: 100
  33. value: 0
  34. on_value: root.click_seek(self.value)
  35.  
  36. BoxLayout:
  37. size_hint: 1, .1
  38. orientation: 'horizontal'
  39. Button:
  40. id: audio_button
  41. size_hint: .1, 1
  42. text: '||'
  43. background_color: 0, .2, 1, 1
  44. on_release: root.play_or_stop()
  45.  
  46. Button:
  47. size_hint: .1, 1
  48. text: '+'
  49. background_color: 0, .2, 1, 1
  50. on_release: root.volume_plus()
  51. Button:
  52. id: volume_value
  53. size_hint: .1, 1
  54. text: '10'
  55. background_color: 0, .2, 1, 1
  56. Button:
  57. size_hint: .1, 1
  58. text: '-'
  59. background_color: 0, .2, 1, 1
  60. on_release: root.volume_minus()
  61.  
  62. Button:
  63. size_hint: .1, 1
  64. text: 'Choose File'
  65. background_color: 0, .4, 1, 1
  66. on_release: root.choose()
  67.  
  68.  
  69. <PopupChooseFile>:
  70. canvas:
  71. Color:
  72. rgba: 0, 0, .4, 1
  73. Rectangle:
  74. pos: self.pos
  75. size: self.size
  76. orientation: "vertical"
  77.  
  78. FileChooserIconView:
  79. size_hint: 1, .9
  80. path: root.current_dir
  81. on_submit: root.select(self.selection[0])
  82. BoxLayout:
  83. size_hint: 1, .1
  84. Button:
  85. text: "Cancel"
  86. background_color: 0,.5,1,1
  87. on_release: root.cancel()



ボリュームの調整はこのメソッドですね。
  1.  
  2. def volume_plus(self):
  3. """ボリュームのアップ"""
  4.  
  5. if not self.sound:
  6. self.status.text = 'Please Select MP3'
  7.  
  8. else:
  9. self.sound.volume += 0.1
  10. if self.sound.volume > 1:
  11. self.sound.volume = 1
  12. value = int(self.sound.volume*10)
  13. self.volume_value.text = str(value)
  14.  
  15. def volume_minus(self):
  16. """ボリュームのダウン"""
  17.  
  18. if not self.sound:
  19. self.status.text = 'Please Select MP3'
  20.  
  21. else:
  22. self.sound.volume -= 0.1
  23. if self.sound.volume < 0:
  24. self.sound.volume = 0
  25. value = int(self.sound.volume*10)
  26. self.volume_value.text = str(value)



volumeプロパティは0から1までですが、それをそのまま表示するとちょっとアレなので、*10して0から10までに見せています。


volume Added in 1.3.0
Volume, in the range 0-1. 1 means full volume, 0 means mute.

volume is a NumericProperty and defaults to 1.




バーと再生時間のテキスト更新のための、タイマー処理です。0.1秒毎にバーとテキストを更新します。
one_beforeは何に使うのかというと、シークバーのクリック時です。
  1.  
  2. def _timer(self, dt):
  3. """バーと再生時間テキストの更新"""
  4.  
  5. # 既に_stopされていた場合
  6. if not self.sound:
  7. return False
  8.  
  9. # バーが最大値を超えたらストップで、タイマー解除
  10. elif self.bar.value >= self.bar.max:
  11. self._stop()
  12. return False
  13.  
  14. else:
  15. self.one_before = self.bar.value
  16. self.bar.value += 0.1
  17. self.time_text.text = get_time_string(
  18. self.bar.value, self.sound.length)


これがシークバークリック時の処理ですが、実際は_timerで値が増減するたびにも呼ばれます。
あくまでon_valueでこのメソッド呼んでますからね。
_timerでは0.1だけbarの値を増やします。つまり、前回の値と比較して0.1しか増えてないなら_timer処理で、そうでなければシークバークリックということにしました。
  1.  
  2. def click_seek(self, position):
  3. """シークバークリックでの、音楽移動処理
  4.  
  5. このメソッドは、実際はスライダーの値が変わるたびに呼ばれるため、シークバークリック時と、
  6. 0.1秒毎(_timerでの値増減後)にもこの関数が呼ばれます。
  7. ユーザがシークバークリックした際と区別するため、以下の条件式で判定
  8. position != self.one_before+0.1
  9. 以前から0.1しか動いていない=タイマーでの移動分で、ユーザのシーククリックではないと判断し何もしない
  10.  
  11. 引数:
  12. position: クリックされたシークバーの位置
  13. """
  14.  
  15. # mp3をロードしてない、もしくは既に_stop後
  16. if not self.sound:
  17. self.status.text = 'Please Select MP3'
  18. self.bar.value = 0
  19.  
  20. # ユーザがシークバークリックをした場合
  21. elif position != self.one_before+0.1:
  22. self._pause()
  23. self._restart(position)



再生時間のテキスト文字列を作成する関数です。_timer内で呼ばれています。
これは結構汎用的に使える関数です。
http://stackoverflow.com/questions/775049/python-time-seconds-to-hms
  1.  
  2. def get_time_string(now, end):
  3. """ '0:15/1:19' のような再生時間の文字列表現を返す
  4.  
  5. 引数:
  6. now: 現在の再生時間を、秒単位で指定。floatでもOK
  7. end: 終了時の再生時間を、秒単位で指定。floatでもOK
  8. """
  9.  
  10. now_m, now_s = map(int, divmod(now, 60))
  11. now_string = "{0:d}:{1:02d}".format(now_m, now_s)
  12.  
  13. end_m, end_s = map(int, divmod(end, 60))
  14. end_string = "{0:d}:{1:02d}".format(end_m, end_s)
  15.  
  16. return "{0}/{1}".format(now_string, end_string)


試しに使ってみます。
  1.  
  2. def get_time_string(now, end):
  3.  
  4. now_m, now_s = map(int, divmod(now, 60))
  5. now_string = "{0:d}:{1:02d}".format(now_m, now_s)
  6.  
  7. end_m, end_s = map(int, divmod(end, 60))
  8. end_string = "{0:d}:{1:02d}".format(end_m, end_s)
  9.  
  10. return "{0}/{1}".format(now_string, end_string)
  11.  
  12.  
  13. now = 100 # 100 second
  14. end = 2000 # 2000 second
  15. result = get_time_string(now, end)
  16. print(result)


良い感じですね。intが必ず来る保障があるなら、map(intは不要です。
  1.  
  2. 1:40/33:20


以前に、

Pythonで、進捗バーを自作する
https://torina.top/detail/263/

というのを作りましたが、この関数を使うと捗りそうですね。

各種便利な関数です。いちいちボタンやラベルのテキストの変更や、タイマー管理等が面倒だったために作成しました。
  1.  
  2. def _restart(self, position=None):
  3. """再スタート"""
  4.  
  5. self.sound.play()
  6. Clock.schedule_interval(self._timer, 0.1)
  7.  
  8. # 再生位置の指定があればそこから、なければpause時の位置から再生
  9. restart_position = position if position else self.pause_position
  10. self.sound.seek(restart_position) # 再生位置の復元
  11.  
  12. self.audio_button.text = "||"
  13. self.status.text = 'Playing {}'.format(self.sound_name)
  14.  
  15. def _pause(self):
  16. """一時停止"""
  17.  
  18. self.sound.stop()
  19. Clock.unschedule(self._timer)
  20.  
  21. self.pause_position = self.sound.get_pos()
  22.  
  23. self.audio_button.text = ">"
  24. self.status.text = 'Stop {}'.format(self.sound_name)
  25.  
  26. def _start(self):
  27. """スタート処理。mp3ファイル選択後に呼ばれる"""
  28.  
  29. self.sound.play()
  30. Clock.schedule_interval(self._timer, 0.1)
  31. self.is_playing = True
  32.  
  33. self.audio_button.text = "||"
  34. self.status.text = 'Playing {}'.format(self.sound_name)
  35.  
  36. self.bar.max = self.sound.length
  37.  
  38. def _stop(self):
  39. """停止"""
  40.  
  41. self.sound.stop()
  42. self.sound = None
  43. Clock.unschedule(self._timer)
  44. self.is_playing = False
  45.  
  46. self.audio_button.text = ">"
  47. self.status.text = 'Stop {}'.format(self.sound_name)
  48.  
  49. self.bar.value = 0
  50. self.time_text.text = '0:00/0:00'