drop PyAudio support
I do not recall anyone reporting that it worked better than the Qt implementation for them, and the lack of recent wheels on PyPI is a pain. We can always add it back in the future if enough people come out of the woodwork to report they were using it.
This commit is contained in:
parent
caa76c8b96
commit
52642d693b
@ -107,13 +107,6 @@
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QComboBox" name="recording_driver">
|
|
||||||
<property name="currentText">
|
|
||||||
<string notr="true"/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="search_text_label">
|
<widget class="QLabel" name="search_text_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -616,7 +609,6 @@
|
|||||||
<tabstop>paste_strips_formatting</tabstop>
|
<tabstop>paste_strips_formatting</tabstop>
|
||||||
<tabstop>nightMode</tabstop>
|
<tabstop>nightMode</tabstop>
|
||||||
<tabstop>useCurrent</tabstop>
|
<tabstop>useCurrent</tabstop>
|
||||||
<tabstop>recording_driver</tabstop>
|
|
||||||
<tabstop>default_search_text</tabstop>
|
<tabstop>default_search_text</tabstop>
|
||||||
<tabstop>uiScale</tabstop>
|
<tabstop>uiScale</tabstop>
|
||||||
<tabstop>showEstimates</tabstop>
|
<tabstop>showEstimates</tabstop>
|
||||||
|
@ -33,7 +33,6 @@ except:
|
|||||||
import PyQt5.QtSvg # type: ignore
|
import PyQt5.QtSvg # type: ignore
|
||||||
import PyQt5.QtMultimedia # type: ignore
|
import PyQt5.QtMultimedia # type: ignore
|
||||||
import socks
|
import socks
|
||||||
import pyaudio
|
|
||||||
|
|
||||||
# legacy compat
|
# legacy compat
|
||||||
import anki.storage
|
import anki.storage
|
||||||
|
@ -9,7 +9,7 @@ from anki.collection import OpChanges
|
|||||||
from anki.consts import newCardSchedulingLabels
|
from anki.consts import newCardSchedulingLabels
|
||||||
from aqt import AnkiQt
|
from aqt import AnkiQt
|
||||||
from aqt.operations.collection import set_preferences
|
from aqt.operations.collection import set_preferences
|
||||||
from aqt.profiles import RecordingDriver, VideoDriver
|
from aqt.profiles import VideoDriver
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import HelpPage, disable_help_button, openHelp, showInfo, showWarning, tr
|
from aqt.utils import HelpPage, disable_help_button, openHelp, showInfo, showWarning, tr
|
||||||
|
|
||||||
@ -134,48 +134,13 @@ class Preferences(QDialog):
|
|||||||
|
|
||||||
def setup_profile(self) -> None:
|
def setup_profile(self) -> None:
|
||||||
"Setup options stored in the user profile."
|
"Setup options stored in the user profile."
|
||||||
self.setup_recording_driver()
|
|
||||||
self.setup_network()
|
self.setup_network()
|
||||||
self.setup_backup()
|
self.setup_backup()
|
||||||
|
|
||||||
def update_profile(self) -> None:
|
def update_profile(self) -> None:
|
||||||
self.update_recording_driver()
|
|
||||||
self.update_network()
|
self.update_network()
|
||||||
self.update_backup()
|
self.update_backup()
|
||||||
|
|
||||||
# Profile: recording driver
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
def setup_recording_driver(self) -> None:
|
|
||||||
self._recording_drivers = [
|
|
||||||
RecordingDriver.QtAudioInput,
|
|
||||||
RecordingDriver.PyAudio,
|
|
||||||
]
|
|
||||||
# The plan is to phase out PyAudio soon, so will hold off on
|
|
||||||
# making this string translatable for now.
|
|
||||||
self.form.recording_driver.addItems(
|
|
||||||
[
|
|
||||||
f"Voice recording driver: {driver.value}"
|
|
||||||
for driver in self._recording_drivers
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.form.recording_driver.setCurrentIndex(
|
|
||||||
self._recording_drivers.index(self.mw.pm.recording_driver())
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_recording_driver(self) -> None:
|
|
||||||
new_audio_driver = self._recording_drivers[
|
|
||||||
self.form.recording_driver.currentIndex()
|
|
||||||
]
|
|
||||||
if self.mw.pm.recording_driver() != new_audio_driver:
|
|
||||||
self.mw.pm.set_recording_driver(new_audio_driver)
|
|
||||||
if new_audio_driver == RecordingDriver.PyAudio:
|
|
||||||
showInfo(
|
|
||||||
"""\
|
|
||||||
The PyAudio driver will likely be removed in a future update. If you find it works better \
|
|
||||||
for you than the default driver, please let us know on the Anki forums."""
|
|
||||||
)
|
|
||||||
|
|
||||||
# Profile: network
|
# Profile: network
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
@ -31,11 +31,6 @@ from aqt.utils import disable_help_button, showWarning, tr
|
|||||||
# - Saves in sqlite rather than a flat file so the config can't be corrupted
|
# - Saves in sqlite rather than a flat file so the config can't be corrupted
|
||||||
|
|
||||||
|
|
||||||
class RecordingDriver(Enum):
|
|
||||||
PyAudio = "PyAudio"
|
|
||||||
QtAudioInput = "Qt"
|
|
||||||
|
|
||||||
|
|
||||||
class VideoDriver(Enum):
|
class VideoDriver(Enum):
|
||||||
OpenGL = "auto"
|
OpenGL = "auto"
|
||||||
ANGLE = "angle"
|
ANGLE = "angle"
|
||||||
@ -556,18 +551,6 @@ create table if not exists profiles
|
|||||||
def set_auto_sync_media_minutes(self, val: int) -> None:
|
def set_auto_sync_media_minutes(self, val: int) -> None:
|
||||||
self.profile["autoSyncMediaMinutes"] = val
|
self.profile["autoSyncMediaMinutes"] = val
|
||||||
|
|
||||||
def recording_driver(self) -> RecordingDriver:
|
|
||||||
if driver := self.profile.get("recordingDriver"):
|
|
||||||
try:
|
|
||||||
return RecordingDriver(driver)
|
|
||||||
except ValueError:
|
|
||||||
# revert to default
|
|
||||||
pass
|
|
||||||
return RecordingDriver.QtAudioInput
|
|
||||||
|
|
||||||
def set_recording_driver(self, driver: RecordingDriver) -> None:
|
|
||||||
self.profile["recordingDriver"] = driver.value
|
|
||||||
|
|
||||||
def show_browser_table_tooltips(self) -> bool:
|
def show_browser_table_tooltips(self) -> bool:
|
||||||
return self.profile.get("browserTableTooltips", True)
|
return self.profile.get("browserTableTooltips", True)
|
||||||
|
|
||||||
|
@ -96,9 +96,3 @@ class QtAudioInputRecorder(Recorder):
|
|||||||
t.timeout.connect(on_stop_timer) # type: ignore
|
t.timeout.connect(on_stop_timer) # type: ignore
|
||||||
t.setSingleShot(True)
|
t.setSingleShot(True)
|
||||||
t.start(500)
|
t.start(500)
|
||||||
|
|
||||||
|
|
||||||
def prompt_for_mic_permission() -> None:
|
|
||||||
from PyQt5.QtMultimedia import QAudioDeviceInfo
|
|
||||||
|
|
||||||
QAudioDeviceInfo.defaultInputDevice()
|
|
||||||
|
100
qt/aqt/sound.py
100
qt/aqt/sound.py
@ -9,7 +9,6 @@ import platform
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import wave
|
import wave
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@ -23,11 +22,9 @@ import aqt
|
|||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.sound import AV_REF_RE, AVTag, SoundOrVideoTag
|
from anki.sound import AV_REF_RE, AVTag, SoundOrVideoTag
|
||||||
from anki.types import assert_exhaustive
|
|
||||||
from anki.utils import isLin, isMac, isWin, namedtmp
|
from anki.utils import isLin, isMac, isWin, namedtmp
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.mpv import MPV, MPVBase, MPVCommandError
|
from aqt.mpv import MPV, MPVBase, MPVCommandError
|
||||||
from aqt.profiles import RecordingDriver
|
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.taskman import TaskManager
|
from aqt.taskman import TaskManager
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
@ -546,16 +543,6 @@ class Recorder(ABC):
|
|||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
def prompt_for_mic_permission() -> None:
|
|
||||||
if qtmajor == 5 and isMac:
|
|
||||||
from .qt5 import prompt_for_mic_permission
|
|
||||||
|
|
||||||
prompt_for_mic_permission()
|
|
||||||
else:
|
|
||||||
# no longer seems to be required, perhaps due to newer macOS sdk?
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class QtAudioInputRecorder(Recorder):
|
class QtAudioInputRecorder(Recorder):
|
||||||
def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None:
|
def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None:
|
||||||
super().__init__(output_path)
|
super().__init__(output_path)
|
||||||
@ -625,87 +612,6 @@ class QtAudioInputRecorder(Recorder):
|
|||||||
t.start(500)
|
t.start(500)
|
||||||
|
|
||||||
|
|
||||||
# PyAudio recording
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pyaudio
|
|
||||||
except:
|
|
||||||
pyaudio = None
|
|
||||||
|
|
||||||
|
|
||||||
PYAU_CHANNELS = 1
|
|
||||||
PYAU_INPUT_INDEX: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class PyAudioThreadedRecorder(threading.Thread):
|
|
||||||
def __init__(self, output_path: str, startup_delay: float) -> None:
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self._output_path = output_path
|
|
||||||
self._startup_delay = startup_delay
|
|
||||||
self.finish = False
|
|
||||||
# though we're using pyaudio here, we rely on Qt to trigger
|
|
||||||
# the permission prompt
|
|
||||||
prompt_for_mic_permission()
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
chunk = 1024
|
|
||||||
p = pyaudio.PyAudio()
|
|
||||||
|
|
||||||
rate = int(p.get_default_input_device_info()["defaultSampleRate"])
|
|
||||||
PYAU_FORMAT = pyaudio.paInt16
|
|
||||||
|
|
||||||
stream = p.open(
|
|
||||||
format=PYAU_FORMAT,
|
|
||||||
channels=PYAU_CHANNELS,
|
|
||||||
rate=rate,
|
|
||||||
input=True,
|
|
||||||
input_device_index=PYAU_INPUT_INDEX,
|
|
||||||
frames_per_buffer=chunk,
|
|
||||||
)
|
|
||||||
|
|
||||||
# swallow the first 300ms to allow audio device to quiesce
|
|
||||||
wait = int(rate * self._startup_delay)
|
|
||||||
stream.read(wait, exception_on_overflow=False)
|
|
||||||
|
|
||||||
# read data in a loop until self.finish is set
|
|
||||||
data = b""
|
|
||||||
while not self.finish:
|
|
||||||
data += stream.read(chunk, exception_on_overflow=False)
|
|
||||||
|
|
||||||
# write out the wave file
|
|
||||||
stream.close()
|
|
||||||
p.terminate()
|
|
||||||
wf = wave.open(self._output_path, "wb")
|
|
||||||
wf.setnchannels(PYAU_CHANNELS)
|
|
||||||
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
|
|
||||||
wf.setframerate(rate)
|
|
||||||
wf.writeframes(data)
|
|
||||||
wf.close()
|
|
||||||
|
|
||||||
|
|
||||||
class PyAudioRecorder(Recorder):
|
|
||||||
def __init__(self, mw: aqt.AnkiQt, output_path: str) -> None:
|
|
||||||
super().__init__(output_path)
|
|
||||||
self.mw = mw
|
|
||||||
|
|
||||||
def start(self, on_done: Callable[[], None]) -> None:
|
|
||||||
self.thread = PyAudioThreadedRecorder(self.output_path, self.STARTUP_DELAY)
|
|
||||||
self.thread.start()
|
|
||||||
super().start(on_done)
|
|
||||||
|
|
||||||
def stop(self, on_done: Callable[[str], None]) -> None:
|
|
||||||
# ensure at least a second captured
|
|
||||||
while self.duration() < 1:
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
def func(fut: Future) -> None:
|
|
||||||
Recorder.stop(self, on_done)
|
|
||||||
|
|
||||||
self.thread.finish = True
|
|
||||||
self.mw.taskman.run_in_background(self.thread.join, func)
|
|
||||||
|
|
||||||
|
|
||||||
# Recording dialog
|
# Recording dialog
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
@ -760,10 +666,6 @@ class RecordDialog(QDialog):
|
|||||||
saveGeom(self, "audioRecorder2")
|
saveGeom(self, "audioRecorder2")
|
||||||
|
|
||||||
def _start_recording(self) -> None:
|
def _start_recording(self) -> None:
|
||||||
driver = self.mw.pm.recording_driver()
|
|
||||||
if driver is RecordingDriver.PyAudio:
|
|
||||||
self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav"))
|
|
||||||
elif driver is RecordingDriver.QtAudioInput:
|
|
||||||
if qtmajor > 5:
|
if qtmajor > 5:
|
||||||
self._recorder = QtAudioInputRecorder(
|
self._recorder = QtAudioInputRecorder(
|
||||||
namedtmp("rec.wav"), self.mw, self._parent
|
namedtmp("rec.wav"), self.mw, self._parent
|
||||||
@ -772,8 +674,6 @@ class RecordDialog(QDialog):
|
|||||||
from .qt5 import QtAudioInputRecorder as Qt5Recorder
|
from .qt5 import QtAudioInputRecorder as Qt5Recorder
|
||||||
|
|
||||||
self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent)
|
self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent)
|
||||||
else:
|
|
||||||
assert_exhaustive(driver)
|
|
||||||
self._recorder.start(self._start_timer)
|
self._recorder.start(self._start_timer)
|
||||||
|
|
||||||
def _start_timer(self) -> None:
|
def _start_timer(self) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user