2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
2019-12-20 10:19:03 +01:00
|
|
|
import atexit
|
2017-12-23 10:36:26 +01:00
|
|
|
import html
|
2019-12-20 10:19:03 +01:00
|
|
|
import os
|
|
|
|
import random
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import threading
|
|
|
|
import time
|
2019-12-21 07:54:40 +01:00
|
|
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
2019-12-19 04:02:45 +01:00
|
|
|
|
2018-04-30 09:12:26 +02:00
|
|
|
from anki.hooks import addHook, runHook
|
2019-03-04 02:22:40 +01:00
|
|
|
from anki.lang import _
|
2020-01-02 10:43:19 +01:00
|
|
|
from anki.sound import allSounds
|
2019-12-20 10:19:03 +01:00
|
|
|
from anki.utils import isLin, isMac, isWin, tmpdir
|
2020-01-02 10:43:19 +01:00
|
|
|
from aqt.mpv import MPV, MPVBase
|
|
|
|
from aqt.qt import *
|
|
|
|
from aqt.utils import restoreGeom, saveGeom, showWarning
|
|
|
|
|
|
|
|
|
|
|
|
def getAudio(parent, encode=True):
|
|
|
|
"Record and return filename"
|
|
|
|
# record first
|
|
|
|
if not Recorder:
|
|
|
|
showWarning("pyaudio not installed")
|
|
|
|
return
|
|
|
|
|
|
|
|
r = Recorder()
|
|
|
|
mb = QMessageBox(parent)
|
|
|
|
restoreGeom(mb, "audioRecorder")
|
|
|
|
mb.setWindowTitle("Anki")
|
|
|
|
mb.setIconPixmap(QPixmap(":/icons/media-record.png"))
|
|
|
|
but = QPushButton(_("Save"))
|
|
|
|
mb.addButton(but, QMessageBox.AcceptRole)
|
|
|
|
but.setDefault(True)
|
|
|
|
but = QPushButton(_("Cancel"))
|
|
|
|
mb.addButton(but, QMessageBox.RejectRole)
|
|
|
|
mb.setEscapeButton(but)
|
|
|
|
t = time.time()
|
|
|
|
r.start()
|
|
|
|
time.sleep(r.startupDelay)
|
|
|
|
QApplication.instance().processEvents()
|
|
|
|
while not mb.clickedButton():
|
|
|
|
txt = _("Recording...<br>Time: %0.1f")
|
|
|
|
mb.setText(txt % (time.time() - t))
|
|
|
|
mb.show()
|
|
|
|
QApplication.instance().processEvents()
|
|
|
|
if mb.clickedButton() == mb.escapeButton():
|
|
|
|
r.stop()
|
|
|
|
r.cleanup()
|
|
|
|
return
|
|
|
|
saveGeom(mb, "audioRecorder")
|
|
|
|
# ensure at least a second captured
|
|
|
|
while time.time() - t < 1:
|
|
|
|
time.sleep(0.1)
|
|
|
|
r.stop()
|
|
|
|
# process
|
|
|
|
r.postprocess(encode)
|
|
|
|
return r.file()
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Shared utils
|
|
|
|
##########################################################################
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def playFromText(text) -> None:
|
2017-12-04 03:45:22 +01:00
|
|
|
for match in allSounds(text):
|
2017-12-23 10:36:26 +01:00
|
|
|
# filename is html encoded
|
|
|
|
match = html.unescape(match)
|
2012-12-21 08:51:59 +01:00
|
|
|
play(match)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2016-07-26 04:15:43 +02:00
|
|
|
# Packaged commands
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
# return modified command array that points to bundled command, and return
|
|
|
|
# required environment
|
2019-12-19 04:02:45 +01:00
|
|
|
def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
|
2016-07-26 04:15:43 +02:00
|
|
|
cmd = cmd[:]
|
|
|
|
env = os.environ.copy()
|
2017-05-22 07:40:04 +02:00
|
|
|
if "LD_LIBRARY_PATH" in env:
|
2019-12-23 01:34:10 +01:00
|
|
|
del env["LD_LIBRARY_PATH"]
|
2016-07-26 04:15:43 +02:00
|
|
|
if isMac:
|
|
|
|
dir = os.path.dirname(os.path.abspath(__file__))
|
2017-01-09 03:52:52 +01:00
|
|
|
exeDir = os.path.abspath(dir + "/../../Resources/audio")
|
2016-07-26 04:15:43 +02:00
|
|
|
else:
|
|
|
|
exeDir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
|
|
if isWin and not cmd[0].endswith(".exe"):
|
|
|
|
cmd[0] += ".exe"
|
|
|
|
path = os.path.join(exeDir, cmd[0])
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return cmd, env
|
|
|
|
cmd[0] = path
|
|
|
|
return cmd, env
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
##########################################################################
|
|
|
|
|
2016-05-12 06:45:35 +02:00
|
|
|
processingSrc = "rec.wav"
|
|
|
|
processingDst = "rec.mp3"
|
2019-12-15 23:17:28 +01:00
|
|
|
processingChain: List[List[str]] = []
|
|
|
|
recFiles: List[str] = []
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
processingChain = [
|
2017-10-25 10:35:39 +02:00
|
|
|
["lame", processingSrc, processingDst, "--noreplaygain", "--quiet"],
|
2019-12-23 01:34:10 +01:00
|
|
|
]
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# don't show box on windows
|
2019-12-20 06:07:40 +01:00
|
|
|
si: Optional[Any]
|
2019-12-15 23:17:28 +01:00
|
|
|
if sys.platform == "win32":
|
2019-12-23 01:34:10 +01:00
|
|
|
si = subprocess.STARTUPINFO() # pytype: disable=module-attr
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
2019-12-23 01:34:10 +01:00
|
|
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
2019-03-04 02:22:40 +01:00
|
|
|
# pylint: disable=no-member
|
2012-12-21 08:51:59 +01:00
|
|
|
# python2.7+
|
2019-12-23 01:34:10 +01:00
|
|
|
si.dwFlags |= (
|
|
|
|
subprocess._subprocess.STARTF_USESHOWWINDOW
|
|
|
|
) # pytype: disable=module-attr
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
si = None
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def retryWait(proc) -> Any:
|
2012-12-21 08:51:59 +01:00
|
|
|
# osx throws interrupted system call errors frequently
|
|
|
|
while 1:
|
|
|
|
try:
|
|
|
|
return proc.wait()
|
|
|
|
except OSError:
|
|
|
|
continue
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-09-30 09:24:56 +02:00
|
|
|
# MPV
|
2012-12-21 08:51:59 +01:00
|
|
|
##########################################################################
|
|
|
|
|
2017-09-30 09:24:56 +02:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
_player: Optional[Callable[[Any], Any]]
|
|
|
|
_queueEraser: Optional[Callable[[], Any]]
|
2019-12-20 06:07:40 +01:00
|
|
|
mpvManager: Optional["MpvManager"] = None
|
2019-12-19 04:02:45 +01:00
|
|
|
|
2017-10-02 08:37:52 +02:00
|
|
|
mpvPath, mpvEnv = _packagedCmd(["mpv"])
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-09-30 09:24:56 +02:00
|
|
|
class MpvManager(MPV):
|
2017-10-02 08:37:52 +02:00
|
|
|
|
|
|
|
executable = mpvPath[0]
|
|
|
|
popenEnv = mpvEnv
|
|
|
|
|
2018-01-21 01:34:29 +01:00
|
|
|
if not isLin:
|
|
|
|
default_argv = MPVBase.default_argv + [
|
|
|
|
"--input-media-keys=no",
|
|
|
|
]
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def __init__(self) -> None:
|
2017-09-30 11:33:09 +02:00
|
|
|
super().__init__(window_id=None, debug=False)
|
2017-09-30 09:24:56 +02:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def queueFile(self, file) -> None:
|
2018-07-23 05:19:01 +02:00
|
|
|
runHook("mpvWillPlay", file)
|
2018-05-31 05:20:10 +02:00
|
|
|
|
2017-09-30 09:24:56 +02:00
|
|
|
path = os.path.join(os.getcwd(), file)
|
2017-11-10 10:52:20 +01:00
|
|
|
self.command("loadfile", path, "append-play")
|
2017-09-30 09:24:56 +02:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def clearQueue(self) -> None:
|
2017-09-30 09:24:56 +02:00
|
|
|
self.command("stop")
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def togglePause(self) -> None:
|
2017-09-30 09:24:56 +02:00
|
|
|
self.set_property("pause", not self.get_property("pause"))
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def seekRelative(self, secs) -> None:
|
2017-09-30 09:24:56 +02:00
|
|
|
self.command("seek", secs, "relative")
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def on_idle(self) -> None:
|
2018-04-30 09:12:26 +02:00
|
|
|
runHook("mpvIdleHook")
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def setMpvConfigBase(base) -> None:
|
2019-04-09 07:32:44 +02:00
|
|
|
mpvConfPath = os.path.join(base, "mpv.conf")
|
|
|
|
MpvManager.default_argv += [
|
|
|
|
"--no-config",
|
2019-12-23 01:34:10 +01:00
|
|
|
"--include=" + mpvConfPath,
|
2019-04-09 07:32:44 +02:00
|
|
|
]
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def setupMPV() -> None:
|
2017-09-30 09:24:56 +02:00
|
|
|
global mpvManager, _player, _queueEraser
|
|
|
|
mpvManager = MpvManager()
|
|
|
|
_player = mpvManager.queueFile
|
|
|
|
_queueEraser = mpvManager.clearQueue
|
2019-04-29 06:36:51 +02:00
|
|
|
atexit.register(cleanupMPV)
|
2017-09-30 09:24:56 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def cleanupMPV() -> None:
|
2017-09-30 09:24:56 +02:00
|
|
|
global mpvManager, _player, _queueEraser
|
|
|
|
if mpvManager:
|
|
|
|
mpvManager.close()
|
|
|
|
mpvManager = None
|
|
|
|
_player = None
|
|
|
|
_queueEraser = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Mplayer in slave mode
|
|
|
|
##########################################################################
|
|
|
|
|
2019-02-06 00:03:39 +01:00
|
|
|
# if anki crashes, an old mplayer instance may be left lying around,
|
|
|
|
# which prevents renaming or deleting the profile
|
2019-12-19 04:02:45 +01:00
|
|
|
def cleanupOldMplayerProcesses() -> None:
|
2019-03-04 02:22:40 +01:00
|
|
|
# pylint: disable=import-error
|
2019-12-23 01:34:10 +01:00
|
|
|
import psutil # pytype: disable=import-error
|
2019-02-06 00:03:39 +01:00
|
|
|
|
|
|
|
exeDir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
|
|
|
2019-02-27 02:51:28 +01:00
|
|
|
for proc in psutil.process_iter():
|
|
|
|
try:
|
2019-12-23 01:34:10 +01:00
|
|
|
info = proc.as_dict(attrs=["pid", "name", "exe"])
|
|
|
|
if not info["exe"] or info["name"] != "mplayer.exe":
|
2019-02-27 02:51:28 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
# not anki's bundled mplayer
|
2019-12-23 01:34:10 +01:00
|
|
|
if os.path.dirname(info["exe"]) != exeDir:
|
2019-02-27 02:51:28 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
print("terminating old mplayer process...")
|
|
|
|
proc.kill()
|
2019-04-08 07:56:06 +02:00
|
|
|
except:
|
|
|
|
print("error iterating mplayer processes")
|
2019-02-06 00:03:39 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2017-09-30 09:24:56 +02:00
|
|
|
mplayerCmd = ["mplayer", "-really-quiet", "-noautosub"]
|
2018-01-20 04:41:12 +01:00
|
|
|
if isWin:
|
|
|
|
mplayerCmd += ["-ao", "win32"]
|
2017-09-30 09:24:56 +02:00
|
|
|
|
2019-02-06 00:03:39 +01:00
|
|
|
cleanupOldMplayerProcesses()
|
|
|
|
|
2019-12-15 23:17:28 +01:00
|
|
|
mplayerQueue: List[str] = []
|
2012-12-21 08:51:59 +01:00
|
|
|
mplayerEvt = threading.Event()
|
|
|
|
mplayerClear = False
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class MplayerMonitor(threading.Thread):
|
|
|
|
|
2019-12-20 06:07:40 +01:00
|
|
|
mplayer: Optional[subprocess.Popen] = None
|
2019-12-16 08:54:04 +01:00
|
|
|
deadPlayers: List[subprocess.Popen] = []
|
|
|
|
|
2019-12-21 07:54:40 +01:00
|
|
|
def run(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global mplayerClear
|
|
|
|
self.mplayer = None
|
|
|
|
self.deadPlayers = []
|
|
|
|
while 1:
|
|
|
|
mplayerEvt.wait()
|
|
|
|
mplayerEvt.clear()
|
|
|
|
# clearing queue?
|
|
|
|
if mplayerClear and self.mplayer:
|
|
|
|
try:
|
2016-12-17 04:47:07 +01:00
|
|
|
self.mplayer.stdin.write(b"stop\n")
|
2016-07-12 08:55:10 +02:00
|
|
|
self.mplayer.stdin.flush()
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
|
|
|
# mplayer quit by user (likely video)
|
|
|
|
self.deadPlayers.append(self.mplayer)
|
|
|
|
self.mplayer = None
|
|
|
|
# loop through files to play
|
|
|
|
while mplayerQueue:
|
|
|
|
# ensure started
|
|
|
|
if not self.mplayer:
|
2019-12-16 08:54:04 +01:00
|
|
|
self.mplayer = self.startProcess()
|
2012-12-21 08:51:59 +01:00
|
|
|
# pop a file
|
|
|
|
try:
|
|
|
|
item = mplayerQueue.pop(0)
|
|
|
|
except IndexError:
|
|
|
|
# queue was cleared by main thread
|
|
|
|
continue
|
|
|
|
if mplayerClear:
|
|
|
|
mplayerClear = False
|
2016-07-12 08:55:10 +02:00
|
|
|
extra = b""
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2016-07-12 08:55:10 +02:00
|
|
|
extra = b" 1"
|
|
|
|
cmd = b'loadfile "%s"%s\n' % (item.encode("utf8"), extra)
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
|
|
|
self.mplayer.stdin.write(cmd)
|
2016-07-12 08:55:10 +02:00
|
|
|
self.mplayer.stdin.flush()
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
|
|
|
# mplayer has quit and needs restarting
|
|
|
|
self.deadPlayers.append(self.mplayer)
|
|
|
|
self.mplayer = None
|
2019-12-16 08:54:04 +01:00
|
|
|
self.mplayer = self.startProcess()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mplayer.stdin.write(cmd)
|
2016-07-12 08:55:10 +02:00
|
|
|
self.mplayer.stdin.flush()
|
2012-12-21 08:51:59 +01:00
|
|
|
# if we feed mplayer too fast it loses files
|
|
|
|
time.sleep(1)
|
|
|
|
# wait() on finished processes. we don't want to block on the
|
|
|
|
# wait, so we keep trying each time we're reactivated
|
|
|
|
def clean(pl):
|
|
|
|
if pl.poll() is not None:
|
|
|
|
pl.wait()
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.deadPlayers = [pl for pl in self.deadPlayers if clean(pl)]
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def kill(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.mplayer:
|
|
|
|
return
|
|
|
|
try:
|
2016-12-17 04:47:07 +01:00
|
|
|
self.mplayer.stdin.write(b"quit\n")
|
2016-07-12 08:55:10 +02:00
|
|
|
self.mplayer.stdin.flush()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.deadPlayers.append(self.mplayer)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
self.mplayer = None
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def startProcess(self) -> subprocess.Popen:
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
|
|
|
cmd = mplayerCmd + ["-slave", "-idle"]
|
2016-07-26 04:15:43 +02:00
|
|
|
cmd, env = _packagedCmd(cmd)
|
2019-12-16 08:54:04 +01:00
|
|
|
return subprocess.Popen(
|
2019-12-23 01:34:10 +01:00
|
|
|
cmd,
|
|
|
|
startupinfo=si,
|
|
|
|
stdin=subprocess.PIPE,
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
|
env=env,
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
except OSError:
|
|
|
|
mplayerEvt.clear()
|
|
|
|
raise Exception("Did you install mplayer?")
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-20 06:07:40 +01:00
|
|
|
mplayerManager: Optional[MplayerMonitor] = None
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def queueMplayer(path) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
ensureMplayerThreads()
|
|
|
|
if isWin and os.path.exists(path):
|
|
|
|
# mplayer on windows doesn't like the encoding, so we create a
|
|
|
|
# temporary file instead. oddly, foreign characters in the dirname
|
|
|
|
# don't seem to matter.
|
|
|
|
dir = tmpdir()
|
2019-12-23 01:34:10 +01:00
|
|
|
name = os.path.join(
|
|
|
|
dir, "audio%s%s" % (random.randrange(0, 1000000), os.path.splitext(path)[1])
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
f = open(name, "wb")
|
|
|
|
f.write(open(path, "rb").read())
|
|
|
|
f.close()
|
|
|
|
# it wants unix paths, too!
|
|
|
|
path = name.replace("\\", "/")
|
|
|
|
mplayerQueue.append(path)
|
|
|
|
mplayerEvt.set()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def clearMplayerQueue() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global mplayerClear, mplayerQueue
|
|
|
|
mplayerQueue = []
|
|
|
|
mplayerClear = True
|
|
|
|
mplayerEvt.set()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def ensureMplayerThreads() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
global mplayerManager
|
|
|
|
if not mplayerManager:
|
|
|
|
mplayerManager = MplayerMonitor()
|
|
|
|
mplayerManager.daemon = True
|
|
|
|
mplayerManager.start()
|
|
|
|
# ensure the tmpdir() exit handler is registered first so it runs
|
|
|
|
# after the mplayer exit
|
|
|
|
tmpdir()
|
|
|
|
# clean up mplayer on exit
|
|
|
|
atexit.register(stopMplayer)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def stopMplayer(*args) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not mplayerManager:
|
|
|
|
return
|
|
|
|
mplayerManager.kill()
|
2019-03-05 00:01:31 +01:00
|
|
|
if isWin:
|
|
|
|
cleanupOldMplayerProcesses()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
addHook("unloadProfile", stopMplayer)
|
|
|
|
|
|
|
|
# PyAudio recording
|
|
|
|
##########################################################################
|
|
|
|
|
2019-02-13 00:36:39 +01:00
|
|
|
try:
|
|
|
|
import pyaudio
|
|
|
|
import wave
|
2014-04-17 21:17:05 +02:00
|
|
|
|
2019-02-13 00:36:39 +01:00
|
|
|
PYAU_FORMAT = pyaudio.paInt16
|
|
|
|
PYAU_CHANNELS = 1
|
2019-12-20 06:07:40 +01:00
|
|
|
PYAU_INPUT_INDEX: Optional[int] = None
|
2019-02-13 00:36:39 +01:00
|
|
|
except:
|
|
|
|
pyaudio = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class _Recorder:
|
2019-12-19 04:02:45 +01:00
|
|
|
def postprocess(self, encode=True) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.encode = encode
|
|
|
|
for c in processingChain:
|
2019-12-23 01:34:10 +01:00
|
|
|
# print c
|
|
|
|
if not self.encode and c[0] == "lame":
|
2012-12-21 08:51:59 +01:00
|
|
|
continue
|
|
|
|
try:
|
2016-07-26 04:15:43 +02:00
|
|
|
cmd, env = _packagedCmd(c)
|
|
|
|
ret = retryWait(subprocess.Popen(cmd, startupinfo=si, env=env))
|
2012-12-21 08:51:59 +01:00
|
|
|
except:
|
|
|
|
ret = True
|
2017-10-25 10:35:39 +02:00
|
|
|
finally:
|
2019-07-14 03:19:29 +02:00
|
|
|
self.cleanup()
|
2012-12-21 08:51:59 +01:00
|
|
|
if ret:
|
2019-12-23 01:34:10 +01:00
|
|
|
raise Exception(_("Error running %s") % " ".join(cmd))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def cleanup(self) -> None:
|
2019-07-14 03:19:29 +02:00
|
|
|
if os.path.exists(processingSrc):
|
|
|
|
os.unlink(processingSrc)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class PyAudioThreadedRecorder(threading.Thread):
|
2019-12-19 04:02:45 +01:00
|
|
|
def __init__(self, startupDelay) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
threading.Thread.__init__(self)
|
2017-12-05 02:07:52 +01:00
|
|
|
self.startupDelay = startupDelay
|
2012-12-21 08:51:59 +01:00
|
|
|
self.finish = False
|
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def run(self) -> Any:
|
2012-12-21 08:51:59 +01:00
|
|
|
chunk = 1024
|
2016-06-23 04:04:48 +02:00
|
|
|
p = pyaudio.PyAudio()
|
2014-04-17 21:17:05 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
rate = int(p.get_default_input_device_info()["defaultSampleRate"])
|
2017-12-05 02:07:52 +01:00
|
|
|
wait = int(rate * self.startupDelay)
|
2014-04-17 21:17:05 +02:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
stream = p.open(
|
|
|
|
format=PYAU_FORMAT,
|
|
|
|
channels=PYAU_CHANNELS,
|
|
|
|
rate=rate,
|
|
|
|
input=True,
|
|
|
|
input_device_index=PYAU_INPUT_INDEX,
|
|
|
|
frames_per_buffer=chunk,
|
|
|
|
)
|
2014-04-17 21:17:05 +02:00
|
|
|
|
2017-12-05 02:07:52 +01:00
|
|
|
stream.read(wait)
|
|
|
|
|
2016-05-31 09:51:16 +02:00
|
|
|
data = b""
|
2012-12-21 08:51:59 +01:00
|
|
|
while not self.finish:
|
2018-12-22 04:41:35 +01:00
|
|
|
data += stream.read(chunk, exception_on_overflow=False)
|
2012-12-21 08:51:59 +01:00
|
|
|
stream.close()
|
|
|
|
p.terminate()
|
2019-12-23 01:34:10 +01:00
|
|
|
wf = wave.open(processingSrc, "wb")
|
2012-12-21 08:51:59 +01:00
|
|
|
wf.setnchannels(PYAU_CHANNELS)
|
|
|
|
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
|
2014-04-17 21:17:05 +02:00
|
|
|
wf.setframerate(rate)
|
2012-12-21 08:51:59 +01:00
|
|
|
wf.writeframes(data)
|
|
|
|
wf.close()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class PyAudioRecorder(_Recorder):
|
|
|
|
|
2017-12-05 02:07:52 +01:00
|
|
|
# discard first 250ms which may have pops/cracks
|
|
|
|
startupDelay = 0.25
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def __init__(self):
|
|
|
|
for t in recFiles + [processingSrc, processingDst]:
|
|
|
|
try:
|
|
|
|
os.unlink(t)
|
|
|
|
except OSError:
|
|
|
|
pass
|
|
|
|
self.encode = False
|
|
|
|
|
|
|
|
def start(self):
|
2017-12-05 02:07:52 +01:00
|
|
|
self.thread = PyAudioThreadedRecorder(startupDelay=self.startupDelay)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.thread.start()
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.thread.finish = True
|
|
|
|
self.thread.join()
|
|
|
|
|
|
|
|
def file(self):
|
|
|
|
if self.encode:
|
2016-05-12 06:45:35 +02:00
|
|
|
tgt = "rec%d.mp3" % time.time()
|
2012-12-21 08:51:59 +01:00
|
|
|
os.rename(processingDst, tgt)
|
|
|
|
return tgt
|
|
|
|
else:
|
|
|
|
return processingSrc
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-02-13 00:36:39 +01:00
|
|
|
if not pyaudio:
|
2019-12-23 01:34:10 +01:00
|
|
|
PyAudioRecorder = None # type: ignore
|
2019-02-13 00:36:39 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Audio interface
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
_player = queueMplayer
|
|
|
|
_queueEraser = clearMplayerQueue
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def play(path) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
_player(path)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2019-12-19 04:02:45 +01:00
|
|
|
def clearAudioQueue() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
_queueEraser()
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
Recorder = PyAudioRecorder
|
2020-01-02 10:43:19 +01:00
|
|
|
if not Recorder:
|
|
|
|
print("pyaudio not installed")
|
|
|
|
|
|
|
|
# add everything from this module into anki.sound for backwards compat
|
|
|
|
_exports = [i for i in locals().items() if not i[0].startswith("__")]
|
|
|
|
for (k, v) in _exports:
|
|
|
|
sys.modules["anki.sound"].__dict__[k] = v
|