見出し画像

音源再生ライブラリ書き換え(4)

(※本内容は、python初心者による試行錯誤の過程の記録です。整理された内容ではありません。)

 プログラムの勉強は習慣づけて毎日続けないとだめですね。ちょっと「別ネタ」でお茶を濁したつもりが、前回の

から、あっという間に一週間過ぎてしまいました。おそろしい。
今回はリハビリを兼ねて。

書き換えの続き

勘違い

自分が勘違いしていたことがあります。
import sounddevice as sd
とやっているsounddeviceは「クラス」だと思い込んでいたのです。前回、ソースコードを見てみたら、
(もちろんコードの中には>class OutputStream(RawOutputStream)のようなクラスも使われていますが)
sounddeviceそれ自体はクラスではなかった。
 そういえば使うとき、別にイニシャライズしていなかったのだから疑問に思うべきでした。

今回の出発点

 さて、書き換え作業継続します。前回何通りか書いたパターンのうち、
「GUIあり、(1)sounddeviceのplay、wait利用」のやり方を出発点にして膨らませて行こうと思います。
(出発点)

import time
from threading import Thread

import customtkinter as ctk
import numpy as np
import sounddevice as sd
from pydub import AudioSegment


class MyPlayer:
    # イニシャライズ
    def __init__(self):
        super().__init__()
        self.filename = None
        self.audio = None
        self.audio_data = None  # オーディオデータ
        self.sr = None  # サンプルレート
        self.is_playing = False

    # ファイル読み込み
    def set_mp3file(self, filename):
        self.filename = filename
        self.audio = AudioSegment.from_mp3(filename)
        self.audio_data = np.array(self.audio.get_array_of_samples())
        if self.audio.channels == 2:
            self.audio_data = self.audio_data.reshape((-1, 2))
        self.sr = self.audio.frame_rate

    def play_audio(self):
        if self.filename is not None:
            if not self.is_playing:
                start_time = time.time()
                self.is_playing = True
                # 音声の再生
                sd.play(self.audio_data, samplerate=self.sr)
                sd.wait()
                self.is_playing = False
                end_time = time.time()
                print(f"{end_time - start_time:.3f}秒")

    def play(self):
        if not self.is_playing:
            Thread(target=self.play_audio).start()

    def stop(self):
        if self.is_playing:
            sd.stop()


if __name__ == "__main__":

    class App(ctk.CTk):
        def __init__(self, title):

            super().__init__()
            self.title(title)

            self.player = MyPlayer()
            self.player.set_mp3file("test2.mp3")

            # widget
            ctk.CTkButton(self, text="再生", command=self.player.play).pack(
                padx=100, pady=10
            )

            ctk.CTkButton(self, text="停止", command=self.player.stop).pack(
                padx=100, pady=10
            )

            ctk.CTkButton(
                self, text="テスト1", command=self.testmethod1
            ).pack(padx=100, pady=10)

            ctk.CTkButton(
                self, text="テスト2", command=self.testmethod2
            ).pack(padx=100, pady=10)

            # mainloop
            self.protocol("WM_DELETE_WINDOW", self.on_closing)
            self.mainloop()

        def testmethod1(self):
            print("テスト1")

        def testmethod2(self):
            print("テスト2")

        def on_closing(self):
            # 閉じる前にやること
            self.player.stop()
            # 閉じる
            self.destroy()

    App("MyMP3Player241103")

ライブラリとしても、そのままテストコードとしても実行できるように
>if __name__ == "__main__":
をつけています。(何か試すときはtestmethodのところに書いてみる)

・再生、停止の動作、
・再生中もGUIが固まらないこと、
・再生中、直接ウィンドウを閉じて終了、の操作正常を確認。
◎実行時間113.030秒(音源ファイルの長さは112.9秒)。
・・・を確認。ここまでは期待通り

足りないメソッド

以前、「just_playback」パッケージを使ったプレイヤークラスで実装していたのは、他に

    @property
    def paused(self):
        return self.playback.paused

    @paused.setter
    def paused(self, flag):
        if flag is True:
            self.playback.pause()
        else:
            self.playback.resume()

    @property
    def pos(self):
        return self.playback.curr_pos

    @pos.setter
    def pos(self, seekpos):
        self.playback.seek(seekpos)

    @property
    def duration(self):
        return self.playback.duration

    @property
    def volume(self):
        return self.playback.volume

    @volume.setter
    def volume(self, vol):
        self.playback.set_volume(vol)

です。できるだけ、これと同じように使えるように実装を試みます。

https://github.com/jiaaro/pydub/blob/master/API.markdown#audiosegment

こちらにpydubのAudioSegmentの使用例がいろいろ出ています。すごく参考になります。

duration

  duration の取得方法もわかりました。pydubにこんなサンプルコードが出ています。

from pydub import AudioSegment
sound = AudioSegment.from_file("sound1.wav")

assert sound.duration_seconds == (len(sound) / 1000.0)


