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!"
|
$ 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.
|
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
|
"""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
|
from .__main__ import _main
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,37 @@
|
|||||||
"""Main entrypoint for the edge-playback package."""
|
"""Main entrypoint for the edge-playback package."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
|
|
||||||
|
from .util import pr_err
|
||||||
def pr_err(msg: str) -> None:
|
|
||||||
"""Print to stderr."""
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def _main() -> None:
|
def _main() -> None:
|
||||||
depcheck_failed = False
|
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):
|
if not which(dep):
|
||||||
pr_err(f"{dep} is not installed.")
|
pr_err(f"{dep} is not installed.")
|
||||||
depcheck_failed = True
|
depcheck_failed = True
|
||||||
@@ -33,31 +50,36 @@ def _main() -> None:
|
|||||||
media.close()
|
media.close()
|
||||||
mp3_fname = media.name
|
mp3_fname = media.name
|
||||||
|
|
||||||
if not srt_fname:
|
if not srt_fname and use_mpv:
|
||||||
subtitle = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
|
subtitle = tempfile.NamedTemporaryFile(suffix=".srt", delete=False)
|
||||||
subtitle.close()
|
subtitle.close()
|
||||||
srt_fname = subtitle.name
|
srt_fname = subtitle.name
|
||||||
|
|
||||||
print(f"Media file: {mp3_fname}")
|
print(f"Media file: {mp3_fname}")
|
||||||
print(f"Subtitle file: {srt_fname}\n")
|
if srt_fname:
|
||||||
with subprocess.Popen(
|
print(f"Subtitle file: {srt_fname}\n")
|
||||||
[
|
|
||||||
"edge-tts",
|
edge_tts_cmd = ["edge-tts", f"--write-media={mp3_fname}"]
|
||||||
f"--write-media={mp3_fname}",
|
if srt_fname:
|
||||||
f"--write-subtitles={srt_fname}",
|
edge_tts_cmd.append(f"--write-subtitles={srt_fname}")
|
||||||
]
|
edge_tts_cmd = edge_tts_cmd + tts_args
|
||||||
+ sys.argv[1:]
|
with subprocess.Popen(edge_tts_cmd) as process:
|
||||||
) as process:
|
|
||||||
process.communicate()
|
process.communicate()
|
||||||
|
|
||||||
with subprocess.Popen(
|
if sys.platform == "win32" and not use_mpv:
|
||||||
[
|
# pylint: disable-next=import-outside-toplevel
|
||||||
"mpv",
|
from .win32_playback import play_mp3_win32
|
||||||
f"--sub-file={srt_fname}",
|
|
||||||
mp3_fname,
|
play_mp3_win32(mp3_fname)
|
||||||
]
|
else:
|
||||||
) as process:
|
with subprocess.Popen(
|
||||||
process.communicate()
|
[
|
||||||
|
"mpv",
|
||||||
|
f"--sub-file={srt_fname}",
|
||||||
|
mp3_fname,
|
||||||
|
]
|
||||||
|
) as process:
|
||||||
|
process.communicate()
|
||||||
finally:
|
finally:
|
||||||
if keep:
|
if keep:
|
||||||
print(f"\nKeeping temporary files: {mp3_fname} and {srt_fname}")
|
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