added feature for playing sounds backwards

This commit is contained in:
jenz 2024-10-15 19:04:18 +02:00
parent 5c9de31835
commit fad8daf05d
3 changed files with 178 additions and 120 deletions

View File

@ -303,8 +303,9 @@ class AudioClip():
def __del__(self): def __del__(self):
self.Logger.info("~AudioClip()") self.Logger.info("~AudioClip()")
def Play(self, seconds = None, rubberband = None, dec_params = None, *args): def Play(self, seconds = None, rubberband = None, dec_params = None, bitrate = None, backwards = None, *args):
return self.AudioPlayer.PlayURI(self.URI, position = seconds, rubberband = rubberband, dec_params = dec_params, *args) return self.AudioPlayer.PlayURI(self.URI, position = seconds, rubberband = rubberband, dec_params = dec_params, bitrate = bitrate,
backwards = backwards, *args)
def Stop(self): def Stop(self):
return self.AudioPlayer.Stop() return self.AudioPlayer.Stop()

View File

@ -8,6 +8,33 @@ import math
from .Utils import Utils, DataHolder from .Utils import Utils, DataHolder
import traceback import traceback
def get_birtate(message):
bitrate = []
try:
for msg in message[1].split(" "): #checking if
if "bitrate=" in msg:
bitrate = int(msg.split("bitrate=",1)[1])
if bitrate < 0.0:
bitrate = 0.01
if bitrate > 2000:
bitrate = 20
bitrate.append(f"bitrate=tempo={bitrate}")
except Exception:
pass
return bitrate
def get_backwards(message):
backwards = None
try:
for msg in message[1].split(" "): #checking if pitch= or tempo= is specified
if "backward=" in msg.lower():
backwards = True
elif "backwards=" in msg.lower():
backwards = True
except Exception:
pass
return backwards
def get_rubberBand(message): def get_rubberBand(message):
rubberband = [] rubberband = []
try: try:
@ -548,6 +575,8 @@ class VoiceCommands(BaseCommand):
return 0 return 0
rubberband = get_rubberBand(message) rubberband = get_rubberBand(message)
backwards = get_backwards(message)
bitrate = get_birtate(message)
if message[0] == "!random": if message[0] == "!random":
Trigger = self.random.choice(list(self.VoiceTriggers.values())) Trigger = self.random.choice(list(self.VoiceTriggers.values()))
@ -614,7 +643,7 @@ class VoiceCommands(BaseCommand):
if not AudioClip: if not AudioClip:
return 1 return 1
return AudioClip.Play(rubberband = rubberband) return AudioClip.Play(rubberband = rubberband, bitrate = bitrate, backwards = backwards)
class YouTube(BaseCommand): class YouTube(BaseCommand):
@ -647,7 +676,9 @@ class YouTube(BaseCommand):
#turning the string into a list where get_rubberband just picks the second element to search in #turning the string into a list where get_rubberband just picks the second element to search in
dline = ['', line.split(" ", 1)[1]] dline = ['', line.split(" ", 1)[1]]
rubberband = get_rubberBand(dline) rubberband = get_rubberBand(dline)
return AudioClip.Play(Time, rubberband = rubberband) backwards = get_backwards(dline)
bitrate = get_birtate(dline)
return AudioClip.Play(Time, rubberband = rubberband, bitrate = bitrate, backwards = backwards)
class YouTubeSearch(BaseCommand): class YouTubeSearch(BaseCommand):
import json import json
@ -693,7 +724,9 @@ class YouTubeSearch(BaseCommand):
#turning the string into a list where get_rubberband just picks the second element to search in #turning the string into a list where get_rubberband just picks the second element to search in
dline = ['', line.split(" ", 1)[1]] dline = ['', line.split(" ", 1)[1]]
rubberband = get_rubberBand(dline) rubberband = get_rubberBand(dline)
return AudioClip.Play(Time, rubberband = rubberband) backwards = get_backwards(dline)
bitrate = get_birtate(dline)
return AudioClip.Play(Time, rubberband = rubberband, bitrate = bitrate, backwards = backwards)
class Say(BaseCommand): class Say(BaseCommand):
@ -721,9 +754,17 @@ class Say(BaseCommand):
try: try:
dline = ['', message.split(" ", 1)[1]] dline = ['', message.split(" ", 1)[1]]
rubberband = get_rubberBand(dline) rubberband = get_rubberBand(dline)
backwards = get_backwards(dline)
except Exception: except Exception:
rubberband = None rubberband = None
if AudioClip.Play(rubberband = rubberband): backwards = None
try:
dline = ['', message.split(" ", 1)[1]]
bitrate = get_birtate(dline)
except Exception:
bitrate = None
if AudioClip.Play(rubberband = rubberband, bitrate = bitrate, backwards = backwards):
AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name))
return 0 return 0
else: else:

