見出し画像

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

一部繰り返しになってしまいますが、動くコードを起点に少しずつ変えて行く試行錯誤・整理です。


GUIなし(コマンドライン)

(1)sounddeviceのplay、wait利用

import time

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

start_time = time.time()

# 音声ファイルを読み込む
audio = AudioSegment.from_file("test2.mp3")
audio_data = np.array(audio.get_array_of_samples())

# ステレオデータの場合、2次元配列に変換する
if audio.channels == 2:
    audio_data = audio_data.reshape((-1, 2))

sr = audio.frame_rate

# 音声の再生
sd.play(audio_data, samplerate=sr)
sd.wait()

end_time = time.time()
print(f"{end_time - start_time:.3f}秒")

・・・鳴ります。114.255秒(音源ファイルの長さは112.9秒)

(2)OutputStream利用

import time

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

start_time = time.time()

# 音声ファイルを読み込む
audio = AudioSegment.from_file("test2.mp3")
audio_data = np.array(audio.get_array_of_samples())

# ステレオデータの場合、2次元配列に変換する
if audio.channels == 2:
    audio_data = audio_data.reshape((-1, 2))

sr = audio.frame_rate

# 音声の再生
# Create OutputStream
output_stream = sd.OutputStream(
    samplerate=sr, channels=audio.channels, dtype="int16"
)

# Start the stream
output_stream.start()

# Write data to the stream
output_stream.write(audio_data)

# Close the stream
output_stream.close()

end_time = time.time()
print(f"{end_time - start_time:.3f}秒")

鳴ります。113.393秒(音源ファイルの長さは112.9秒)

二つの間の変更点

# 音声の再生
sd.play(audio_data, samplerate=sr)
sd.wait()
# 音声の再生
# Create OutputStream
output_stream = sd.OutputStream(
    samplerate=sr, channels=audio.channels, dtype="int16"
)

# Start the stream
output_stream.start()

# Write data to the stream
output_stream.write(audio_data)

# Close the stream
output_stream.close()

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 MyPlayer2:
    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()


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

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

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

        # widget
        ctk.CTkButton(self, text="再生", command=self.player.play).pack(
            padx=50, pady=10
        )
        # mainloop
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.mainloop()

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


App("MyMP3Player")

再生ボタンクリックで鳴ります。113.023秒。
※別スレッドで動いているのでGUIが固まることは無い。
(再生中、右上×を使って閉じた時にはそこまでの秒数が出る)
ちなみにplayのところをスレッドを使わずに

    def play(self):
        if not self.is_playing:
            self.play_audio()

            # Thread(target=self.play_audio).start()

と書くと、見事、固まります(笑)

(2)OutputStream利用

import time
from threading import Thread

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


class MyPlayer2:
    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()
                # Create OutputStream
                output_stream = sd.OutputStream(
                    samplerate=self.sr,
                    channels=self.audio.channels,
                    dtype="int16",
                )

                # Start the stream
                output_stream.start()

                # Write data to the stream
                output_stream.write(self.audio_data)

                # Close the stream
                output_stream.close()

                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()


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

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

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

        # widget
        ctk.CTkButton(self, text="再生", command=self.player.play).pack(
            padx=50, pady=10
        )
        # mainloop
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.mainloop()

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


App("MyMP3Player")

再生ボタンクリックで鳴ります。113.036秒
※別スレッドで動いているのでGUIが固まることは無い。

 いい調子と思っていたら罠があった。GUIは固まらない。再生中、右上×を使って閉じた時、ウィンドウはさっと閉じる。しかし音楽再生が続いてしまう。

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

では再生は止まらない。def play_audio(self): 中のoutput_stream がローカルなオブジェクトになっているためだと思われる。
クラスのイニシャライザに

self.output_stream = None

を追加してoutput_stream をself.output_streamに置換して

    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()
                # Create OutputStream
                self.output_stream = sd.OutputStream(
                    samplerate=self.sr,
                    channels=self.audio.channels,
                    dtype="int16",
                )

                # Start the stream
                self.output_stream.start()

                # Write data to the stream
                self.output_stream.write(self.audio_data)

                # Close the stream
                self.output_stream.close()

                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()
            self.output_stream.stop()

とやってみたが、右上×でウインドウを閉じた時も再生が止まらない。
どうやら、streamをstopした後、close も必要であるらしい。

    def stop(self):
        if self.is_playing:
            # sd.stop()
            self.output_stream.stop()
            self.output_stream.close()

これで実行。
最初の一回は再生停止がうまく行って、これで行けると思った。
ところが二回目の実行では

   Exception in thread Thread-5 (play_audio):
(中略)
    raise PortAudioError(errormsg, err, hosterror_info)