AudioSegmentのオブジェクトを引数にしてlenをとれば、ミリ秒単位でのdurationが取得できるということね。
 MyPlayerクラスで音源ファイル読み込み後は、self.audioにAudioSegmentが入っているわけですから、これのlenを取って1000で割れば秒単位になる。class MyPlayer に次のプロパティを追加

    @property
    def duration(self):
        return len(self.audio)/1000

・・・おっと違う違う、寝ぼけていました。duration_secondsというプロパティを読み出せばそもそも解決という話。

    @property
    def duration(self):
        return self.audio.duration_seconds

そして、class Appに

        def testmethod1(self):
            print(self.player.duration)

を追加。実行テスト。テスト1クリックで(テストデータ「残業戦士」

の場合)1分52秒のデータですが、112.87510204081633と表示されました。よしよし。ここまではいい。

pos

 再生中に「現在位置」を取得できるか?という問題。再生機能はpydubではなく、あくまでもsounddevice側の問題。sounddeviceのサンプルを見ていると

簡単な実装例としては

This example could simply be implemented like this::

    import sounddevice as sd
    import soundfile as sf

    data, fs = sf.read('my-file.wav')
    sd.play(data, fs)
    sd.wait()

... but in this example we show a more low-level implementation
using a callback stream.

と、sd.play, sd.waitを使うが、ここではlow-levelの実装を・・なんて書いてある。そのlow-levelの実装内容というのが、前回自分が

OutputStreamを直接いじるという無謀なことは今はやめておこう(^_^;)

としていた、まさにそのやめておいたOutputStreamをいじる内容のようなので、さて、この沼に足を突っ込むか否か・・悩ましいところです。まあ、一通り見てみますか。
https://github.com/spatialaudio/python-sounddevice/blob/0.5.1/examples/play_file.py

#!/usr/bin/env python3
"""Load an audio file into memory and play its contents.

NumPy and the soundfile module (https://python-soundfile.readthedocs.io/)
must be installed for this to work.

This example program loads the whole file into memory before starting
playback.
To play very long files, you should use play_long_file.py instead.

This example could simply be implemented like this::

    import sounddevice as sd
    import soundfile as sf

    data, fs = sf.read('my-file.wav')
    sd.play(data, fs)
    sd.wait()

... but in this example we show a more low-level implementation
using a callback stream.

"""
import argparse
import threading

import sounddevice as sd
import soundfile as sf


def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text


parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
    '-l', '--list-devices', action='store_true',
    help='show list of audio devices and exit')
args, remaining = parser.parse_known_args()
if args.list_devices:
    print(sd.query_devices())
    parser.exit(0)
parser = argparse.ArgumentParser(
    description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    parents=[parser])
parser.add_argument(
    'filename', metavar='FILENAME',
    help='audio file to be played back')
parser.add_argument(
    '-d', '--device', type=int_or_str,
    help='output device (numeric ID or substring)')
args = parser.parse_args(remaining)

event = threading.Event()

try:
    data, fs = sf.read(args.filename, always_2d=True)

    current_frame = 0

    def callback(outdata, frames, time, status):
        global current_frame
        if status:
            print(status)
        chunksize = min(len(data) - current_frame, frames)
        outdata[:chunksize] = data[current_frame:current_frame + chunksize]
        if chunksize < frames:
            outdata[chunksize:] = 0
            raise sd.CallbackStop()
        current_frame += chunksize

    stream = sd.OutputStream(
        samplerate=fs, device=args.device, channels=data.shape[1],
        callback=callback, finished_callback=event.set)
    with stream:
        event.wait()  # Wait until playback is finished
except KeyboardInterrupt:
    parser.exit('\nInterrupted by user')
except Exception as e:
    parser.exit(type(e).__name__ + ': ' + str(e))

うーん、コールバックというのを勉強しないと私には読み解けそうもありません。

邪道実装(?)

 本来はちゃんと、ストリームとかコールバックとか理解してから、現在位置を求めるやりかたを実装すべきなのでしょう。
 けど、ちょっと逃げて、再生時間をtime関数で測ってposにしてしまうやり方を試してみます。
 play実行するときにstarttime記録して、pos呼ばれた際のtimeとの差分を取ればposを返せるよね?
MyPlayerクラスにself.playstarttimeという変数を用意して

    def play(self):
        if not self.is_playing:
            self.playstarttime=time.time()
            Thread(target=self.play_audio).start()

    def stop(self):
        if self.is_playing:
            sd.stop()
            self.playstarttime=None

再生中にposプロパティが読みだされたら、開始からの秒数を出す。

    @property
    def pos(self):
        if self.is_playing:
            return time.time() - self.playstarttime
        else:
            return None

試しにこれで。

        def testmethod2(self):
            print(self.player.pos)

testmethodのところにこれを書いておいて、再生中にポチポチ、ボタンクリックしてみる。

2.8174376487731934
4.522406101226807
4.59666895866394
7.312045097351074
9.217336177825928
11.376790285110474

うん、取得できているっぽい。なお、再生が終わってからクリックするとNoneと表示されます。

ただ、これだけでは、再生中に一時停止入れるとき困りますね。また考えてみます。


いいなと思ったら応援しよう!