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:
182
bookstorm.py
182
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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user