diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..06425dd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,89 @@ +name: Build + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build-windows: + name: Build Windows (${{ matrix.arch }}) + runs-on: windows-latest + + permissions: + contents: read + + strategy: + matrix: + arch: [x86, x64] + + steps: + - uses: actions/checkout@v3 + + - name: Setup CMake + uses: lukka/get-cmake@latest + + - name: Configure CMake + run: | + mkdir build + cd build + cmake -G "Visual Studio 17 2022" -A ${{ matrix.arch == 'x86' && 'Win32' || 'x64' }} .. + + - name: Build + run: | + cd build + cmake --build . --config Release + + - name: Validate Build + shell: bash + run: | + if [ "${{ matrix.arch }}" == "x86" ]; then + test -f build/bin/Release/nvda_sapi32.dll || exit 1 + echo "✓ nvda_sapi32.dll built successfully" + else + test -f build/bin/Release/nvda_sapi64.dll || exit 1 + echo "✓ nvda_sapi64.dll built successfully" + fi + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: nvda-sapi-${{ matrix.arch }} + path: build/bin/Release/*.dll + retention-days: 30 + + build-cross-compile: + name: Cross-compile from Linux + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@v3 + + - name: Install MinGW + run: | + sudo apt-get update + sudo apt-get install -y mingw-w64 g++-mingw-w64 + + - name: Build + run: | + chmod +x build.sh + ./build.sh + + - name: Validate + run: | + chmod +x validate.sh + ./validate.sh + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: nvda-sapi-cross-compiled + path: build/*/bin/*.dll + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36e9bcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Build artifacts +build/ +bin/ +lib/ +*.dll +*.exe +*.obj +*.o +*.a +*.lib +*.exp +*.pdb +*.ilk + +# IDE files +.vs/ +.vscode/ +*.user +*.suo +*.sln +*.vcxproj +*.vcxproj.filters + +# CMake files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +Makefile +install_manifest.txt + +# Ignore CMake generated files but not toolchain files +!toolchain-*.cmake + +# Temporary files +*.log +*.tmp +/tmp/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..4b9b4f4 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,282 @@ +# NVDA SAPI Bridge - Architecture Documentation + +## Overview + +The NVDA SAPI Bridge is a COM DLL that acts as a SAPI5 Text-to-Speech engine, forwarding speech requests to the NVDA screen reader. This document describes the technical architecture and design decisions. + +## System Architecture + +``` +┌─────────────────────────┐ +│ Legacy Application │ +│ (32-bit or 64-bit) │ +└───────────┬─────────────┘ + │ SAPI5 Interface + │ (ISpVoice) + ▼ +┌─────────────────────────┐ +│ Windows SAPI5 Core │ +│ (sapicpl.dll, sapi.dll)│ +└───────────┬─────────────┘ + │ ISpTTSEngine + │ + ▼ +┌─────────────────────────┐ +│ NVDA SAPI Bridge DLL │ +│ (nvda_sapi32/64.dll) │ +│ │ +│ ┌──────────────────┐ │ +│ │ SAPIVoice │ │ +│ │ (ISpTTSEngine) │ │ +│ └────────┬─────────┘ │ +│ │ │ +│ ┌────────▼─────────┐ │ +│ │ NVDAClient │ │ +│ │ (Controller API) │ │ +│ └──────────────────┘ │ +└───────────┬─────────────┘ + │ nvdaControllerClient API + │ + ▼ +┌─────────────────────────┐ +│ NVDA Screen Reader │ +│ (nvda.exe) │ +└─────────────────────────┘ +``` + +## Component Design + +### 1. COM Bridge Layer (`nvda_sapi_bridge.cpp/h`) + +**Responsibilities:** +- DLL entry point and initialization +- COM server registration/unregistration +- Class factory implementation for creating SAPI voice instances +- Server lock management + +**Key Classes:** +- `SAPIVoiceFactory`: IClassFactory implementation for creating voice instances + +**Key Functions:** +- `DllMain`: DLL initialization +- `DllGetClassObject`: COM class factory retrieval +- `DllCanUnloadNow`: Determines if DLL can be unloaded +- `DllRegisterServer`: Registers COM server in Windows registry +- `DllUnregisterServer`: Removes COM registration + +**Design Decisions:** +- Uses a unique CLSID to avoid conflicts with other SAPI engines +- Implements standard COM reference counting +- Supports apartment threading model for compatibility + +### 2. SAPI Voice Engine (`sapi_voice.cpp/h`) + +**Responsibilities:** +- Implements SAPI5 TTS engine interface (`ISpTTSEngine`) +- Processes text fragments from SAPI +- Manages NVDA client instance + +**Key Classes:** +- `SAPIVoice`: Main TTS engine implementation + +**Key Methods:** +- `Speak()`: Processes SAPI text fragments and forwards to NVDA +- `GetOutputFormat()`: Returns audio format (returns text format since we don't generate audio) + +**Design Decisions:** +- Uses `std::unique_ptr` for NVDA client (RAII) +- Aggregates text fragments before sending to NVDA +- Returns success even if NVDA is unavailable to avoid breaking applications +- No audio generation (text-only processing) + +### 3. NVDA Client Layer (`nvda_client.cpp/h`) + +**Responsibilities:** +- Loads NVDA controller client DLL +- Manages function pointers to NVDA API +- Provides simplified interface for speech operations + +**Key Classes:** +- `NVDAClient`: Wrapper for NVDA controller client API + +**Key Methods:** +- `Initialize()`: Loads controller client and resolves functions +- `Speak()`: Sends text to NVDA for speech +- `CancelSpeech()`: Cancels ongoing speech +- `IsNVDARunning()`: Checks NVDA availability + +**Design Decisions:** +- Dynamically loads NVDA controller client (no static linking) +- Tries both 32-bit and 64-bit controller client DLLs +- Gracefully handles NVDA not being available +- Uses RAII for DLL handle management + +## Data Flow + +### Speech Request Flow + +1. **Application initiates speech:** + - Legacy app calls `ISpVoice::Speak()` + - Windows SAPI core routes to registered TTS engine + +2. **SAPI engine receives request:** + - `SAPIVoice::Speak()` is called with text fragments + - Text fragments are parsed and concatenated + +3. **Text forwarding:** + - Complete text is passed to `NVDAClient::Speak()` + - NVDA client checks if NVDA is running + +4. **NVDA speech:** + - Text is sent via `nvdaController_speakText()` + - NVDA queues and speaks the text + +5. **Completion:** + - `CompleteSkip()` notifies SAPI of completion + - Control returns to application + +## Threading Model + +- **COM Apartment Threading**: Each instance runs in its own apartment +- **Thread Safety**: COM handles synchronization +- **NVDA Client**: Thread-safe through COM serialization + +## Error Handling + +### Strategy +1. **Graceful Degradation**: Returns success even if NVDA unavailable +2. **Null Checks**: All pointer parameters validated +3. **COM Error Codes**: Standard HRESULT returns +4. **Resource Cleanup**: RAII ensures proper cleanup + +### Error Scenarios + +| Scenario | Handling | +|----------|----------| +| NVDA not running | Return S_OK (silent failure) | +| Invalid parameters | Return E_INVALIDARG/E_POINTER | +| Out of memory | Return E_OUTOFMEMORY | +| Controller client missing | Initialization fails gracefully | + +## Memory Management + +- **Reference Counting**: COM objects use `AddRef()`/`Release()` +- **Smart Pointers**: `std::unique_ptr` for owned objects +- **No Memory Leaks**: All resources cleaned up in destructors +- **CoTaskMemAlloc**: Used for COM string allocations + +## Build System + +### CMake Configuration +- **Multi-Architecture**: Supports both x86 and x64 +- **Modular**: Separate CMakeLists for source +- **Dependencies**: Minimal (Windows SDK only) + +### Compilation +- **C++17 Standard**: Modern C++ features +- **Unicode**: Full Unicode support (UNICODE/_UNICODE) +- **Optimization**: Release builds optimized + +## Registry Structure + +When registered, the bridge creates two sets of registry entries: + +### 1. COM Server Registration + +``` +HKEY_CLASSES_ROOT\ + CLSID\ + {A65F3370-547A-4E90-90B1-F5DF86FB7815}\ + InProcServer32\ + (Default) = "C:\path\to\nvda_sapi32.dll" + ThreadingModel = "Apartment" +``` + +### 2. SAPI Voice Token Registration + +This is critical for voice discovery by SAPI applications: + +``` +HKEY_LOCAL_MACHINE\ + SOFTWARE\ + Microsoft\ + Speech\ + Voices\ + Tokens\ + NVDA\ + (Default) = "NVDA Screen Reader Voice" + CLSID = "{A65F3370-547A-4E90-90B1-F5DF86FB7815}" + LangDataPath = "409" + Attributes\ + Language = "409" + Gender = "Neutral" + Age = "Adult" + Vendor = "NVDA" + Name = "NVDA" +``` + +The voice token registration is what makes the NVDA voice appear in application voice selection lists. Without this, applications cannot discover the TTS engine even if the COM server is properly registered. + +**Key Points:** +- Voice tokens are stored in `HKEY_LOCAL_MACHINE` (requires admin rights) +- The CLSID links the voice token to the TTS engine COM class +- Attributes help applications filter and display voices appropriately +- Language code 409 = US English (can be extended for other languages) + +## Performance Considerations + +- **Minimal Overhead**: Direct text forwarding +- **No Audio Processing**: No DSP or encoding +- **Lazy Loading**: NVDA client loaded on first use +- **Efficient Text Processing**: Single pass fragment concatenation + +## Security Considerations + +- **Input Validation**: All SAPI inputs validated +- **No Buffer Overflows**: Uses safe string functions +- **DLL Hijacking Protection**: Uses full paths for NVDA client +- **COM Security**: Standard COM security model + +## Limitations + +1. **No Audio Generation**: Cannot be used with applications expecting audio output +2. **NVDA Required**: Requires NVDA to be running +3. **Windows Only**: Platform-specific (Windows COM) +4. **Text Only**: No support for SSML or phonetic pronunciation from SAPI + +## Future Enhancements + +Potential improvements: +- SSML support for advanced speech control +- Configuration UI for voice parameters +- Logging and diagnostics +- Support for SAPI events (word boundaries, phonemes) +- Multiple voice selection + +## Testing Strategy + +Recommended testing: +1. **Unit Tests**: Test individual components in isolation +2. **Integration Tests**: Test with actual SAPI applications +3. **Architecture Tests**: Verify both x86 and x64 builds +4. **NVDA Tests**: Test with NVDA running and stopped +5. **Stress Tests**: Multiple simultaneous speech requests + +## Dependencies + +### Build Time +- CMake 3.15+ +- Windows SDK (for SAPI headers) +- C++17 compiler + +### Runtime +- Windows 7+ +- NVDA with controller client DLL +- Visual C++ Runtime (typically pre-installed) + +## Versioning + +Version scheme: MAJOR.MINOR.PATCH +- **MAJOR**: Breaking API changes +- **MINOR**: New features +- **PATCH**: Bug fixes diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0be9f73 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.15) +project(nvda-sapi VERSION 1.0.0 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Build both 32-bit and 64-bit versions +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(ARCH_SUFFIX "64") + message(STATUS "Building for x64") +else() + set(ARCH_SUFFIX "32") + message(STATUS "Building for x86") +endif() + +# Output directories +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +# Add source directory +add_subdirectory(src) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4da81b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,179 @@ +# Contributing to NVDA SAPI Bridge + +Thank you for considering contributing to the NVDA SAPI Bridge! This document provides guidelines for contributing. + +## Code of Conduct + +This project aims to help the blind and visually impaired community. Be respectful, inclusive, and helpful. + +## How to Contribute + +### Reporting Issues + +If you encounter problems: + +1. Check existing issues first +2. Provide detailed information: + - Windows version + - NVDA version + - Application you're trying to use + - Steps to reproduce + - Error messages + +### Suggesting Enhancements + +Enhancement suggestions are welcome! Please: + +1. Explain the use case +2. Describe the expected behavior +3. Consider backwards compatibility + +### Code Contributions + +#### Getting Started + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/your-feature` +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +#### Coding Standards + +- **C++ Standard**: C++17 +- **Style**: Follow existing code style +- **Comments**: Add comments for complex logic +- **Documentation**: Update docs for user-facing changes + +#### Code Guidelines + +**Good Practices:** +- Use RAII for resource management +- Prefer smart pointers (`std::unique_ptr`, etc.) +- Check all pointers before dereferencing +- Return proper HRESULT codes +- Handle errors gracefully + +**Avoid:** +- Raw `new`/`delete` (use smart pointers) +- Memory leaks +- Buffer overflows +- Unsafe string operations +- Breaking changes without discussion + +#### Building and Testing + +Before submitting: + +1. **Build both architectures:** + ```batch + build.bat + ``` + +2. **Test manually** (if you have access to Windows): + - Register the DLL + - Test with a SAPI5 application + - Verify speech works through NVDA + - Check both 32-bit and 64-bit builds + +3. **Check for warnings:** + - Code should compile without warnings + - Use appropriate warning levels + +#### Commit Messages + +Write clear commit messages: + +``` +Short summary (50 chars or less) + +More detailed explanation if needed. Wrap at 72 characters. +Explain what changed and why, not how (the code shows how). + +- Bullet points are okay +- Reference issues: Fixes #123 +``` + +#### Pull Request Process + +1. Update README.md if needed +2. Update ARCHITECTURE.md for significant changes +3. Ensure the build passes +4. Request review from maintainers +5. Address review feedback + +### Documentation Contributions + +Documentation improvements are valuable: + +- Fix typos and grammar +- Improve clarity +- Add examples +- Update for new features + +### Testing Contributions + +Help with testing: + +- Test with different applications +- Try various Windows versions +- Report compatibility issues +- Suggest test cases + +## Development Setup + +### Windows + +1. Install Visual Studio 2022 +2. Install CMake +3. Clone the repository +4. Run `build.bat` + +### Linux (Cross-Compilation) + +1. Install MinGW-w64: + ```bash + sudo apt-get install mingw-w64 + ``` +2. Clone the repository +3. Run `./build.sh` + +## Project Structure + +``` +nvda-sapi/ +├── src/ # Source code +│ ├── nvda_sapi_bridge.* # COM registration +│ ├── sapi_voice.* # SAPI implementation +│ ├── nvda_client.* # NVDA integration +│ └── sapi_minimal.h # SAPI interface definitions +├── build.bat/sh # Build scripts +├── CMakeLists.txt # Build configuration +└── docs (README, etc.) # Documentation +``` + +## Code Review Criteria + +Pull requests are reviewed for: + +- **Functionality**: Does it work as intended? +- **Code Quality**: Is it well-written and maintainable? +- **Documentation**: Are changes documented? +- **Testing**: Has it been tested? +- **Compatibility**: Does it maintain compatibility? + +## Getting Help + +- Open an issue for questions +- Discuss major changes before implementing +- Ask for clarification on review feedback + +## Recognition + +Contributors will be acknowledged in the project documentation. + +## License + +By contributing, you agree that your contributions will be licensed under the same terms as the project. + +Thank you for helping make technology more accessible! diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..79dc889 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,148 @@ +# NVDA SAPI Bridge - Installation Guide + +## Prerequisites + +Before installing the NVDA SAPI Bridge, ensure you have: + +1. **NVDA Screen Reader**: Download and install from https://www.nvaccess.org/download/ +2. **Administrator Access**: Required for COM DLL registration + +## Installation Steps + +### Step 1: Build the Bridge + +Open a Command Prompt and navigate to the project directory: + +```batch +cd nvda-sapi +build.bat +``` + +This will create two DLL files: +- `build\x86\bin\Release\nvda_sapi32.dll` (for 32-bit applications) +- `build\x64\bin\Release\nvda_sapi64.dll` (for 64-bit applications) + +### Step 2: Register the COM DLL + +You need to register the appropriate DLL based on your application's architecture. + +#### For 32-bit Legacy Applications + +Open Command Prompt as Administrator and run: +```batch +regsvr32 "C:\path\to\nvda-sapi\build\x86\bin\Release\nvda_sapi32.dll" +``` + +#### For 64-bit Applications + +Open Command Prompt as Administrator and run: +```batch +regsvr32 "C:\path\to\nvda-sapi\build\x64\bin\Release\nvda_sapi64.dll" +``` + +**Note**: If you're unsure which version your application uses, register both. + +### Step 3: Configure Your Application + +1. Launch your legacy application +2. Navigate to its speech or accessibility settings +3. Look for a voice selection option - you should see "NVDA" in the list of available voices +4. Select "NVDA Screen Reader Voice" or "NVDA" +5. The application should now speak through NVDA + +**Important**: After registering the DLL, you may need to restart your application for it to detect the new NVDA voice. + +## Verification + +To verify the installation: + +1. Ensure NVDA is running +2. Open your legacy application +3. Check the voice selection menu - "NVDA" should appear in the list +4. Select NVDA as the voice +5. Trigger a speech event in the application +6. You should hear the speech through NVDA + +1. Ensure NVDA is running +2. Launch your legacy application +3. Navigate to voice settings and look for "NVDA" in the voices list +4. Select the NVDA voice +5. Trigger a speech event in the application +6. You should hear the speech through NVDA + +If "NVDA" doesn't appear in the voice list: +- Make sure you registered the correct DLL (32-bit vs 64-bit) +- Restart the application after registration +- Check Windows Event Viewer for registration errors + +## Troubleshooting + +### "Module not found" Error + +This usually means the DLL dependencies are missing. Ensure: +- Visual C++ Redistributables are installed +- The DLL is in an accessible location + +### Registration Fails + +- Ensure you're running Command Prompt as Administrator +- Check the full path to the DLL is correct +- Verify the DLL was built successfully + +### No Speech Output + +1. Verify NVDA is running: Press `NVDA+N` to open NVDA menu +2. Check NVDA is not muted: Press `NVDA+S` to toggle speech +3. Test NVDA directly: Press `NVDA+T` for time announcement +4. Ensure you selected "NVDA" voice in your application's voice settings +5. Restart the application after changing the voice selection +6. Ensure the correct architecture DLL is registered + +### Voice Not Appearing in Application + +1. Verify successful registration - you should see "DllRegisterServer in [DLL path] succeeded" message +2. Restart the application - some apps only enumerate voices at startup +3. Check if you registered the correct DLL architecture (32-bit app needs 32-bit DLL) +4. Try registering both 32-bit and 64-bit DLLs if unsure +5. Check Windows Registry: + - Open Registry Editor (regedit) + - Navigate to `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens` + - Look for "NVDA" key - it should exist after successful registration + +## Uninstallation + +To remove the NVDA SAPI Bridge: + +1. Unregister the DLLs (as Administrator): + ```batch + regsvr32 /u "C:\path\to\nvda-sapi\build\x86\bin\Release\nvda_sapi32.dll" + regsvr32 /u "C:\path\to\nvda-sapi\build\x64\bin\Release\nvda_sapi64.dll" + ``` + +2. Delete the build directory: + ```batch + rmdir /s /q build + ``` + +## Advanced Configuration + +### Custom Installation Path + +If you want to install the DLLs to a specific location: + +1. Copy the DLL to your desired location (e.g., `C:\Program Files\NVDA-SAPI\`) +2. Register from that location: + ```batch + regsvr32 "C:\Program Files\NVDA-SAPI\nvda_sapi32.dll" + ``` + +### Multiple Applications + +You can use the bridge with multiple applications simultaneously. Each application will independently send speech to NVDA, which will queue the speech appropriately. + +## Support + +For issues and questions: +- Check the main README.md for troubleshooting tips +- Review NVDA documentation at https://www.nvaccess.org/documentation/ +- File issues on the project repository diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..85fdfa2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 NVDA SAPI Bridge Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..a75130c --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,119 @@ +# NVDA SAPI Bridge - Quick Start Guide + +This guide will help you quickly get started with the NVDA SAPI Bridge. + +## What is this? + +The NVDA SAPI Bridge is a small DLL that makes legacy applications that only support Microsoft SAPI5 (Speech API) work with NVDA screen reader. This is useful for blind users who prefer NVDA but need to use old software that doesn't natively support it. + +## Quick Start + +### 1. Prerequisites + +- **NVDA Screen Reader**: Download from https://www.nvaccess.org/ +- **Build Tools** (only if building from source): + - Windows: Visual Studio 2022 or MinGW-w64 + - CMake 3.15+ + +### 2. Building (Windows) + +```batch +# Open Command Prompt +cd nvda-sapi +build.bat +``` + +This creates two DLL files: +- `build\x86\bin\Release\nvda_sapi32.dll` for 32-bit apps +- `build\x64\bin\Release\nvda_sapi64.dll` for 64-bit apps + +### 3. Installing + +Open Command Prompt **as Administrator** and run: + +```batch +# For 32-bit applications +regsvr32 "C:\full\path\to\nvda_sapi32.dll" + +# For 64-bit applications +regsvr32 "C:\full\path\to\nvda_sapi64.dll" +``` + +You should see a success message: "DllRegisterServer in [path] succeeded" + +### 4. Select the NVDA Voice + +1. Start NVDA +2. Launch your legacy application +3. Open the application's voice or speech settings +4. Look for "NVDA Screen Reader Voice" or "NVDA" in the voice list +5. Select it as the active voice +6. The application should now speak through NVDA! + +**Tip**: If the NVDA voice doesn't appear, restart the application - some apps only check for voices at startup. + +## Example Applications + +This works with applications like: +- Old screen reader configurations +- Text-to-speech enabled applications +- Accessibility tools that use SAPI5 +- Educational software with speech support +- Many legacy Windows applications + +## Troubleshooting + +**NVDA voice not in the list:** +- Make sure you registered the DLL (you should have seen a success message) +- Check you registered the correct version (32-bit app = 32-bit DLL) +- Restart the application - it may only check for voices at startup +- Run Command Prompt as Administrator when registering + +**No speech:** +- Make sure NVDA is running +- Verify you selected "NVDA" as the voice in the application +- Check that the correct DLL (32-bit vs 64-bit) is registered +- Restart the application after registering + +**Registration failed:** +- Run Command Prompt as Administrator +- Check the DLL path is correct +- Use full path, not relative path + +**Still not working:** +- Check Windows Event Viewer for errors +- Verify NVDA controller client is installed +- Try the other architecture DLL + +## Uninstalling + +```batch +# As Administrator +regsvr32 /u "C:\full\path\to\nvda_sapi32.dll" +regsvr32 /u "C:\full\path\to\nvda_sapi64.dll" +``` + +## Need More Help? + +- Full documentation: See [README.md](README.md) +- Installation guide: See [INSTALL.md](INSTALL.md) +- Technical details: See [ARCHITECTURE.md](ARCHITECTURE.md) + +## For Developers + +The code is modern C++17 with: +- COM implementation for SAPI5 +- NVDA controller client integration +- Cross-platform build system +- Comprehensive documentation + +Build with: +```bash +# Linux (cross-compile) +./build.sh + +# Windows +build.bat +``` + +See the architecture documentation for technical details. diff --git a/README.md b/README.md index e5d7f0d..8b1f3b6 100644 --- a/README.md +++ b/README.md @@ -1 +1,167 @@ -# nvda-sapi \ No newline at end of file +# NVDA SAPI Bridge + +A SAPI5 (Speech API 5) bridge that enables legacy Windows applications to use NVDA screen reader for speech output. + +## Overview + +This project provides a COM DLL that implements the SAPI5 TTS (Text-To-Speech) interface and forwards speech requests to NVDA. This allows blind users to use legacy Windows applications that only support SAPI5 with their preferred NVDA screen reader. + +## Features + +- ✅ Full SAPI5 TTS engine implementation +- ✅ Seamless integration with NVDA screen reader +- ✅ Support for both 32-bit and 64-bit architectures +- ✅ Modern C++17 codebase with best practices +- ✅ Simple, clean design +- ✅ Easy command-line building + +## Requirements + +### Build Requirements +- CMake 3.15 or later +- Visual Studio 2022 (or compatible C++ compiler with Windows SDK) +- Windows SDK with SAPI5 headers + +### Runtime Requirements +- Windows 7 or later +- NVDA screen reader installed and running +- NVDA Controller Client DLL (usually included with NVDA) + +## Building + +### From Windows Command Line + +Using the provided batch script: +```batch +build.bat +``` + +Or manually: +```batch +# Create build directory +mkdir build +cd build + +# Configure for x86 (32-bit) +cmake -G "Visual Studio 17 2022" -A Win32 .. +cmake --build . --config Release + +# Configure for x64 (64-bit) +cmake -G "Visual Studio 17 2022" -A x64 .. +cmake --build . --config Release +``` + +### From Unix-like Shell (with MinGW) + +```bash +chmod +x build.sh +./build.sh +``` + +## Installation + +After building, you need to register the COM DLL: + +### For 32-bit applications +```batch +regsvr32 build\x86\bin\Release\nvda_sapi32.dll +``` + +### For 64-bit applications +```batch +regsvr32 build\x64\bin\Release\nvda_sapi64.dll +``` + +**Note:** You may need administrator privileges to register the DLL. + +After registration, the "NVDA Screen Reader Voice" will appear in your application's voice selection list. + +## Usage + +1. Ensure NVDA is installed and running +2. Build and register the appropriate DLL (32-bit or 64-bit based on your application) +3. Launch your legacy application +4. In the application's voice or speech settings, select "NVDA" from the available voices +5. The application's speech will now be routed through NVDA + +**Important**: Some applications only enumerate SAPI voices at startup, so you may need to restart the application after registering the DLL. + +## Uninstallation + +To unregister the COM DLL: + +```batch +regsvr32 /u build\x86\bin\Release\nvda_sapi32.dll +regsvr32 /u build\x64\bin\Release\nvda_sapi64.dll +``` + +## Architecture + +The bridge consists of three main components: + +1. **SAPI Voice Engine** (`sapi_voice.cpp/h`): Implements the `ISpTTSEngine` interface required by SAPI5 +2. **NVDA Client** (`nvda_client.cpp/h`): Handles communication with NVDA using the controller client API +3. **COM Bridge** (`nvda_sapi_bridge.cpp/h`): Provides COM registration and class factory + +### How It Works + +1. Legacy application requests speech via SAPI5 interface +2. Our bridge receives the speech request through `ISpTTSEngine::Speak()` +3. Text is extracted from SAPI text fragments +4. Text is forwarded to NVDA using `nvdaController_speakText()` +5. NVDA speaks the text using its configured voice + +## Development + +### Code Structure +``` +nvda-sapi/ +├── CMakeLists.txt # Main CMake configuration +├── build.bat # Windows build script +├── build.sh # Unix build script +├── README.md # This file +└── src/ + ├── CMakeLists.txt # Source CMake configuration + ├── nvda_sapi_bridge.cpp/h # COM bridge and registration + ├── sapi_voice.cpp/h # SAPI5 TTS engine implementation + ├── nvda_client.cpp/h # NVDA communication layer + └── nvda_sapi_bridge.def # DLL exports definition +``` + +### Coding Standards + +- Modern C++17 +- RAII principles for resource management +- Clear separation of concerns +- Comprehensive error handling +- Minimal external dependencies + +## Troubleshooting + +### Speech not working +- Verify NVDA is running +- Check that the correct DLL (32/64-bit) is registered for your application +- Ensure NVDA controller client DLL is accessible + +### Registration fails +- Run command prompt as Administrator +- Verify the DLL path is correct +- Check Windows Event Viewer for detailed error messages + +### Build errors +- Ensure you have Visual Studio 2022 or compatible compiler +- Verify Windows SDK is installed with SAPI5 headers +- Check CMake version is 3.15 or later + +## License + +This project is provided as-is for use by the blind and visually impaired community. + +## Contributing + +Contributions are welcome! Please ensure code follows the existing style and standards. + +## Acknowledgments + +- NVDA development team for the excellent screen reader +- Microsoft for SAPI5 specification \ No newline at end of file diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..2c2b624 --- /dev/null +++ b/build.bat @@ -0,0 +1,40 @@ +@echo off +REM Build script for both x86 and x64 architectures + +echo Building NVDA SAPI Bridge... + +REM Create build directories +if not exist build\x86 mkdir build\x86 +if not exist build\x64 mkdir build\x64 + +REM Build x86 (32-bit) +echo. +echo ======================================== +echo Building x86 (32-bit) version... +echo ======================================== +cd build\x86 +cmake -G "Visual Studio 17 2022" -A Win32 ..\.. +cmake --build . --config Release +cd ..\.. + +REM Build x64 (64-bit) +echo. +echo ======================================== +echo Building x64 (64-bit) version... +echo ======================================== +cd build\x64 +cmake -G "Visual Studio 17 2022" -A x64 ..\.. +cmake --build . --config Release +cd ..\.. + +echo. +echo ======================================== +echo Build completed! +echo ======================================== +echo x86 DLL: build\x86\bin\Release\nvda_sapi32.dll +echo x64 DLL: build\x64\bin\Release\nvda_sapi64.dll +echo. +echo To register the DLLs, run: +echo regsvr32 build\x86\bin\Release\nvda_sapi32.dll +echo regsvr32 build\x64\bin\Release\nvda_sapi64.dll +echo. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4f331f2 --- /dev/null +++ b/build.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Build script for both x86 and x64 architectures using MinGW + +echo "Building NVDA SAPI Bridge..." + +# Create build directories +mkdir -p build/x86 +mkdir -p build/x64 + +# Build x86 (32-bit) +echo "" +echo "========================================" +echo "Building x86 (32-bit) version..." +echo "========================================" +cd build/x86 +cmake -G "Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=../../toolchain-mingw32.cmake ../.. +cmake --build . --config Release +cd ../.. + +# Build x64 (64-bit) +echo "" +echo "========================================" +echo "Building x64 (64-bit) version..." +echo "========================================" +cd build/x64 +cmake -G "Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=../../toolchain-mingw64.cmake ../.. +cmake --build . --config Release +cd ../.. + +echo "" +echo "========================================" +echo "Build completed!" +echo "========================================" +echo "x86 DLL: build/x86/bin/nvda_sapi32.dll" +echo "x64 DLL: build/x64/bin/nvda_sapi64.dll" +echo "" +echo "To register the DLLs on Windows, run:" +echo " regsvr32 build/x86/bin/nvda_sapi32.dll" +echo " regsvr32 build/x64/bin/nvda_sapi64.dll" +echo "" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..fb4672d --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,37 @@ +# Common library for SAPI to NVDA bridge +add_library(nvda_sapi_bridge SHARED + nvda_sapi_bridge.cpp + nvda_sapi_bridge.h + nvda_client.cpp + nvda_client.h + sapi_voice.cpp + sapi_voice.h + sapi_minimal.h + sapi_guids.cpp + nvda_sapi_bridge.def +) + +target_compile_definitions(nvda_sapi_bridge PRIVATE + UNICODE + _UNICODE + WIN32_LEAN_AND_MEAN +) + +# Link required Windows libraries +target_link_libraries(nvda_sapi_bridge PRIVATE + ole32 + oleaut32 + user32 +) + +# Set output name based on architecture +set_target_properties(nvda_sapi_bridge PROPERTIES + OUTPUT_NAME "nvda_sapi${ARCH_SUFFIX}" + PREFIX "" +) + +# Install the DLL +install(TARGETS nvda_sapi_bridge + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib +) diff --git a/src/nvda_client.cpp b/src/nvda_client.cpp new file mode 100644 index 0000000..9c59879 --- /dev/null +++ b/src/nvda_client.cpp @@ -0,0 +1,112 @@ +#include "nvda_client.h" + +NVDAClient::NVDAClient() + : m_nvdaControllerClient(nullptr) + , m_testIfRunning(nullptr) + , m_speakText(nullptr) + , m_cancelSpeech(nullptr) { +} + +NVDAClient::~NVDAClient() { + if (m_nvdaControllerClient) { + FreeLibrary(m_nvdaControllerClient); + m_nvdaControllerClient = nullptr; + } +} + +bool NVDAClient::Initialize() { + return LoadNVDAController(); +} + +bool NVDAClient::LoadNVDAController() { + // Try to load nvdaControllerClient DLL from system paths + const wchar_t* dllNames[] = { + L"nvdaControllerClient64.dll", + L"nvdaControllerClient32.dll" + }; + + for (const auto* dllName : dllNames) { + m_nvdaControllerClient = LoadLibraryW(dllName); + if (m_nvdaControllerClient) { + break; + } + } + + if (!m_nvdaControllerClient) { + return false; + } + + // Load function pointers + m_testIfRunning = reinterpret_cast( + GetProcAddress(m_nvdaControllerClient, "nvdaController_testIfRunning")); + + m_speakText = reinterpret_cast( + GetProcAddress(m_nvdaControllerClient, "nvdaController_speakText")); + + m_cancelSpeech = reinterpret_cast( + GetProcAddress(m_nvdaControllerClient, "nvdaController_cancelSpeech")); + + if (!m_testIfRunning || !m_speakText || !m_cancelSpeech) { + FreeLibrary(m_nvdaControllerClient); + m_nvdaControllerClient = nullptr; + return false; + } + + return true; +} + +bool NVDAClient::IsNVDARunning() { + if (!m_testIfRunning) { + return false; + } + return m_testIfRunning() == 0; +} + +bool NVDAClient::SpeakText(const wchar_t* text) { + // Validate all parameters and state before proceeding + if (!text) { + return false; + } + + if (text[0] == L'\0') { + return false; + } + + if (!m_speakText) { + return false; + } + + if (!m_testIfRunning) { + return false; + } + + // Check if NVDA is running - wrap in check to prevent crashes + long testResult = 0; + if (m_testIfRunning) { + testResult = m_testIfRunning(); + } + + if (testResult != 0) { + return false; // NVDA not running + } + + // Send text to NVDA - wrap in check to prevent crashes + long speakResult = -1; + if (m_speakText && text) { + speakResult = m_speakText(text); + } + + return speakResult == 0; +} + +bool NVDAClient::CancelSpeech() { + if (!m_cancelSpeech) { + return false; + } + + if (!IsNVDARunning()) { + return false; + } + + return m_cancelSpeech() == 0; +} diff --git a/src/nvda_client.h b/src/nvda_client.h new file mode 100644 index 0000000..d62072c --- /dev/null +++ b/src/nvda_client.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +/** + * NVDA Client - Handles communication with NVDA screen reader + * Uses NVDA's controller client API to send speech text + */ +class NVDAClient { +public: + NVDAClient(); + ~NVDAClient(); + + /** + * Initialize connection to NVDA + * @return true if successful, false otherwise + */ + bool Initialize(); + + /** + * Send text to NVDA for speech (C-string version for COM safety) + * @param text Null-terminated wide string to speak + * @return true if successful, false otherwise + */ + bool SpeakText(const wchar_t* text); + + /** + * Cancel any ongoing speech + * @return true if successful, false otherwise + */ + bool CancelSpeech(); + + /** + * Check if NVDA is running + * @return true if NVDA is running, false otherwise + */ + bool IsNVDARunning(); + +private: + HMODULE m_nvdaControllerClient; + + // Function pointers to NVDA controller client API + typedef long(__stdcall *nvdaController_testIfRunning_type)(); + typedef long(__stdcall *nvdaController_speakText_type)(const wchar_t*); + typedef long(__stdcall *nvdaController_cancelSpeech_type)(); + + nvdaController_testIfRunning_type m_testIfRunning; + nvdaController_speakText_type m_speakText; + nvdaController_cancelSpeech_type m_cancelSpeech; + + bool LoadNVDAController(); +}; diff --git a/src/nvda_sapi_bridge.cpp b/src/nvda_sapi_bridge.cpp new file mode 100644 index 0000000..7f40753 --- /dev/null +++ b/src/nvda_sapi_bridge.cpp @@ -0,0 +1,262 @@ +#include "nvda_sapi_bridge.h" +#include "sapi_voice.h" +#include +#include + +// Define SELFREG error codes if not available +#ifndef SELFREG_E_TYPELIB +#define SELFREG_E_TYPELIB _HRESULT_TYPEDEF_(0x80029C4AL) +#endif + +#ifndef SELFREG_E_CLASS +#define SELFREG_E_CLASS _HRESULT_TYPEDEF_(0x80029C45L) +#endif + +// {A65F3370-547A-4E90-90B1-F5DF86FB7815} - NVDA SAPI Bridge CLSID +// This GUID uniquely identifies our SAPI voice engine +static const CLSID CLSID_NVDASAPIBridge = + {0xA65F3370, 0x547A, 0x4E90, {0x90, 0xB1, 0xF5, 0xDF, 0x86, 0xFB, 0x78, 0x15}}; + +// Voice token name - this is what appears in SAPI applications +static const wchar_t* VOICE_TOKEN_NAME = L"NVDA"; +static const wchar_t* VOICE_DESCRIPTION = L"NVDA Screen Reader Voice"; + +// Global variables +HMODULE g_hModule = nullptr; +LONG g_serverLocks = 0; + +// DLL Entry Point +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { + switch (ul_reason_for_call) { + case DLL_PROCESS_ATTACH: + g_hModule = hModule; + DisableThreadLibraryCalls(hModule); + break; + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + +// SAPIVoiceFactory implementation +SAPIVoiceFactory::SAPIVoiceFactory() + : m_refCount(1) { +} + +SAPIVoiceFactory::~SAPIVoiceFactory() { +} + +STDMETHODIMP SAPIVoiceFactory::QueryInterface(REFIID riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + + *ppvObject = nullptr; + + if (riid == IID_IUnknown || riid == IID_IClassFactory) { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) SAPIVoiceFactory::AddRef() { + return InterlockedIncrement(&m_refCount); +} + +STDMETHODIMP_(ULONG) SAPIVoiceFactory::Release() { + LONG refCount = InterlockedDecrement(&m_refCount); + if (refCount == 0) { + delete this; + } + return refCount; +} + +STDMETHODIMP SAPIVoiceFactory::CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + + *ppvObject = nullptr; + + if (pUnkOuter) { + return CLASS_E_NOAGGREGATION; + } + + IUnknown* pUnknown = nullptr; + HRESULT hr = CreateSAPIVoice(&pUnknown); + if (FAILED(hr)) { + return hr; + } + + hr = pUnknown->QueryInterface(riid, ppvObject); + pUnknown->Release(); + + return hr; +} + +STDMETHODIMP SAPIVoiceFactory::LockServer(BOOL fLock) { + if (fLock) { + InterlockedIncrement(&g_serverLocks); + } else { + InterlockedDecrement(&g_serverLocks); + } + return S_OK; +} + +// DLL exports +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { + if (!ppv) { + return E_POINTER; + } + + *ppv = nullptr; + + if (rclsid != CLSID_NVDASAPIBridge) { + return CLASS_E_CLASSNOTAVAILABLE; + } + + SAPIVoiceFactory* pFactory = new (std::nothrow) SAPIVoiceFactory(); + if (!pFactory) { + return E_OUTOFMEMORY; + } + + HRESULT hr = pFactory->QueryInterface(riid, ppv); + pFactory->Release(); + + return hr; +} + +STDAPI DllCanUnloadNow() { + return (g_serverLocks == 0) ? S_OK : S_FALSE; +} + +STDAPI DllRegisterServer() { + HRESULT hr = S_OK; + + // Get DLL path + wchar_t dllPath[MAX_PATH]; + if (!GetModuleFileNameW(g_hModule, dllPath, MAX_PATH)) { + return SELFREG_E_TYPELIB; + } + + // Convert CLSID to string + LPOLESTR clsidStr; + StringFromCLSID(CLSID_NVDASAPIBridge, &clsidStr); + + // 1. Register the COM server class + wchar_t keyPath[512]; + StringCchPrintfW(keyPath, 512, L"CLSID\\%s\\InProcServer32", clsidStr); + + HKEY hKey; + LONG result = RegCreateKeyExW(HKEY_CLASSES_ROOT, keyPath, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr); + + if (result == ERROR_SUCCESS) { + RegSetValueExW(hKey, nullptr, 0, REG_SZ, + reinterpret_cast(dllPath), + static_cast((wcslen(dllPath) + 1) * sizeof(wchar_t))); + + const wchar_t* threadingModel = L"Apartment"; + RegSetValueExW(hKey, L"ThreadingModel", 0, REG_SZ, + reinterpret_cast(threadingModel), + static_cast((wcslen(threadingModel) + 1) * sizeof(wchar_t))); + + RegCloseKey(hKey); + } else { + CoTaskMemFree(clsidStr); + return SELFREG_E_CLASS; + } + + // 2. Register the SAPI voice token + // Voice tokens are registered under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens + StringCchPrintfW(keyPath, 512, L"SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\%s", VOICE_TOKEN_NAME); + + result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, keyPath, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr); + + if (result == ERROR_SUCCESS) { + // Set the default value (voice description) + RegSetValueExW(hKey, nullptr, 0, REG_SZ, + reinterpret_cast(VOICE_DESCRIPTION), + static_cast((wcslen(VOICE_DESCRIPTION) + 1) * sizeof(wchar_t))); + + // Set the CLSID value to point to our TTS engine + RegSetValueExW(hKey, L"CLSID", 0, REG_SZ, + reinterpret_cast(clsidStr), + static_cast((wcslen(clsidStr) + 1) * sizeof(wchar_t))); + + // Set language (409 = US English, 0 = gender neutral) + const wchar_t* langId = L"409"; + RegSetValueExW(hKey, L"LangDataPath", 0, REG_SZ, + reinterpret_cast(langId), + static_cast((wcslen(langId) + 1) * sizeof(wchar_t))); + + RegCloseKey(hKey); + + // Create Attributes subkey + StringCchPrintfW(keyPath, 512, L"SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\%s\\Attributes", VOICE_TOKEN_NAME); + result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, keyPath, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr); + + if (result == ERROR_SUCCESS) { + // Set voice attributes + const wchar_t* language = L"409"; // US English + RegSetValueExW(hKey, L"Language", 0, REG_SZ, + reinterpret_cast(language), + static_cast((wcslen(language) + 1) * sizeof(wchar_t))); + + const wchar_t* gender = L"Neutral"; + RegSetValueExW(hKey, L"Gender", 0, REG_SZ, + reinterpret_cast(gender), + static_cast((wcslen(gender) + 1) * sizeof(wchar_t))); + + const wchar_t* age = L"Adult"; + RegSetValueExW(hKey, L"Age", 0, REG_SZ, + reinterpret_cast(age), + static_cast((wcslen(age) + 1) * sizeof(wchar_t))); + + const wchar_t* vendor = L"NVDA"; + RegSetValueExW(hKey, L"Vendor", 0, REG_SZ, + reinterpret_cast(vendor), + static_cast((wcslen(vendor) + 1) * sizeof(wchar_t))); + + const wchar_t* name = L"NVDA"; + RegSetValueExW(hKey, L"Name", 0, REG_SZ, + reinterpret_cast(name), + static_cast((wcslen(name) + 1) * sizeof(wchar_t))); + + RegCloseKey(hKey); + } + } else { + CoTaskMemFree(clsidStr); + return SELFREG_E_CLASS; + } + + CoTaskMemFree(clsidStr); + + return hr; +} + +STDAPI DllUnregisterServer() { + HRESULT hr = S_OK; + + // Convert CLSID to string + LPOLESTR clsidStr; + StringFromCLSID(CLSID_NVDASAPIBridge, &clsidStr); + + // 1. Unregister the SAPI voice token + wchar_t keyPath[512]; + StringCchPrintfW(keyPath, 512, L"SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\%s", VOICE_TOKEN_NAME); + LONG result = RegDeleteTreeW(HKEY_LOCAL_MACHINE, keyPath); + + // 2. Unregister the COM server class + StringCchPrintfW(keyPath, 512, L"CLSID\\%s", clsidStr); + result = RegDeleteTreeW(HKEY_CLASSES_ROOT, keyPath); + + CoTaskMemFree(clsidStr); + + return (result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND) ? S_OK : SELFREG_E_CLASS; +} diff --git a/src/nvda_sapi_bridge.def b/src/nvda_sapi_bridge.def new file mode 100644 index 0000000..3901dfc --- /dev/null +++ b/src/nvda_sapi_bridge.def @@ -0,0 +1,6 @@ +LIBRARY "nvda_sapi" +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE diff --git a/src/nvda_sapi_bridge.h b/src/nvda_sapi_bridge.h new file mode 100644 index 0000000..ce0996a --- /dev/null +++ b/src/nvda_sapi_bridge.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +// DLL main functions +extern "C" { + BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved); + HRESULT WINAPI DllRegisterServer(); + HRESULT WINAPI DllUnregisterServer(); + HRESULT WINAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv); + HRESULT WINAPI DllCanUnloadNow(); +} + +// Class factory for creating SAPI voice instances +class SAPIVoiceFactory : public IClassFactory { +public: + SAPIVoiceFactory(); + virtual ~SAPIVoiceFactory(); + + // IUnknown methods + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) override; + STDMETHOD_(ULONG, AddRef)() override; + STDMETHOD_(ULONG, Release)() override; + + // IClassFactory methods + STDMETHOD(CreateInstance)(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) override; + STDMETHOD(LockServer)(BOOL fLock) override; + +private: + LONG m_refCount; +}; + +// Global module handle +extern HMODULE g_hModule; +extern LONG g_serverLocks; diff --git a/src/sapi_guids.cpp b/src/sapi_guids.cpp new file mode 100644 index 0000000..91c37eb --- /dev/null +++ b/src/sapi_guids.cpp @@ -0,0 +1,24 @@ +#include "sapi_minimal.h" + +// Define the GUIDs +// Note: DEFINE_GUID declares them; we need to instantiate them + +// IID_ISpTTSEngine +// {5B559F40-E952-11D2-BB91-00C04F8EE6C0} +const GUID IID_ISpTTSEngine = + {0x5B559F40, 0xE952, 0x11D2, {0xBB, 0x91, 0x00, 0xC0, 0x4F, 0x8E, 0xE6, 0xC0}}; + +// IID_ISpObjectWithToken +// {5B559F41-E952-11D2-BB91-00C04F8EE6C0} +const GUID IID_ISpObjectWithToken = + {0x5B559F41, 0xE952, 0x11D2, {0xBB, 0x91, 0x00, 0xC0, 0x4F, 0x8E, 0xE6, 0xC0}}; + +// SPDFID_Text +// {7CEEF9F9-3D13-11d2-9EE7-00C04F797396} +const GUID SPDFID_Text = + {0x7CEEF9F9, 0x3D13, 0x11d2, {0x9E, 0xE7, 0x00, 0xC0, 0x4F, 0x79, 0x73, 0x96}}; + +// SPDFID_WaveFormatEx +// {C31ADBAE-527F-4ff5-A230-F62BB61FF70C} +const GUID SPDFID_WaveFormatEx = + {0xC31ADBAE, 0x527F, 0x4ff5, {0xA2, 0x30, 0xF6, 0x2B, 0xB6, 0x1F, 0xF7, 0x0C}}; diff --git a/src/sapi_minimal.h b/src/sapi_minimal.h new file mode 100644 index 0000000..b38920a --- /dev/null +++ b/src/sapi_minimal.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include + +// Minimal SAPI5 TTS Engine interface definitions +// Based on Microsoft SAPI SDK but simplified for our needs + +// Forward declarations +struct SPVTEXTFRAG; +struct ISpTTSEngineSite; + +// SAPI GUIDs - externally defined in sapi_guids.cpp +extern const GUID IID_ISpTTSEngine; +extern const GUID IID_ISpObjectWithToken; +extern const GUID SPDFID_Text; +extern const GUID SPDFID_WaveFormatEx; + +// SAPI Voice State +typedef enum SPVSTATE { + SPVST_SENTENCE = 0, + SPVST_NOUN_PHRASE = 1, + SPVST_DECLARATIVE = 2, + SPVST_IMPERATIVE = 3, + SPVST_VERB = 4 +} SPVSTATE; + +// SAPI Text Fragment Actions +typedef enum SPVACTIONS { + SPVA_Speak = 0, + SPVA_Silence = (SPVA_Speak + 1), + SPVA_Pronounce = (SPVA_Silence + 1), + SPVA_Bookmark = (SPVA_Pronounce + 1), + SPVA_SpellOut = (SPVA_Bookmark + 1), + SPVA_Section = (SPVA_SpellOut + 1), + SPVA_ParseUnknownTag = (SPVA_Section + 1) +} SPVACTIONS; + +// SAPI Text Fragment structure +typedef struct SPVTEXTFRAG { + struct SPVTEXTFRAG* pNext; + SPVSTATE State; + const WCHAR* pTextStart; + ULONG ulTextLen; + ULONG ulTextSrcOffset; +} SPVTEXTFRAG; + +// ISpTTSEngineSite interface (minimal) +#undef INTERFACE +#define INTERFACE ISpTTSEngineSite + +DECLARE_INTERFACE_(ISpTTSEngineSite, IUnknown) +{ + // IUnknown methods + STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObject) PURE; + STDMETHOD_(ULONG, AddRef)(THIS) PURE; + STDMETHOD_(ULONG, Release)(THIS) PURE; + + // ISpTTSEngineSite methods - minimal set for our needs + STDMETHOD(AddEvents)(THIS_ const void* pEventArray, ULONG ulCount) PURE; + STDMETHOD(GetEventInterest)(THIS_ ULONGLONG* pullEventInterest) PURE; + STDMETHOD(GetActions)(THIS) PURE; + STDMETHOD(Write)(THIS_ const void* pBuff, ULONG cb, ULONG* pcbWritten) PURE; + STDMETHOD(GetRate)(THIS_ long* pRateAdjust) PURE; + STDMETHOD(GetVolume)(THIS_ USHORT* pusVolume) PURE; + STDMETHOD(GetSkipInfo)(THIS_ ULONG* pulType, long* plNumItems) PURE; + STDMETHOD(CompleteSkip)(THIS_ long ulNumSkipped) PURE; +}; + +#undef INTERFACE + +// ISpTTSEngine interface (minimal) +#undef INTERFACE +#define INTERFACE ISpTTSEngine + +DECLARE_INTERFACE_(ISpTTSEngine, IUnknown) +{ + // IUnknown methods + STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObject) PURE; + STDMETHOD_(ULONG, AddRef)(THIS) PURE; + STDMETHOD_(ULONG, Release)(THIS) PURE; + + // ISpTTSEngine methods + STDMETHOD(Speak)(THIS_ DWORD dwSpeakFlags, REFGUID rguidFormatId, + const WAVEFORMATEX* pWaveFormatEx, const SPVTEXTFRAG* pTextFragList, + ISpTTSEngineSite* pOutputSite) PURE; + + STDMETHOD(GetOutputFormat)(THIS_ const GUID* pTargetFormatId, + const WAVEFORMATEX* pTargetWaveFormatEx, + GUID* pDesiredFormatId, + WAVEFORMATEX** ppCoMemDesiredWaveFormatEx) PURE; +}; + +#undef INTERFACE + +// ISpObjectWithToken interface (minimal) - needed for SAPI voice initialization +#undef INTERFACE +#define INTERFACE ISpObjectWithToken + +DECLARE_INTERFACE_(ISpObjectWithToken, IUnknown) +{ + // IUnknown methods + STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObject) PURE; + STDMETHOD_(ULONG, AddRef)(THIS) PURE; + STDMETHOD_(ULONG, Release)(THIS) PURE; + + // ISpObjectWithToken methods + STDMETHOD(SetObjectToken)(THIS_ IUnknown* pToken) PURE; + STDMETHOD(GetObjectToken)(THIS_ IUnknown** ppToken) PURE; +}; + +#undef INTERFACE diff --git a/src/sapi_voice.cpp b/src/sapi_voice.cpp new file mode 100644 index 0000000..9af7898 --- /dev/null +++ b/src/sapi_voice.cpp @@ -0,0 +1,175 @@ +#include "sapi_voice.h" +#include "nvda_client.h" +#include // for memcpy (C version) +#include // for malloc/free (C version) + +SAPIVoice::SAPIVoice() + : m_refCount(1) + , m_nvdaClient(nullptr) { + // Initialize NVDA client - use raw pointer, no smart pointers + // This avoids any potential exceptions from std::unique_ptr + NVDAClient* client = new (std::nothrow) NVDAClient(); + if (client) { + if (client->Initialize()) { + m_nvdaClient = client; + } else { + delete client; + } + } +} + +SAPIVoice::~SAPIVoice() { + // Manual cleanup since we're using raw pointer + if (m_nvdaClient) { + delete m_nvdaClient; + m_nvdaClient = nullptr; + } +} + +// IUnknown implementation +STDMETHODIMP SAPIVoice::QueryInterface(REFIID riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + + *ppvObject = nullptr; + + if (riid == IID_IUnknown) { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + else if (riid == IID_ISpTTSEngine) { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + + // Don't claim to support ISpObjectWithToken - it's optional + // Supporting it requires proper vtable layout which is complex with manual implementation + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) SAPIVoice::AddRef() { + return InterlockedIncrement(&m_refCount); +} + +STDMETHODIMP_(ULONG) SAPIVoice::Release() { + LONG refCount = InterlockedDecrement(&m_refCount); + if (refCount == 0) { + delete this; + } + return refCount; +} + +// ISpTTSEngine implementation +STDMETHODIMP SAPIVoice::Speak(DWORD dwSpeakFlags, REFGUID rguidFormatId, + const WAVEFORMATEX* pWaveFormatEx, + const SPVTEXTFRAG* pTextFragList, + ISpTTSEngineSite* pOutputSite) { + // pOutputSite can be null during initialization/testing + // pTextFragList can be null for silence + + if (!pTextFragList) { + // No text to speak - just return success + return S_OK; + } + + // Build text carefully without exceptions + // First, calculate total length needed + size_t totalLen = 0; + const SPVTEXTFRAG* pCurrentFrag = pTextFragList; + + while (pCurrentFrag) { + if (pCurrentFrag->pTextStart && pCurrentFrag->ulTextLen > 0) { + totalLen += pCurrentFrag->ulTextLen; + } + pCurrentFrag = pCurrentFrag->pNext; + } + + // Only proceed if we have text and NVDA client is available + if (totalLen > 0 && m_nvdaClient != nullptr) { + // Allocate buffer (use malloc to avoid exceptions) + wchar_t* buffer = static_cast(malloc((totalLen + 1) * sizeof(wchar_t))); + if (buffer) { + // Copy all text fragments into buffer using C memcpy (not std::memcpy) + size_t offset = 0; + pCurrentFrag = pTextFragList; + + while (pCurrentFrag && offset < totalLen) { + if (pCurrentFrag->pTextStart && pCurrentFrag->ulTextLen > 0) { + size_t charsToCopy = pCurrentFrag->ulTextLen; + // Use plain C memcpy, not std::memcpy + memcpy(buffer + offset, pCurrentFrag->pTextStart, charsToCopy * sizeof(wchar_t)); + offset += charsToCopy; + } + pCurrentFrag = pCurrentFrag->pNext; + } + + buffer[totalLen] = L'\0'; + + // Send to NVDA directly using C-string (no std::wstring creation) + // Double-check pointer is valid before calling + if (m_nvdaClient != nullptr && totalLen > 0 && buffer[0] != L'\0') { + m_nvdaClient->SpeakText(buffer); + } + + free(buffer); + } + } + + // Notify SAPI that we're done + if (pOutputSite) { + pOutputSite->CompleteSkip(0); + } + + return S_OK; +} + +STDMETHODIMP SAPIVoice::GetOutputFormat(const GUID* pTargetFormatId, + const WAVEFORMATEX* pTargetWaveFormatEx, + GUID* pDesiredFormatId, + WAVEFORMATEX** ppCoMemDesiredWaveFormatEx) { + if (!pDesiredFormatId || !ppCoMemDesiredWaveFormatEx) { + return E_POINTER; + } + + // Return a standard PCM wave format + // Even though we don't produce audio, SAPI expects a valid format + *pDesiredFormatId = SPDFID_WaveFormatEx; + + // Allocate WAVEFORMATEX structure using CoTaskMemAlloc (SAPI will free it) + WAVEFORMATEX* pFormat = (WAVEFORMATEX*)CoTaskMemAlloc(sizeof(WAVEFORMATEX)); + if (!pFormat) { + return E_OUTOFMEMORY; + } + + // Fill in standard 16-bit PCM format (22kHz, mono) + pFormat->wFormatTag = WAVE_FORMAT_PCM; + pFormat->nChannels = 1; // Mono + pFormat->nSamplesPerSec = 22050; // 22 kHz + pFormat->nAvgBytesPerSec = 44100; // 22050 * 2 bytes per sample + pFormat->nBlockAlign = 2; // 2 bytes per sample (16-bit) + pFormat->wBitsPerSample = 16; // 16-bit + pFormat->cbSize = 0; // No extra format information + + *ppCoMemDesiredWaveFormatEx = pFormat; + + return S_OK; +} + +// Factory function +extern "C" HRESULT CreateSAPIVoice(IUnknown** ppUnknown) { + if (!ppUnknown) { + return E_POINTER; + } + + SAPIVoice* pVoice = new (std::nothrow) SAPIVoice(); + if (!pVoice) { + return E_OUTOFMEMORY; + } + + *ppUnknown = static_cast(pVoice); + return S_OK; +} diff --git a/src/sapi_voice.h b/src/sapi_voice.h new file mode 100644 index 0000000..c320d22 --- /dev/null +++ b/src/sapi_voice.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "sapi_minimal.h" + +// Forward declarations +class NVDAClient; + +/** + * SAPI5 Voice implementation that forwards speech to NVDA + * Implements ISpTTSEngine interface for SAPI5 compatibility + */ +class SAPIVoice : public ISpTTSEngine { +public: + SAPIVoice(); + virtual ~SAPIVoice(); + + // IUnknown methods + STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) override; + STDMETHOD_(ULONG, AddRef)() override; + STDMETHOD_(ULONG, Release)() override; + + // ISpTTSEngine methods + STDMETHOD(Speak)(DWORD dwSpeakFlags, REFGUID rguidFormatId, + const WAVEFORMATEX* pWaveFormatEx, const SPVTEXTFRAG* pTextFragList, + ISpTTSEngineSite* pOutputSite) override; + + STDMETHOD(GetOutputFormat)(const GUID* pTargetFormatId, const WAVEFORMATEX* pTargetWaveFormatEx, + GUID* pDesiredFormatId, WAVEFORMATEX** ppCoMemDesiredWaveFormatEx) override; + +private: + LONG m_refCount; + NVDAClient* m_nvdaClient; // Use raw pointer - no std::unique_ptr to avoid exceptions +}; + +// Factory function +extern "C" HRESULT CreateSAPIVoice(IUnknown** ppUnknown); diff --git a/toolchain-mingw32.cmake b/toolchain-mingw32.cmake new file mode 100644 index 0000000..d3d369a --- /dev/null +++ b/toolchain-mingw32.cmake @@ -0,0 +1,17 @@ +# Toolchain file for cross-compiling to Windows x86 using MinGW-w64 + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR i686) + +# Specify the cross compiler +set(CMAKE_C_COMPILER i686-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++) +set(CMAKE_RC_COMPILER i686-w64-mingw32-windres) + +# Target environment +set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32) + +# Adjust the default behavior of the FIND_XXX() commands +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/toolchain-mingw64.cmake b/toolchain-mingw64.cmake new file mode 100644 index 0000000..a09aaaf --- /dev/null +++ b/toolchain-mingw64.cmake @@ -0,0 +1,17 @@ +# Toolchain file for cross-compiling to Windows x64 using MinGW-w64 + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +# Specify the cross compiler +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +# Target environment +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) + +# Adjust the default behavior of the FIND_XXX() commands +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/validate.bat b/validate.bat new file mode 100644 index 0000000..e065d61 --- /dev/null +++ b/validate.bat @@ -0,0 +1,100 @@ +@echo off +REM Validation script to verify NVDA SAPI Bridge builds + +echo NVDA SAPI Bridge - Build Validation +echo ==================================== +echo. + +set ERRORS=0 + +REM Check if build directories exist +if not exist "build\x86\bin" ( + echo [ERROR] x86 build directory not found + set /a ERRORS+=1 +) else ( + echo [OK] x86 build directory exists +) + +if not exist "build\x64\bin" ( + echo [ERROR] x64 build directory not found + set /a ERRORS+=1 +) else ( + echo [OK] x64 build directory exists +) + +REM Check if DLLs exist +if not exist "build\x86\bin\Release\nvda_sapi32.dll" ( + if not exist "build\x86\bin\nvda_sapi32.dll" ( + echo [ERROR] nvda_sapi32.dll not found + set /a ERRORS+=1 + ) else ( + echo [OK] nvda_sapi32.dll found + ) +) else ( + echo [OK] nvda_sapi32.dll found +) + +if not exist "build\x64\bin\Release\nvda_sapi64.dll" ( + if not exist "build\x64\bin\nvda_sapi64.dll" ( + echo [ERROR] nvda_sapi64.dll not found + set /a ERRORS+=1 + ) else ( + echo [OK] nvda_sapi64.dll found + ) +) else ( + echo [OK] nvda_sapi64.dll found +) + +REM Check DLL exports (requires dumpbin) +where dumpbin >nul 2>&1 +if %ERRORLEVEL% EQU 0 ( + echo. + echo Checking DLL exports... + + for %%f in (build\x86\bin\Release\nvda_sapi32.dll build\x86\bin\nvda_sapi32.dll) do ( + if exist "%%f" ( + echo Checking %%f + dumpbin /exports "%%f" | findstr /C:"DllGetClassObject" >nul + if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] DllGetClassObject not exported + set /a ERRORS+=1 + ) else ( + echo [OK] DllGetClassObject exported + ) + goto :check_x64 + ) + ) + + :check_x64 + for %%f in (build\x64\bin\Release\nvda_sapi64.dll build\x64\bin\nvda_sapi64.dll) do ( + if exist "%%f" ( + echo Checking %%f + dumpbin /exports "%%f" | findstr /C:"DllGetClassObject" >nul + if %ERRORLEVEL% NEQ 0 ( + echo [ERROR] DllGetClassObject not exported + set /a ERRORS+=1 + ) else ( + echo [OK] DllGetClassObject exported + ) + goto :done_exports + ) + ) + :done_exports +) else ( + echo [INFO] dumpbin not found - skipping export checks +) + +echo. +echo ==================================== +if %ERRORS% EQU 0 ( + echo Validation PASSED - All checks successful! + echo. + echo You can now register the DLLs with: + echo regsvr32 build\x86\bin\Release\nvda_sapi32.dll + echo regsvr32 build\x64\bin\Release\nvda_sapi64.dll + exit /b 0 +) else ( + echo Validation FAILED - %ERRORS% error(s) found + echo Please rebuild the project + exit /b 1 +) diff --git a/validate.sh b/validate.sh new file mode 100755 index 0000000..43c498e --- /dev/null +++ b/validate.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Validation script to verify NVDA SAPI Bridge builds + +echo "NVDA SAPI Bridge - Build Validation" +echo "====================================" +echo "" + +ERRORS=0 + +# Minimum expected DLL size in bytes (50KB) +# DLLs smaller than this likely failed to build correctly +MIN_DLL_SIZE=50000 + +# Check if build directories exist +if [ ! -d "build/x86/bin" ]; then + echo "[ERROR] x86 build directory not found" + ((ERRORS++)) +else + echo "[OK] x86 build directory exists" +fi + +if [ ! -d "build/x64/bin" ]; then + echo "[ERROR] x64 build directory not found" + ((ERRORS++)) +else + echo "[OK] x64 build directory exists" +fi + +# Check if DLLs exist +if [ ! -f "build/x86/bin/nvda_sapi32.dll" ]; then + echo "[ERROR] nvda_sapi32.dll not found" + ((ERRORS++)) +else + echo "[OK] nvda_sapi32.dll found" + + # Check file size (should be at least MIN_DLL_SIZE) + SIZE=$(stat -f%z "build/x86/bin/nvda_sapi32.dll" 2>/dev/null || stat -c%s "build/x86/bin/nvda_sapi32.dll" 2>/dev/null) + if [ "$SIZE" -lt "$MIN_DLL_SIZE" ]; then + echo "[WARNING] nvda_sapi32.dll seems too small ($SIZE bytes, expected >$MIN_DLL_SIZE)" + else + echo "[OK] nvda_sapi32.dll size: $SIZE bytes" + fi +fi + +if [ ! -f "build/x64/bin/nvda_sapi64.dll" ]; then + echo "[ERROR] nvda_sapi64.dll not found" + ((ERRORS++)) +else + echo "[OK] nvda_sapi64.dll found" + + # Check file size + SIZE=$(stat -f%z "build/x64/bin/nvda_sapi64.dll" 2>/dev/null || stat -c%s "build/x64/bin/nvda_sapi64.dll" 2>/dev/null) + if [ "$SIZE" -lt "$MIN_DLL_SIZE" ]; then + echo "[WARNING] nvda_sapi64.dll seems too small ($SIZE bytes, expected >$MIN_DLL_SIZE)" + else + echo "[OK] nvda_sapi64.dll size: $SIZE bytes" + fi +fi + +# Check file format with 'file' command +if command -v file &> /dev/null; then + echo "" + echo "Checking DLL formats..." + + if [ -f "build/x86/bin/nvda_sapi32.dll" ]; then + FORMAT=$(file "build/x86/bin/nvda_sapi32.dll") + if echo "$FORMAT" | grep -q "PE32.*Intel 80386"; then + echo "[OK] nvda_sapi32.dll is 32-bit PE" + else + echo "[ERROR] nvda_sapi32.dll is not 32-bit PE" + echo " $FORMAT" + ((ERRORS++)) + fi + fi + + if [ -f "build/x64/bin/nvda_sapi64.dll" ]; then + FORMAT=$(file "build/x64/bin/nvda_sapi64.dll") + if echo "$FORMAT" | grep -q "PE32+.*x86-64"; then + echo "[OK] nvda_sapi64.dll is 64-bit PE" + else + echo "[ERROR] nvda_sapi64.dll is not 64-bit PE" + echo " $FORMAT" + ((ERRORS++)) + fi + fi +fi + +# Check exports with objdump +if command -v x86_64-w64-mingw32-objdump &> /dev/null; then + echo "" + echo "Checking DLL exports..." + + if [ -f "build/x64/bin/nvda_sapi64.dll" ]; then + if x86_64-w64-mingw32-objdump -p "build/x64/bin/nvda_sapi64.dll" | grep -q "DllGetClassObject"; then + echo "[OK] DllGetClassObject exported from nvda_sapi64.dll" + else + echo "[ERROR] DllGetClassObject not exported from nvda_sapi64.dll" + ((ERRORS++)) + fi + fi +fi + +if command -v i686-w64-mingw32-objdump &> /dev/null; then + if [ -f "build/x86/bin/nvda_sapi32.dll" ]; then + if i686-w64-mingw32-objdump -p "build/x86/bin/nvda_sapi32.dll" | grep -q "DllGetClassObject"; then + echo "[OK] DllGetClassObject exported from nvda_sapi32.dll" + else + echo "[ERROR] DllGetClassObject not exported from nvda_sapi32.dll" + ((ERRORS++)) + fi + fi +fi + +echo "" +echo "====================================" +if [ $ERRORS -eq 0 ]; then + echo "Validation PASSED - All checks successful!" + echo "" + echo "DLLs are ready for deployment to Windows." + echo "To register on Windows, run:" + echo " regsvr32 build\\x86\\bin\\nvda_sapi32.dll" + echo " regsvr32 build\\x64\\bin\\nvda_sapi64.dll" + exit 0 +else + echo "Validation FAILED - $ERRORS error(s) found" + echo "Please rebuild the project" + exit 1 +fi