__author__ = "Roland Trouville"
__copyright__ = "Copyright 2015+, Consortium MonPaGe"
__license__ = "Creative Commons 4.0 By-Nc-Sa"
__maintainer__ = "Roland Trouville"
__email__ = "contact.monpage@gmail.com"
__status__ = "Production"

import logging
import os
import queue
import threading
import typing
from abc import abstractmethod

import numpy
import numpy as np
import sounddevice as sd
import soundfile as sf
from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import QGroupBox, QLabel, QProgressBar

from tools.options import Options
from tools.ui_tools import UITools

logger = logging.getLogger(__name__)


class SDIOThread(QThread):
	is_thread_finished = False
	signal_pbarval = Signal(int, name="PBAR_VAL_CHANGED")
	signal_pbarmax = Signal(int, name="PBAR_MAX_CHANGED")
	signal_next = Signal()
	signal_timer = Signal(float, name="TIMER_VALUE_CHANGED")

	prog = None
	label = None
	control_group = None

	def __init__(
		self,
		prog: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
	):
		QThread.__init__(self)
		self.signal_pbarval.connect(UITools.on_val_changed)
		self.signal_pbarmax.connect(UITools.on_max_changed)
		self.signal_next.connect(AudioManager.start_next_thread)
		self.signal_timer.connect(UITools.on_timer_changed)
		self.prog = prog
		self.label = label
		self.control_group = control_group

	def update_progressbar(self, val: int, maxval: int = None):
		"""
		Updates the progress bar by emitting a signal for the current value,
		and optionally updates the maximum value of the progress bar.

		Args:
			val (int): The current progress value.
			maxval (int, optional): The maximum value for the progress bar. If provided, also emits signal to update max.
		"""
		self.signal_pbarval.emit(val)
		if maxval is not None:
			self.signal_pbarmax.emit(maxval)
			if maxval < val:
				logger.warning(
					"Probleme dans la mise a jour de la progress bar: val > max"
				)

	def broadcast_ended(self):
		"""
		Emits a signal indicating that a broadcasting process has finished.
		"""
		logger.info("envoi signal broadcast fini")
		self.signal_next.emit()

	def update_timer(self, time: float):
		"""
		Emits a signal to update a timer value.

		Args:
			time (float): The current time value to update the timer with.
		"""
		self.signal_timer.emit(time)

	def run(self):
		"""Pass"""
		pass


