This project provides a robust, asynchronous 32-bit bridge for the IBMTTS (IBM Text-to-Speech) ECI engine. It is designed to allow modern 64-bit applications (or other 32-bit clients) to communicate with the legacy 32-bit eci.dll through a high-performance Inter-Process Communication (IPC) layer.
The host is built with a multi-threaded, asynchronous architecture using Rust. It acts as a middleware that manages the lifecycle of the TTS engine and handles requests from multiple clients.
- Named Pipes (IPC Layer):
The primary communication channel. The host creates a server at
\\.\pipe\ibmtts_host32using Message Mode. Unlike byte streams, message mode ensures that awritefrom the client corresponds exactly to areadon the server, preserving packet boundaries.
- Overlapped I/O: Uses Windows Completion Routines (APCs) to handle communication without blocking the main thread.
- The Handler System:
Located in
worker.rs, this system dispatches incoming binary packets to specific Rust functions. It uses aRegistry(a HashMap of IDs to function pointers) to route requests efficiently. - The Worker: The worker logic translates generic IPC requests into specific calls to the IBMTTS library. It handles data types like signed/unsigned integers, byte strings, and UTF-8 text.
- LibLoader:
A dynamic wrapper around
libloading. It uses a custom macrodefine_eci_api!to map the C-style exports ofeci.dllinto a type-safe Rust structure at runtime.
cargo build --target=i686-pc-windows-msvc or cargo run --target=i686-pc-windows-msvc or cargo build --release --lib --target i686-pc-windows-msvc
In this architecture, the Shared Memory is the high-speed highway used exclusively for audio data and real-time callbacks. While the Named Pipe handles commands (like "set volume"), the Shared Memory handles the heavy lifting of audio samples to avoid the overhead of the IPC pipe.
IBMTTS requires a callback function to return audio samples. Since callbacks happen on the Host's side, we use Shared Memory to stream high-bandwidth audio data back to the client without the overhead of pipe copies.
- Shared Memory: A memory-mapped file named
Local\eci_shm_[handle]. - Events: Two Windows Events (
h_evt_readyandh_evt_processed) act as a hardware-like handshake.
- Host writes audio to Shared Memory.
- Host signals
ready. - Client reads audio.
- Client signals
processed.
When the client calls set_buffer (API ID 5), the host creates a memory-mapped file. The structure is strictly defined so both the Rust Host and your Client (e.g., Python) know exactly where every byte sits.
You must specify the samples of the audio buffer, e.g. 3300.
The memory is divided into a Header (first 12 bytes) and a Data Buffer (the rest).
| Offset (Bytes) | Field Name | Type | Description |
|---|---|---|---|
| 0 - 3 | h_engine |
u32 |
Input/Output: In callback, the Engine Handle. After processed, the Client writes the return code here. |
| 4 - 7 | msg |
u32 |
Output: The ECI Message ID (e.g., eciWaveformBuffer, eciIndexReply). |
| 8 - 11 | lparam |
i32 |
Output: Message-specific data (e.g., number of samples or index value). |
| 12 - End | audio_data |
i16[] |
Output: The raw PCM audio samples (16-bit Mono). |
Because Shared Memory is "passive," we use two Windows Events to synchronize access. This prevents the Client from reading while the Host is still writing.
- Host writes to the Shared Memory (Header + Audio).
- Host signals
h_evt_ready(The Client wakes up). - Client reads the
msg,lparam, andaudio_data. - Client writes a result code (usually
1for Success) into the first 4 bytes (h_engine). - Client signals
h_evt_processed(The Host wakes up).
When the Client receives the h_evt_ready signal, it should interpret the bytes in the shared memory as follows:
The Client must check the u32 at offset 4 to know what just happened in the engine:
eciWaveformBuffer(Value: 3): New audio is available. Look atlparam(Offset 8) to see how many samples were written. The audio starts at Offset 12.eciIndexReply(Value: 4): A text index has been reached.lparamcontains the index ID you inserted.eciPhonemeBuffer(Value: 0): Phoneme data is available (if enabled).
If the message is audio (eciWaveformBuffer), the total bytes to read from the buffer starting at offset 12 is:
(Since each sample is a 16-bit / 2-byte integer).
The Host's eci_callback function is waiting for a return value to tell the legacy DLL what to do next. The client must write this value back to Offset 0 before signaling processed:
1(eciDataProcessed): Everything is fine, continue synthesis.2(eciDataAbort): Stop synthesis immediately.0(eciDataNotProcessed): The client ignored this specific message.
If you need to support more functions from the legacy DLL:
- Open
libLoader.rs. - Add the function signature inside the
define_eci_api!macro block. - Ensure the signature matches the C header (usually
stdcall/system).
define_eci_api! {
eciNewFunction(arg1: i32) -> i32;
}To expose a new feature to the IPC client:
- Go to
worker.rs. - Create a function and decorate it with the
#[api(ID)]attribute. - Use the
RequestContextto extract arguments.
#[api(20)] // New unique ID
fn my_new_feature(ctx: &RequestContext) -> Vec<u8> {
let value = ctx.get_int(0);
// Call eci()...
pack_int(1)
}The host is compiled as a dynamic library (.dll) to be launched via rundll32.exe.
To launch the host correctly from Python or a Command Prompt, use the following syntax:
rundll32.exe "path\to\ibmtts_host32.dll",StartHost [PID]
- The Parent PID: You must pass the Process ID (PID) of the client application (e.g., your Python script).
- The Suicidal Thread: The host starts a "Parent Monitor" thread. It checks every 5 seconds if the provided PID is still alive. If the parent process crashes or is closed, the host will automatically terminate to avoid leaving "zombie" processes in the system.
To talk to this host, your client must:
- Connect: Open
\\.\pipe\ibmtts_host32as a file. - Request Format: Send a binary packet where:
Bytes 0-1: Function ID (Little Endianu16).Bytes 2+: Parameters (4-byte integers or offsets to strings).- after parameters: strings if there any, null terminated. The start offset of the strings must be set in the parameters.
See the file src\worker.rs to know about the order of the parameters for each endpoint.
- Read Response:
Byte 0: Type (0=Int, 1=UInt, 2=Bytes, 3=UTF8).Remaining bytes: The data response.
Example Flow:
- Send ID
1(LoadLibrary) with the path toeci.dll. - Send ID
3(eciNew) to get an engine handle. Please store the handle. - Send ID
5(set_buffer) to initialize shared memory audio streaming. - Set the events and open the shared memory block.
flowchart LR
%% STYLES
classDef driver fill:#E3F2FD,stroke:#1565C0,stroke-width:2px,color:#0D47A1,font-weight:bold
classDef proxy fill:#FFF8E1,stroke:#FF8F00,stroke-width:2px,color:#7F5100,font-weight:bold
classDef host fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#1B5E20,font-weight:bold
classDef ipc fill:#F5F5F5,stroke:#78909C,stroke-width:2px,stroke-dasharray:5 5,color:#37474F,font-weight:bold
classDef logic fill:#FFFFFF,stroke:#37474F,stroke-width:1px,color:#000
%% DRIVER LANE
subgraph DRIVER_LANE["Driver"]
direction TB
A["1. π» NVDA Driver"]:::driver --> B{"2. 32-bit or 64-bit?"}:::logic
B -- "32-bit" --> C["3. Load native eci.dll"]:::driver
B -- "64-bit" --> D["3. Proxy (Python EciDLL)"]:::proxy
end
%% CONTROL PIPE
subgraph PIPE_LANE["Named Pipe (Control / RPC)"]
E["\\\\.\\pipe\\ibmtts_host32"]:::ipc
end
D -- "4. Send command + params" --> E
E -- "5. Execute command" --> F["6. Host 32-bit"]:::host
%% HOST PROCESS
subgraph HOST_ZONE["Host 32-bit Process"]
direction TB
F --- G["7. eci.dll"]:::host
end
%% AUDIO ENGINE
subgraph AUDIO_ENGINE["Audio Engine / Low Latency"]
H["1. Shared Memory Buffer"]:::ipc
I["2. Event: READY"]:::ipc
J["3. Event: PROCESSED"]:::ipc
end
%% FLOW LOGIC (Callback Thread)
G -.->|4. Callback writes audio| H
G -.->|5. Signal READY event| I
J -.->|6. Unblock callback| G
%% FLOW LOGIC (Proxy Worker Thread)
I -.->|7. Wait for READY event| D
D -.->|8. Read and process audio| H
D -.->|9. Signal PROCESSED event| J
%% CONTAINER STYLES
style DRIVER_LANE fill:#fcfcfc,stroke:#CCC,stroke-width:1px
style PIPE_LANE fill:#fcfcfc,stroke:#CCC,stroke-width:1px
style HOST_ZONE fill:#fcfcfc,stroke:#CCC,stroke-width:1px
style AUDIO_ENGINE fill:#fafafa,stroke:#BBB,stroke-width:1px,stroke-dasharray:5 5