| 1489 | // ============================================================================= |
| 1490 | |
| 1491 | int rcli_speak_streaming(RCLIHandle handle, const char* text, |
| 1492 | RCLIEventCallback callback, void* user_data) { |
| 1493 | if (!handle || !text) return -1; |
| 1494 | auto* engine = static_cast<RCLIEngine*>(handle); |
| 1495 | if (!engine->initialized) return -1; |
| 1496 | |
| 1497 | std::lock_guard<std::mutex> lock(engine->mutex); |
| 1498 | engine->streaming_cancelled.store(false, std::memory_order_release); |
| 1499 | |
| 1500 | auto t_start = std::chrono::steady_clock::now(); |
| 1501 | |
| 1502 | auto* rb = engine->pipeline.playback_ring_buffer(); |
| 1503 | if (!rb) return rcli_speak(handle, text); // fallback to afplay |
| 1504 | |
| 1505 | // Ensure CoreAudio playback is running |
| 1506 | if (!engine->pipeline.audio().is_running()) { |
| 1507 | engine->pipeline.audio().start(); |
| 1508 | } |
| 1509 | rb->clear(); |
| 1510 | |
| 1511 | // Split text into sentences and stream through TTS |
| 1512 | std::mutex tts_queue_mutex; |
| 1513 | std::condition_variable tts_queue_cv; |
| 1514 | std::vector<std::string> tts_queue; |
| 1515 | bool feeding_done = false; |
| 1516 | bool first_audio_fired = false; |
| 1517 | double total_tts_ms = 0; |
| 1518 | int sentence_count = 0; |
| 1519 | |
| 1520 | // TTS worker thread |
| 1521 | std::thread tts_worker([&]() { |
| 1522 | pthread_setname_np("rcli.tts.speak"); |
| 1523 | while (true) { |
| 1524 | std::string sentence; |
| 1525 | { |
| 1526 | std::unique_lock<std::mutex> lk(tts_queue_mutex); |
| 1527 | tts_queue_cv.wait(lk, [&]() { |
| 1528 | return !tts_queue.empty() || feeding_done; |
| 1529 | }); |
| 1530 | if (tts_queue.empty() && feeding_done) break; |
| 1531 | if (tts_queue.empty()) continue; |
| 1532 | sentence = std::move(tts_queue.front()); |
| 1533 | tts_queue.erase(tts_queue.begin()); |
| 1534 | } |
| 1535 | |
| 1536 | if (engine->streaming_cancelled.load(std::memory_order_acquire)) break; |
| 1537 | |
| 1538 | std::vector<float> samples; |
| 1539 | if (engine->pipeline.using_metalrt_tts()) { |
| 1540 | samples = engine->pipeline.metalrt_tts().synthesize(sentence); |
| 1541 | total_tts_ms += engine->pipeline.metalrt_tts().last_synthesis_ms(); |
| 1542 | } else { |
| 1543 | samples = engine->pipeline.tts().synthesize(sentence); |
| 1544 | } |
| 1545 | // Write with backpressure |
| 1546 | size_t offset = 0; |
| 1547 | while (offset < samples.size() && |
| 1548 | !engine->streaming_cancelled.load(std::memory_order_acquire)) { |
no test coverage detected