From dc5ce3b9a25c339e4808804e564a1f74dd7ba624 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 18 Dec 2020 18:59:10 +1000 Subject: [PATCH] experiment with lower-level QAudioInput Allows us to discard the start of the recording like PyAudio, instead of just muting it. --- qt/aqt/profiles.py | 3 +- qt/aqt/sound.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 9d2340f07..a6e7b7a89 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -35,6 +35,7 @@ from aqt.utils import TR, locale_dir, showWarning, tr class RecordingDriver(Enum): QtRecorder = "qtrecorder" PyAudio = "pyaudio" + QtAudioInput = "qtaudioinput" metaConf = dict( @@ -641,7 +642,7 @@ create table if not exists profiles def recording_driver(self) -> RecordingDriver: if driver := self.profile.get("recordingDriver"): return driver - return RecordingDriver.QtRecorder + return RecordingDriver.QtAudioInput def set_recording_driver(self, driver: RecordingDriver): self.profile["recordingDriver"] = driver.value() diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index b6eccf92e..0ba978d27 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -558,6 +558,79 @@ class QtRecorder(Recorder): self._recorder.setMuted(False) +# QAudioInput recording +########################################################################## + + +class QtAudioInputRecorder(Recorder): + def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget): + super().__init__(output_path) + + self.mw = mw + + from PyQt5.QtMultimedia import ( + QAudio, + QAudioDeviceInfo, + QAudioFormat, + QAudioInput, + ) + + format = QAudioFormat() + format.setChannelCount(1) + format.setSampleRate(44100) + format.setSampleSize(16) + format.setCodec("audio/pcm") + format.setByteOrder(QAudioFormat.LittleEndian) + format.setSampleType(QAudioFormat.SignedInt) + + device = QAudioDeviceInfo.defaultInputDevice() + if not device.isFormatSupported(format): + format = device.nearestFormat(format) + print("format changed") + print("channels", format.channelCount()) + print("rate", format.sampleRate()) + print("size", format.sampleSize()) + self._format = format + + self._audio_input = QAudioInput(device, format, parent) + + def start(self, on_done: Callable[[], None]) -> None: + self._iodevice = self._audio_input.start() + self._buffer = b"" + self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore + super().start(on_done) + + def _on_read_ready(self): + self._buffer += self._iodevice.readAll() + + def stop(self, on_done: Callable[[str], None]): + self._audio_input.stop() + + if err := self._audio_input.error(): + showWarning(f"recording failed: {err}") + return + + # read anything remaining in buffer & close + self._on_read_ready() + self._iodevice.close() + + # swallow the first 300ms to allow audio device to quiesce + wait = int(44100 * self.STARTUP_DELAY) + if len(self._buffer) <= wait: + return + self._buffer = self._buffer[wait:] + + # write out the wave file + wf = wave.open(self.output_path, "wb") + wf.setnchannels(self._format.channelCount()) + wf.setsampwidth(self._format.sampleSize() // 8) + wf.setframerate(self._format.sampleRate()) + wf.writeframes(self._buffer) + wf.close() + + super().stop(on_done) + + # PyAudio recording ########################################################################## @@ -697,6 +770,10 @@ class RecordDialog(QDialog): self._recorder = QtRecorder(namedtmp("rec.wav"), self._parent) elif driver is RecordingDriver.PyAudio: self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav")) + elif driver is RecordingDriver.QtAudioInput: + self._recorder = QtAudioInputRecorder( + namedtmp("rec.wav"), self.mw, self._parent + ) else: assert_exhaustive(driver) self._recorder.start(self._start_timer)