From 0f40b8e51fbb9648e0a11a8581e5d347f3fa3045 Mon Sep 17 00:00:00 2001
From: jenz <jenz@jenz.jenz>
Date: Tue, 15 Oct 2024 19:04:18 +0200
Subject: [PATCH] added feature for playing sounds backwards

---
 .../torchlight3/Torchlight/AudioManager.py    |   5 +-
 .../torchlight3/Torchlight/Commands.py        |  49 +++-
 .../Torchlight/FFmpegAudioPlayer.py           | 244 ++++++++++--------
 3 files changed, 178 insertions(+), 120 deletions(-)

diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py
index 71c6d4f5..e46df87d 100755
--- a/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py
+++ b/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py
@@ -303,8 +303,9 @@ class AudioClip():
 	def __del__(self):
 		self.Logger.info("~AudioClip()")
 
-    def Play(self, seconds = None, rubberband = None, dec_params = None, *args):
-        return self.AudioPlayer.PlayURI(self.URI, position = seconds, rubberband = rubberband, dec_params = dec_params, *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, bitrate = bitrate, 
+            backwards = backwards, *args)
 
 	def Stop(self):
 		return self.AudioPlayer.Stop()
diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py b/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py
index c2ddfb1c..f2810cd7 100755
--- a/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py
+++ b/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py
@@ -8,6 +8,33 @@ import math
 from .Utils import Utils, DataHolder
 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):
     rubberband = []
     try:
@@ -548,6 +575,8 @@ class VoiceCommands(BaseCommand):
             return 0
         
         rubberband = get_rubberBand(message)
+        backwards = get_backwards(message)
+        bitrate = get_birtate(message)
 
         if message[0] == "!random":
             Trigger = self.random.choice(list(self.VoiceTriggers.values()))
@@ -614,7 +643,7 @@ class VoiceCommands(BaseCommand):
         if not AudioClip:
             return 1
 
-        return AudioClip.Play(rubberband = rubberband)
+        return AudioClip.Play(rubberband = rubberband, bitrate = bitrate, backwards = backwards)
 
 
 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
         dline = ['', line.split(" ", 1)[1]]
         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):
     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
         dline = ['', line.split(" ", 1)[1]]
         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):
@@ -721,9 +754,17 @@ class Say(BaseCommand):
         try:
             dline = ['', message.split(" ", 1)[1]]
             rubberband = get_rubberBand(dline)
+            backwards = get_backwards(dline)
         except Exception:
             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))
             return 0
         else:
diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py b/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py
index b0ff290b..2b8db621 100755
--- a/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py
+++ b/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py
@@ -12,52 +12,53 @@ import sys
 SAMPLEBYTES = 2
 
 class FFmpegAudioPlayerFactory():
-	VALID_CALLBACKS = ["Play", "Stop", "Update"]
+    VALID_CALLBACKS = ["Play", "Stop", "Update"]
 
-	def __init__(self, master):
-		self.Logger = logging.getLogger(__class__.__name__)
-		self.Master = master
-		self.Torchlight = self.Master.Torchlight
+    def __init__(self, master):
+        self.Logger = logging.getLogger(__class__.__name__)
+        self.Master = master
+        self.Torchlight = self.Master.Torchlight
 
-	def __del__(self):
-		self.Master.Logger.info("~FFmpegAudioPlayerFactory()")
-		self.Quit()
+    def __del__(self):
+        self.Master.Logger.info("~FFmpegAudioPlayerFactory()")
+        self.Quit()
 
-	def NewPlayer(self):
-		self.Logger.debug(sys._getframe().f_code.co_name)
-		Player = FFmpegAudioPlayer(self)
-		return Player
+    def NewPlayer(self):
+        self.Logger.debug(sys._getframe().f_code.co_name)
+        Player = FFmpegAudioPlayer(self)
+        return Player
 
-	def Quit(self):
-		self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()")
+    def Quit(self):
+        self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()")
 
 
 class FFmpegAudioPlayer():
-	def __init__(self, master):
-		self.Master = master
-		self.Torchlight = self.Master.Torchlight
-		self.Playing = False
+    def __init__(self, master):
+        self.Master = master
+        self.Torchlight = self.Master.Torchlight
+        self.Playing = False
 
-		self.Host = (
-			self.Torchlight().Config["VoiceServer"]["Host"],
-			self.Torchlight().Config["VoiceServer"]["Port"]
-		)
-		self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"])
+        self.Host = (
+            self.Torchlight().Config["VoiceServer"]["Host"],
+            self.Torchlight().Config["VoiceServer"]["Port"]
+        )
+        self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"])
 
-		self.StartedPlaying = None
-		self.StoppedPlaying = None
-		self.Seconds = 0.0
+        self.StartedPlaying = None
+        self.StoppedPlaying = None
+        self.Seconds = 0.0
 
-		self.Writer = None
-		self.Process = None
+        self.Writer = None
+        self.Process = None
 
-		self.Callbacks = []
+        self.Callbacks = []
 
-	def __del__(self):
-		self.Master.Logger.debug("~FFmpegAudioPlayer()")
-		self.Stop()
+    def __del__(self):
+        self.Master.Logger.debug("~FFmpegAudioPlayer()")
+        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:
             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]
@@ -68,122 +69,137 @@ class FFmpegAudioPlayer():
         if dec_params:
             Command += dec_params
 
