音源再生ライブラリ書き換え(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を直接いじるという無謀なことは今はやめておこう(^_^;)