Pythonのkivyで音楽プレイヤー その3 リピート再生


# はじめに

[PythonのKivyで音楽プレイヤー](https://qiita.com/mkgask/items/dcd0f173998168fe5614)

[PythonのKivyで音楽プレイヤー その2 ファイル分割](https://qiita.com/mkgask/items/f2b0a8a23d0d11608f45)

の続きです。

だいぶ手が止まってましたが、実はまだ作ってます。

*（2018/08/18追記：前回、前々回の記事のリンクを追加）*



# 想定環境

- Windows 10（Pythonでkivyなので他OSでもほぼ同等です）
- Python 3.6.5
- kivy 1.10.0
- Nuitka 0.5.30（基本的には出てきませんが、実行ファイル形式出力が可能な状態を維持します）

*（2018/08/18追記：想定環境の項目を追加）*



# リピート再生の仕様

今回はリピート処理を作ります。

仕様としては以下の通りとなります。

- リピート再生モードの変更ボタンを追加
- リピート再生モードは、リピート再生する、しないの2種類のみ
- 曲の再生が終了した自動停止の時に、リピート再生するになっていた時だけリピート再生

全コードはGitHubにあります。

[https://github.com/mkgask/kivySimplePlayer](https://github.com/mkgask/kivySimplePlayer)



# 実装



## 曲の停止時の処理

`kivy.core.audio.Sound` には、`on_stop` という曲の停止時に発行されるコールバックが用意されているので、これを使います。

```python:simpleplayer/musicplayer.py
#(前略)

    def select(self, path):
        print('select')
        if self.sound:
            self._stop()
        
        self.sound = SoundLoader.load(path)
        self.sound_path = path
        self.sound_name = basename(path)
        self.sound.on_stop = self._on_stop    # <- 追加

        try:
            self._volume(50)
            self._start()
        finally:
            if self.popup:
                self.popup.dismiss()

    def _on_stop(self):
        print('_on_stop')
        return

#(後略)
```

ファイルを選択後にSoundLoaderをloadしたところで、`on_stop` コールバックを設定します。

ただ、これだと一時停止も停止も同じようにコールバックが起きてしまうので、
中で判別して別々の処理が出来るようにします。

```python:simpleplayer/musicplayer.py
#(前略)

    def _on_stop(self):    # <- まるごと追加
        print('_on_stop')
        if 0 < self.pause_pos:
            self._on_sound_pause()
        else:
            self._on_sound_stop()
        return

    def _on_sound_pause(self):    # <- まるごと追加
        print('_on_sound_pause')
        return

    def _on_sound_stop(self):    # <- まるごと追加
        print('_on_sound_stop')
        return

#(後略)
```

これで一時停止は分離出来ましたが、まだ、曲の再生が終わった時の停止と、停止ボタンを押した時の停止が区別出来ていません。

現状のコードではうまく判別出来る要素が無いので、`is_manual_stop` フラグを追加します。

```python:simpleplayer/musicplayer.py
#(前略)

class MusicPlayer(BoxLayout):
    sound_path = ''
    sound = None
    popup = None
    is_playing = False
    is_pausing = False
    is_manual_stop = False    # <- 追加
    pause_pos = 0
    value_before = 0
    lengh = 0

#(中略)

    def _start(self):
        print('_start')
        self.sound.play()
        self.is_playing = True
        self.is_pausing = False
        self.is_manual_stop = False    # <- 追加
        Clock.schedule_interval(self._timer, 0.1)

        self.time_bar.max = self.sound.length

        self.play_button.text = 'Interval'
        self.status.text = 'Playing {}'.format(self.sound_name)

        self.lengh = self.sound.length

#(中略)

    def stop(self):
        print('stop')
        if self.is_playing:
            self.is_manual_stop = True    # <- 追加
            self._stop()

#(後略)
```

これで手動停止が分けられるようになったので、`_on_sound_stop()` メソッドに判別処理を追加します。

```python:simpleplayer/musicplayer.py
#(前略)

    def _on_stop(self):
        print('_on_stop')
        if 0 < self.pause_pos:
            self._on_sound_pause()
        else:
            self._on_sound_stop()
        return

    def _on_sound_pause(self):
        print('_on_sound_pause')
        return

    def _on_sound_stop(self):
        print('_on_sound_stop')

        if self.is_manual_stop:    # <- 追加
            # 手動停止はリピート再生チェックしない
            return
        
        # リピート再生チェック
        return

#(後略)
```

手動停止の時は処理なしでそのまま終了、そうでなければリピート再生チェック処理を実行します。

リピート再生チェックでは、リピート再生モードになっていればリピート再生、そうでなければ再生終了とします。



## リピート再生モードボタンの追加

リピート再生モードの切り替えがまだ出来ないので、そのボタンを追加します。

```yml:mainwindow.kv
#(前略)
<MusicPlayer>:
    status: status_text
    play_button: play_button
    stop_button: stop_button
    repeat_button: repeat_button    # <- 追加
    time_bar: time_bar
    time_text: time_text
    volume_bar: volume_bar
    volume_text: volume_text
    orientation: 'vertical'

#(中略)

    BoxLayout:
        size_hint: 1, 0.1

        Button:
            id: play_button
            size_hint: 0.1, 1    # <- 追加
            text: '中止'
            on_release: root.play_or_stop()

        Button:
            id: stop_button
            size_hint: 0.1, 1    # <- 追加
            text: '停止'
            on_release: root.stop()

        Button:    # <- まるごと追加
            id: repeat_button
            size_hint: 0.15, 1
            text: 'リピートなし'
            on_release: root.repeat_mode_toggle()

        Label:
            id: volume_text
            size_hint: 0.05, 1    # <- 追加
            text: '50'
        
        Slider:
            id: volume_bar
            size_hint: 0.45, 1    # <- 追加
            max: 100
            value: 50
            on_value: root.volume_change(self.value)

        Button:
            size_hint: 0.15, 1    # <- 追加
            text: 'ファイル選択'
            on_release: root.choose()

#(後略)
```

ボタンをどこにいれるかちょっと迷いましたが、最下段の各要素に長さの指定が無かったので、
そこを整理して停止ボタンの右側に入れました。



## リピート再生チェック && リピート再生

リピート再生モードの変更ボタンを押すと `repeat_mode_toggle()` メソッドが呼ばれるようにしてあるので、このメソッドを実装します。

```python:simpleplayer/musicplayer.py
#(前略)
class MusicPlayer(BoxLayout):
    sound_path = ''
    sound = None
    popup = None
    is_playing = False
    is_pausing = False
    is_manual_stop = False
    is_repeating = False    # <- 追加
    pause_pos = 0
    value_before = 0
    lengh = 0

#(中略)

    def repeat_mode_toggle(self):    # <- まるごと追加
        if self.is_repeating:
            self.is_repeating = False
            self.repeat_button.text = 'リピートなし'
        else:
            self.is_repeating = True
            self.repeat_button.text = 'リピート'
        return

#(後略)
```

これでリピート再生するかどうかが判別出来るようになったので、その処理を `_on_sound_stop` メソッドに実装します。

```python:simpleplayer/musicplayer.py
#(前略)

    def _on_sound_stop(self):
        print('_on_sound_stop')
        print('self.is_manual_stop: ' + str(self.is_manual_stop))
        print('self.is_repeating: ' + str(self.is_repeating))

        if self.is_manual_stop or not self.is_repeating:    # <- 変更
            # 手動停止またはリピート再生なしの時はリピート再生しない
            print('_on_sound_stop(): Stop')
            return

        # リピート再生
        print('_on_sound_stop(): Repeat start')
        self._start()    # <- 追加
        return

#(後略)
```

起動して、リピートモードの変更ボタンを押してリピートするモードに変更、曲をかけて、時間のスライダーを最後近くまで持っていってちょっと待つと、リピート再生してくれました。


# バグ修正 1

レイアウトの縦の数値が間違っていたので修正。
Label 0.8 ＋ BoxLayout 0.1 ＋ BoxLayout 0.1 であわせて 1.0 でないといけないところ、
Label 0.9 ＋ BoxLayout 0.1 ＋ BoxLayout 0.1 の 1.1 になっていました。

指定値間違ってても特に問題なく表示してくれるkivyすごいのですが、Warningくらいはもしかしたら出ていたのでしょうか。

```yml:mainwindow.kv
#(前略)
<MusicPlayer>:
    status: status_text
    play_button: play_button
    stop_button: stop_button
    time_bar: time_bar
    time_text: time_text
    volume_bar: volume_bar
    volume_text: volume_text
    orientation: 'vertical'

    Label:
        size_hint: 1, 0.8    # <- 0.9を0.8に
        id: status_text
        text: ''
        center: root.center
#(後略)
```



# バグ修正 2

一時停止中や停止中などに、再生時間を指定してからPlayボタンを押しても、最初から再生されてしまっていました。
（時間表記とバー表示は指定時間から進む）

```python:simpleplayer/musicplayer.py
#(前略)
    def time_change(self, value):
        if not self.sound:
            self.status.text = 'Select music file'
        elif self.is_playing and value != self.value_before + 0.1:
            self.sound.seek(value)
        elif not self.is_playing:
            self.time_bar.value = value
            self.pause_pos = value
#(後略)
```

再生時間の操作が行われた時に、再生中だったらsoundを直接シーク、そうでない場合は時間テキストの表示変更と次の再生時の開始時間をセットするようにしました。

あわせて `_start()` メソッドも更新しており、再生開始時に次の再生開始時間がセットされていたら、そこから再生するようになっています。

```python:simpleplayer/musicplayer.py
#(前略)
    def _start(self):
        print('_start')

        self.sound.play()

        if 0 < self.pause_pos:    # <- 追加
            self.sound.seek(self.pause_pos)    # <- 追加

        self.is_playing = True
        self.is_pausing = False
        self.is_manual_stop = False
        Clock.schedule_interval(self._timer, 0.1)

        self.play_button.text = 'Interval'
        self.status.text = 'Playing {}'.format(self.sound_name)

        self.lengh = self.sound.length
        self.pause_pos = 0    # <- 追加

    def _restart(self, pos=None):
        print('_restart')
        self._start()
        self.sound.seek(pos if pos else self.pause_pos)
        self.pause_pos = 0

#(後略)
```

・・・ん？ これでは `_restart()` メソッドの機能を食ってしまっています。
リスタートなんて要らなかったんや・・・

と、ここまで来たところで、 `_stop` メソッドと `_pause()` メソッドも機能の整理ができることに気が付きました。

```python:simpleplayer/musicplayer.py
#(前略)
    def _stop(self, pause_pos=0):    # <- 変更
        print('_stop')

        self.sound.stop()
        Clock.unschedule(self._timer)

        # <- 変更（if not pause削除）
        self.is_playing = False
        self.pause_pos = pause_pos    # <- 変更
        self.time_bar.value = pause_pos    # <- 変更
        
        self.time_text.text = self._time_string(
            self.time_bar.value,
            self.lengh
        )

        self.play_button.text = '再生'
        self.status.text = 'Stop {}'.format(self.sound_name)

    def _pause(self):
        print('_pause')
        pos = self.sound.get_pos() * 0.999
        print('pos save: ' + str(pos))
        self._stop(pause_pos=pos)

#(後略)
```

ポーズと停止の違いは、停止中に次回の再生開始時間を保持するかどうかだけなので、ポーズの時は現在の時間を保持、停止の場合はゼロに戻す処理を分岐せずに一つのコードで表現できました。

あわせてポーズ中フラグを作った割に使っていなかったので削除しています。



# 感想

kivyの並列処理対策が出来ていなくて、その辺に起因していそうなバグがまだ発生します。
（リピート再生がかかった時に二重に再生が走り、停止ボタンが効かなくなるなど）

kivyアプリのテスト作りたいんですが、公式ドキュメントと日本語訳を両方当たってみた限りでは、（見落としは普通にありそうですが）kivy「を」ユニットテストする方法しかのってない感じでした。

kivyアプリが起動した状態でGUI操作の自動テストを特にしたいので、テストモードをアプリに内蔵するのが一番速そうなのかなーと思っていますが、もうちょっと考えて色々試していかないといけなそうです。


*（2018/08/18追記：ソースコードのシンタックス、ファイル名の指定が誤っていたのを修正）*