class SDOThread(SDIOThread):
	buffersize = None
	blocksize = None
	output_data_index = 0

	filename = None

	output_queue = None
	output_data = None
	output_data_len: int = None
	output_stream = None
	output_event = None

	max_frames: int = 0
	number_of_frames: int = 0
	framerate: int = 0

	data: typing.List[int]
	channels: int

	def __init__(
		self,
		buffersize,
		blocksize,
		prog: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
	):
		SDIOThread.__init__(self, prog, label, control_group)
		self.buffersize = buffersize
		self.blocksize = blocksize
		self.output_queue = queue.Queue(buffersize)
		self.max_frames = 0
		self.number_of_frames = 0

	def set_audio_data(
		self, sound_data: typing.List[int], framerate: int, channels: int
	):
		"""
		Sets the audio data to be played.

		Args:
			sound_data (List[int]): A list of audio samples.
			framerate (int): The sampling rate of the audio (in Hz).
			channels (int): Number of audio channels (e.g., 1 for mono, 2 for stereo).
		"""
		self.framerate = framerate
		self.data = sound_data
		self.channels = channels

	def kill(self):
		"""
		Stops audio playback and resets relevant audio state.

		- Stops and closes the output audio stream if active.
		- Clears the output queue and resets internal buffers.
		- Signals the end of playback and resets progress bar and frame counters.
		- Exits the current thread/process if applicable.
		"""
		logger.info("killing play")
		if self.output_stream is not None:
			if self.output_stream.active:
				self.output_stream.stop()
			self.output_stream.close(True)
		self.output_queue.empty()
		self.output_data = []
		self.output_event.set()
		self.update_progressbar(0, 100)
		self.number_of_frames = 0
		self.exit()
		logger.info("fin killing play")

	def callback_output_end(self):
		"""
		Called when audio output playback has finished.

		This method sets the output event to signal that playback is complete,
		and logs the event for debugging purposes.

		Returns:
			None
		"""
		logger.info("fin callback")
		self.output_event.set()
		return None

	def callback_output(self, outdata, frames, time, status):
		"""
		Callback invoked when audio output completes.

		- Signals that output is done by setting the output_event flag.
		- Can be used to trigger any post-playback logic.

		Returns:
			None
		"""
		data = None
		# self.number_of_frames += frames
		# current_time = round(((1.0 / self.framerate) * self.number_of_frames), 2)
		# self.update_timer(current_time)
		try:
			tmp = self.output_queue.get_nowait()
			data = tmp[0]
		except queue.Empty:
			pass
		if data is not None:
			if len(data) < len(outdata):
				toto = data.copy()
				shape = outdata.shape
				toto.resize(shape)
				outdata[:] = toto
			else:
				outdata[:] = data
		else:
			raise sd.CallbackStop
		# self.update_progressbar(round(self.number_of_frames / 1000))
		# current_time = round(((1.0 / self.framerate) * self.number_of_frames), 2)
		# self.update_timer(current_time)

	def get_output_data_block(self, blocksize, nchannels=1):
		"""
		Retrieves the next block of audio data for playback.

		Args:
			blocksize (int): Number of audio frames per block.
			nchannels (int, optional): Number of audio channels. Default is 1.

		Returns:
			list or None: A list of audio samples for the current block, or None if all data has been read.
		"""
		start = self.output_data_index * self.blocksize
		end = (self.output_data_index + 1) * self.blocksize
		self.output_data_index += 1
		if start < self.max_frames:
			if end <= self.max_frames:
				return self.output_data[start:end]
			else:
				return self.output_data[start:]
		else:
			return None

	@abstractmethod
	def run(self):
		"""
		Main execution method for the audio playback thread.

		This method:
		- Prepares and initializes audio playback parameters.
		- Queues the first block of audio data.
		- Starts the audio output stream using `sounddevice`.
		- Feeds audio blocks into the stream until playback is complete.
		- Waits for the playback to finish before signaling completion.

		Handles:
		- Thread-safe signaling for UI updates and progress.
		- Logging and error reporting through `UITools`.

		This method must be implemented by subclasses.
		"""
		logger.info("debut thread play")
		UITools.set_audio_progressbar(self.prog)
		UITools.set_timer_label(self.label)
		try:
			self.output_event = threading.Event()
			data_full = self.data
			fs = self.framerate
			channels = self.channels

			self.max_frames = len(data_full)
			self.framerate = fs
			logger.info(str(self.max_frames))
			duration = self.max_frames * 1.0 / fs
			logger.info(
				"Lecture du fichier "
				+ str(self.filename)
				+ " (duree: "
				+ ("%6.2F" % duration)
				+ "s frequence: "
				+ str(self.framerate)
				+ "Hz) sur "
				+ AudioManager.output_device_to_str(),
			)
			self.update_progressbar(0, round(self.max_frames / 1000))
			self.output_data = data_full

			data = self.get_output_data_block(self.blocksize)
			self.output_queue.put_nowait((data, self.output_data_index))

			try:
				self.output_stream = sd.OutputStream(
					blocksize=self.blocksize,
					channels=channels,
					device=AudioManager.output_device_id,
					dtype="float32",
					samplerate=fs,
					callback=self.callback_output,
					finished_callback=self.callback_output_end,
				)
				# timeout = self.blocksize * self.buffersize / fs
				# val = self.blocksize
				with self.output_stream:
					while data is not None:
						# current_time = round(((1.0 / fs) * val), 2)
						# self.update_timer(current_time)
						data = self.get_output_data_block(self.blocksize)
						# self.output_queue.put(data, timeout=timeout)
						self.output_queue.put((data, self.output_data_index))
					# val += self.blocksize

					self.output_event.wait()

			except Exception as e:
				UITools.error_log(e)
		except Exception as e:
			UITools.error_log(e)
		self.is_thread_finished = True
		self.broadcast_ended()
		logger.info("fin thread play")


