diff --git a/bookstorm.py b/bookstorm.py index 6d459b7..9118a35 100755 --- a/bookstorm.py +++ b/bookstorm.py @@ -73,8 +73,7 @@ class BookReader: self.parser = None # Will be set based on file type self.bookmarkManager = BookmarkManager() self.speechEngine = SpeechEngine() # UI feedback - self.audioPlayer = MpvPlayer() - self.ttsMpvProcess = None # For direct mpv subprocess for TTS + self.audioPlayer = MpvPlayer() # Used for both audio books and TTS playback # Configure speech engine from saved settings speechRate = self.config.get_speech_rate() @@ -736,31 +735,25 @@ class BookReader: # Start next chapter self._start_paragraph_playback() elif readerEngine == 'piper': - # Check piper-tts subprocess state - # The TTS mpv process runs independently, so we need to check if it's still running - playbackFinished = False - if self.ttsMpvProcess: - # 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 + # Check if TTS audio has finished playing + # Only auto-advance if NOT paused (to avoid skipping when user pauses) + if not self.audioPlayer.is_paused(): + playbackFinished = not self.audioPlayer.is_audio_file_playing() - if playbackFinished: - # Current paragraph finished, advance - if not self.next_paragraph(): - self.displayText = "End of book reached" - self.isPlaying = False - self.save_bookmark(speakFeedback=False) - else: - # Start next paragraph with error recovery - try: - self._start_paragraph_playback() - except Exception as e: - print(f"Error starting playback: {e}") - self.speechEngine.speak("Playback error") + if playbackFinished: + # Current paragraph finished, advance + if not self.next_paragraph(): + self.displayText = "End of book reached" self.isPlaying = False + self.save_bookmark(speakFeedback=False) + else: + # Start next paragraph with error recovery + try: + self._start_paragraph_playback() + except Exception as e: + print(f"Error starting playback: {e}") + self.speechEngine.speak("Playback error") + self.isPlaying = False # Render the screen self._render_screen() @@ -807,18 +800,10 @@ class BookReader: if readerEngine == 'speechd': self.readingEngine.cancel_reading() else: + # Stop audio player (handles both TTS and audio books) self.audioPlayer.stop() - - # Clean up TTS mpv subprocess if it's still running - 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}") + if self.audioPlayer.is_audio_file_loaded(): + self.audioPlayer.stop_audio_file() # Close Audiobookshelf session if active if self.sessionId and self.absClient: @@ -919,13 +904,13 @@ class BookReader: self.speechEngine.speak("Paused") self.readingEngine.pause_reading() else: - # Handle piper-tts pause/resume + # Handle piper-tts pause/resume (now uses audio file methods) if self.audioPlayer.is_paused(): self.speechEngine.speak("Resuming") - self.audioPlayer.resume() + self.audioPlayer.resume_audio_file() else: self.speechEngine.speak("Paused") - self.audioPlayer.pause() + self.audioPlayer.pause_audio_file() elif event.key == pygame.K_n: if not self.book: @@ -1985,17 +1970,13 @@ class BookReader: readerEngine = self.config.get_reader_engine() isAudioBook = self.book and hasattr(self.book, 'isAudioBook') and self.book.isAudioBook - if isAudioBook: - # Audio books: instant speed change via MpvPlayer + if isAudioBook or (readerEngine == 'piper' and self.isPlaying): + # Both audio books and Piper-TTS use MpvPlayer: instant speed change self.audioPlayer.set_speed(newSpeed) - elif readerEngine == 'piper' and self.isPlaying: - # Piper-TTS: restart current paragraph with new speed - # Stop current subprocess - 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() + # For Piper-TTS, restart current paragraph to apply new speed immediately + if not isAudioBook and self.isPlaying: + self.audioPlayer.stop_audio_file() + self._start_paragraph_playback() # Speak feedback speedPercent = int(newSpeed * 100) @@ -2066,11 +2047,11 @@ class BookReader: self.config.set_last_book(bookPath) self.config.set_books_directory(str(self.bookPath.parent)) - # Reset position - self.currentChapter = 0 - self.currentParagraph = 0 + # Reset audio position state + self.savedAudioPosition = 0.0 + self.bookmarkCleared = False - # Load new book + # Load new book (which will restore bookmark if it exists) try: self.load_book() self.speechEngine.speak("Ready") @@ -2088,28 +2069,17 @@ class BookReader: isAudioBook = hasattr(self.book, 'isAudioBook') and self.book.isAudioBook readerEngine = self.config.get_reader_engine() - if isAudioBook: - # Stop audio file playback - self.audioPlayer.stop_audio_file() + if isAudioBook or readerEngine == 'piper': + # 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() + else: + self.audioPlayer.stop() elif readerEngine == 'speechd': # Cancel speech-dispatcher 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): """ @@ -2177,67 +2147,24 @@ class BookReader: wavData = self.ttsEngine.text_to_wav_data(paragraph) if wavData: - # Stop any existing TTS mpv process - if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None: - self.ttsMpvProcess.terminate() - self.ttsMpvProcess.wait() + # Stop any existing audio playback + if self.audioPlayer.is_audio_file_playing(): + self.audioPlayer.stop_audio_file() - # 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 playbackSpeed = self.config.get_playback_speed() - mpvCmd = [ - 'mpv', - '--no-terminal', - f'--speed={playbackSpeed}', - '--', '-' - ] - self.ttsMpvProcess = subprocess.Popen( - mpvCmd, - stdin=subprocess.PIPE - ) - - # Write WAV data to mpv's stdin in a separate thread - def write_mpv_stdin(process, data): - 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() + # Play WAV data through MpvPlayer (which supports pause/resume) + if self.audioPlayer.play_wav_data(wavData, playbackSpeed=playbackSpeed): + # Start buffering next paragraph in background + self._buffer_next_paragraph() + else: + print("Error: Failed to start TTS playback") + self.isPlaying = False # Explicitly delete wavData after playback starts to free memory del wavData wavData = None - - # Start buffering next paragraph in background - self._buffer_next_paragraph() else: print("Warning: No audio data generated") except Exception as e: @@ -2430,10 +2357,7 @@ class BookReader: self.sessionId = None self._cancel_buffer() - # Terminate the TTS mpv subprocess if it's running - if self.ttsMpvProcess and self.ttsMpvProcess.poll() is None: - self.ttsMpvProcess.terminate() - self.ttsMpvProcess.wait(timeout=1) + # Cleanup audio player (handles both TTS and audio books) self.audioPlayer.cleanup() self.speechEngine.cleanup() if self.readingEngine: diff --git a/src/mpv_player.py b/src/mpv_player.py index f6fca50..d6b3259 100644 --- a/src/mpv_player.py +++ b/src/mpv_player.py @@ -62,13 +62,55 @@ class MpvPlayer: print(f"Warning: Could not initialize mpv: {e}") 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. - TTS playback is now handled directly by BookReader using subprocess. + Play WAV data directly from memory (for TTS) + + 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.") - return False + 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 def pause(self):