Repository: livekit/agents
File: livekit-agents/livekit/agents/cli/cli.py (Python)

Code before:

            self._io_loop.call_soon_threadsafe(self._io_audio_input.push_frame, frame)

    def _sd_output_callback(self, outdata: np.ndarray, frames: int, time: Any, *_: Any) -> None:
        if not self.io_acquired:
            outdata[:] = 0
            return

        self._output_delay = time.outputBufferDacTime - time.currentTime

        FRAME_SAMPLES = 240
        with self._io_audio_output.audio_lock:
            if self._io_audio_output.paused:
                outdata[:] = 0
            else:
                bytes_needed = frames * 2
                if len(self._io_audio_output.audio_buffer) < bytes_needed:
                    available_bytes = len(self._io_audio_output.audio_buffer)
                    outdata[: available_bytes // 2, 0] = np.frombuffer(
                        self._io_audio_output.audio_buffer,
                        dtype=np.int16,
                        count=available_bytes // 2,
                    )
                    outdata[available_bytes // 2 :, 0] = 0
                    del self._io_audio_output.audio_buffer[:available_bytes]  # TODO: optimize
                    self._io_audio_output.mark_output_empty()
                else:
                    chunk = self._io_audio_output.audio_buffer[:bytes_needed]
                    outdata[:, 0] = np.frombuffer(chunk, dtype=np.int16, count=frames)
                    del self._io_audio_output.audio_buffer[:bytes_needed]

        num_chunks = frames // FRAME_SAMPLES
        for i in range(num_chunks):
            start = i * FRAME_SAMPLES
            end = start + FRAME_SAMPLES
            render_chunk = outdata[start:end, 0]
            render_frame_for_aec = rtc.AudioFrame(
                data=render_chunk.tobytes(),
                samples_per_channel=FRAME_SAMPLES,
                sample_rate=24000,
                num_channels=1,
            )
            self._apm.process_reverse_stream(render_frame_for_aec)


class FrequencyVisualizer:
    def __init__(self, agents_console: AgentsConsole, *, label: str = "Unlabeled microphone"):
        self.label = label
        self.height_chars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
        self.c = agents_console


Review comment (suggestion):
The sounddevice callback thread might cause issues with asyncio events, we should use `self._loop.call_soon_threadsafe(self._output_buf_empty.set)` in `mark_output_empty` instead. [Reference](https://python-sounddevice.readthedocs.io/en/0.4.6/examples.html#using-a-stream-in-an-asyncio-coroutine)

Code after:

            self._io_loop.call_soon_threadsafe(self._io_audio_input.push_frame, frame)

    def _sd_output_callback(self, outdata: np.ndarray, frames: int, time: Any, *_: Any) -> None:
        if not self.io_acquired:
            outdata[:] = 0
            return

        self._output_delay = time.outputBufferDacTime - time.currentTime

        FRAME_SAMPLES = 240
        with self._io_audio_output.audio_lock:
            if self._io_audio_output.paused:
                outdata[:] = 0
            else:
                bytes_needed = frames * 2
                if len(self._io_audio_output.audio_buffer) < bytes_needed:
                    available_bytes = len(self._io_audio_output.audio_buffer)
                    outdata[: available_bytes // 2, 0] = np.frombuffer(
                        self._io_audio_output.audio_buffer,
                        dtype=np.int16,
                        count=available_bytes // 2,
                    )
                    outdata[available_bytes // 2 :, 0] = 0
                    del self._io_audio_output.audio_buffer[:available_bytes]  # TODO: optimize
                    self.io_loop.call_soon_threadsafe(self._io_audio_output.mark_output_empty)
                else:
                    chunk = self._io_audio_output.audio_buffer[:bytes_needed]
                    outdata[:, 0] = np.frombuffer(chunk, dtype=np.int16, count=frames)
                    del self._io_audio_output.audio_buffer[:bytes_needed]

        num_chunks = frames // FRAME_SAMPLES
        for i in range(num_chunks):
            start = i * FRAME_SAMPLES
            end = start + FRAME_SAMPLES
            render_chunk = outdata[start:end, 0]
            render_frame_for_aec = rtc.AudioFrame(
                data=render_chunk.tobytes(),
                samples_per_channel=FRAME_SAMPLES,
                sample_rate=24000,
                num_channels=1,
            )
            self._apm.process_reverse_stream(render_frame_for_aec)


class FrequencyVisualizer:
    def __init__(self, agents_console: AgentsConsole, *, label: str = "Unlabeled microphone"):
        self.label = label
        self.height_chars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
        self.c = agents_console