mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
support interrupting say; fix race in simple player
This commit is contained in:
parent
1beae4f858
commit
01f3fd06d6
2 changed files with 60 additions and 15 deletions
|
@ -10,6 +10,7 @@ import threading
|
||||||
import time
|
import time
|
||||||
import wave
|
import wave
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from concurrent.futures import Future
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
|
||||||
|
|
||||||
import pyaudio
|
import pyaudio
|
||||||
|
@ -41,7 +42,9 @@ class Player(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"Optional."
|
"""Optional.
|
||||||
|
|
||||||
|
If implemented, the player must not call on_done() when the audio is stopped."""
|
||||||
|
|
||||||
|
|
||||||
class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
class SoundOrVideoPlayer(Player): # pylint: disable=abstract-method
|
||||||
|
@ -153,24 +156,32 @@ def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerInterrupted(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SimpleProcessPlayer(SoundOrVideoPlayer):
|
class SimpleProcessPlayer(SoundOrVideoPlayer):
|
||||||
"A player that invokes a new process for each file to play."
|
"A player that invokes a new process for each file to play."
|
||||||
|
|
||||||
_on_done: Optional[OnDoneCallback]
|
|
||||||
_terminate_flag = False
|
|
||||||
args: List[str] = []
|
args: List[str] = []
|
||||||
env: Optional[Dict[str, str]] = None
|
env: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
def __init__(self, taskman: TaskManager):
|
def __init__(self, taskman: TaskManager):
|
||||||
self._taskman = taskman
|
self._taskman = taskman
|
||||||
|
_terminate_flag = False
|
||||||
|
|
||||||
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
|
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
|
||||||
stag = cast(SoundOrVideoTag, tag)
|
stag = cast(SoundOrVideoTag, tag)
|
||||||
self._terminate_flag = False
|
self._terminate_flag = False
|
||||||
self._taskman.run(lambda: self._play(stag.filename), lambda res: on_done())
|
self._taskman.run(
|
||||||
|
lambda: self._play(stag.filename), lambda res: self._on_done(res, on_done)
|
||||||
|
)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._terminate_flag = True
|
self._terminate_flag = True
|
||||||
|
# block until stopped
|
||||||
|
while self._terminate_flag:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
def _play(self, filename: str) -> None:
|
def _play(self, filename: str) -> None:
|
||||||
process = subprocess.Popen(self.args + [filename], env=self.env)
|
process = subprocess.Popen(self.args + [filename], env=self.env)
|
||||||
|
@ -178,12 +189,22 @@ class SimpleProcessPlayer(SoundOrVideoPlayer):
|
||||||
try:
|
try:
|
||||||
process.wait(0.1)
|
process.wait(0.1)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise Exception(f"player got return code: {process.returncode}")
|
print(f"player got return code: {process.returncode}")
|
||||||
return
|
return
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
pass
|
pass
|
||||||
if self._terminate_flag:
|
if self._terminate_flag:
|
||||||
process.terminate()
|
process.terminate()
|
||||||
|
self._terminate_flag = False
|
||||||
|
raise PlayerInterrupted()
|
||||||
|
|
||||||
|
def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
|
||||||
|
try:
|
||||||
|
ret.result()
|
||||||
|
except PlayerInterrupted:
|
||||||
|
# don't fire done callback when interrupted
|
||||||
|
return
|
||||||
|
cb()
|
||||||
|
|
||||||
|
|
||||||
class SimpleMpvPlayer(SimpleProcessPlayer):
|
class SimpleMpvPlayer(SimpleProcessPlayer):
|
||||||
|
|
|
@ -3,11 +3,12 @@ todo
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Callable, cast
|
from typing import cast
|
||||||
|
|
||||||
from anki.sound import AVTag, TTSTag
|
from anki.sound import AVTag, TTSTag
|
||||||
from aqt.sound import OnDoneCallback, Player
|
from aqt.sound import OnDoneCallback, Player, PlayerInterrupted
|
||||||
from aqt.taskman import TaskManager
|
from aqt.taskman import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,25 +20,48 @@ class TTSPlayer(Player): # pylint: disable=abstract-method
|
||||||
class MacTTSPlayer(TTSPlayer):
|
class MacTTSPlayer(TTSPlayer):
|
||||||
def __init__(self, taskman: TaskManager):
|
def __init__(self, taskman: TaskManager):
|
||||||
self._taskman = taskman
|
self._taskman = taskman
|
||||||
|
self._terminate_flag = False
|
||||||
|
|
||||||
def play(self, tag: AVTag, on_done: Callable[[], None]) -> None:
|
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
|
||||||
ttag = cast(TTSTag, tag)
|
ttag = cast(TTSTag, tag)
|
||||||
self._taskman.run(
|
self._taskman.run(
|
||||||
lambda: self._play(ttag), lambda ret: self._on_done(ret, on_done)
|
lambda: self._play(ttag), lambda ret: self._on_done(ret, on_done)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _play(self, tag: TTSTag) -> None:
|
def _play(self, tag: TTSTag) -> None:
|
||||||
ret = subprocess.run(
|
process = subprocess.Popen(
|
||||||
["say", "-v", "Alex", "-f", "-"],
|
["say", "-v", "Alex", "-f", "-"],
|
||||||
input=tag.text,
|
stdin=subprocess.PIPE,
|
||||||
encoding="utf8",
|
stdout=subprocess.DEVNULL,
|
||||||
check=True,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
# write the input text to stdin
|
||||||
|
process.stdin.write(tag.text.encode("utf8"))
|
||||||
|
process.stdin.close()
|
||||||
|
# and wait for termination
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
process.wait(0.1)
|
||||||
|
if process.returncode != 0:
|
||||||
|
print(f"player got return code: {process.returncode}")
|
||||||
|
return
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
|
if self._terminate_flag:
|
||||||
|
process.terminate()
|
||||||
|
self._terminate_flag = False
|
||||||
|
raise PlayerInterrupted()
|
||||||
|
|
||||||
def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
|
def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
|
||||||
# will raise on error
|
try:
|
||||||
ret.result()
|
ret.result()
|
||||||
|
except PlayerInterrupted:
|
||||||
|
# don't fire done callback when interrupted
|
||||||
|
return
|
||||||
cb()
|
cb()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
self._terminate_flag = True
|
||||||
|
# block until stopped
|
||||||
|
while self._terminate_flag:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
Loading…
Reference in a new issue