[播放平滑修复]:完成可打断播报平滑播放,包含单输出流分块播放和TTS采样率保真
This commit is contained in:
@@ -6,6 +6,7 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -106,6 +107,23 @@ class MemoryAudioTransport:
|
||||
self.played_segments.append(segment)
|
||||
return PlaybackResult(True, segment.duration_ms)
|
||||
|
||||
def play_pcm_chunks(
|
||||
self,
|
||||
segment: AudioSegment,
|
||||
*,
|
||||
chunk_ms: int,
|
||||
after_chunk: Callable[[AudioSegment, int], bool] | None = None,
|
||||
) -> PlaybackResult:
|
||||
elapsed_ms = 0
|
||||
for chunk in _raw_pcm_chunks(segment, chunk_ms=chunk_ms):
|
||||
playback = self.play_pcm(chunk)
|
||||
if playback.error:
|
||||
return playback
|
||||
elapsed_ms += chunk.duration_ms
|
||||
if after_chunk is not None and after_chunk(chunk, elapsed_ms):
|
||||
return PlaybackResult(True, elapsed_ms)
|
||||
return PlaybackResult(True, elapsed_ms)
|
||||
|
||||
def flush_input(self) -> int:
|
||||
self.flush_count += 1
|
||||
if not self._flush_clears_input:
|
||||
@@ -288,6 +306,59 @@ class SoundDeviceAudioTransport:
|
||||
)
|
||||
return PlaybackResult(True, segment.duration_ms)
|
||||
|
||||
def play_pcm_chunks(
|
||||
self,
|
||||
segment: AudioSegment,
|
||||
*,
|
||||
chunk_ms: int,
|
||||
after_chunk: Callable[[AudioSegment, int], bool] | None = None,
|
||||
) -> PlaybackResult:
|
||||
if self._sd is None or segment.metadata.get("format") in {"aiff", "wav", "mp3", "m4a", "aac"}:
|
||||
playback = self.play_pcm(segment)
|
||||
if playback.error:
|
||||
return playback
|
||||
if after_chunk is not None:
|
||||
after_chunk(segment, segment.duration_ms)
|
||||
return playback
|
||||
if not segment.pcm:
|
||||
return PlaybackResult(
|
||||
False,
|
||||
0,
|
||||
ProviderError(
|
||||
ErrorCode.AUDIO_STREAM_UNDERRUN,
|
||||
"cannot play empty audio segment",
|
||||
True,
|
||||
"sounddevice-transport",
|
||||
"transport",
|
||||
),
|
||||
)
|
||||
elapsed_ms = 0
|
||||
try:
|
||||
with self._sd.RawOutputStream(
|
||||
samplerate=segment.sample_rate,
|
||||
channels=segment.channels,
|
||||
dtype="int16",
|
||||
device=_coerce_device_id(self._output_device),
|
||||
) as stream:
|
||||
for chunk in _raw_pcm_chunks(segment, chunk_ms=chunk_ms):
|
||||
stream.write(chunk.pcm)
|
||||
elapsed_ms += chunk.duration_ms
|
||||
if after_chunk is not None and after_chunk(chunk, elapsed_ms):
|
||||
return PlaybackResult(True, elapsed_ms)
|
||||
except Exception as exc:
|
||||
return PlaybackResult(
|
||||
False,
|
||||
elapsed_ms,
|
||||
ProviderError(
|
||||
ErrorCode.AUDIO_OUTPUT_DEVICE_MISSING,
|
||||
f"cannot play through sounddevice output stream: {exc}",
|
||||
False,
|
||||
"sounddevice-transport",
|
||||
"transport",
|
||||
),
|
||||
)
|
||||
return PlaybackResult(True, elapsed_ms)
|
||||
|
||||
def stop(self) -> None:
|
||||
if self._stream is None:
|
||||
return None
|
||||
@@ -418,3 +489,30 @@ def _play_file_bytes_with_afplay(segment: AudioSegment) -> PlaybackResult:
|
||||
),
|
||||
)
|
||||
return PlaybackResult(True, segment.duration_ms)
|
||||
|
||||
|
||||
def _raw_pcm_chunks(segment: AudioSegment, *, chunk_ms: int) -> list[AudioSegment]:
|
||||
if chunk_ms <= 0 or segment.duration_ms <= chunk_ms:
|
||||
return [segment]
|
||||
bytes_per_ms = max(1, int(segment.sample_rate * segment.channels * 2 / 1000))
|
||||
chunk_bytes = max(2 * segment.channels, bytes_per_ms * chunk_ms)
|
||||
chunk_bytes -= chunk_bytes % (2 * segment.channels)
|
||||
chunks: list[AudioSegment] = []
|
||||
offset = 0
|
||||
start_ms = segment.start_time_ms
|
||||
while offset < len(segment.pcm):
|
||||
data = segment.pcm[offset : offset + chunk_bytes]
|
||||
duration_ms = max(1, int(len(data) / bytes_per_ms))
|
||||
chunks.append(
|
||||
AudioSegment(
|
||||
data,
|
||||
segment.sample_rate,
|
||||
segment.channels,
|
||||
start_ms,
|
||||
start_ms + duration_ms,
|
||||
dict(segment.metadata),
|
||||
)
|
||||
)
|
||||
offset += len(data)
|
||||
start_ms += duration_ms
|
||||
return chunks
|
||||
|
||||
Reference in New Issue
Block a user