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 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):

View file

@ -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)