Pythonメモ torinaブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

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

プログラミング関連 Bitbucketにソースあり Kivy
約166日前 2016年10月15日3:36
Bitbucketにソースをおきました。
https://bitbucket.org/toritoritorina/kivy-simple-music

Kivyで、シンプルなミュージックプレイヤー①
https://torina.top/main/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/main/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'