class SDOSoundThread(SDOThread):
	CHANNEL_NB: int = 1

	sound_play_progress = Signal(float, name="SOUND_PLAY_PROGRESSED")
	sound_play_end = Signal(name="SOUND_PLAY_ENDED")

	def __init__(self, buffersize, blocksize):
		SDOThread.__init__(
			self, buffersize, blocksize, prog=None, label=None, control_group=None
		)

	def set_data(self, sound_data: list[int], sample_rate: int):
		"""
		Sets the audio data and sampling rate for playback.

		Args:
			sound_data (list[int]): The raw audio data to be played.
			sample_rate (int): The sample rate (in Hz) of the audio data.
		"""
		super().set_audio_data(sound_data, sample_rate, self.CHANNEL_NB)

	def set_progress_callback(self, callback):
		"""
		Connects a callback function to be triggered on playback progress updates.

		Args:
			callback (function): A function to call with the current playback progress (as a float between 0 and 1).
		"""
		self.sound_play_progress.connect(callback)

	def set_end_callback(self, callback):
		"""
		Connects a callback function to be triggered when playback ends.

		Args:
			callback (function): A function to call when playback has finished.
		"""
		self.sound_play_end.connect(callback)

	def callback_output(self, outdata, frames, time, status):
		"""
		Audio stream callback that handles the output buffer during playback.

		Emits the current progress as a percentage of total frames.

		Args:
			outdata (ndarray): Buffer to fill with audio data.
			frames (int): Number of frames to write to outdata.
			time: Timing information for the callback.
			status: Status flag indicating any errors or warnings.
		"""
		self.number_of_frames += frames
		round(((1.0 / self.framerate) * self.number_of_frames), 2)
		current_percent = self.number_of_frames / self.max_frames

		self.sound_play_progress.emit(current_percent)
		super().callback_output(outdata, frames, time, status)

	def callback_output_end(self):
		"""
		Callback triggered when audio playback is finished.

		Emits a signal to notify that playback has ended and calls the parent class implementation.
		"""
		self.sound_play_end.emit()
		super().callback_output_end()

	def run(self):
		"""
		Starts the audio playback thread.

		Delegates execution to the parent class's run() method, which manages streaming and playback logic.
		"""
		super().run()


class SDOWaveThread(SDOThread):
	def __init__(
		self,
		buffersize,
		blocksize,
		prog: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
	):
		SDOThread.__init__(self, buffersize, blocksize, prog, label, control_group)

	def set_wave_data(self, filename):
		"""
		Loads audio data from a WAV file and initializes playback parameters.

		Args:
			filename (str): Path to the WAV file to be loaded.

		This method reads the WAV file using the AudioManager and passes the data,
		sample rate, and channel count to the parent class for setup.
		"""
		data_full, fs = AudioManager.__read_wav_to_numpy__(filename)
		channels = data_full.shape[1]
		super().set_audio_data(data_full, fs, channels)

	def callback_output(self, outdata, frames, time, status):
		"""
		Callback function used during audio playback.

		Args:
			outdata: The output buffer to write audio data to.
			frames (int): Number of frames to write.
			time: Timing information for the callback.
			status: Status of the audio stream.

		Updates the playback progress and timer, and delegates buffer management to the parent class.
		"""
		self.number_of_frames += frames
		current_time = round(((1.0 / self.framerate) * self.number_of_frames), 2)
		self.update_timer(current_time)

		super().callback_output(outdata, frames, time, status)

		self.update_progressbar(round(self.number_of_frames / 1000))
		current_time = round(((1.0 / self.framerate) * self.number_of_frames), 2)
		self.update_timer(current_time)


