Kivyで、シンプルなミュージックプレイヤー②
Bitbucketにソースをおきました。
https://bitbucket.org/toritoritorina/kivy-simple-music
Kivyで、シンプルなミュージックプレイヤー①
https://torina.top/detail/302/
の続きです。
音量のアップ、マイナスとシークバーに対応してみました。

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

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

main.py
増えた関数があるのと、全体的にかわりました。
- import os
- from kivy.app import App
- from kivy.core.audio import SoundLoader
- from kivy.clock import Clock
- from kivy.properties import ObjectProperty
- from kivy.uix.boxlayout import BoxLayout
- from kivy.uix.popup import Popup
- def get_time_string(now, end):
- """ '0:15/1:19' のような再生時間の文字列表現を返す
- 引数:
- now: 現在の再生時間を、秒単位で指定。floatでもOK
- end: 終了時の再生時間を、秒単位で指定。floatでもOK
- """
- now_m, now_s = map(int, divmod(now, 60))
- now_string = "{0:d}:{1:02d}".format(now_m, now_s)
- end_m, end_s = map(int, divmod(end, 60))
- end_string = "{0:d}:{1:02d}".format(end_m, end_s)
- return "{0}/{1}".format(now_string, end_string)
- class PopupChooseFile(BoxLayout):
- # 現在のカレントディレクトリ。FileChooserIconViewのpathに渡す
- current_dir = os.path.dirname(os.path.abspath(__file__))
- # MusicPlayerクラス内で参照するための設定
- select = ObjectProperty(None)
- cancel = ObjectProperty(None)
- class MusicPlayer(BoxLayout):
- audio_button = ObjectProperty(None) # >や||のボタンへのアクセス
- status = ObjectProperty(None) # 画面中央のお知らせとなるテキスト部分
- volume_value = ObjectProperty(None) # 音量の値
- bar = ObjectProperty(None) # 再生位置
- time_text = ObjectProperty(None) # 再生時間のテキスト
- is_playing = False # 再生中か否か。一時停止時はTrue
- sound = None
- def choose(self):
- """Choose File押下時に呼び出され、ポップアップでファイル選択させる"""
- content = PopupChooseFile(select=self.select, cancel=self.cancel)
- self.popup = Popup(title="Select MP3", content=content)
- self.popup.open()
- def play_or_stop(self):
- """||や>押下時。再生中なら一時停止、停止中なら再生する"""
- if not self.sound:
- self.status.text = 'Please Select MP3'
- else:
- # 再生中ならポーズ処理
- if self.sound.state == "play":
- self._pause()
- # 一時停止中なら再スタート
- elif self.sound.state == "stop":
- self._restart()
- def volume_plus(self):
- """ボリュームのアップ"""
- if not self.sound:
- self.status.text = 'Please Select MP3'
- else:
- self.sound.volume += 0.1
- if self.sound.volume > 1:
- self.sound.volume = 1
- value = int(self.sound.volume*10)
- self.volume_value.text = str(value)
- def volume_minus(self):
- """ボリュームのダウン"""
- if not self.sound:
- self.status.text = 'Please Select MP3'
- else:
- self.sound.volume -= 0.1
- if self.sound.volume < 0:
- self.sound.volume = 0
- value = int(self.sound.volume*10)
- self.volume_value.text = str(value)
- def cancel(self):
- """ファイル選択画面でキャンセル"""
- self.popup.dismiss()
- def select(self, path):
- """ファイル選択画面で、ファイル選択時"""
- if self.sound:
- self._stop()
- self.sound = SoundLoader.load(path)
- self.sound_name = os.path.basename(path)
- # 再生を試みて、できない(mp3じゃない)ならexcept。MP3にしろとメッセージ
- try:
- self._start()
- except AttributeError:
- self.status.text = 'Should MP3'
- finally:
- self.popup.dismiss()
- def click_seek(self, position):
- """シークバークリックでの、音楽移動処理
- 実際はスライダーの値が変わるたびに呼ばれるため、シークバークリック時と、
- 0.1秒毎(_timerでの値増減後)にもこの関数が呼ばれます。
- ユーザがシークバークリックした際と区別するため、以下の条件式で判定
- position != self.one_before+0.1
- 以前から0.1しか動いていない=タイマーでの移動分で、ユーザのシーククリックではないと判断し何もしない
- 引数:
- position: クリックされたシークバーの位置
- """
- # mp3をロードしてない、もしくは既に_stop後
- if not self.sound:
- self.status.text = 'Please Select MP3'
- self.bar.value = 0
- # ユーザがシークバークリックをした場合
- elif position != self.one_before+0.1:
- self._pause()
- self._restart(position)
- def _timer(self, dt):
- """バーと再生時間テキストの更新"""
- # 既に_stopされていた場合
- if not self.sound:
- return False
- # バーが最大値を超えたらストップで、タイマー解除
- elif self.bar.value >= self.bar.max:
- self._stop()
- return False
- else:
- self.one_before = self.bar.value
- self.bar.value += 0.1
- self.time_text.text = get_time_string(
- self.bar.value, self.sound.length)
- def _restart(self, position=None):
- """再スタート"""
- self.sound.play()
- Clock.schedule_interval(self._timer, 0.1)
- # 再生位置の指定があればそこから、なければpause時の位置から再生
- restart_position = position if position else self.pause_position
- self.sound.seek(restart_position) # 再生位置の復元
- self.audio_button.text = "||"
- self.status.text = 'Playing {}'.format(self.sound_name)
- def _pause(self):
- """一時停止"""
- self.sound.stop()
- Clock.unschedule(self._timer)
- self.pause_position = self.sound.get_pos()
- self.audio_button.text = ">"
- self.status.text = 'Stop {}'.format(self.sound_name)
- def _start(self):
- """スタート処理。mp3ファイル選択後に呼ばれる"""
- self.sound.play()
- Clock.schedule_interval(self._timer, 0.1)
- self.is_playing = True
- self.audio_button.text = "||"
- self.status.text = 'Playing {}'.format(self.sound_name)
- self.bar.max = self.sound.length
- def _stop(self):
- """停止"""
- self.sound.stop()
- self.sound = None
- Clock.unschedule(self._timer)
- self.is_playing = False
- self.audio_button.text = ">"
- self.status.text = 'Stop {}'.format(self.sound_name)
- self.bar.value = 0
- self.time_text.text = '0:00/0:00'
- class Music(App):
- icon = "ico.png"
- def build(self):
- return MusicPlayer()
- if __name__ == "__main__":
- Music().run()
music.kv
- <MusicPlayer>:
- status: status_text
- audio_button: audio_button
- volume_value: volume_value
- bar: bar
- time_text: time_text
- orientation: 'vertical'
- padding: 20
- Label:
- size_hint: 1, .8
- id: status_text
- text: ""
- center: root.center
- color: 1, 0, 0, 1
- BoxLayout:
- size_hint: 1, .1
- orientation: 'horizontal'
- Label:
- id: time_text
- size_hint: .1, 1
- text: '0:00/0:00'
- background_color: 0, .2, 1, 1
- Slider:
- id: bar
- size_hint: .9, 1
- max: 100
- value: 0
- on_value: root.click_seek(self.value)
- BoxLayout:
- size_hint: 1, .1
- orientation: 'horizontal'
- Button:
- id: audio_button
- size_hint: .1, 1
- text: '||'
- background_color: 0, .2, 1, 1
- on_release: root.play_or_stop()
- Button:
- size_hint: .1, 1
- text: '+'
- background_color: 0, .2, 1, 1
- on_release: root.volume_plus()
- Button:
- id: volume_value
- size_hint: .1, 1
- text: '10'
- background_color: 0, .2, 1, 1
- Button:
- size_hint: .1, 1
- text: '-'
- background_color: 0, .2, 1, 1
- on_release: root.volume_minus()
- Button:
- size_hint: .1, 1
- text: 'Choose File'
- background_color: 0, .4, 1, 1
- on_release: root.choose()
- <PopupChooseFile>:
- canvas:
- Color:
- rgba: 0, 0, .4, 1
- Rectangle:
- pos: self.pos
- size: self.size
- orientation: "vertical"
- FileChooserIconView:
- size_hint: 1, .9
- path: root.current_dir
- on_submit: root.select(self.selection[0])
- BoxLayout:
- size_hint: 1, .1
- Button:
- text: "Cancel"
- background_color: 0,.5,1,1
- on_release: root.cancel()
ボリュームの調整はこのメソッドですね。
- def volume_plus(self):
- """ボリュームのアップ"""
- if not self.sound:
- self.status.text = 'Please Select MP3'
- else:
- self.sound.volume += 0.1
- if self.sound.volume > 1:
- self.sound.volume = 1
- value = int(self.sound.volume*10)
- self.volume_value.text = str(value)
- def volume_minus(self):
- """ボリュームのダウン"""
- if not self.sound:
- self.status.text = 'Please Select MP3'
- else:
- self.sound.volume -= 0.1
- if self.sound.volume < 0:
- self.sound.volume = 0
- value = int(self.sound.volume*10)
- 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は何に使うのかというと、シークバーのクリック時です。
- def _timer(self, dt):
- """バーと再生時間テキストの更新"""
- # 既に_stopされていた場合
- if not self.sound:
- return False
- # バーが最大値を超えたらストップで、タイマー解除
- elif self.bar.value >= self.bar.max:
- self._stop()
- return False
- else:
- self.one_before = self.bar.value
- self.bar.value += 0.1
- self.time_text.text = get_time_string(
- self.bar.value, self.sound.length)
これがシークバークリック時の処理ですが、実際は_timerで値が増減するたびにも呼ばれます。
あくまでon_valueでこのメソッド呼んでますからね。
_timerでは0.1だけbarの値を増やします。つまり、前回の値と比較して0.1しか増えてないなら_timer処理で、そうでなければシークバークリックということにしました。
- def click_seek(self, position):
- """シークバークリックでの、音楽移動処理
- このメソッドは、実際はスライダーの値が変わるたびに呼ばれるため、シークバークリック時と、
- 0.1秒毎(_timerでの値増減後)にもこの関数が呼ばれます。
- ユーザがシークバークリックした際と区別するため、以下の条件式で判定
- position != self.one_before+0.1
- 以前から0.1しか動いていない=タイマーでの移動分で、ユーザのシーククリックではないと判断し何もしない
- 引数:
- position: クリックされたシークバーの位置
- """
- # mp3をロードしてない、もしくは既に_stop後
- if not self.sound:
- self.status.text = 'Please Select MP3'
- self.bar.value = 0
- # ユーザがシークバークリックをした場合
- elif position != self.one_before+0.1:
- self._pause()
- self._restart(position)
再生時間のテキスト文字列を作成する関数です。_timer内で呼ばれています。
これは結構汎用的に使える関数です。
http://stackoverflow.com/questions/775049/python-time-seconds-to-hms
- def get_time_string(now, end):
- """ '0:15/1:19' のような再生時間の文字列表現を返す
- 引数:
- now: 現在の再生時間を、秒単位で指定。floatでもOK
- end: 終了時の再生時間を、秒単位で指定。floatでもOK
- """
- now_m, now_s = map(int, divmod(now, 60))
- now_string = "{0:d}:{1:02d}".format(now_m, now_s)
- end_m, end_s = map(int, divmod(end, 60))
- end_string = "{0:d}:{1:02d}".format(end_m, end_s)
- return "{0}/{1}".format(now_string, end_string)
試しに使ってみます。
- def get_time_string(now, end):
- now_m, now_s = map(int, divmod(now, 60))
- now_string = "{0:d}:{1:02d}".format(now_m, now_s)
- end_m, end_s = map(int, divmod(end, 60))
- end_string = "{0:d}:{1:02d}".format(end_m, end_s)
- return "{0}/{1}".format(now_string, end_string)
- now = 100 # 100 second
- end = 2000 # 2000 second
- result = get_time_string(now, end)
- print(result)
良い感じですね。intが必ず来る保障があるなら、map(intは不要です。
- 1:40/33:20
以前に、
Pythonで、進捗バーを自作する
https://torina.top/detail/263/
というのを作りましたが、この関数を使うと捗りそうですね。
各種便利な関数です。いちいちボタンやラベルのテキストの変更や、タイマー管理等が面倒だったために作成しました。
- def _restart(self, position=None):
- """再スタート"""
- self.sound.play()
- Clock.schedule_interval(self._timer, 0.1)
- # 再生位置の指定があればそこから、なければpause時の位置から再生
- restart_position = position if position else self.pause_position
- self.sound.seek(restart_position) # 再生位置の復元
- self.audio_button.text = "||"
- self.status.text = 'Playing {}'.format(self.sound_name)
- def _pause(self):
- """一時停止"""
- self.sound.stop()
- Clock.unschedule(self._timer)
- self.pause_position = self.sound.get_pos()
- self.audio_button.text = ">"
- self.status.text = 'Stop {}'.format(self.sound_name)
- def _start(self):
- """スタート処理。mp3ファイル選択後に呼ばれる"""
- self.sound.play()
- Clock.schedule_interval(self._timer, 0.1)
- self.is_playing = True
- self.audio_button.text = "||"
- self.status.text = 'Playing {}'.format(self.sound_name)
- self.bar.max = self.sound.length
- def _stop(self):
- """停止"""
- self.sound.stop()
- self.sound = None
- Clock.unschedule(self._timer)
- self.is_playing = False
- self.audio_button.text = ">"
- self.status.text = 'Stop {}'.format(self.sound_name)
- self.bar.value = 0
- self.time_text.text = '0:00/0:00'