Support native playback on Windows (#360)
* Support native playback on Windows * Use mypy-blessed platform switching logic * Address feedback * I think mypy is happy now * Don't print srt when filename is None * Fix ctypes imports * Fix pylint and black complaints Signed-off-by: rany <rany2@riseup.net> * Have play_mp3_win32 use a guard clause to reduce nesting Signed-off-by: rany <rany2@riseup.net> --------- Signed-off-by: rany <rany2@riseup.net> Co-authored-by: rany <rany2@riseup.net>
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
8
src/edge_playback/util.py
Normal file
8
src/edge_playback/util.py
Normal file
@@ -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)
|
||||
52
src/edge_playback/win32_playback.py
Normal file
52
src/edge_playback/win32_playback.py
Normal file
@@ -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")
|
||||
Reference in New Issue
Block a user