diff --git a/src/calibre/gui2/tts/linux_config.py b/src/calibre/gui2/tts/linux_config.py index 5a532b88d9..2eab61443c 100644 --- a/src/calibre/gui2/tts/linux_config.py +++ b/src/calibre/gui2/tts/linux_config.py @@ -81,7 +81,7 @@ class Widget(QWidget): l.addRow(_('&Speed of speech:'), s) s.setRange(self.tts_client.min_rate, self.tts_client.max_rate) s.setSingleStep(10) - s.setTickInterval((self.tts_client.max_rate - self.tts_client.min_rate) // 2) + s.setTickInterval((s.maximum() - s.minimum()) // 2) self.output_modules = om = QComboBox(self) with BusyCursor(): diff --git a/src/calibre/gui2/tts/macos.py b/src/calibre/gui2/tts/macos.py index 67b19d4df8..eb9c2fa495 100644 --- a/src/calibre/gui2/tts/macos.py +++ b/src/calibre/gui2/tts/macos.py @@ -10,6 +10,8 @@ class Client: mark_template = '[[sync 0x{:x}]]' END_MARK = 0xffffffff name = 'nsss' + min_rate = 10 + max_rate = 340 @classmethod def escape_marked_text(cls, text): @@ -24,12 +26,20 @@ class Client: self.current_marked_text = self.last_mark = None self.dispatch_on_main_thread = dispatch_on_main_thread self.status = {'synthesizing': False, 'paused': False} - self.apply_settings(settings) + self.settings = settings + self.ignore_next_stop_event = False + self.apply_settings() def apply_settings(self, new_settings=None): - settings = new_settings or {} - self.nsss.set_current_rate(settings.get('rate', self.default_system_rate)) - self.nsss.set_current_voice(settings.get('voice') or self.default_system_voice) + if self.status['paused']: + self.nsss.resume() + self.ignore_next_stop_event = True + self.status = {'synthesizing': False, 'paused': False} + if new_settings is not None: + self.settings = new_settings + self.nsss.set_current_voice(self.settings.get('voice') or self.default_system_voice) + rate = self.settings.get('rate', self.default_system_rate) + self.nsss.set_current_rate(rate) def __del__(self): self.nsss = None @@ -40,15 +50,13 @@ class Client: event = None if message_type == MARK: self.last_mark = data - if data == self.END_MARK: - event = Event(EventType.end) - self.status = {'synthesizing': False, 'paused': False} - else: - event = Event(EventType.mark, data) + event = Event(EventType.mark, data) elif message_type == END: - if not data: # normal end event is handled by END_MARK - event = Event(EventType.cancel) - self.status = {'synthesizing': False, 'paused': False} + if self.ignore_next_stop_event: + self.ignore_next_stop_event = False + return + event = Event(EventType.end if data else EventType.cancel) + self.status = {'synthesizing': False, 'paused': False} if event is not None and self.current_callback is not None: try: self.current_callback(event) @@ -64,9 +72,6 @@ class Client: def speak_marked_text(self, text, callback): self.current_callback = callback - # on macOS didFinishSpeaking is never called for some reason, so work - # around it by adding an extra, special mark at the end - text += self.mark_template.format(self.END_MARK) self.current_marked_text = text self.last_mark = None self.nsss.speak(text) @@ -88,17 +93,22 @@ class Client: self.current_callback(Event(EventType.resume)) def resume_after_configure(self): - if self.status['paused'] and self.last_mark is not None and self.current_marked_text: + if self.status['paused']: + self.resume() + return + if self.last_mark is None: + idx = -1 + else: mark = self.mark_template.format(self.last_mark) idx = self.current_marked_text.find(mark) - if idx == -1: - text = self.current_marked_text - else: - text = self.current_marked_text[idx:] - self.nsss.speak(text) - self.status = {'synthesizing': True, 'paused': False} - if self.current_callback is not None: - self.current_callback(Event(EventType.resume)) + if idx == -1: + text = self.current_marked_text + else: + text = self.current_marked_text[idx:] + self.nsss.speak(text) + self.status = {'synthesizing': True, 'paused': False} + if self.current_callback is not None: + self.current_callback(Event(EventType.resume)) def stop(self): self.nsss.stop() @@ -121,3 +131,18 @@ class Client: def config_widget(self, backend_settings, parent): from calibre.gui2.tts.macos_config import Widget return Widget(self, backend_settings, parent) + + def change_rate(self, steps=1): + rate = current_rate = self.settings.get('rate', self.default_system_rate) + step_size = (self.max_rate - self.min_rate) // 10 + rate += steps * step_size + rate = max(self.min_rate, min(rate, self.max_rate)) + if rate != current_rate: + self.settings['rate'] = rate + prev_state = self.status.copy() + self.pause() + self.apply_settings() + if prev_state['synthesizing']: + self.status = {'synthesizing': True, 'paused': False} + self.resume_after_configure() + return self.settings diff --git a/src/calibre/gui2/tts/macos_config.py b/src/calibre/gui2/tts/macos_config.py index 05825c5e11..0f0f84a596 100644 --- a/src/calibre/gui2/tts/macos_config.py +++ b/src/calibre/gui2/tts/macos_config.py @@ -83,8 +83,9 @@ class Widget(QWidget): self.speed = s = QSlider(Qt.Orientation.Horizontal, self) s.setMinimumWidth(200) l.addRow(_('&Speed of speech (words per minute):'), s) - delta = self.default_system_rate - 50 - s.setRange(self.default_system_rate - delta, self.default_system_rate + delta) + s.setRange(self.tts_client.min_rate, self.tts_client.max_rate) + s.setTickPosition(QSlider.TickPosition.TicksAbove) + s.setTickInterval((s.maximum() - s.minimum()) // 2) s.setSingleStep(10) self.voices = v = QTableView(self) diff --git a/src/calibre/gui2/tts/windows.py b/src/calibre/gui2/tts/windows.py index 64cac9dd33..08e98425de 100644 --- a/src/calibre/gui2/tts/windows.py +++ b/src/calibre/gui2/tts/windows.py @@ -139,8 +139,11 @@ class Client: if self.status['paused']: self.resume() else: - mark = self.mark_template.format(self.last_mark) - idx = self.current_marked_text.find(mark) + if self.last_mark is None: + idx = -1 + else: + mark = self.mark_template.format(self.last_mark) + idx = self.current_marked_text.find(mark) if idx == -1: text = self.current_marked_text else: diff --git a/src/calibre/gui2/viewer/tts.py b/src/calibre/gui2/viewer/tts.py index 0ff7461490..4d2e596065 100644 --- a/src/calibre/gui2/viewer/tts.py +++ b/src/calibre/gui2/viewer/tts.py @@ -95,7 +95,7 @@ class TTS(QObject): def play(self, data): marked_text = add_markup(data['marked_text'], self.tts_client_class.mark_template) - self.tts_client.speak_marked_text(marked_text, self.callback) + self.tts_client.speak_marked_text(marked_text.strip(), self.callback) def pause(self, data): self.tts_client.pause()