View File

@ -12,52 +12,53 @@ import sys
SAMPLEBYTES = 2 SAMPLEBYTES = 2
class FFmpegAudioPlayerFactory(): class FFmpegAudioPlayerFactory():
VALID_CALLBACKS = ["Play", "Stop", "Update"] VALID_CALLBACKS = ["Play", "Stop", "Update"]
def __init__(self, master): def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__) self.Logger = logging.getLogger(__class__.__name__)
self.Master = master self.Master = master
self.Torchlight = self.Master.Torchlight self.Torchlight = self.Master.Torchlight
def __del__(self): def __del__(self):
self.Master.Logger.info("~FFmpegAudioPlayerFactory()") self.Master.Logger.info("~FFmpegAudioPlayerFactory()")
self.Quit() self.Quit()
def NewPlayer(self): def NewPlayer(self):
self.Logger.debug(sys._getframe().f_code.co_name) self.Logger.debug(sys._getframe().f_code.co_name)
Player = FFmpegAudioPlayer(self) Player = FFmpegAudioPlayer(self)
return Player return Player
def Quit(self): def Quit(self):
self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()") self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()")
class FFmpegAudioPlayer(): class FFmpegAudioPlayer():
def __init__(self, master): def __init__(self, master):
self.Master = master self.Master = master
self.Torchlight = self.Master.Torchlight self.Torchlight = self.Master.Torchlight
self.Playing = False self.Playing = False
self.Host = ( self.Host = (
self.Torchlight().Config["VoiceServer"]["Host"], self.Torchlight().Config["VoiceServer"]["Host"],
self.Torchlight().Config["VoiceServer"]["Port"] self.Torchlight().Config["VoiceServer"]["Port"]
) )
self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"]) self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"])
self.StartedPlaying = None self.StartedPlaying = None
self.StoppedPlaying = None self.StoppedPlaying = None
self.Seconds = 0.0 self.Seconds = 0.0
self.Writer = None self.Writer = None
self.Process = None self.Process = None
self.Callbacks = [] self.Callbacks = []
def __del__(self): def __del__(self):
self.Master.Logger.debug("~FFmpegAudioPlayer()") self.Master.Logger.debug("~FFmpegAudioPlayer()")
self.Stop() self.Stop()
def PlayURI(self, uri, position, rubberband = None, dec_params = None, *args): def PlayURI(self, uri, position, rubberband = None, dec_params = None, bitrate = None,
backwards = None, *args):
if position: if position:
PosStr = str(datetime.timedelta(seconds = position)) PosStr = str(datetime.timedelta(seconds = position))
Command = ["/usr/bin/ffmpeg", "-ss", PosStr, "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args] Command = ["/usr/bin/ffmpeg", "-ss", PosStr, "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args]
@ -68,122 +69,137 @@ class FFmpegAudioPlayer():
if dec_params: if dec_params:
Command += dec_params Command += dec_params
if rubberband: if rubberband and backwards:
Command += ["-filter:a"] Command += ["-filter:a"]
rubberCommand = "" rubberCommand = ""
for rubber in rubberband: for rubber in rubberband:
rubberCommand += rubber + ", " rubberCommand += rubber + ", "
rubberCommand = rubberCommand[:-2] rubberCommand = rubberCommand[:-2]
Command += [rubberCommand] Command += [rubberCommand + "[reversed];[reversed]areverse"] #[reversed] is intermediate stream label so reverse knows what stream label to reverse
else:
if rubberband:
Command += ["-filter:a"]
rubberCommand = ""
for rubber in rubberband:
rubberCommand += rubber + ", "
rubberCommand = rubberCommand[:-2]
Command += [rubberCommand]
if backwards:
Command += ["-af"]
Command += ["areverse"]
if bitrate:
Command += ["-ab ", str(bitrate), "k"]
self.Master.Logger.debug(f"command: {Command}")
Command += ["-"] Command += ["-"]
#self.Master.Logger.debug(f"command: {Command}")
asyncio.ensure_future(self._stream_subprocess(Command)) asyncio.ensure_future(self._stream_subprocess(Command))
return True return True
def Stop(self, force = True): def Stop(self, force = True):
if not self.Playing: if not self.Playing:
return False return False
if self.Process: if self.Process:
try: try:
self.Process.terminate() self.Process.terminate()
self.Process.kill() self.Process.kill()
self.Process = None self.Process = None
except ProcessLookupError: except ProcessLookupError:
pass pass
if self.Writer: if self.Writer:
if force: if force:
Socket = self.Writer.transport.get_extra_info("socket") Socket = self.Writer.transport.get_extra_info("socket")
if Socket: if Socket:
Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
struct.pack("ii", 1, 0)) struct.pack("ii", 1, 0))
self.Writer.transport.abort() self.Writer.transport.abort()
self.Writer.close() self.Writer.close()
self.Playing = False self.Playing = False
self.Callback("Stop") self.Callback("Stop")
del self.Callbacks del self.Callbacks
return True return True
def AddCallback(self, cbtype, cbfunc): def AddCallback(self, cbtype, cbfunc):
if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS: if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS:
return False return False
self.Callbacks.append((cbtype, cbfunc)) self.Callbacks.append((cbtype, cbfunc))
return True return True
def Callback(self, cbtype, *args, **kwargs): def Callback(self, cbtype, *args, **kwargs):
for Callback in self.Callbacks: for Callback in self.Callbacks:
if Callback[0] == cbtype: if Callback[0] == cbtype:
try: try:
Callback[1](*args, **kwargs) Callback[1](*args, **kwargs)
except Exception as e: except Exception as e:
self.Master.Logger.error(traceback.format_exc()) self.Master.Logger.error(traceback.format_exc())
async def _updater(self): async def _updater(self):
LastSecondsElapsed = 0.0 LastSecondsElapsed = 0.0
while self.Playing: while self.Playing:
SecondsElapsed = time.time() - self.StartedPlaying SecondsElapsed = time.time() - self.StartedPlaying
if SecondsElapsed > self.Seconds: if SecondsElapsed > self.Seconds:
SecondsElapsed = self.Seconds SecondsElapsed = self.Seconds
self.Callback("Update", LastSecondsElapsed, SecondsElapsed) self.Callback("Update", LastSecondsElapsed, SecondsElapsed)
if SecondsElapsed >= self.Seconds: if SecondsElapsed >= self.Seconds:
if not self.StoppedPlaying: if not self.StoppedPlaying:
print("BUFFER UNDERRUN!") print("BUFFER UNDERRUN!")
self.Stop(False) self.Stop(False)
return return
LastSecondsElapsed = SecondsElapsed LastSecondsElapsed = SecondsElapsed
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
async def _read_stream(self, stream, writer): async def _read_stream(self, stream, writer):
Started = False Started = False
while stream and self.Playing: while stream and self.Playing:
Data = await stream.read(65536) Data = await stream.read(65536)
if Data: if Data:
writer.write(Data) writer.write(Data)
await writer.drain() await writer.drain()
Bytes = len(Data) Bytes = len(Data)
Samples = Bytes / SAMPLEBYTES Samples = Bytes / SAMPLEBYTES
Seconds = Samples / self.SampleRate Seconds = Samples / self.SampleRate
self.Seconds += Seconds self.Seconds += Seconds
if not Started: if not Started:
Started = True Started = True
self.Callback("Play") self.Callback("Play")
self.StartedPlaying = time.time() self.StartedPlaying = time.time()
asyncio.ensure_future(self._updater()) asyncio.ensure_future(self._updater())
else: else:
self.Process = None self.Process = None
break break
self.StoppedPlaying = time.time() self.StoppedPlaying = time.time()
async def _stream_subprocess(self, cmd): async def _stream_subprocess(self, cmd):
if not self.Playing: if not self.Playing:
return return
_, self.Writer = await asyncio.open_connection(self.Host[0], self.Host[1]) _, self.Writer = await asyncio.open_connection(self.Host[0], self.Host[1])
Process = await asyncio.create_subprocess_exec(*cmd, Process = await asyncio.create_subprocess_exec(*cmd,
stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL) stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL)
self.Process = Process self.Process = Process
await self._read_stream(Process.stdout, self.Writer) await self._read_stream(Process.stdout, self.Writer)
await Process.wait() await Process.wait()
if self.Seconds == 0.0: if self.Seconds == 0.0:
self.Stop() self.Stop()