2020年07月26日

Python 3 で, Ogg Vorbis と Wave を再生する Player を書きました 続き

pysoundsample.png

前回 の続きです. 上の画像は, 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 追記

説明文を追加.

ラベル:Python download
posted by Takenoko at 09:42| Comment(0) | programming | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

×

この広告は180日以上新しい記事の投稿がないブログに表示されております。