diff --git a/README.md b/README.md index 15de9c3..77af79b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you wish to play it back immediately with subtitles, you could use the `edge- $ edge-playback --text "Hello, world!" -Note that `edge-playback` requires the installation of the [`mpv` command line player](https://mpv.io/). +Note that `edge-playback` requires the installation of the [`mpv` command line player](https://mpv.io/), except on Windows. All `edge-tts` commands work with `edge-playback` with the exception of the `--write-media`, `--write-subtitles` and `--list-voices` options. diff --git a/src/edge_playback/__init__.py b/src/edge_playback/__init__.py index 1e26470..130c437 100644 --- a/src/edge_playback/__init__.py +++ b/src/edge_playback/__init__.py @@ -1,5 +1,6 @@ """The edge_playback package wraps the functionality of mpv and edge-tts to generate -text-to-speech (TTS) using edge-tts and then plays back the generated audio using mpv.""" +text-to-speech (TTS) using edge-tts and then plays back the generated audio using mpv. +""" from .__main__ import _main diff --git a/src/edge_playback/__main__.py b/src/edge_playback/__main__.py index 89a5c27..9fe4ce5 100644 --- a/src/edge_playback/__main__.py +++ b/src/edge_playback/__main__.py @@ -1,20 +1,37 @@ """Main entrypoint for the edge-playback package.""" +import argparse import os import subprocess import sys import tempfile from shutil import which - -def pr_err(msg: str) -> None: - """Print to stderr.""" - print(msg, file=sys.stderr) +from .util import pr_err def _main() -> None: depcheck_failed = False - for dep in ("edge-tts", "mpv"): + + parser = argparse.ArgumentParser( + prog="edge-playback", + description="Speak text using Microsoft Edge's online text-to-speech API", + epilog="See `edge-tts` for additional arguments", + ) + parser.add_argument( + "--mpv", + action="store_true", + help="Use mpv to play audio. By default, false on Windows and true on all other platforms", + ) + args, tts_args = parser.parse_known_args() + + use_mpv = sys.platform != "win32" or args.mpv + + deps = ["edge-tts"] + if use_mpv: + deps.append("mpv") + + for dep in deps: if not which(dep): pr_err(f"{dep} is not installed.") depcheck_failed = True @@ -33,31 +50,36 @@ def _main() -> None: media.close() mp3_fname = media.name - if not srt_fname: + if not srt_fname and use_mpv: subtitle = tempfile.NamedTemporaryFile(suffix=".srt", delete=False) subtitle.close() srt_fname = subtitle.name print(f"Media file: {mp3_fname}") - print(f"Subtitle file: {srt_fname}\n") - with subprocess.Popen( - [ - "edge-tts", - f"--write-media={mp3_fname}", - f"--write-subtitles={srt_fname}", - ] - + sys.argv[1:] - ) as process: + if srt_fname: + print(f"Subtitle file: {srt_fname}\n") + + edge_tts_cmd = ["edge-tts", f"--write-media={mp3_fname}"] + if srt_fname: + edge_tts_cmd.append(f"--write-subtitles={srt_fname}") + edge_tts_cmd = edge_tts_cmd + tts_args + with subprocess.Popen(edge_tts_cmd) as process: process.communicate() - with subprocess.Popen( - [ - "mpv", - f"--sub-file={srt_fname}", - mp3_fname, - ] - ) as process: - process.communicate() + if sys.platform == "win32" and not use_mpv: + # pylint: disable-next=import-outside-toplevel + from .win32_playback import play_mp3_win32 + + play_mp3_win32(mp3_fname) + else: + with subprocess.Popen( + [ + "mpv", + f"--sub-file={srt_fname}", + mp3_fname, + ] + ) as process: + process.communicate() finally: if keep: print(f"\nKeeping temporary files: {mp3_fname} and {srt_fname}") diff --git a/src/edge_playback/util.py b/src/edge_playback/util.py new file mode 100644 index 0000000..6f9ad04 --- /dev/null +++ b/src/edge_playback/util.py @@ -0,0 +1,8 @@ +"""Utility functions for edge-playback""" + +import sys + + +def pr_err(msg: str) -> None: + """Print to stderr.""" + print(msg, file=sys.stderr) diff --git a/src/edge_playback/win32_playback.py b/src/edge_playback/win32_playback.py new file mode 100644 index 0000000..7ee2fa0 --- /dev/null +++ b/src/edge_playback/win32_playback.py @@ -0,0 +1,52 @@ +"""Functions to play audio on Windows using native win32 APIs""" + +import sys + +from .util import pr_err + + +def play_mp3_win32(mp3_fname: str) -> None: + """Play mp3 file with given path using win32 API""" + + if sys.platform != "win32": + raise NotImplementedError("Function only available on Windows") + + # pylint: disable-next=import-outside-toplevel + from ctypes import create_unicode_buffer, windll, wintypes # type: ignore + + _get_short_path_name_w = windll.kernel32.GetShortPathNameW + _get_short_path_name_w.argtypes = [ + wintypes.LPCWSTR, + wintypes.LPWSTR, + wintypes.DWORD, + ] + _get_short_path_name_w.restype = wintypes.DWORD + + def get_short_path_name(long_name: str) -> str: + """ + Gets the DOS-safe short path name of a given long path. + http://stackoverflow.com/a/23598461/200291 + """ + output_buf_size = 0 + while True: + output_buf = create_unicode_buffer(output_buf_size) + needed = _get_short_path_name_w(long_name, output_buf, output_buf_size) + if output_buf_size >= needed: + return output_buf.value + output_buf_size = needed + + mci_send_string_w = windll.winmm.mciSendStringW + + def mci_send(msg: str) -> None: + """Send MCI command string""" + result = mci_send_string_w(msg, 0, 0, 0) + if result != 0: + pr_err(f"Error {result} in mciSendString {msg}. Exiting.") + sys.exit(1) + + mp3_shortname = get_short_path_name(mp3_fname) + + mci_send("Close All") + mci_send(f'Open "{mp3_shortname}" Type MPEGVideo Alias theMP3') + mci_send("Play theMP3 Wait") + mci_send("Close theMP3")