sounddevice.PortAudioError: Unanticipated host error [PaErrorCode -9999]: 'Cannot perform this operation while media data is still playing.  Reset the device, or wait until the data is finished playing.' [MME error 33]    

Windowを閉じた時のタイミングでエラーが出たり出なかったりする、また、閉じるときに妙な雑音が入ったりすることもある嫌なパターン。

(3)もういちどsounddeviceのplay、wait利用

 もしかして「(1)sounddeviceのplay、wait利用」でも再生停止を「がしゃがしゃ」いじるとエラーでることもある?
 いじわるなぐらい「がしゃがしゃ」いじりましたけど、エラーになりません。エレガント(笑)

GitHubでソースコード確認

 どう書いたらエレガントな動作になるのか。ソースコードを読んで勉強しよう。ソースはここにあるんだよね。後述のplay,stop,waitはここから。

写真怖いな(^_^;)

playメソッドの定義

def play(data, samplerate=None, mapping=None, blocking=False, loop=False,
         **kwargs):
    """Play back a NumPy array containing audio data.

    This is a convenience function for interactive use and for small
    scripts.  It cannot be used for multiple overlapping playbacks.

    This function does the following steps internally:

    * Call `stop()` to terminate any currently running invocation
      of `play()`, `rec()` and `playrec()`.

    * Create an `OutputStream` and a callback function for taking care
      of the actual playback.

    * Start the stream.

    * If ``blocking=True`` was given, wait until playback is done.
      If not, return immediately
      (to start waiting at a later point, `wait()` can be used).

    If you need more control (e.g. block-wise gapless playback, multiple
    overlapping playbacks, ...), you should explicitly create an
    `OutputStream` yourself.
    If NumPy is not available, you can use a `RawOutputStream`.

    Parameters
    ----------
    data : array_like
        Audio data to be played back.  The columns of a two-dimensional
        array are interpreted as channels, one-dimensional arrays are
        treated as mono data.
        The data types *float64*, *float32*, *int32*, *int16*, *int8*
        and *uint8* can be used.
        *float64* data is simply converted to *float32* before passing
        it to PortAudio, because it's not supported natively.
    mapping : array_like, optional
        List of channel numbers (starting with 1) where the columns of
        *data* shall be played back on.  Must have the same length as
        number of channels in *data* (except if *data* is mono, in which
        case the signal is played back on all given output channels).
        Each channel number may only appear once in *mapping*.
    blocking : bool, optional
        If ``False`` (the default), return immediately (but playback
        continues in the background), if ``True``, wait until playback
        is finished.  A non-blocking invocation can be stopped with
        `stop()` or turned into a blocking one with `wait()`.
    loop : bool, optional
        Play *data* in a loop.

    Other Parameters
    ----------------
    samplerate, **kwargs
        All parameters of `OutputStream` -- except *channels*, *dtype*,
        *callback* and *finished_callback* -- can be used.

    Notes
    -----
    If you don't specify the correct sampling rate
    (either with the *samplerate* argument or by assigning a value to
    `default.samplerate`), the audio data will be played back,
    but it might be too slow or too fast!

    See Also
    --------
    rec, playrec

    """
    ctx = _CallbackContext(loop=loop)
    ctx.frames = ctx.check_data(data, mapping, kwargs.get('device'))

    def callback(outdata, frames, time, status):
        assert len(outdata) == frames
        ctx.callback_enter(status, outdata)
        ctx.write_outdata(outdata)
        ctx.callback_exit()

    ctx.start_stream(OutputStream, samplerate, ctx.output_channels,
                     ctx.output_dtype, callback, blocking,
                     prime_output_buffers_using_stream_callback=False,
                     **kwargs)

stopメソッドの定義

def stop(ignore_errors=True):
    """Stop playback/recording.

    This only stops `play()`, `rec()` and `playrec()`, but has no
    influence on streams created with `Stream`, `InputStream`,
    `OutputStream`, `RawStream`, `RawInputStream`, `RawOutputStream`.

    """
    if _last_callback:
        # Calling stop() before close() is necessary for older PortAudio
        # versions, see issue #87:
        _last_callback.stream.stop(ignore_errors)
        _last_callback.stream.close(ignore_errors)

waitメソッドの定義

def wait(ignore_errors=True):
    """Wait for `play()`/`rec()`/`playrec()` to be finished.

    Playback/recording can be stopped with a `KeyboardInterrupt`.

    Returns
    -------
    CallbackFlags or None
        If at least one buffer over-/underrun happened during the last
        playback/recording, a `CallbackFlags` object is returned.

    See Also
    --------
    get_status

    """
    if _last_callback:
        return _last_callback.wait(ignore_errors)

※説明部分を除くとソースコード自体は実に簡潔。stop もこれだけなのに実行時の動作が安定している。
 コールバックの使い方知らない自分には、この辺理解できないのかな。

方針修正

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

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