Skip to content

Commit

Permalink
reworking to minimise changes in driver proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
willwade committed Nov 4, 2024
1 parent d59af8e commit c5d231c
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 49 deletions.
10 changes: 5 additions & 5 deletions pyttsx3/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def __init__(self, engine, driverName, debug):
self._engine = engine
self._queue = []
self._busy = True
self._looping = False
self._name = None
self._iterator = None
self._debug = debug
Expand Down Expand Up @@ -194,8 +193,6 @@ def startLoop(self, useDriverLoop):
"""
Called by the engine to start an event loop.
"""
print("driver.startLoop setting looping to true..")
self._looping = True
if useDriverLoop:
self._driver.startLoop()
else:
Expand All @@ -205,13 +202,16 @@ def endLoop(self, useDriverLoop):
"""
Called by the engine to stop an event loop.
"""
print("DriverProxy.endLoop called, setting looping to False")
self._looping = False
print("DriverProxy.endLoop called; useDriverLoop:", useDriverLoop)
self._queue = []
self._driver.stop()
if useDriverLoop:
print("DriverProxy.endLoop calling driver.endLoop")
self._driver.endLoop()
else:
print(
"DriverProxy.endLoop; not calling driver.endLoop, setting iterator to None"
)
self._iterator = None
self.setBusy(True)

Expand Down
129 changes: 86 additions & 43 deletions pyttsx3/drivers/espeak.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ def __init__(self, proxy):
EspeakDriver._defaultVoice = "default"
EspeakDriver._moduleInitialized = True
self._proxy = proxy
print("espeak init setting looping to false..")
self._proxy._looping = False
self._looping = False
self._stopping = False
self._speaking = False
self._text_to_say = None
Expand All @@ -60,10 +59,14 @@ def destroy():
_espeak.SetSynthCallback(None)

def stop(self):
print("EspeakDriver.stop called, setting _stopping to True")
if _espeak.IsPlaying():
self._stopping = True
_espeak.Cancel()
if not self._stopping:
print("[DEBUG] EspeakDriver.stop called")
if self._looping:
self._stopping = True
self._looping = False
if _espeak.IsPlaying():
_espeak.Cancel()
self._proxy.setBusy(False)

@staticmethod
def getProperty(name: str):
Expand Down Expand Up @@ -169,17 +172,54 @@ def _onSynth(self, wav, numsamples, events):
location=event.text_position,
length=event.length,
)

elif event.type == _espeak.EVENT_MSG_TERMINATED:
print("EVENT_MSG_TERMINATED detected, ending loop.")
# Ensure the loop stops when synthesis completes
self._proxy._looping = False
# Final event indicating synthesis completion
if self._save_file:
try:
with wave.open(self._save_file, "wb") as f:
f.setnchannels(1) # Mono
f.setsampwidth(2) # 16-bit samples
f.setframerate(22050) # 22,050 Hz sample rate
f.writeframes(self._data_buffer)
print(f"Audio saved to {self._save_file}")
except Exception as e:
raise RuntimeError(f"Error saving WAV file: {e}")
else:
try:
with NamedTemporaryFile(
suffix=".wav", delete=False
) as temp_wav:
with wave.open(temp_wav, "wb") as f:
f.setnchannels(1) # Mono
f.setsampwidth(2) # 16-bit samples
f.setframerate(22050) # 22,050 Hz sample rate
f.writeframes(self._data_buffer)

temp_wav_name = temp_wav.name
temp_wav.flush()

# Playback functionality (for say method)
if platform.system() == "Darwin": # macOS
subprocess.run(["afplay", temp_wav_name], check=True)
elif platform.system() == "Linux":
os.system(f"aplay {temp_wav_name} -q")
elif platform.system() == "Windows":
winsound.PlaySound(temp_wav_name, winsound.SND_FILENAME)

# Remove the file after playback
os.remove(temp_wav_name)
except Exception as e:
print(f"Playback error: {e}")

# Clear the buffer and mark as finished
self._data_buffer = b""
self._speaking = False
self._proxy.notify("finished-utterance", completed=True)
self._proxy.setBusy(False)
if not self._is_external_loop:
self.endLoop() # End loop only if not in an external loop
self.endLoop()
break
elif event.type == _espeak.EVENT_END:
print("EVENT_END detected.")
# Optional: handle end of an utterance if applicable
pass

i += 1

Expand All @@ -192,50 +232,53 @@ def _onSynth(self, wav, numsamples, events):
return 0

def endLoop(self):
print("Ending loop...")
print("EspeakDriver.endLoop called, setting looping to False")
self._proxy._looping = False
print("endLoop called; external:", self._is_external_loop)
self._looping = False

def startLoop(self, external=False):
print(f"EspeakDriver: Entering startLoop (external={external})")
self._proxy._looping = True
self._is_external_loop = external # Track if it's an external loop
timeout = time.time() + 10
while self._proxy._looping:
if time.time() > timeout:
print("Exiting startLoop due to timeout.")
self._proxy._looping = False
first = True
self._looping = True
self._is_external_loop = external
if external:
self._iterator = self.iterate() or iter([])

while self._looping:
if not self._looping:
print("Exiting loop")
break
print("EspeakDriver loop iteration")
if self._text_to_say:
self._start_synthesis(self._text_to_say)
if first:
print("Starting loop on first")
self._proxy.setBusy(False)
first = False
if self._text_to_say:
print("Synthesizing text on first")
self._start_synthesis(self._text_to_say)
self._text_to_say = None # Avoid re-synthesizing

try:
next(self.iterate())
if not external:
print("Iterating on not external")
next(self.iterate())
time.sleep(0.01)
except StopIteration:
print("StopIteration in startLoop")
break
time.sleep(0.01)

print("EspeakDriver: Exiting startLoop")

def iterate(self):
print("running espeak iterate once")
if not self._proxy._looping:
print("Not looping, returning from iterate...")
"""Process events within an external loop context."""
if not self._looping:
return

if self._stopping:
# Cancel the current utterance if stopping
_espeak.Cancel()
print("Exiting iterate due to stop.")
self._stopping = False
self._proxy.notify("finished-utterance", completed=False)
self._proxy.setBusy(False)
self._proxy._looping = False # Mark the loop as done
return
self.endLoop() # Set `_looping` to False, signaling exit

# Only call endLoop in an internal loop, leave external control to external loop handler
if not self._is_external_loop:
self.endLoop()
yield # Yield back to `startLoop`
# Yield only if in an external loop to hand control back
if self._is_external_loop:
yield

def say(self, text):
self._text_to_say = text
2 changes: 1 addition & 1 deletion test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def demo_interrupting_utterance():
print("\nRunning demo_interrupting_utterance...")
engine.say("The quick brown fox jumped over the lazy dog.")
engine.runAndWait()
engine.endLoop()


# Demo for test_external_event_loop
Expand All @@ -53,6 +52,7 @@ def external_loop():
engine.say("The quick brown fox jumped over the lazy dog.")
engine.startLoop(False) # Start loop without blocking
external_loop()
print("Calling endLoop from external demo...")
engine.endLoop() # End the event loop explicitly


Expand Down

0 comments on commit c5d231c

Please sign in to comment.