macOS transcode via afconvert; document ducking no-op
- convert_audio: use ffmpeg when present (Linux path unchanged); on macOS without ffmpeg, fall back to the built-in afconvert for m4a, with a clear "install ffmpeg for mp3/ogg/flac" error for the formats it can't encode. - media_duck: document why macOS is left as a deliberate no-op — the only built-in volume control is system-wide and would dim our own afplay voice; proper per-app ducking needs CoreAudio, not osascript.
This commit is contained in:
parent
2711e90dce
commit
3d5d8dad36
@ -94,13 +94,19 @@ class ConversionError(RuntimeError):
|
||||
"""Raised when format conversion fails."""
|
||||
|
||||
|
||||
# macOS built-in afconvert can only encode a subset (AAC/m4a) — no mp3/ogg/flac
|
||||
# encoders ship with it. Those still need ffmpeg (brew install ffmpeg).
|
||||
_AFCONVERT_ARGS: dict[str, list[str]] = {
|
||||
"m4a": ["-f", "m4af", "-d", "aac"], # AAC in an MPEG-4 container
|
||||
}
|
||||
|
||||
|
||||
async def convert_audio(src_wav: Path, dest: Path, fmt: str) -> Path:
|
||||
"""Convert a source WAV to the requested format at dest.
|
||||
|
||||
`fmt == "wav"` shells out to a plain copy (no ffmpeg invocation).
|
||||
Other formats use ffmpeg with codec args from _FFMPEG_CODEC_ARGS.
|
||||
|
||||
Raises ConversionError on ffmpeg non-zero exit or unsupported format.
|
||||
`fmt == "wav"` is a plain copy. Otherwise ffmpeg is used when available; on
|
||||
macOS without ffmpeg, afconvert covers m4a (mp3/ogg/flac still need ffmpeg).
|
||||
Raises ConversionError on failure or unsupported format.
|
||||
"""
|
||||
if fmt not in SUPPORTED_FORMATS:
|
||||
raise ConversionError(
|
||||
@ -113,20 +119,44 @@ async def convert_audio(src_wav: Path, dest: Path, fmt: str) -> Path:
|
||||
shutil.copyfile(src_wav, dest)
|
||||
return dest
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ffmpeg", "-y", "-loglevel", "error", "-i", str(src_wav),
|
||||
*_FFMPEG_CODEC_ARGS[fmt],
|
||||
str(dest),
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise ConversionError(
|
||||
f"ffmpeg failed ({proc.returncode}) converting to {fmt}: "
|
||||
f"{stderr.decode(errors='replace').strip()[:500]}"
|
||||
if platform_audio.has_ffmpeg():
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ffmpeg", "-y", "-loglevel", "error", "-i", str(src_wav),
|
||||
*_FFMPEG_CODEC_ARGS[fmt],
|
||||
str(dest),
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
return dest
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise ConversionError(
|
||||
f"ffmpeg failed ({proc.returncode}) converting to {fmt}: "
|
||||
f"{stderr.decode(errors='replace').strip()[:500]}"
|
||||
)
|
||||
return dest
|
||||
|
||||
# No ffmpeg — fall back to the macOS built-in afconvert for what it can do.
|
||||
if platform_audio.IS_MACOS:
|
||||
args = _AFCONVERT_ARGS.get(fmt)
|
||||
if args is None:
|
||||
raise ConversionError(
|
||||
f"macOS afconvert can't encode {fmt!r} (only m4a/wav without ffmpeg). "
|
||||
f"Install ffmpeg (brew install ffmpeg) for mp3/ogg/flac."
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"afconvert", *args, str(src_wav), str(dest),
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise ConversionError(
|
||||
f"afconvert failed ({proc.returncode}) converting to {fmt}: "
|
||||
f"{stderr.decode(errors='replace').strip()[:500]}"
|
||||
)
|
||||
return dest
|
||||
|
||||
raise ConversionError(f"ffmpeg not found — needed to convert to {fmt!r}. Install ffmpeg.")
|
||||
|
||||
|
||||
class PlaybackError(RuntimeError):
|
||||
|
||||
@ -11,6 +11,13 @@ protocol connection, while pactl (using libpulse) works reliably.
|
||||
|
||||
If pactl is not installed or the PulseAudio socket is unavailable, all
|
||||
operations silently no-op — TTS still works, just without ducking.
|
||||
|
||||
macOS: intentionally left as that no-op. The only built-in volume control
|
||||
(`osascript ... set volume output volume`) is SYSTEM-WIDE, and our TTS plays
|
||||
through afplay as system audio — so ducking would dim our own voice, the
|
||||
opposite of the goal. Per-app ducking on macOS needs CoreAudio (not a
|
||||
built-in), so proper ducking is a future native-helper task, not an osascript
|
||||
one. Don't "fix" this by adding osascript volume control.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user