From 8fda2a03af52202a841719b00041c255fbbd040b Mon Sep 17 00:00:00 2001 From: michalcourson Date: Tue, 24 Feb 2026 19:08:27 -0500 Subject: [PATCH] server playback --- audio-service/metadata.json | 6 +- audio-service/settings.json | 6 +- .../__pycache__/audio_clip.cpython-313.pyc | Bin 0 -> 4408 bytes .../src/__pycache__/audio_io.cpython-313.pyc | Bin 0 -> 8147 bytes .../src/__pycache__/settings.cpython-313.pyc | Bin 5214 -> 5336 bytes .../__pycache__/windows_audio.cpython-313.pyc | Bin 5611 -> 4425 bytes audio-service/src/audio_clip.py | 64 +++++++ audio-service/src/audio_io.py | 166 ++++++++++++++++++ audio-service/src/audio_recorder.py | 154 ---------------- audio-service/src/main.py | 4 +- .../routes/__pycache__/device.cpython-313.pyc | Bin 1211 -> 1199 bytes .../__pycache__/recording.cpython-313.pyc | Bin 3064 -> 3262 bytes audio-service/src/routes/device.py | 4 +- audio-service/src/routes/recording.py | 14 +- audio-service/src/settings.py | 9 +- audio-service/src/windows_audio.py | 47 ++--- electron-ui/src/main/service.ts | 2 +- 17 files changed, 268 insertions(+), 208 deletions(-) create mode 100644 audio-service/src/__pycache__/audio_clip.cpython-313.pyc create mode 100644 audio-service/src/__pycache__/audio_io.cpython-313.pyc create mode 100644 audio-service/src/audio_clip.py create mode 100644 audio-service/src/audio_io.py delete mode 100644 audio-service/src/audio_recorder.py diff --git a/audio-service/metadata.json b/audio-service/metadata.json index aad62ef..386665c 100644 --- a/audio-service/metadata.json +++ b/audio-service/metadata.json @@ -17,9 +17,9 @@ "endTime": 30, "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav", "name": "Pee pee\npoo poo", - "playbackType": "playStop", - "startTime": 27.587412587412587, - "volume": 1 + "playbackType": "playOverlap", + "startTime": 27.76674010920584, + "volume": 0.25 }, { "endTime": 27.516843118383072, diff --git a/audio-service/settings.json b/audio-service/settings.json index ed12104..6b19aa6 100644 --- a/audio-service/settings.json +++ b/audio-service/settings.json @@ -8,10 +8,10 @@ "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings", "recording_length": 30, "output_device": { + "channels": 2, "default_samplerate": 48000, - "index": 40, - "max_output_channels": 2, - "name": "Speakers (Realtek(R) Audio)" + "index": 45, + "name": "VM to Discord (VB-Audio Voicemeeter VAIO)" }, "http_port": 5010 } \ No newline at end of file diff --git a/audio-service/src/__pycache__/audio_clip.cpython-313.pyc b/audio-service/src/__pycache__/audio_clip.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8850eb9caad779779933b1b80870140e60e4ad2d GIT binary patch literal 4408 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 delta 255 zcmcbic~67)GcPX}0}uph&C5)j$eY2(>sXqSnIDvzoL`iZTC}nDBRiwv?I2u@DnZf1VK!L#`d_Z}9ee+-jX3EXF94C0;K&nP3C&Y;QaS0n>8v`896 zNCJslteJT!sTG>+lLdtQ`T2@c(~44yGvbR&ic%AECwB??b7uloH89)}R+wx*`H4^% z&jSvD>m2eIIpnW$C{9ile!{`Ys`P;YNK6hC5$ESi1i3Q42Iw27zdX z$&3OrlRxvQF|tl};FV-tIXQ~An%@g3)xdCrTl^xo{0(7|$sc%KC;RXmmXQRC7wG^A zO}1OA>8U00$)!a_sd**w`K2Y3wRqJmlt2pPK!gR5Cx+h P;U{2lFd7z#0NnxrKciav delta 1548 zcmb_cOOM-B6!vujnqbMU?{`TYDz`il*J&7N!((Z$LQJ_ z)FP3J)$CBYtPx8#uwm0BY$_qKm^2_qrvCs5m1!lGaF0D{JC8~%xQov|$LHMh-SeI6 zpO5_UV)B733mjX2J@>Ue&fH4=g5JON` z%uQ}A&J4vv*fBe6Z3`3AwQ%oB;M%5x?e1FN#Vu>yZdzqQs1nQVlWrC3`=z`U z9AQU}5FRo@R92Q81USq<@U#4n(5><_4OE9_ z>{_dF`knOb8>@lZ@J&YxkuavqH~6}|E1sFF@rmzIt0Rp_U7;UD)IreemB+ol?zmP5 zcTDymc)i!|cXl-PY&Zwl3FE>5e5_KgV7G^lmn)j^ww53uvoA6M^)<`9W)a6vEH+(x zo%Mvkq5PO(+bx4c24E!L>09Lbxi0&7#cW!>$`UUG3@!%*R;+catvAiKNwjFF%namB z+mBM>Arl7*W3~dh{4|IZQZ>Q=|Alw$Zmai+v)FIhy$dE);_<_aN15`*)Hql0a>Ze;cu&HfR2oX9ds2QZr5;R04+tC6_XRFf@Y32at^GPz8>L^} zh>w*6o>E~7kCYRei4!Ep_HDvYO%`nn)BGI}{iRuu@TsZ~_EjCU{BNp98_~KlsrvW{ z*KT2(5azBTcKI#}`sDIGF%W|%Ct}ScW@mdsj5a7##i3j{u=XDc@*1m476IUx`M&(7s(Sz zFQE+++PCw!qrWB21e$A+;NL=yup-HTw}!|ZGc<+%q+Al=-$IrFR%kwT9xYHaRm^SS mso3$Yj2wIA?}KuzcAsNFzes(K-lLiHG37IaYFiv&P~%@u6M@nI diff --git a/audio-service/src/audio_clip.py b/audio-service/src/audio_clip.py new file mode 100644 index 0000000..d1a8297 --- /dev/null +++ b/audio-service/src/audio_clip.py @@ -0,0 +1,64 @@ +import scipy.signal +import scipy.io.wavfile as wavfile +import numpy as np +import os + +class AudioClip: + def __init__(self, metadata, target_sample_rate=44100): + """ + metadata: dict with keys 'filename', 'start', 'end' (seconds) + target_sample_rate: sample rate for playback + """ + self.metadata = metadata + self.file_path = metadata['filename'] + self.start = metadata.get('startTime', 0) + self.end = metadata.get('endTime', None) + self.target_sample_rate = target_sample_rate + self.volume = metadata.get('volume', 1.0) + self.finished = False + self.audio_data, self.sample_rate = self._load_and_process_audio() + print(f"AudioClip created for {self.file_path} with start={self.start}s, end={self.end}s, sample_rate={self.sample_rate}Hz, length={len(self.audio_data)/self.sample_rate:.2f}s") + self.position = 0 # sample index for playback + + def _load_and_process_audio(self): + # Load audio file + sample_rate, data = wavfile.read(self.file_path) + # Convert to float32 + if data.dtype != np.float32: + data = data.astype(np.float32) / np.max(np.abs(data)) + # Convert to mono if needed + if len(data.shape) > 1: + data = np.mean(data, axis=1) + # Resample if needed + if sample_rate != self.target_sample_rate: + num_samples = int(len(data) * self.target_sample_rate / sample_rate) + data = scipy.signal.resample(data, num_samples) + sample_rate = self.target_sample_rate + # Cache only the clip region + start_sample = int(self.start * sample_rate) + end_sample = int(self.end * sample_rate) if self.end else len(data) + cached = data[start_sample:end_sample] + cached *= self.volume # Apply volume + return cached, sample_rate + + def get_samples(self, num_samples): + # Return next chunk for playback + if self.position >= len(self.audio_data): + self.finished = True + return np.zeros(num_samples, dtype=np.float32) + end_pos = min(self.position + num_samples, len(self.audio_data)) + chunk = self.audio_data[self.position:end_pos] + self.position = end_pos + if self.position >= len(self.audio_data): + self.finished = True + # Pad if chunk is short + if len(chunk) < num_samples: + chunk = np.pad(chunk, (0, num_samples - len(chunk)), mode='constant') + return chunk + + def is_finished(self): + return self.finished + + def reset(self): + self.position = 0 + self.finished = False \ No newline at end of file diff --git a/audio-service/src/audio_io.py b/audio-service/src/audio_io.py new file mode 100644 index 0000000..72e52b9 --- /dev/null +++ b/audio-service/src/audio_io.py @@ -0,0 +1,166 @@ +import sounddevice as sd +import numpy as np +import os +from datetime import datetime +import scipy.io.wavfile as wavfile +from metadata_manager import MetaDataManager +from audio_clip import AudioClip + + +# AudioClip class for clip playback + + +class AudioIO: + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + # print("Creating new AudioRecorder instance") + cls._instance = super().__new__(cls) + cls._instance.init() + return cls._instance + def init(self): + self.duration = 30 + self.channels = 2 + self.input_sample_rate = 44100 + self.output_sample_rate = 44100 + self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32) + self.recordings_dir = "recordings" + + sd.default.latency = 'low' + + self.in_stream = sd.InputStream( + callback=self.record_callback + ) + + self.out_stream = sd.OutputStream( + callback=self.playback_callback, + latency=3 + ) + + self.clip_map = {} + + + def refresh_streams(self): + was_active = self.in_stream.active + if was_active: + self.in_stream.stop() + self.out_stream.stop() + + self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32) + # print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}") + self.in_stream = sd.InputStream( + callback=self.record_callback + ) + + self.out_stream = sd.OutputStream( + callback=self.playback_callback + ) + + if was_active: + self.in_stream.start() + self.out_stream.start() + + + + def record_callback(self, indata, frames, time, status): + if status: + # print(f"Recording status: {status}") + pass + + # Circular buffer implementation + self.buffer = np.roll(self.buffer, -frames, axis=0) + self.buffer[-frames:] = indata + + def playback_callback(self, outdata, frames, time, status): + if status: + # print(f"Playback status: {status}") + pass + + outdata.fill(0) + + # Iterate over a copy of the items to avoid modifying the dictionary during iteration + for clip_id, clip_list in list(self.clip_map.items()): + for clip in clip_list[:]: # Iterate over a copy of the list + if not clip.is_finished(): + samples = clip.get_samples(frames) + outdata[:] += samples.reshape(-1, 1) # Mix into output + if clip.is_finished(): + self.clip_map[clip_id].remove(clip) + if len(self.clip_map[clip_id]) == 0: + del self.clip_map[clip_id] + break # Exit inner loop since the key is deleted + + + def save_last_n_seconds(self): + # Create output directory if it doesn't exist + os.makedirs(self.recordings_dir, exist_ok=True) + + # Generate filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav") + + # Normalize audio to prevent clipping + audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5 + + # Convert float32 to int16 for WAV file + audio_data_int16 = (audio_data * 32767).astype(np.int16) + + # Write buffer to file + wavfile.write(filename, int(self.input_sample_rate), audio_data_int16) + + meta = MetaDataManager() + + clip_metadata = { + "filename": filename, + "name": f"Clip {timestamp}", + "playbackType":"playStop", + "volume": 1.0, + } + + meta.add_clip_to_collection("Uncategorized", clip_metadata ) + + + return clip_metadata + + def set_buffer_duration(self, duration): + self.duration = duration + self.buffer = np.zeros((int(duration * self.input_sample_rate), self.channels), dtype=np.float32) + + def set_recording_directory(self, directory): + self.recordings_dir = directory + + def start_recording(self): + if(self.in_stream.active): + # print("Already recording") + return + # print('number of channels', self.channels) + + self.in_stream.start() + self.out_stream.start() + self.output_sample_rate = self.out_stream.samplerate + self.input_sample_rate = self.in_stream.samplerate + + def stop_recording(self): + if(not self.in_stream.active): + # print("Already stopped") + return + + self.in_stream.stop() + self.out_stream.stop() + + def is_recording(self): + return self.in_stream.active + + def play_clip(self, clip_metadata): + print(f"Playing clip: {clip_metadata}") + clip_id = clip_metadata.get("filename") + if clip_metadata.get("playbackType") == "playStop": + if clip_id in self.clip_map: + del self.clip_map[clip_id] + return + else: + self.clip_map[clip_id] = [] + if clip_id not in self.clip_map: + self.clip_map[clip_id] = [] + self.clip_map[clip_id].append(AudioClip(clip_metadata, target_sample_rate=self.output_sample_rate)) \ No newline at end of file diff --git a/audio-service/src/audio_recorder.py b/audio-service/src/audio_recorder.py deleted file mode 100644 index d66ed94..0000000 --- a/audio-service/src/audio_recorder.py +++ /dev/null @@ -1,154 +0,0 @@ -import sounddevice as sd -import numpy as np -import os -from datetime import datetime -import scipy.io.wavfile as wavfile -from metadata_manager import MetaDataManager - -class AudioRecorder: - _instance = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - # print("Creating new AudioRecorder instance") - cls._instance = super().__new__(cls) - cls._instance.init() - return cls._instance - def init(self): - """ - Initialize audio recorder with configurable parameters. - - :param duration: Length of audio buffer in seconds - :param sample_rate: Audio sample rate (if None, use default device sample rate) - :param channels: Number of audio channels - """ - # print(f"Initializing AudioRecorder") - self.duration = 30 - self.sample_rate = 44100 - self.channels = 2 - self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32) - self.recordings_dir = "recordings" - - self.stream = sd.InputStream( - callback=self.record_callback - ) - - def refresh_stream(self): - """ - Refresh the audio stream with updated parameters. - """ - was_active = self.stream.active - if was_active: - self.stream.stop() - - self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32) - # print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}") - self.stream = sd.InputStream( - callback=self.record_callback - ) - - if was_active: - self.stream.start() - - - - - def record_callback(self, indata, frames, time, status): - """ - Circular buffer callback for continuous recording. - - :param indata: Input audio data - :param frames: Number of frames - :param time: Timestamp - :param status: Recording status - """ - if status: - # print(f"Recording status: {status}") - pass - - # Circular buffer implementation - self.buffer = np.roll(self.buffer, -frames, axis=0) - self.buffer[-frames:] = indata - - def save_last_n_seconds(self): - """ - Save the last n seconds of audio to a file. - - :param output_dir: Directory to save recordings - :return: Path to saved audio file - """ - # Create output directory if it doesn't exist - os.makedirs(self.recordings_dir, exist_ok=True) - - # Generate filename with timestamp - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav") - - # Normalize audio to prevent clipping - audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5 - - # Convert float32 to int16 for WAV file - audio_data_int16 = (audio_data * 32767).astype(np.int16) - - # Write buffer to file - wavfile.write(filename, int(self.sample_rate), audio_data_int16) - - meta = MetaDataManager() - - clip_metadata = { - "filename": filename, - "name": f"Clip {timestamp}", - "playbackType":"playStop", - "volume": 1.0, - } - - meta.add_clip_to_collection("Uncategorized", clip_metadata ) - - - return clip_metadata - - def set_buffer_duration(self, duration): - """ - Set the duration of the audio buffer. - - :param duration: New buffer duration in seconds - """ - self.duration = duration - self.buffer = np.zeros((int(duration * self.sample_rate), self.channels), dtype=np.float32) - - def set_recording_directory(self, directory): - """ - Set the directory where recordings will be saved. - - :param directory: Path to the recordings directory - """ - self.recordings_dir = directory - - def start_recording(self): - """ - Start continuous audio recording with circular buffer. - """ - if(self.stream.active): - # print("Already recording") - return - # print('number of channels', self.channels) - - self.stream.start() - - def stop_recording(self): - """ - Stop continuous audio recording with circular buffer. - """ - if(not self.stream.active): - # print("Already stopped") - return - - self.stream.stop() - - def is_recording(self): - """ - Check if the audio stream is currently active. - - :return: True if recording, False otherwise - """ - return self.stream.active \ No newline at end of file diff --git a/audio-service/src/main.py b/audio-service/src/main.py index adc432d..b14ff0e 100644 --- a/audio-service/src/main.py +++ b/audio-service/src/main.py @@ -1,7 +1,7 @@ import argparse import os import sys -from audio_recorder import AudioRecorder +from audio_io import AudioIO from windows_audio import WindowsAudioManager import sounddevice as sd from metadata_manager import MetaDataManager @@ -41,6 +41,8 @@ def main(): os.makedirs(settings.get_settings('save_path'), exist_ok=True) + io = AudioIO() + io.start_recording() # Register blueprints app.register_blueprint(recording_bp) app.register_blueprint(device_bp) diff --git a/audio-service/src/routes/__pycache__/device.cpython-313.pyc b/audio-service/src/routes/__pycache__/device.cpython-313.pyc index 7a8e875f24502028369d5b3bf32b7f8ec71f369e..2c050b2e3bac5fa229a6f08e01515305f6bb075e 100644 GIT binary patch delta 80 zcmdnZxt^2vGcPX}0}zOt&dYo`k@qDByJKleX1=HYW){YuOq?8vAYOcC{$w7OOevX% hJfaga7I0kV(QM#+D4={Y=A9RT5O88iR@ delta 90 zcmZ3_xto*sGcPX}0}yNqot^o6BJWE+UdPgu%>1C#0H7O8>=HXw0}vjC_$9;}lUB&Z1@ zw19*r^DP#T@GYjy{32}zhDe5DW}vWwf0ltPBTPxg3p{5AsVm>M|cxX9TfzS%IXcz%ACaoW$bnB9IdxUI4lF7Dpl|0^>9D zCpU08%YdA)lA%Ztqz3Fp4x8Nkl+v73yCRFp_qik)WhVdRdM(1m=)(A!nSsgjBNK=T GRu2G(1aZ>< delta 466 zcmdld`9qxdGcPX}0}yb^&CAr<$UB3PkJqs@B{M%LH95a1CADbtTE<>RW|{8#&9=@-6PEYb#2#USS?6bS&yTYLpMiIqu-$=UJ6C5c5P zMS4I%5H5BG5)BL=*jQLy7@zQq&EQy|c|ly~GQaKx4&5JjxJ5oQFiyV5rRpF8RM*7A z&(P0h!#q>MoOvUo4fA$Z8)i*`TdZk0iN)DP%s>@IEFc2pnp=E{pumnVf`;~FH*RM} zy~$nNQqrbCX^_*3Ie_E`W=2NF=M1_R8FcS5XiVPE{aS>L(S`9dGXs<5M { - console.error(`Python stderr: ${data.toString()}`); + // console.error(`Python stderr: ${data.toString()}`); const lines = data.toString().split('\n'); // eslint-disable-next-line no-restricted-syntax for (const line of lines) {