class SDIThread(SDIOThread):
	max_frames = 0
	number_of_frames = 0
	framerate = 0
	filename = None

	input_queue = None
	input_stream = None
	input_file_handler = None

	def __init__(
		self,
		filename,
		duration,
		prog: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
	):
		SDIOThread.__init__(self, prog, label, control_group)

		self.max_duration = duration
		self.number_of_frames = 0
		self.filename = filename
		self.input_queue = queue.Queue()
		self.framerate = AudioManager.input_frame_rate

		self.max_frames = duration * self.framerate

	def kill(self):
		"""
		Stops the audio recording process and releases resources.

		Closes the input audio stream if active, clears the input queue,
		and closes all open input file handlers.
		"""
		logger.info("killing rec")
		if self.input_stream is not None and self.input_stream.active:
			self.input_stream.close(True)
		self.input_queue.empty()
		for f in self.input_file_handler:
			f.close()
		logger.info("rec killed ok")

	def callback_input(self, indata, frames, time, status):
		"""
		Audio input callback function called for each audio block.

		Args:
			indata: The recorded audio data for the current block.
			frames (int): Number of frames in the current block.
			time: Timing information for the audio block.
			status: Status of the audio stream.

		Adds the copied input data to the input queue and updates the frame count.
		"""
		self.number_of_frames += frames
		self.input_queue.put(indata.copy())

	def run(self):
		"""
		Main thread method for recording audio.

		Sets up the recording UI elements and opens sound files for writing.
		Initializes the audio input stream and writes incoming audio data to files
		until the maximum recording duration is reached or stopped.

		Handles progress bar updates, timer updates, and changes UI control colors
		during recording. Properly closes files and signals thread completion.
		"""
		logger.info("debut thread rec")
		UITools.set_audio_progressbar(self.prog)
		self.update_progressbar(0, int(self.max_duration * 1000))
		UITools.set_control_group(self.control_group)
		# Moved the group color changed to the file writing loop to be sure it does not become red before actually
		# recording
		# UITools.change_control_group_color(True)
		UITools.set_timer_label(self.label)

		try:
			if not isinstance(self.filename, list):
				filenames = [self.filename]
			else:
				filenames = self.filename

			self.input_file_handler = []
			for f in filenames:
				tmp = sf.SoundFile(
					f,
					format="WAV",
					mode="w",
					samplerate=self.framerate,
					channels=1,
					subtype="PCM_16",
				)
				self.input_file_handler.append(tmp)
			self.input_stream = sd.InputStream(
				samplerate=AudioManager.input_frame_rate,
				channels=1,
				device=AudioManager.input_device_id,
				callback=self.callback_input,
			)
			logger.info(
				"Enregistrement dans le fichier "
				+ filenames[0]
				+ " (device: "
				+ AudioManager.input_device_to_str()
				+ ")",
			)

			already_got_data = False
			with self.input_stream:
				while True and self.number_of_frames <= self.max_frames:
					tmp = (self.number_of_frames / self.framerate) * 1000
					self.update_progressbar(round(tmp))
					current_time = round(
						((1.0 / self.framerate) * self.number_of_frames), 2
					)
					self.update_timer(current_time)
					data = self.input_queue.get()
					if not already_got_data:
						UITools.change_control_group_color(True)
						already_got_data = True
					for file in self.input_file_handler:
						file.write(data)

			self.update_progressbar(1, 1)
		except Exception as e:
			UITools.error_log(e, "Erreur dans l'enregistrement")
		for file in self.input_file_handler:
			file.close()
		self.is_thread_finished = True
		UITools.change_control_group_color(False)
		self.broadcast_ended()
		logger.info("fin thread rec")


