前回 の続きです. 上の画像は, sample の gui です.
Ogg Vorbis ファイルと, Wave ファイルを複数同時に再生できるプレイヤーです. MIT License です. 使用方法は, zip 内の readme.txt と, sample.py に書いてあります. 下にあるコードも, zip 内に全て入っています.
マルチスレッドによるエラーを対策したり, wave の再生終了まで待機できるようにしたりしました. 他の変更点はコード冒頭 update.txt に書いてあります.
'''
thread2.py v1.1
参考ページ: Python スレッドの停止と再開の簡易サンプル
https://qiita.com/BlueSilverCat/items/44a0a2a3c45fc3e88b19
'''
from threading import Thread, Event
class Thread2(Thread):
def __init__(self, daemon = True, callback = 0):
super().__init__(daemon = daemon)
self.started = Event()
self.alive = 1
self.active = 1
self.callback = callback
self.start()
def __del__(self):
self.kill()
def begin(self):
self.started.set()
self.active = 1
def end(self):
self.active = 0
self.started.clear()
if callable(self.callback):
self.callback()
def kill(self):
self.started.set()
self.active = 0
self.alive = 0
self.join()
'''
soundthread.py v1.2
'''
from threading import Lock
from pyaudio import PyAudio
from .thread2 import Thread2
class SoundThread(Thread2):
__lock = Lock()
def __init__(self, callback):
super().__init__(callback = callback)
def close(self):
self.stream.stop_stream()
self.stream.close()
self.p.terminate()
def kill(self):
self.close()
self.end()
super().kill()
def open(self, format, channels, rate):
SoundThread.__lock.acquire()
self.p = PyAudio() # 使い回すとエラーが出ることがある
self.stream = self.p.open(format = format,
channels = channels,
rate = rate,
output = True)
SoundThread.__lock.release()
def run(self):
while self.active:
self.play() # Sub Class で実装すること
self.started.wait()
'''
player.py v1.0
'''
class Player:
def __init__(self, enabled):
self.__enabled = enabled # False で関数呼び出しを無視する
def is_enabled(self):
return self.__enabled
def on(self):
if self.__enabled:
return
self.__enabled = 1
def off(self):
if not self.__enabled:
return
self.__enabled = 0
'''
vorbisplayer.py v1.3
Copyright (c) 2020 Takenoko (http://nekokiss.seesaa.net/)
Released under the MIT license.
see https://opensource.org/licenses/MIT
PyAudio, PyOgg, audio-metadata が install されている必要があります.
'''
import time
from threading import Event
import pyogg as po
import pyaudio as pa
from .soundthread import SoundThread
from .player import Player
class Vorbis:
def __init__(self, ogg, chunk, loop, tagloop):
self.ogg = ogg
self.loop = loop
self.file = po.VorbisFile(ogg)
'''
bit rate size
1, 3, 4 等の場合もあるかもしれないが,
取得方法がわからないので 2 で固定
'''
self.sampwidth = 2
# block size
self.block = (self.sampwidth
* self.file.channels)
self.format = pa.get_format_from_width(self.sampwidth)
self.cache = {}
'''
Loop Tag
'''
self.loopstart = 0
self.loopend = (self.file.buffer_length
* self.block)
if loop and tagloop:
import audio_metadata as am
md = am.load(ogg)
if 'tags' in md and 'loopstart' in md.tags:
self.loopstart = int(md.tags.loopstart[0]) * self.block
if 'looplength' in md.tags:
self.loopend = (int(md.tags.looplength[0])
* self.block + self.loopstart)
self.chunk = chunk
if self.loopend - self.loopstart < self.chunk:
self.chunk = self.loopend - self.loopstart
def getdata(self, start):
if start not in self.cache:
end = start + self.chunk
if end > self.loopend:
data = self.file.buffer[start:self.loopend]
if self.loop:
end = self.loopstart + end - self.loopend
data += self.file.buffer[self.loopstart:end]
else:
end = -1
self.cache[start] = data, end
else:
self.cache[start] = self.file.buffer[start:end], end
return self.cache[start]
class VorbisThread(SoundThread):
def __init__(self, vorbis, callback):
self.vorbis = vorbis
self.paused = Event()
super().__init__(callback)
def rebegin(self):
self.paused.set()
if self.active:
self.end()
# sleep しないと曲の先頭に戻らないことがある
time.sleep(0.1)
self.begin()
def play(self):
self.paused.set()
self.open(self.vorbis.format, self.vorbis.file.channels,
self.vorbis.file.frequency)
start = 0
while self.active:
data, start = self.vorbis.getdata(start)
self.stream.write(data)
self.paused.wait()
if start < 0:
self.end()
self.close()
class VorbisPlayer(Player):
def __init__(self, chunk = 1024, loop = 1, tagloop = 1,
enabled = 1):
super().__init__(enabled)
self.__chunk = chunk
self.__vorbises = {}
self.__thread = 0
self.__loop = loop # True で loop する
self.__tagloop = tagloop # True で LOOPSTART & LOOPLENGTH を使う
def off(self):
super().off()
self.stop()
def play(self, ogg, callback = 0):
if not self.is_enabled():
return
if ogg in self.__vorbises:
v = self.__vorbises[ogg]
else:
v = Vorbis(ogg, self.__chunk, self.__loop, self.__tagloop)
self.__vorbises[ogg] = v
if self.__thread:
self.__thread.vorbis = v
self.__thread.callback = 0
self.__thread.rebegin()
self.__thread.callback = callback
else:
self.__thread = VorbisThread(v, callback)
def stop(self):
if self.__thread and self.__thread.active:
self.__thread.end()
def pause(self):
if not self.is_enabled():
return
if self.__thread:
self.__thread.paused.clear()
def resume(self):
if not self.is_enabled():
return
if self.__thread:
self.__thread.paused.set()
'''
waveplayer.py v1.4
Copyright (c) 2020 Takenoko (http://nekokiss.seesaa.net/)
Released under the MIT license.
see https://opensource.org/licenses/MIT
PyAudio が install されている必要があります.
'''
from threading import Event
import pyaudio as pa
import wave
from .soundthread import SoundThread
from .player import Player
class Wave:
def __init__(self, wav, chunk):
self.file = wav
with wave.open(wav, 'rb') as wf:
self.format = pa.get_format_from_width(wf.getsampwidth())
self.channels = wf.getnchannels()
self.rate = wf.getframerate()
self.buffer = []
while 1:
data = wf.readframes(chunk)
if len(data) <= 0:
break
self.buffer.append(data)
class WavThread(SoundThread):
def __init__(self, wp, wav, callback):
self.wp = wp
self.wav = wav
super().__init__(callback)
def close(self):
super().close()
self.wp._WavePlayer__blocked.set()
def play(self):
self.open(self.wav.format, self.wav.channels,
self.wav.rate)
for data in self.wav.buffer:
if self.active:
self.stream.write(data)
if self.active:
self.end()
self.close()
class WavePlayer(Player):
def __init__(self, chunk = 1024, maxthreads = 8, enabled = 1):
super().__init__(enabled)
self.__chunk = chunk
self.__waves = {}
self.__threads = []
self.__blocked = Event()
if maxthreads < 1:
maxthreads = 1
self.__maxthreads = maxthreads # 最大同時再生 Thread 数
def __getwav(self, wav):
if wav not in self.__waves:
self.__waves[wav] = Wave(wav, self.__chunk)
return self.__waves[wav]
def off(self):
super().off()
self.stopall()
def getmaxthreads(self):
return self.__maxthreads
def setmaxthreads(self, v):
if v < 1:
return
over = min(self.__maxthreads, len(self.__threads)) - v
if over > 0:
for i in range(over):
t = self.__threads.pop(0)
if t.active:
t.end()
self.__threads.append(t)
self.__maxthreads = v
def play(self, wav, block = 0, callback = 0):
if not self.is_enabled():
return
if block:
self.__blocked.clear()
for i, t in enumerate(self.__threads):
if i < self.__maxthreads:
if not t.active:
t.wav = self.__getwav(wav)
t.callback = callback
t.begin()
break
else:
if len(self.__threads) >= self.__maxthreads:
t = self.__threads.pop(0)
t.end()
t.wav = self.__getwav(wav)
t.callback = callback
t.begin()
else:
t = WavThread(self, self.__getwav(wav), callback)
self.__threads.append(t)
if block:
self.__blocked.wait()
def stop(wav):
for t in self.__threads:
if t.wav.file == wav and t.active:
t.end()
return 1
return 0
def stopall(self):
for t in self.__threads:
if t.active:
t.end()
vwp200727.zip
スレッドについて, 少しだけわかった気がします.
20/07/27 追記
ファイルを差し替えました. 更新履歴を, ソースコードではなく update.txt にまとめただけですが. あと, この記事にコードを貼り付けました. コードに問題があった場合, 指摘して頂けるとありがたいです.
20/07/28 追記
説明文を追加.