-        if rubberband:
+        if rubberband and backwards:
             Command += ["-filter:a"]
             rubberCommand = ""
             for rubber in rubberband:
                 rubberCommand += rubber + ", "
             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 += ["-"]
+        #self.Master.Logger.debug(f"command: {Command}")
         asyncio.ensure_future(self._stream_subprocess(Command))
         return True
 
-	def Stop(self, force = True):
-		if not self.Playing:
-			return False
+    def Stop(self, force = True):
+        if not self.Playing:
+            return False
 
-		if self.Process:
-			try:
-				self.Process.terminate()
-				self.Process.kill()
-				self.Process = None
-			except ProcessLookupError:
-				pass
+        if self.Process:
+            try:
+                self.Process.terminate()
+                self.Process.kill()
+                self.Process = None
+            except ProcessLookupError:
+                pass
 
-		if self.Writer:
-			if force:
-				Socket = self.Writer.transport.get_extra_info("socket")
-				if Socket:
-					Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
-						struct.pack("ii", 1, 0))
+        if self.Writer:
+            if force:
+                Socket = self.Writer.transport.get_extra_info("socket")
+                if Socket:
+                    Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
+                        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")
-		del self.Callbacks
+        self.Callback("Stop")
+        del self.Callbacks
 
-		return True
+        return True
 
-	def AddCallback(self, cbtype, cbfunc):
-		if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS:
-			return False
+    def AddCallback(self, cbtype, cbfunc):
+        if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS:
+            return False
 
-		self.Callbacks.append((cbtype, cbfunc))
-		return True
+        self.Callbacks.append((cbtype, cbfunc))
+        return True
 
-	def Callback(self, cbtype, *args, **kwargs):
-		for Callback in self.Callbacks:
-			if Callback[0] == cbtype:
-				try:
-					Callback[1](*args, **kwargs)
-				except Exception as e:
-					self.Master.Logger.error(traceback.format_exc())
+    def Callback(self, cbtype, *args, **kwargs):
+        for Callback in self.Callbacks:
+            if Callback[0] == cbtype:
+                try:
+                    Callback[1](*args, **kwargs)
+                except Exception as e:
+                    self.Master.Logger.error(traceback.format_exc())
 
-	async def _updater(self):
-		LastSecondsElapsed = 0.0
+    async def _updater(self):
+        LastSecondsElapsed = 0.0
 
-		while self.Playing:
-			SecondsElapsed = time.time() - self.StartedPlaying
+        while self.Playing:
+            SecondsElapsed = time.time() - self.StartedPlaying
 
-			if SecondsElapsed > self.Seconds:
-				SecondsElapsed = self.Seconds
+            if SecondsElapsed > self.Seconds:
+                SecondsElapsed = self.Seconds
 
-			self.Callback("Update", LastSecondsElapsed, SecondsElapsed)
+            self.Callback("Update", LastSecondsElapsed, SecondsElapsed)
 
-			if SecondsElapsed >= self.Seconds:
-				if not self.StoppedPlaying:
-					print("BUFFER UNDERRUN!")
-				self.Stop(False)
-				return
+            if SecondsElapsed >= self.Seconds:
+                if not self.StoppedPlaying:
+                    print("BUFFER UNDERRUN!")
+                self.Stop(False)
+                return
 
-			LastSecondsElapsed = SecondsElapsed
+            LastSecondsElapsed = SecondsElapsed
 
-			await asyncio.sleep(0.1)
+            await asyncio.sleep(0.1)
 
-	async def _read_stream(self, stream, writer):
-		Started = False
+    async def _read_stream(self, stream, writer):
+        Started = False
 
-		while stream and self.Playing:
-			Data = await stream.read(65536)
+        while stream and self.Playing:
+            Data = await stream.read(65536)
 
-			if Data:
-				writer.write(Data)
-				await writer.drain()
+            if Data:
+                writer.write(Data)
+                await writer.drain()
 
-				Bytes = len(Data)
-				Samples = Bytes / SAMPLEBYTES
-				Seconds = Samples / self.SampleRate
+                Bytes = len(Data)
+                Samples = Bytes / SAMPLEBYTES
+                Seconds = Samples / self.SampleRate
 
-				self.Seconds += Seconds
+                self.Seconds += Seconds
 
-				if not Started:
-					Started = True
-					self.Callback("Play")
-					self.StartedPlaying = time.time()
-					asyncio.ensure_future(self._updater())
-			else:
-				self.Process = None
-				break
+                if not Started:
+                    Started = True
+                    self.Callback("Play")
+                    self.StartedPlaying = time.time()
+                    asyncio.ensure_future(self._updater())
+            else:
+                self.Process = None
+                break
 
-		self.StoppedPlaying = time.time()
+        self.StoppedPlaying = time.time()
 
-	async def _stream_subprocess(self, cmd):
-		if not self.Playing:
-			return
+    async def _stream_subprocess(self, cmd):
+        if not self.Playing:
+            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,
-				stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL)
-		self.Process = Process
+        Process = await asyncio.create_subprocess_exec(*cmd,
+                stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL)
+        self.Process = Process
 
-		await self._read_stream(Process.stdout, self.Writer)
-		await Process.wait()
+        await self._read_stream(Process.stdout, self.Writer)
+        await Process.wait()
 
-		if self.Seconds == 0.0:
-			self.Stop()
+        if self.Seconds == 0.0:
+            self.Stop()