class AudioManager(object):
	"""
	Managing audio input/output
	"""

	valid_input_device = False
	valid_out_device = False

	input_device_list = []
	output_device_list = []

	input_device_id = None
	output_device_id = None

	input_frame_rate = 22050

	buffersize = 256
	blocksize = 256

	SDIOthread = None
	threadList = []

	@staticmethod
	def init():
		"""
		Initialize audio device settings for the AudioManager.

		- Retrieves the system's default input and output device IDs.
		- Queries all available audio devices.
		- Populates AudioManager's input and output device lists with devices
		that support input and output channels respectively.
		- Calls AudioManager.set_devices() to apply the selected devices.
		"""
		AudioManager.input_device_id, AudioManager.output_device_id = sd.default.device

		sd.query_devices()
		tmp = sd.query_devices()
		id = 0
		for t in tmp:
			if t["max_input_channels"] > 0:
				AudioManager.input_device_list.append((id, t))
			if t["max_output_channels"] > 0:
				AudioManager.output_device_list.append((id, t))
			id += 1
		AudioManager.set_devices()

	@staticmethod
	def get_input_list() -> list:
		"""
		Returns the list of available audio input devices.

		Each device is represented as a tuple (device_id, device_info).
		"""
		return AudioManager.input_device_list

	@staticmethod
	def get_output_list() -> list:
		"""
		Returns the list of available audio output devices.

		Each device is represented as a tuple (device_id, device_info).
		"""
		return AudioManager.output_device_list

	@staticmethod
	def change_input_device(device_name: str):
		"""
		Change the current audio input device based on the device name string.

		The device_name is expected to start with the device ID followed by " - ".
		If the device ID cannot be parsed or the device is invalid, the input device
		is set to None. Finally, AudioManager.set_devices() is called to apply changes.

		Args:
			device_name (str): The device name string formatted as "<device_id> - <device_info>"
		"""
		try:
			tmp = device_name.split(" - ")
			device_id = int(tmp[0])
			tmp = sd.query_devices(device_id)
			AudioManager.input_device_id = device_id
		except ValueError:
			AudioManager.input_device_id = None
		AudioManager.set_devices()

	@staticmethod
	def change_output_device(device_name):
		"""
		Change the current audio output device based on the device name string.

		The device_name is expected to start with the device ID followed by " - ".
		If the device ID cannot be parsed or the device is invalid, the output device
		is set to None. Finally, AudioManager.set_devices() is called to apply changes.

		Args:
			device_name (str): The device name string formatted as "<device_id> - <device_info>"
		"""
		try:
			tmp = device_name.split(" - ")
			device_id = int(tmp[0])
			tmp = sd.query_devices(device_id)
			AudioManager.output_device_id = device_id
		except ValueError:
			AudioManager.output_device_id = None
		AudioManager.set_devices()

	@staticmethod
	def input_device_to_str() -> str:
		"""
		Return a string describing the current audio input device.

		Includes device name, current frame rate, and default sample rate.

		Returns:
			str: Description of the current input device or
				"Could not find" if device not found.
		"""
		for tup in AudioManager.input_device_list:
			if tup[0] == AudioManager.input_device_id:
				return (
					tup[1]["name"]
					+ " | frequence: "
					+ str(AudioManager.input_frame_rate)
					+ " (defaut : "
					+ str(tup[1]["default_samplerate"])
					+ ")"
				)
		return "Could not find"

	@staticmethod
	def output_device_to_str() -> str:
		"""
		Return a string describing the current audio output device.

		Returns:
			str: Name of the current output device or "Could not find" if not found.
		"""
		for tup in AudioManager.output_device_list:
			if tup[0] == AudioManager.output_device_id:
				return tup[1]["name"]
		return "Could not find"

	@staticmethod
	def set_devices():
		"""
		Apply the currently selected input and output audio devices as defaults.

		Updates the PyAudio/PortAudio default devices and validates their capabilities.
		Sets flags indicating whether the input and output devices are valid.
		"""
		AudioManager.valid_input_device = False
		AudioManager.valid_output_device = False
		sd.default.device = AudioManager.input_device_id, AudioManager.output_device_id
		try:
			AudioManager.calc_input_capabilities()
		except sd.PortAudioError:
			return
		AudioManager.valid_input_device = True
		AudioManager.valid_output_device = True

	@staticmethod
	def calc_input_capabilities():
		"""
		Check and set the optimal input device sampling rate from available options.

		Attempts to use the default samplerate of the input device or
		specified forced sample rates if enabled. Sets AudioManager.input_frame_rate
		to the first valid sample rate found.

		Logs errors if any sample rate check fails.
		"""
		input = sd.query_devices(AudioManager.input_device_id)
		wanted_fr = [int(input["default_samplerate"])]
		if Options.is_enabled(Options.Option.FORCE_SAMPLERATE):
			wanted_fr = [22050, 44100, input["default_samplerate"]]
		for fr in wanted_fr:
			try:
				sd.check_input_settings(AudioManager.input_device_id, samplerate=fr)
				AudioManager.input_frame_rate = fr
				break
			except sd.PortAudioError as e:
				UITools.error_log(e)
				pass
		logger.info(
			"Microphone regle sur une fe de " + str(AudioManager.input_frame_rate)
		)

	@staticmethod
	def __read_wav_to_numpy__(filename: str) -> (numpy.array, int):
		"""
		Read a WAV file into a NumPy array with padding to a multiple of blocksize.

		Args:
			filename (str): Path to the WAV file.

		Returns:
			Tuple[numpy.array, int]: A tuple with the audio data as a 2D float32 numpy array
									and the sampling rate of the file.
		"""
		with sf.SoundFile(filename, "r+") as f:
			frames = f.frames
			f.close()
		nb = frames // AudioManager.blocksize
		theoric = (nb + 1) * AudioManager.blocksize
		# print(frames, nb, theoric)
		return sf.read(
			filename, frames=theoric, fill_value=0, dtype="float32", always_2d=True
		)

	@staticmethod
	def __write_numpy_to_wav__(filename, samplerate, data):
		"""
		Write a NumPy array as a WAV file.

		Args:
			filename (str): Path where to save the WAV file.
			samplerate (int): Sampling rate of the audio.
			data (numpy.array): Audio data array to write.
		"""
		sf.write(filename, samplerate=samplerate, data=data)

	@staticmethod
	def close():
		"""
		Stop all audio threads and perform cleanup.
		"""
		AudioManager.stop()

	@staticmethod
	def stop():
		"""
		Stop all running audio threads managed by AudioManager.

		Clears thread list and resets UI control group colors.
		Logs any errors encountered.
		"""
		try:
			for thread in AudioManager.threadList:
				if thread.isRunning():
					thread.kill()
			AudioManager.threadList.clear()
			UITools.change_control_group_color(False)
		except Exception as e:
			UITools.error_log(e)

	@staticmethod
	def play_accentuated_wave(
		module_directory: str,
		accent: str,
		filename: str,
		progress_bar: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
	):
		"""
		Play an audio wave file for a given accent module and filename.

		Tries to find the audio file in the directory for the specific accent,
		falling back to the base module directory if not found.

		Raises:
			ValueError: If the audio file cannot be found for the given accent or default.

		Args:
			module_directory (str): Base directory for module audio files.
			accent (str): Accent subdirectory to search.
			filename (str): Filename of the wave file to play.
			progress_bar (QProgressBar, optional): Progress bar to update during playback.
			label (QLabel, optional): Label to update timer display.
			control_group (QGroupBox, optional): Control group UI component to indicate playback state.
		"""
		target = module_directory + accent + "/" + filename
		if not os.path.isfile(target):
			target = module_directory + filename
		if not os.path.isfile(target):
			raise ValueError(
				"Impossible de trouver un fichier "
				+ filename
				+ " du module "
				+ module_directory
				+ " pour "
				"l'accent " + accent + ", ou par défaut"
			)

		AudioManager.play_wave(target, progress_bar, label, control_group)

	@staticmethod
	def play_wave(
		filename: str,
		prog: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
	):
		"""
		Play a wave file asynchronously using a dedicated playback thread.

		Args:
			filename (str): Path to the WAV file to play.
			prog (QProgressBar, optional): Progress bar widget to update playback progress.
			label (QLabel, optional): Label widget to display timing info.
			control_group (QGroupBox, optional): Control group UI element to indicate playback state.
		"""
		tmp = SDOWaveThread(
			AudioManager.buffersize, AudioManager.blocksize, prog, label, control_group
		)
		tmp.set_wave_data(filename)
		AudioManager.threadList.append(tmp)
		AudioManager.start_next_thread()

	@staticmethod
	def play_wav_from_data(
		wav_data: np.ndarray,
		sampling_frequency: int,
		blocksize: typing.Optional[int] = None,
		callback: typing.Optional[typing.Callable[[], None]] = None,
		end_callback: typing.Optional[typing.Callable[[], None]] = None,
	):
		"""
		Play audio from raw numpy array data asynchronously.

		Args:
			wav_data (np.ndarray): Audio data array to play.
			sampling_frequency (int): Sample rate of the audio data.
			blocksize (int, optional): Playback block size. Defaults to sampling_frequency if not set.
			callback (Callable, optional): Function to call with playback progress updates.
			end_callback (Callable, optional): Function to call when playback ends.
		"""
		if blocksize is None:
			blocksize = sampling_frequency

		sound_thread = SDOSoundThread(len(wav_data), blocksize)

		sound_thread.set_data(wav_data.tolist(), sampling_frequency)

		if callback is not None:
			sound_thread.set_progress_callback(callback)
		if end_callback is not None:
			sound_thread.set_end_callback(end_callback)

		AudioManager.threadList.append(sound_thread)
		AudioManager.start_next_thread()

	@staticmethod
	def record_wave(
		filenames: list,
		duration: float,
		prog: QProgressBar = None,
		label: QLabel = None,
		control_group: QGroupBox = None,
		simultaneous_playback_file: str = None,
	):
		"""
		Start recording audio to one or more files asynchronously.

		Args:
			filenames (list): List of filenames to save the recorded audio.
			duration (float): Maximum duration of the recording in seconds.
			prog (QProgressBar, optional): Progress bar widget to update recording progress.
			label (QLabel, optional): Label widget to display timing info.
			control_group (QGroupBox, optional): Control group UI element to indicate recording state.
			simultaneous_playback_file (str, optional): File to play simultaneously during recording (not implemented).
		"""
		tmp = SDIThread(filenames, duration, prog, label, control_group)
		AudioManager.threadList.append(tmp)
		AudioManager.start_next_thread()

	@staticmethod
	def start_next_thread():
		"""
		Start the next unfinished audio thread in the queue.

		Sets the audio progress bar UI element to the thread's progress bar before starting.

		Logs errors if starting any thread fails.
		"""
		logger.info("thread suivant")
		try:
			for thread in AudioManager.threadList:
				if thread.is_thread_finished is False:
					UITools.set_audio_progressbar(thread.prog)
					thread.start()
					return
		except Exception as e:
			UITools.error_log(e)

	@staticmethod
	def mix_frames(frames):
		"""
		Deprecated: Placeholder method for mixing audio frames.

		Args:
			frames: Audio frames input.

		Returns:
			None
		"""
		# deprecated
		return None

	@staticmethod
	def demux_frames(frames, channels=1):
		"""
		Deprecated: Placeholder method for demultiplexing audio frames.

		Args:
			frames: Audio frames input.
			channels (int): Number of audio channels.

		Returns:
			None
		"""
		# deprecated
		return None
