Fixed pause which broke in the switch to mpv. Fixed bookmarks being lost when switching books using the recent bookmarks.

This commit is contained in:
Storm Dragon
2025-10-19 02:32:55 -04:00
parent 0bdc5bdf17
commit a934c06f6f
2 changed files with 100 additions and 134 deletions
+38 -114
View File
@@ -73,8 +73,7 @@ class BookReader:
self.parser = None # Will be set based on file type self.parser = None # Will be set based on file type
self.bookmarkManager = BookmarkManager() self.bookmarkManager = BookmarkManager()
self.speechEngine = SpeechEngine() # UI feedback self.speechEngine = SpeechEngine() # UI feedback
self.audioPlayer = MpvPlayer() self.audioPlayer = MpvPlayer() # Used for both audio books and TTS playback
self.ttsMpvProcess = None # For direct mpv subprocess for TTS
# Configure speech engine from saved settings # Configure speech engine from saved settings
speechRate = self.config.get_speech_rate() speechRate = self.config.get_speech_rate()
@@ -736,16 +735,10 @@ class BookReader:
# Start next chapter # Start next chapter
self._start_paragraph_playback() self._start_paragraph_playback()
elif readerEngine == 'piper': elif readerEngine == 'piper':
# Check piper-tts subprocess state # Check if TTS audio has finished playing
# The TTS mpv process runs independently, so we need to check if it's still running # Only auto-advance if NOT paused (to avoid skipping when user pauses)
playbackFinished = False if not self.audioPlayer.is_paused():
if self.ttsMpvProcess: playbackFinished = not self.audioPlayer.is_audio_file_playing()
# Check if the mpv subprocess has finished
if self.ttsMpvProcess.poll() is not None:
playbackFinished = True
else:
# No process exists, consider playback finished
playbackFinished = True
if playbackFinished: if playbackFinished:
# Current paragraph finished, advance # Current paragraph finished, advance
@@ -807,18 +800,10 @@ class BookReader:
if readerEngine == 'speechd': if readerEngine == 'speechd':
self.readingEngine.cancel_reading() self.readingEngine.cancel_reading()
else: else:
# Stop audio player (handles both TTS and audio books)
self.audioPlayer.stop() self.audioPlayer.stop()
if self.audioPlayer.is_audio_file_loaded():
# Clean up TTS mpv subprocess if it's still running self.audioPlayer.stop_audio_file()
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
try:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=2)
except subprocess.TimeoutExpired:
print("Warning: TTS subprocess didn't terminate, force killing")
self.ttsMpvProcess.kill()
except Exception as e:
print(f"Error cleaning up TTS subprocess: {e}")
# Close Audiobookshelf session if active # Close Audiobookshelf session if active
if self.sessionId and self.absClient: if self.sessionId and self.absClient:
@@ -919,13 +904,13 @@ class BookReader:
self.speechEngine.speak("Paused") self.speechEngine.speak("Paused")
self.readingEngine.pause_reading() self.readingEngine.pause_reading()
else: else:
# Handle piper-tts pause/resume # Handle piper-tts pause/resume (now uses audio file methods)
if self.audioPlayer.is_paused(): if self.audioPlayer.is_paused():
self.speechEngine.speak("Resuming") self.speechEngine.speak("Resuming")
self.audioPlayer.resume() self.audioPlayer.resume_audio_file()
else: else:
self.speechEngine.speak("Paused") self.speechEngine.speak("Paused")
self.audioPlayer.pause() self.audioPlayer.pause_audio_file()
elif event.key == pygame.K_n: elif event.key == pygame.K_n:
if not self.book: if not self.book:
@@ -1985,16 +1970,12 @@ class BookReader:
readerEngine = self.config.get_reader_engine() readerEngine = self.config.get_reader_engine()
isAudioBook = self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook isAudioBook = self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook
if isAudioBook: if isAudioBook or (readerEngine == 'piper' and self.isPlaying):
# Audio books: instant speed change via MpvPlayer # Both audio books and Piper-TTS use MpvPlayer: instant speed change
self.audioPlayer.set_speed(newSpeed) self.audioPlayer.set_speed(newSpeed)
elif readerEngine == 'piper' and self.isPlaying: # For Piper-TTS, restart current paragraph to apply new speed immediately
# Piper-TTS: restart current paragraph with new speed if not isAudioBook and self.isPlaying:
# Stop current subprocess self.audioPlayer.stop_audio_file()
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=0.5)
# Restart playback of current paragraph
self._start_paragraph_playback() self._start_paragraph_playback()
# Speak feedback # Speak feedback
@@ -2066,11 +2047,11 @@ class BookReader:
self.config.set_last_book(bookPath) self.config.set_last_book(bookPath)
self.config.set_books_directory(str(self.bookPath.parent)) self.config.set_books_directory(str(self.bookPath.parent))
# Reset position # Reset audio position state
self.currentChapter = 0 self.savedAudioPosition = 0.0
self.currentParagraph = 0 self.bookmarkCleared = False
# Load new book # Load new book (which will restore bookmark if it exists)
try: try:
self.load_book() self.load_book()
self.speechEngine.speak("Ready") self.speechEngine.speak("Ready")
@@ -2088,28 +2069,17 @@ class BookReader:
isAudioBook = hasattr(self.book, 'isAudioBook') and self.book.isAudioBook isAudioBook = hasattr(self.book, 'isAudioBook') and self.book.isAudioBook
readerEngine = self.config.get_reader_engine() readerEngine = self.config.get_reader_engine()
if isAudioBook: if isAudioBook or readerEngine == 'piper':
# Stop audio file playback # Stop audio playback (audio books or TTS via MpvPlayer)
if readerEngine == 'piper':
self._cancel_buffer()
if self.audioPlayer.is_audio_file_loaded():
self.audioPlayer.stop_audio_file() self.audioPlayer.stop_audio_file()
else:
self.audioPlayer.stop()
elif readerEngine == 'speechd': elif readerEngine == 'speechd':
# Cancel speech-dispatcher reading # Cancel speech-dispatcher reading
self.readingEngine.cancel_reading() self.readingEngine.cancel_reading()
else:
# Stop piper-tts playback and cancel buffering
self._cancel_buffer()
# Terminate the TTS mpv subprocess if it's running
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
try:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=1)
except subprocess.TimeoutExpired:
# Force kill if it doesn't terminate gracefully
print("Warning: TTS subprocess didn't terminate, force killing")
self.ttsMpvProcess.kill()
self.ttsMpvProcess.wait(timeout=1)
except Exception as e:
print(f"Error terminating TTS subprocess: {e}")
self.audioPlayer.stop()
def _restart_current_paragraph(self): def _restart_current_paragraph(self):
""" """
@@ -2177,67 +2147,24 @@ class BookReader:
wavData = self.ttsEngine.text_to_wav_data(paragraph) wavData = self.ttsEngine.text_to_wav_data(paragraph)
if wavData: if wavData:
# Stop any existing TTS mpv process # Stop any existing audio playback
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None: if self.audioPlayer.is_audio_file_playing():
self.ttsMpvProcess.terminate() self.audioPlayer.stop_audio_file()
self.ttsMpvProcess.wait()
# Get audio parameters from TTS engine
audioParams = self.ttsEngine.get_audio_params()
sampleRate = audioParams['sampleRate']
sampleWidth = audioParams['sampleWidth']
channels = audioParams['channels']
# Determine mpv audio format string
# piper-tts outputs 16-bit signed PCM
mpvAudioFormat = 's16' # 16-bit signed integer
if channels == 2: # Stereo
mpvAudioFormat += 'le' # Little-endian (default for WAV)
# Launch mpv subprocess to read from stdin
# Get current playback speed from config # Get current playback speed from config
playbackSpeed = self.config.get_playback_speed() playbackSpeed = self.config.get_playback_speed()
mpvCmd = [
'mpv',
'--no-terminal',
f'--speed={playbackSpeed}',
'--', '-'
]
self.ttsMpvProcess = subprocess.Popen( # Play WAV data through MpvPlayer (which supports pause/resume)
mpvCmd, if self.audioPlayer.play_wav_data(wavData, playbackSpeed=playbackSpeed):
stdin=subprocess.PIPE # Start buffering next paragraph in background
) self._buffer_next_paragraph()
else:
# Write WAV data to mpv's stdin in a separate thread print("Error: Failed to start TTS playback")
def write_mpv_stdin(process, data): self.isPlaying = False
try:
process.stdin.write(data)
process.stdin.flush()
process.stdin.close()
# Explicitly delete data to free memory immediately
del data
except Exception as e:
print(f"Error writing WAV data to mpv stdin: {e}")
finally:
# Wait for mpv to finish and clean up the process
try:
process.wait()
except:
pass
threading.Thread(
target=write_mpv_stdin,
args=(self.ttsMpvProcess, wavData),
daemon=True
).start()
# Explicitly delete wavData after playback starts to free memory # Explicitly delete wavData after playback starts to free memory
del wavData del wavData
wavData = None wavData = None
# Start buffering next paragraph in background
self._buffer_next_paragraph()
else: else:
print("Warning: No audio data generated") print("Warning: No audio data generated")
except Exception as e: except Exception as e:
@@ -2430,10 +2357,7 @@ class BookReader:
self.sessionId = None self.sessionId = None
self._cancel_buffer() self._cancel_buffer()
# Terminate the TTS mpv subprocess if it's running # Cleanup audio player (handles both TTS and audio books)
if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None:
self.ttsMpvProcess.terminate()
self.ttsMpvProcess.wait(timeout=1)
self.audioPlayer.cleanup() self.audioPlayer.cleanup()
self.speechEngine.cleanup() self.speechEngine.cleanup()
if self.readingEngine: if self.readingEngine:
+46 -4
View File
@@ -62,12 +62,54 @@ class MpvPlayer:
print(f"Warning: Could not initialize mpv: {e}") print(f"Warning: Could not initialize mpv: {e}")
self.isInitialized = False self.isInitialized = False
def play_wav_data(self, wavData): def play_wav_data(self, wavData, playbackSpeed=None):
""" """
This method is no longer used for TTS playback. Play WAV data directly from memory (for TTS)
TTS playback is now handled directly by BookReader using subprocess.
Args:
wavData: WAV file data as bytes
playbackSpeed: Playback speed (0.5 to 2.0), uses current speed if None
Returns:
True if playback started successfully
""" """
print("Warning: MpvPlayer.play_wav_data is deprecated and should not be called.") if not self.isInitialized or not self.player:
return False
import tempfile
tempFile = None
try:
# Create a temporary file for the WAV data
# python-mpv needs a file path, it can't play from memory directly
tempFile = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
tempFile.write(wavData)
tempFile.close()
# Use current playback speed if not specified
if playbackSpeed is None:
playbackSpeed = self.playbackSpeed
# Load and play the temp file
success = self.load_audio_file(tempFile.name, playbackSpeed=playbackSpeed)
if success:
success = self.play_audio_file()
# Clean up temp file after a delay (mpv needs time to load it)
if tempFile:
import threading
import time
def cleanup_temp_file(filepath):
time.sleep(5) # Wait for mpv to fully load the file
try:
os.unlink(filepath)
except:
pass
threading.Thread(target=cleanup_temp_file, args=(tempFile.name,), daemon=True).start()
return success
except Exception as e:
print(f"Error playing WAV data: {e}")
return False return False