support interrupting say; fix race in simple player

This commit is contained in:
Damien Elmes 2020-01-21 07:28:19 +10:00
parent 1beae4f858
commit 01f3fd06d6
2 changed files with 60 additions and 15 deletions

View file

@ -10,6 +10,7 @@ import threading
import time
import wave
from abc import ABC, abstractmethod
from concurrent.futures import Future
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
import pyaudio
@ -41,7 +42,9 @@ class Player(ABC):
pass
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
@ -153,24 +156,32 @@ def _packagedCmd(cmd) -> Tuple[Any, Dict[str, str]]:
##########################################################################
class PlayerInterrupted(Exception):
pass
class SimpleProcessPlayer(SoundOrVideoPlayer):
"A player that invokes a new process for each file to play."
_on_done: Optional[OnDoneCallback]
_terminate_flag = False
args: List[str] = []
env: Optional[Dict[str, str]] = None
def __init__(self, taskman: TaskManager):
self._taskman = taskman
_terminate_flag = False
def play(self, tag: AVTag, on_done: OnDoneCallback) -> None:
stag = cast(SoundOrVideoTag, tag)
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):
self._terminate_flag = True
# block until stopped
while self._terminate_flag:
time.sleep(0.1)
def _play(self, filename: str) -> None:
process = subprocess.Popen(self.args + [filename], env=self.env)
@ -178,12 +189,22 @@ class SimpleProcessPlayer(SoundOrVideoPlayer):
try:
process.wait(0.1)
if process.returncode != 0:
raise Exception(f"player got return code: {process.returncode}")
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:
try:
ret.result()
except PlayerInterrupted:
# don't fire done callback when interrupted
return
cb()
class SimpleMpvPlayer(SimpleProcessPlayer):

View file

@ -3,11 +3,12 @@ todo
"""
import subprocess
import time
from concurrent.futures import Future
from typing import Callable, cast
from typing import cast
from anki.sound import AVTag, TTSTag
from aqt.sound import OnDoneCallback, Player
from aqt.sound import OnDoneCallback, Player, PlayerInterrupted
from aqt.taskman import TaskManager
@ -19,25 +20,48 @@ class TTSPlayer(Player): # pylint: disable=abstract-method
class MacTTSPlayer(TTSPlayer):
def __init__(self, taskman: TaskManager):
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)
self._taskman.run(
lambda: self._play(ttag), lambda ret: self._on_done(ret, on_done)
)
def _play(self, tag: TTSTag) -> None:
ret = subprocess.run(
process = subprocess.Popen(
["say", "-v", "Alex", "-f", "-"],
input=tag.text,
encoding="utf8",
check=True,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
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:
# will raise on error
ret.result()
try:
ret.result()
except PlayerInterrupted:
# don't fire done callback when interrupted
return
cb()
def stop(self):
pass
self._terminate_flag = True
# block until stopped
while self._terminate_flag:
time.sleep(0.1)