diff --git a/CMakeLists.txt b/CMakeLists.txt index d730968..ea81b5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,12 +8,18 @@ set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Enable testing +enable_testing() + # Platform-specific builds if(WIN32) # Windows-specific targets add_subdirectory(windows/asio) - add_subdirectory(windows/torture) + add_subdirectory(windows/simulator) else() # Linux-specific targets add_subdirectory(linux) + + # Protocol tests (Linux only - tests not needed on Windows) + add_subdirectory(protocol/test) endif() \ No newline at end of file diff --git a/README.md b/README.md index 63a6a14..d4e14b9 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Download the Windows ASIO driver from the [releases page](https://github.com/rip 3. **Configure settings** through the intuitive interface: - Set your Windows host IP address - Choose audio buffer size and sample rate - - Enable/disable oneshot mode and variable buffer sizes + - Enable/disable variable buffer sizes 4. **Start the relay** with the click of a button ### Using the Command Line @@ -105,7 +105,7 @@ Download the Windows ASIO driver from the [releases page](https://github.com/rip - ⚡ **Ultra-low latency**: Real-time, zero-drift audio streaming - 🔄 **Bidirectional audio**: Stream audio both ways between Windows and Linux - 🎛️ **Variable buffer sizes**: Runtime adjustment of buffer size and latency -- 🎯 **Oneshot mode**: Optimized single-packet transmission for minimal latency + - 🖥️ **Qt-based GUI**: Intuitive graphical interface for easy configuration - 💻 **CLI support**: Command-line interface for headless and scripted setups - 🪟 **ASIO driver**: Native Windows ASIO driver for maximum compatibility @@ -133,7 +133,7 @@ Options: --ip IP_ADDRESS, -i IP_ADDRESS Target Windows host IP address (default: 192.168.66.3) --port PORT, -p PORT UDP port to use (default: 8321) --buffer_size SIZE, -b SIZE Audio buffer size in frames (default: 64) - --oneshot Enable oneshot mode + --passthrough_test, -pt Enable passthrough test mode ``` @@ -141,8 +141,7 @@ Options: ## 🎛️ Key Features Explained -### Oneshot Mode -Oneshot mode optimizes for ultra-low latency by sending audio in single packets rather than streaming continuously. This significantly reduces latency but may increase CPU usage. + ### Variable Buffer Sizes Allows runtime adjustment of buffer sizes to balance between latency and stability. Smaller buffers = lower latency but require more CPU and stable network. @@ -220,7 +219,7 @@ Test binaries will be in `protocol/test/build/`. ### Common Issues - **No audio streaming**: Ensure both machines are on the same network and firewall allows UDP traffic on port 8321 -- **High latency**: Try enabling oneshot mode and reducing buffer sizes +- **High latency**: Try reducing buffer sizes - **Audio dropouts**: Increase buffer size or check network stability - **ASIO driver not found**: Make sure you've registered the DLL with `regsvr32.exe` @@ -228,7 +227,7 @@ Test binaries will be in `protocol/test/build/`. - Use wired network connections for best results - Match sample rates between Windows and Linux (48kHz recommended) - Close unnecessary applications to reduce CPU load -- Consider using oneshot mode for minimal latency scenarios + --- diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 93de600..8230977 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -1,56 +1,112 @@ -# CMakeLists.txt for Linux targets +# CMakeLists.txt for Linux targets - Audio Backend Agnostic cmake_minimum_required(VERSION 3.15) -# Find required packages +# Math library (always required) +find_library(MATH_LIB m) + +# Find required packages with optional fallbacks find_package(PkgConfig REQUIRED) -pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3) -# Find Qt5 for GUI -find_package(Qt5 COMPONENTS Core Widgets Quick Qml QUIET) +# Try to find PipeWire (optional) +pkg_check_modules(PIPEWIRE libpipewire-0.3) +if(PIPEWIRE_FOUND) + message(STATUS "PipeWire found - PipeWire backend will be available") + set(HAVE_PIPEWIRE TRUE) +else() + message(STATUS "PipeWire not found - PipeWire backend will be disabled") + set(HAVE_PIPEWIRE FALSE) +endif() -# Math library -find_library(MATH_LIB m) +# Try to find ALSA (optional) +pkg_check_modules(ALSA alsa) +if(ALSA_FOUND) + message(STATUS "ALSA found - ALSA backend will be available") + set(HAVE_ALSA TRUE) +else() + message(STATUS "ALSA not found - ALSA backend will be disabled") + set(HAVE_ALSA FALSE) +endif() + +# Ensure at least one audio backend is available +if(NOT HAVE_PIPEWIRE AND NOT HAVE_ALSA) + message(FATAL_ERROR "Neither PipeWire nor ALSA found. At least one audio backend is required.") +endif() + +# Find Qt5 for GUI (optional) +find_package(Qt5 COMPONENTS Core Widgets Quick Qml QUIET) # Include directories include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${CMAKE_SOURCE_DIR}/protocol) -# Protocol sources +# Protocol sources (backend agnostic) set(PROTOCOL_SOURCES - ${CMAKE_SOURCE_DIR}/protocol/pwar_router.c - ${CMAKE_SOURCE_DIR}/protocol/pwar_rcv_buffer.c + ${CMAKE_SOURCE_DIR}/protocol/pwar_ring_buffer.c ${CMAKE_SOURCE_DIR}/protocol/latency_manager.c ) -# Build shared library +# Audio backend sources (conditional compilation) +set(AUDIO_BACKEND_SOURCES + audio_backend.c + simulated_backend.c # Always available for testing +) + +if(HAVE_ALSA) + list(APPEND AUDIO_BACKEND_SOURCES alsa_backend.c) +endif() + +if(HAVE_PIPEWIRE) + list(APPEND AUDIO_BACKEND_SOURCES pipewire_backend.c) +endif() + +# Build shared library with agnostic audio backend support add_library(pwar SHARED libpwar.c ${PROTOCOL_SOURCES} + ${AUDIO_BACKEND_SOURCES} ) -target_include_directories(pwar PRIVATE - ${PIPEWIRE_INCLUDE_DIRS} -) +# Set compile definitions based on available backends +target_compile_definitions(pwar PRIVATE HAVE_SIMULATED=1) # Always available -target_link_libraries(pwar - ${PIPEWIRE_LIBRARIES} - ${MATH_LIB} -) +if(HAVE_ALSA) + target_compile_definitions(pwar PRIVATE HAVE_ALSA=1) + target_include_directories(pwar PRIVATE ${ALSA_INCLUDE_DIRS}) +endif() -target_compile_options(pwar PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) +if(HAVE_PIPEWIRE) + target_compile_definitions(pwar PRIVATE HAVE_PIPEWIRE=1) + target_include_directories(pwar PRIVATE ${PIPEWIRE_INCLUDE_DIRS}) +endif() + +# Link libraries conditionally +target_link_libraries(pwar ${MATH_LIB} pthread) + +if(HAVE_ALSA) + target_link_libraries(pwar ${ALSA_LIBRARIES}) + target_compile_options(pwar PRIVATE ${ALSA_CFLAGS_OTHER}) +endif() -# CLI executable +if(HAVE_PIPEWIRE) + target_link_libraries(pwar ${PIPEWIRE_LIBRARIES}) + target_compile_options(pwar PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) +endif() + +# CLI executable (agnostic) add_executable(pwar_cli pwar_cli.c ) -target_link_libraries(pwar_cli - pwar - ${PIPEWIRE_LIBRARIES} - ${MATH_LIB} -) +target_link_libraries(pwar_cli pwar) -target_compile_options(pwar_cli PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) +# Set compile definitions for CLI based on available backends +if(HAVE_ALSA) + target_compile_definitions(pwar_cli PRIVATE HAVE_ALSA=1) +endif() + +if(HAVE_PIPEWIRE) + target_compile_definitions(pwar_cli PRIVATE HAVE_PIPEWIRE=1) +endif() # GUI executable (only if Qt5 is found) if(Qt5_FOUND) @@ -74,52 +130,36 @@ if(Qt5_FOUND) Qt5::Widgets Qt5::Quick Qt5::Qml - ${PIPEWIRE_LIBRARIES} - ${MATH_LIB} ) - target_compile_options(pwar_gui PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) + # Set compile definitions for GUI based on available backends + if(HAVE_ALSA) + target_compile_definitions(pwar_gui PRIVATE HAVE_ALSA=1) + endif() + + if(HAVE_PIPEWIRE) + target_compile_definitions(pwar_gui PRIVATE HAVE_PIPEWIRE=1) + endif() message(STATUS "Qt5 found - building GUI application") else() message(STATUS "Qt5 not found - skipping GUI application") endif() -# Torture test executable -add_executable(pwar_torture - torture.c -) - -target_include_directories(pwar_torture PRIVATE - ${PIPEWIRE_INCLUDE_DIRS} -) - -target_link_libraries(pwar_torture - ${PIPEWIRE_LIBRARIES} - ${MATH_LIB} -) - -target_compile_options(pwar_torture PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) - -# Windows simulator executable -add_executable(windows_sim - windows_sim.c +# Client simulator executable (agnostic) +add_executable(client_simulator + client_simulator.c ${PROTOCOL_SOURCES} ) -target_include_directories(windows_sim PRIVATE - ${PIPEWIRE_INCLUDE_DIRS} -) - -target_link_libraries(windows_sim - ${PIPEWIRE_LIBRARIES} - ${MATH_LIB} -) - -target_compile_options(windows_sim PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) +target_link_libraries(client_simulator pwar) -# Integration test subdirectory -add_subdirectory(test) +# Integration test subdirectory (only if PipeWire is available) +if(HAVE_PIPEWIRE) + add_subdirectory(test) +else() + message(STATUS "PipeWire not found - skipping integration tests") +endif() # Install targets install(TARGETS pwar pwar_cli DESTINATION bin) @@ -141,3 +181,30 @@ add_custom_target(install-local COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${CMAKE_SOURCE_DIR}/install COMMENT "Installing to local directory for testing" ) + +# Print build configuration summary +message(STATUS "=== PWAR Build Configuration ===") +message(STATUS "Audio Backends:") +if(HAVE_ALSA) + message(STATUS " ALSA: ENABLED") +else() + message(STATUS " ALSA: DISABLED (not found)") +endif() + +if(HAVE_PIPEWIRE) + message(STATUS " PipeWire: ENABLED") +else() + message(STATUS " PipeWire: DISABLED (not found)") +endif() + +message(STATUS " Simulated: ENABLED (always available for testing)") + +message(STATUS "Executables:") +message(STATUS " pwar_cli: YES (agnostic)") +if(Qt5_FOUND) + message(STATUS " pwar_gui: YES (agnostic)") +else() + message(STATUS " pwar_gui: NO (Qt5 not found)") +endif() +message(STATUS " client_simulator: YES (agnostic)") +message(STATUS "================================") diff --git a/linux/PwarController.cpp b/linux/PwarController.cpp index 7df4a72..7043a6a 100644 --- a/linux/PwarController.cpp +++ b/linux/PwarController.cpp @@ -3,13 +3,14 @@ #include #include #include +#include "audio_backend.h" PwarController::PwarController(QObject *parent) : QObject(parent), m_status("Ready"), m_initialized(false), m_audioProcMinMs(0.0), m_audioProcMaxMs(0.0), m_audioProcAvgMs(0.0), m_jitterMinMs(0.0), m_jitterMaxMs(0.0), m_jitterAvgMs(0.0), m_rttMinMs(0.0), m_rttMaxMs(0.0), m_rttAvgMs(0.0), - m_xruns(0), m_currentWindowsBufferSize(0) { + m_ringBufferAvgMs(0.0), m_xruns(0), m_currentWindowsBufferSize(0) { // Initialize QSettings with organization and application name m_settings = new QSettings("PWAR", "PwarController", this); @@ -18,8 +19,18 @@ PwarController::PwarController(QObject *parent) strcpy(m_config.stream_ip, "192.168.66.3"); m_config.stream_port = 8321; m_config.passthrough_test = 0; - m_config.oneshot_mode = 0; - m_config.buffer_size = 64; + m_config.device_buffer_size = 64; // For GUI, keep device and windows packet same + m_config.windows_packet_size = 64; // Same as device buffer for simplicity + m_config.ring_buffer_depth = 2048; + m_config.backend_type = AUDIO_BACKEND_PIPEWIRE; + + // Initialize audio config for PipeWire + m_config.audio_config.device_playback = nullptr; // PipeWire uses NULL for auto-detection + m_config.audio_config.device_capture = nullptr; // PipeWire uses NULL for auto-detection + m_config.audio_config.sample_rate = 48000; + m_config.audio_config.frames = 64; + m_config.audio_config.playback_channels = 2; + m_config.audio_config.capture_channels = 1; // Populate port lists updateInputPorts(); @@ -97,28 +108,32 @@ void PwarController::setPassthroughTest(bool enabled) { } } -bool PwarController::oneshotMode() const { - return m_config.oneshot_mode; +int PwarController::bufferSize() const { + return m_config.device_buffer_size; // Return device buffer size for GUI compatibility } -void PwarController::setOneshotMode(bool enabled) { - if (m_config.oneshot_mode != enabled) { - m_config.oneshot_mode = enabled; - emit oneshotModeChanged(); - applyRuntimeConfig(); +void PwarController::setBufferSize(int size) { + if (m_config.device_buffer_size != size) { + m_config.device_buffer_size = size; + m_config.windows_packet_size = size; // Keep them the same for GUI simplicity + m_config.audio_config.frames = size; // Keep audio config in sync + emit bufferSizeChanged(); + if (pwar_is_running()) { + setStatus("Buffer size changed - stop and start to apply"); + } } } -int PwarController::bufferSize() const { - return m_config.buffer_size; +int PwarController::ringBufferDepth() const { + return m_config.ring_buffer_depth; } -void PwarController::setBufferSize(int size) { - if (m_config.buffer_size != size) { - m_config.buffer_size = size; - emit bufferSizeChanged(); +void PwarController::setRingBufferDepth(int depth) { + if (m_config.ring_buffer_depth != depth) { + m_config.ring_buffer_depth = depth; + emit ringBufferDepthChanged(); if (pwar_is_running()) { - setStatus("Buffer size changed - stop and start to apply"); + setStatus("Ring buffer depth changed - stop and start to apply"); } } } @@ -256,12 +271,12 @@ void PwarController::loadSettings() { bool savedPassthrough = m_settings->value("audio/passthroughTest", m_config.passthrough_test).toBool(); setPassthroughTest(savedPassthrough); - bool savedOneshot = m_settings->value("audio/oneshotMode", m_config.oneshot_mode).toBool(); - setOneshotMode(savedOneshot); - - int savedBufferSize = m_settings->value("audio/bufferSize", m_config.buffer_size).toInt(); + int savedBufferSize = m_settings->value("audio/bufferSize", m_config.device_buffer_size).toInt(); setBufferSize(savedBufferSize); + int savedRingBufferDepth = m_settings->value("audio/ringBufferDepth", m_config.ring_buffer_depth).toInt(); + setRingBufferDepth(savedRingBufferDepth); + // Load port selections m_selectedInputPort = m_settings->value("audio/selectedInputPort", "").toString(); m_selectedOutputLeftPort = m_settings->value("audio/selectedOutputLeftPort", "").toString(); @@ -282,8 +297,8 @@ void PwarController::saveSettings() { // Save audio settings m_settings->setValue("audio/passthroughTest", passthroughTest()); - m_settings->setValue("audio/oneshotMode", oneshotMode()); m_settings->setValue("audio/bufferSize", bufferSize()); + m_settings->setValue("audio/ringBufferDepth", ringBufferDepth()); // Save port selections m_settings->setValue("audio/selectedInputPort", m_selectedInputPort); @@ -405,6 +420,10 @@ double PwarController::rttAvgMs() const { return m_rttAvgMs; } +double PwarController::ringBufferAvgMs() const { + return m_ringBufferAvgMs; +} + uint32_t PwarController::xruns() const { return m_xruns; } @@ -423,34 +442,6 @@ void PwarController::updateLatencyMetrics() { bool changed = false; - // Update audio processing metrics - if (m_audioProcMinMs != metrics.audio_proc_min_ms) { - m_audioProcMinMs = metrics.audio_proc_min_ms; - changed = true; - } - if (m_audioProcMaxMs != metrics.audio_proc_max_ms) { - m_audioProcMaxMs = metrics.audio_proc_max_ms; - changed = true; - } - if (m_audioProcAvgMs != metrics.audio_proc_avg_ms) { - m_audioProcAvgMs = metrics.audio_proc_avg_ms; - changed = true; - } - - // Update jitter metrics - if (m_jitterMinMs != metrics.jitter_min_ms) { - m_jitterMinMs = metrics.jitter_min_ms; - changed = true; - } - if (m_jitterMaxMs != metrics.jitter_max_ms) { - m_jitterMaxMs = metrics.jitter_max_ms; - changed = true; - } - if (m_jitterAvgMs != metrics.jitter_avg_ms) { - m_jitterAvgMs = metrics.jitter_avg_ms; - changed = true; - } - // Update RTT metrics if (m_rttMinMs != metrics.rtt_min_ms) { m_rttMinMs = metrics.rtt_min_ms; @@ -465,6 +456,12 @@ void PwarController::updateLatencyMetrics() { changed = true; } + // Update ring buffer average + if (m_ringBufferAvgMs != metrics.ring_buffer_avg_ms) { + m_ringBufferAvgMs = metrics.ring_buffer_avg_ms; + changed = true; + } + // Update xruns if (m_xruns != metrics.xruns) { m_xruns = metrics.xruns; diff --git a/linux/PwarController.h b/linux/PwarController.h index 7430c20..f9927a4 100644 --- a/linux/PwarController.h +++ b/linux/PwarController.h @@ -11,8 +11,8 @@ class PwarController : public QObject { Q_PROPERTY(QString streamIp READ streamIp WRITE setStreamIp NOTIFY streamIpChanged) Q_PROPERTY(int streamPort READ streamPort WRITE setStreamPort NOTIFY streamPortChanged) Q_PROPERTY(bool passthroughTest READ passthroughTest WRITE setPassthroughTest NOTIFY passthroughTestChanged) - Q_PROPERTY(bool oneshotMode READ oneshotMode WRITE setOneshotMode NOTIFY oneshotModeChanged) Q_PROPERTY(int bufferSize READ bufferSize WRITE setBufferSize NOTIFY bufferSizeChanged) + Q_PROPERTY(int ringBufferDepth READ ringBufferDepth WRITE setRingBufferDepth NOTIFY ringBufferDepthChanged) Q_PROPERTY(QStringList outputPorts READ outputPorts NOTIFY outputPortsChanged) Q_PROPERTY(QStringList inputPorts READ inputPorts NOTIFY inputPortsChanged) Q_PROPERTY(QString selectedInputPort READ selectedInputPort WRITE setSelectedInputPort NOTIFY selectedInputPortChanged) @@ -29,6 +29,7 @@ class PwarController : public QObject { Q_PROPERTY(double rttMinMs READ rttMinMs NOTIFY latencyMetricsChanged) Q_PROPERTY(double rttMaxMs READ rttMaxMs NOTIFY latencyMetricsChanged) Q_PROPERTY(double rttAvgMs READ rttAvgMs NOTIFY latencyMetricsChanged) + Q_PROPERTY(double ringBufferAvgMs READ ringBufferAvgMs NOTIFY latencyMetricsChanged) Q_PROPERTY(uint32_t xruns READ xruns NOTIFY latencyMetricsChanged) // Current Windows buffer size property @@ -48,10 +49,10 @@ class PwarController : public QObject { void setStreamPort(int port); bool passthroughTest() const; void setPassthroughTest(bool enabled); - bool oneshotMode() const; - void setOneshotMode(bool enabled); int bufferSize() const; void setBufferSize(int size); + int ringBufferDepth() const; + void setRingBufferDepth(int depth); QStringList outputPorts() const; QStringList inputPorts() const; @@ -80,6 +81,7 @@ class PwarController : public QObject { double rttMinMs() const; double rttMaxMs() const; double rttAvgMs() const; + double ringBufferAvgMs() const; uint32_t xruns() const; // Current Windows buffer size getter @@ -93,8 +95,8 @@ class PwarController : public QObject { void streamIpChanged(); void streamPortChanged(); void passthroughTestChanged(); - void oneshotModeChanged(); void bufferSizeChanged(); + void ringBufferDepthChanged(); void outputPortsChanged(); void inputPortsChanged(); void selectedInputPortChanged(); @@ -128,6 +130,7 @@ class PwarController : public QObject { double m_rttMinMs; double m_rttMaxMs; double m_rttAvgMs; + double m_ringBufferAvgMs; uint32_t m_xruns; QTimer *m_latencyUpdateTimer; diff --git a/linux/alsa_backend.c b/linux/alsa_backend.c new file mode 100644 index 0000000..c8368ee --- /dev/null +++ b/linux/alsa_backend.c @@ -0,0 +1,415 @@ +// ALSA Audio Backend for PWAR +// Provides ALSA-specific audio interface implementation + +#include "audio_backend.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ALSA-specific statistics +typedef struct { + unsigned long total_iterations; + unsigned long capture_xruns; + unsigned long playback_xruns; + double total_loop_time; + double min_loop_time; + double max_loop_time; + struct timeval start_time; +} alsa_stats_t; + +// ALSA backend private data +typedef struct { + snd_pcm_t *playback_handle; + snd_pcm_t *capture_handle; + audio_config_t config; + + // Audio buffers + int32_t *playback_buffer; + int32_t *capture_buffer; + float *input_float_buffer; + float *output_left_buffer; + float *output_right_buffer; + + // Threading + pthread_t audio_thread; + volatile int should_stop; + + // Statistics + alsa_stats_t stats; + + // Latency measurement + float latency_ms; +} alsa_backend_data_t; + +static double get_ms_elapsed(const struct timeval *t0, const struct timeval *t1) { + return (t1->tv_sec - t0->tv_sec) * 1000.0 + + (t1->tv_usec - t0->tv_usec) / 1000.0; +} + +// Convert int32_t samples from ALSA to float +static void int32_to_float(int32_t *input, float *output, int samples) { + for (int i = 0; i < samples; i++) { + output[i] = (float)input[i] / 2147483648.0f; // 2^31 + } +} + +// Convert float samples to int32_t for ALSA +static void float_to_int32(float *input, int32_t *output, int samples) { + for (int i = 0; i < samples; i++) { + // Clamp to [-1.0, 1.0] and convert to int32 + float clamped = fmaxf(-1.0f, fminf(1.0f, input[i])); + output[i] = (int32_t)(clamped * 2147483647.0f); // 2^31 - 1 + } +} + +static int setup_pcm(snd_pcm_t **handle, + const char *device, + snd_pcm_stream_t stream, + unsigned int rate, + unsigned int channels, + snd_pcm_uframes_t period, + alsa_backend_data_t *data) +{ + int err; + snd_pcm_hw_params_t *hw; + snd_pcm_sw_params_t *sw; + + if ((err = snd_pcm_open(handle, device, stream, 0)) < 0) { + fprintf(stderr, "%s open error: %s\n", + stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture", + snd_strerror(err)); + return err; + } + + // Hardware parameters + snd_pcm_hw_params_alloca(&hw); + snd_pcm_hw_params_any(*handle, hw); + snd_pcm_hw_params_set_access(*handle, hw, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(*handle, hw, SND_PCM_FORMAT_S32_LE); + snd_pcm_hw_params_set_channels(*handle, hw, channels); + + unsigned int rr = rate; + int dir = 0; + snd_pcm_hw_params_set_rate_near(*handle, hw, &rr, &dir); + + snd_pcm_uframes_t per = period; + snd_pcm_hw_params_set_period_size_near(*handle, hw, &per, &dir); + + snd_pcm_uframes_t buf = per * 2; // 2 periods for slightly more safety + snd_pcm_hw_params_set_buffer_size_near(*handle, hw, &buf); + + if ((err = snd_pcm_hw_params(*handle, hw)) < 0) { + fprintf(stderr, "hw_params error: %s\n", snd_strerror(err)); + return err; + } + + // Software parameters - important for xrun behavior + snd_pcm_sw_params_alloca(&sw); + snd_pcm_sw_params_current(*handle, sw); + + // Start threshold + snd_pcm_sw_params_set_start_threshold(*handle, sw, per); + + // Minimum available frames to wake up + snd_pcm_sw_params_set_avail_min(*handle, sw, per); + + if ((err = snd_pcm_sw_params(*handle, sw)) < 0) { + fprintf(stderr, "sw_params error: %s\n", snd_strerror(err)); + return err; + } + + snd_pcm_prepare(*handle); + + // Print configuration + snd_pcm_hw_params_get_rate(hw, &rr, &dir); + snd_pcm_hw_params_get_period_size(hw, &per, &dir); + snd_pcm_hw_params_get_buffer_size(hw, &buf); + + float latency = rr ? ((float)buf * 1000.0f / rr) : 0.0f; + printf("ALSA %s: %u Hz, %u ch, period=%lu, buffer=%lu (%.2f ms buffer)\n", + stream == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture", + rr, channels, (unsigned long)per, (unsigned long)buf, + latency); + if (data) { + data->latency_ms += latency; + } + + return 0; +} + +static void *alsa_audio_thread(void *arg) { + audio_backend_t *backend = (audio_backend_t *)arg; + alsa_backend_data_t *data = (alsa_backend_data_t *)backend->private_data; + + struct timeval loop_start, loop_end; + unsigned long clean_loops = 0; + + gettimeofday(&data->stats.start_time, NULL); + data->stats.min_loop_time = 999999; + + printf("\nStarting ALSA audio processing thread. Press Ctrl+C for statistics.\n"); + printf("Legend: . = 1000 clean loops, C = capture xrun, P = playback xrun\n\n"); + + while (!data->should_stop) { + gettimeofday(&loop_start, NULL); + + // 1) Capture + int err = snd_pcm_readi(data->capture_handle, data->capture_buffer, data->config.frames); + if (err == -EPIPE || err == -ESTRPIPE) { + printf("C"); + fflush(stdout); + data->stats.capture_xruns++; + snd_pcm_prepare(data->capture_handle); + continue; + } else if (err < 0) { + fprintf(stderr, "\nCapture error: %s\n", snd_strerror(err)); + snd_pcm_prepare(data->capture_handle); + continue; + } + + // 2) Convert to float and process + int32_to_float(data->capture_buffer, data->input_float_buffer, + data->config.frames * data->config.capture_channels); + + // Extract input samples (guitar is on right channel if stereo) + float *mono_input = data->input_float_buffer; + if (data->config.capture_channels > 1) { + // Extract right channel for guitar input + for (uint32_t i = 0; i < data->config.frames; i++) { + mono_input[i] = data->input_float_buffer[i * data->config.capture_channels + 1]; + } + } + + // Call the PWAR processing callback + if (backend->callback) { + backend->callback(mono_input, data->output_left_buffer, data->output_right_buffer, + data->config.frames, backend->userdata); + } + + // 3) Convert back to int32 and playback + for (uint32_t i = 0; i < data->config.frames; i++) { + // Convert left channel + float clamped_left = fmaxf(-1.0f, fminf(1.0f, data->output_left_buffer[i])); + data->playback_buffer[i * data->config.playback_channels + 0] = + (int32_t)(clamped_left * 2147483647.0f); + + // Convert right channel + if (data->config.playback_channels > 1) { + float clamped_right = fmaxf(-1.0f, fminf(1.0f, data->output_right_buffer[i])); + data->playback_buffer[i * data->config.playback_channels + 1] = + (int32_t)(clamped_right * 2147483647.0f); + } + } + + err = snd_pcm_writei(data->playback_handle, data->playback_buffer, data->config.frames); + if (err == -EPIPE || err == -ESTRPIPE) { + printf("P"); + fflush(stdout); + data->stats.playback_xruns++; + snd_pcm_prepare(data->playback_handle); + continue; + } else if (err < 0) { + fprintf(stderr, "\nPlayback error: %s\n", snd_strerror(err)); + snd_pcm_prepare(data->playback_handle); + continue; + } + + // Update statistics + gettimeofday(&loop_end, NULL); + double loop_time = get_ms_elapsed(&loop_start, &loop_end); + + data->stats.total_loop_time += loop_time; + if (loop_time < data->stats.min_loop_time) data->stats.min_loop_time = loop_time; + if (loop_time > data->stats.max_loop_time) data->stats.max_loop_time = loop_time; + + data->stats.total_iterations++; + clean_loops++; + + // Progress indicator every 1000 clean loops + if (0 && (clean_loops >= 1000)) { + printf("."); + fflush(stdout); + clean_loops = 0; + } + } + + return NULL; +} + +static int alsa_init(audio_backend_t *backend, const audio_config_t *config, + audio_process_callback_t callback, void *userdata) { + alsa_backend_data_t *data = malloc(sizeof(alsa_backend_data_t)); + if (!data) { + return -1; + } + + memset(data, 0, sizeof(alsa_backend_data_t)); + data->config = *config; + backend->private_data = data; + backend->callback = callback; + backend->userdata = userdata; + + // Allocate audio buffers + data->playback_buffer = calloc(config->frames * config->playback_channels, sizeof(int32_t)); + data->capture_buffer = calloc(config->frames * config->capture_channels, sizeof(int32_t)); + data->input_float_buffer = calloc(config->frames * config->capture_channels, sizeof(float)); + data->output_left_buffer = calloc(config->frames, sizeof(float)); + data->output_right_buffer = calloc(config->frames, sizeof(float)); + + if (!data->playback_buffer || !data->capture_buffer || !data->input_float_buffer || + !data->output_left_buffer || !data->output_right_buffer) { + alsa_backend_data_t *d = data; + free(d->playback_buffer); + free(d->capture_buffer); + free(d->input_float_buffer); + free(d->output_left_buffer); + free(d->output_right_buffer); + free(data); + return -1; + } + + // Setup ALSA devices + data->latency_ms = 0.0f; + if (setup_pcm(&data->playback_handle, config->device_playback, SND_PCM_STREAM_PLAYBACK, + config->sample_rate, config->playback_channels, config->frames, data) < 0) { + return -1; + } + + if (setup_pcm(&data->capture_handle, config->device_capture, SND_PCM_STREAM_CAPTURE, + config->sample_rate, config->capture_channels, config->frames, data) < 0) { + snd_pcm_close(data->playback_handle); + return -1; + } + + return 0; +} + +static int alsa_start(audio_backend_t *backend) { + alsa_backend_data_t *data = (alsa_backend_data_t *)backend->private_data; + if (!data || backend->running) { + return -1; + } + + data->should_stop = 0; + if (pthread_create(&data->audio_thread, NULL, alsa_audio_thread, backend) != 0) { + return -1; + } + + backend->running = 1; + return 0; +} + +static int alsa_stop(audio_backend_t *backend) { + alsa_backend_data_t *data = (alsa_backend_data_t *)backend->private_data; + if (!data || !backend->running) { + return -1; + } + + data->should_stop = 1; + pthread_join(data->audio_thread, NULL); + backend->running = 0; + return 0; +} + +static void alsa_cleanup(audio_backend_t *backend) { + alsa_backend_data_t *data = (alsa_backend_data_t *)backend->private_data; + if (!data) { + return; + } + + if (backend->running) { + alsa_stop(backend); + } + + // Print final statistics + struct timeval now; + gettimeofday(&now, NULL); + double runtime = (now.tv_sec - data->stats.start_time.tv_sec) + + (now.tv_usec - data->stats.start_time.tv_usec) / 1000000.0; + + printf("\n========== ALSA Statistics ==========\n"); + printf("Runtime: %.1f seconds\n", runtime); + printf("Total iterations: %lu\n", data->stats.total_iterations); + printf("Capture XRUNs: %lu (%.3f%%)\n", + data->stats.capture_xruns, + data->stats.total_iterations > 0 ? (100.0 * data->stats.capture_xruns / data->stats.total_iterations) : 0); + printf("Playback XRUNs: %lu (%.3f%%)\n", + data->stats.playback_xruns, + data->stats.total_iterations > 0 ? (100.0 * data->stats.playback_xruns / data->stats.total_iterations) : 0); + + if (data->stats.total_iterations > 0) { + double avg_loop = data->stats.total_loop_time / data->stats.total_iterations; + printf("Loop time: avg=%.3f ms, min=%.3f ms, max=%.3f ms\n", + avg_loop, data->stats.min_loop_time, data->stats.max_loop_time); + printf("Theoretical min latency: %.3f ms (%.1f samples @ %d Hz)\n", + (double)data->config.frames * 1000.0 / data->config.sample_rate, + (double)data->config.frames, data->config.sample_rate); + } + printf("====================================\n"); + + // Check final device state + if (data->capture_handle) { + snd_pcm_state_t cap_state = snd_pcm_state(data->capture_handle); + printf("Final capture state: %s\n", snd_pcm_state_name(cap_state)); + snd_pcm_close(data->capture_handle); + } + + if (data->playback_handle) { + snd_pcm_state_t pb_state = snd_pcm_state(data->playback_handle); + printf("Final playback state: %s\n", snd_pcm_state_name(pb_state)); + snd_pcm_close(data->playback_handle); + } + + free(data->playback_buffer); + free(data->capture_buffer); + free(data->input_float_buffer); + free(data->output_left_buffer); + free(data->output_right_buffer); + free(data); + backend->private_data = NULL; +} + +static int alsa_is_running(audio_backend_t *backend) { + return backend->running; +} + +static void alsa_get_stats(audio_backend_t *backend, void *stats) { + alsa_backend_data_t *data = (alsa_backend_data_t *)backend->private_data; + if (data && stats) { + memcpy(stats, &data->stats, sizeof(alsa_stats_t)); + } +} + +// Return the sum of playback and capture buffer latencies in ms +static float alsa_get_latency(audio_backend_t *backend) { + alsa_backend_data_t *data = (alsa_backend_data_t *)backend->private_data; + if (!data) return 0.0f; + return data->latency_ms; +} + +static const audio_backend_ops_t alsa_ops = { + .init = alsa_init, + .start = alsa_start, + .stop = alsa_stop, + .cleanup = alsa_cleanup, + .is_running = alsa_is_running, + .get_stats = alsa_get_stats, + .get_latency = alsa_get_latency +}; + +audio_backend_t* audio_backend_create_alsa(void) { + audio_backend_t *backend = malloc(sizeof(audio_backend_t)); + if (!backend) { + return NULL; + } + + memset(backend, 0, sizeof(audio_backend_t)); + backend->ops = &alsa_ops; + return backend; +} diff --git a/linux/audio_backend.c b/linux/audio_backend.c new file mode 100644 index 0000000..a3f753b --- /dev/null +++ b/linux/audio_backend.c @@ -0,0 +1,93 @@ +// Generic Audio Backend Implementation +// Provides common interface for all audio backends + +#include "audio_backend.h" +#include + +// Generic backend operations +int audio_backend_init(audio_backend_t *backend, const audio_config_t *config, + audio_process_callback_t callback, void *userdata) { + if (!backend || !backend->ops || !backend->ops->init) { + return -1; + } + return backend->ops->init(backend, config, callback, userdata); +} + +int audio_backend_start(audio_backend_t *backend) { + if (!backend || !backend->ops || !backend->ops->start) { + return -1; + } + return backend->ops->start(backend); +} + +int audio_backend_stop(audio_backend_t *backend) { + if (!backend || !backend->ops || !backend->ops->stop) { + return -1; + } + return backend->ops->stop(backend); +} + +void audio_backend_cleanup(audio_backend_t *backend) { + if (!backend || !backend->ops || !backend->ops->cleanup) { + return; + } + backend->ops->cleanup(backend); + free(backend); +} + +int audio_backend_is_running(audio_backend_t *backend) { + if (!backend || !backend->ops || !backend->ops->is_running) { + return 0; + } + return backend->ops->is_running(backend); +} + +void audio_backend_get_stats(audio_backend_t *backend, void *stats) { + if (!backend || !backend->ops || !backend->ops->get_stats) { + return; + } + backend->ops->get_stats(backend, stats); +} + +float audio_backend_get_latency(audio_backend_t *backend) { + if (!backend || !backend->ops || !backend->ops->get_latency) { + return 0.0f; + } + return backend->ops->get_latency(backend); +} + +// Unified factory function +audio_backend_t* audio_backend_create(audio_backend_type_t type) { + switch (type) { +#ifdef HAVE_ALSA + case AUDIO_BACKEND_ALSA: + return audio_backend_create_alsa(); +#endif +#ifdef HAVE_PIPEWIRE + case AUDIO_BACKEND_PIPEWIRE: + return audio_backend_create_pipewire(); +#endif + case AUDIO_BACKEND_SIMULATED: + return audio_backend_create_simulated(); + default: + return NULL; + } +} + +// Check if a backend type is available at runtime +int audio_backend_is_available(audio_backend_type_t type) { + switch (type) { +#ifdef HAVE_ALSA + case AUDIO_BACKEND_ALSA: + return 1; +#endif +#ifdef HAVE_PIPEWIRE + case AUDIO_BACKEND_PIPEWIRE: + return 1; +#endif + case AUDIO_BACKEND_SIMULATED: + return 1; // Always available + default: + return 0; + } +} diff --git a/linux/audio_backend.h b/linux/audio_backend.h new file mode 100644 index 0000000..82748d9 --- /dev/null +++ b/linux/audio_backend.h @@ -0,0 +1,107 @@ +#ifndef AUDIO_BACKEND +#define AUDIO_BACKEND + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Audio backend types +typedef enum { + AUDIO_BACKEND_ALSA, + AUDIO_BACKEND_PIPEWIRE, + AUDIO_BACKEND_SIMULATED // Always available for testing +} audio_backend_type_t; + +// Audio configuration +typedef struct { + const char *device_playback; // For ALSA: "hw:3,0", for PipeWire: NULL + const char *device_capture; // For ALSA: "hw:3,0", for PipeWire: NULL + uint32_t sample_rate; + uint32_t frames; // Buffer size in frames + uint32_t playback_channels; + uint32_t capture_channels; +} audio_config_t; + +// Forward declaration of audio backend instance +struct audio_backend; + +// Audio processing callback function type +// Called by audio backend when new audio data is available +// in: input audio samples (interleaved) +// out_left, out_right: output audio samples (non-interleaved) +// n_samples: number of samples per channel +// userdata: user data passed to backend initialization +typedef void (*audio_process_callback_t)(float *in, float *out_left, float *out_right, + uint32_t n_samples, void *userdata); + +// Audio backend interface +typedef struct audio_backend_ops { + // Initialize the audio backend + int (*init)(struct audio_backend *backend, const audio_config_t *config, + audio_process_callback_t callback, void *userdata); + + // Start audio processing + int (*start)(struct audio_backend *backend); + + // Stop audio processing + int (*stop)(struct audio_backend *backend); + + // Cleanup resources + void (*cleanup)(struct audio_backend *backend); + + // Check if backend is running + int (*is_running)(struct audio_backend *backend); + + // Get backend-specific statistics (optional) + void (*get_stats)(struct audio_backend *backend, void *stats); + + // Get current backend latency in milliseconds + float (*get_latency)(struct audio_backend *backend); +} audio_backend_ops_t; + +// Audio backend instance +typedef struct audio_backend { + const audio_backend_ops_t *ops; + void *private_data; + audio_process_callback_t callback; + void *userdata; + int running; +} audio_backend_t; + +// Factory functions for creating backends (conditionally available) +#ifdef HAVE_ALSA +audio_backend_t* audio_backend_create_alsa(void); +#endif + +#ifdef HAVE_PIPEWIRE +audio_backend_t* audio_backend_create_pipewire(void); +#endif + +// Simulated backend (always available for testing) +audio_backend_t* audio_backend_create_simulated(void); + +// Unified factory function (always available) +audio_backend_t* audio_backend_create(audio_backend_type_t type); + +// Check if a backend type is available at runtime +int audio_backend_is_available(audio_backend_type_t type); + +// Generic backend operations +int audio_backend_init(audio_backend_t *backend, const audio_config_t *config, + audio_process_callback_t callback, void *userdata); +int audio_backend_start(audio_backend_t *backend); +int audio_backend_stop(audio_backend_t *backend); +void audio_backend_cleanup(audio_backend_t *backend); +int audio_backend_is_running(audio_backend_t *backend); +void audio_backend_get_stats(audio_backend_t *backend, void *stats); + +// Get current backend latency in milliseconds +float audio_backend_get_latency(audio_backend_t *backend); + +#ifdef __cplusplus +} +#endif + +#endif /* AUDIO_BACKEND */ diff --git a/linux/client_simulator.c b/linux/client_simulator.c new file mode 100644 index 0000000..aa73a2a --- /dev/null +++ b/linux/client_simulator.c @@ -0,0 +1,293 @@ +/* + * client_simulator.c - PWAR Client Simulator + * + * Simulates a PWAR client (like Windows ASIO driver) for testing + * Receives audio from PWAR server, processes it, and sends it back + * + * (c) 2025 Philip K. Gisslow + * This file is part of the PipeWire ASIO Relay (PWAR) project. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../protocol/pwar_packet.h" +#include "../protocol/latency_manager.h" + +// Default configuration (matching your existing setup) +#define DEFAULT_SERVER_IP "127.0.0.1" +#define DEFAULT_SERVER_PORT 8321 +#define DEFAULT_CLIENT_PORT 8322 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_PACKET_SIZE 512 + +// Configuration structure +typedef struct { + char server_ip[64]; + int server_port; + int client_port; + int channels; + int packet_size; // Changed from buffer_size to packet_size + int verbose; +} client_config_t; + +// Global state +static int recv_sockfd; +static int send_sockfd; +static struct sockaddr_in servaddr; +static volatile int keep_running = 1; +static client_config_t config; + +static void print_usage(const char *program_name) { + printf("PWAR Client Simulator - Simulates a PWAR client for testing\n\n"); + printf("Usage: %s [options]\n", program_name); + printf("Options:\n"); + printf(" -s, --server Server IP address (default: %s)\n", DEFAULT_SERVER_IP); + printf(" --server-port Server port (default: %d)\n", DEFAULT_SERVER_PORT); + printf(" -c, --client-port Client listening port (default: %d)\n", DEFAULT_CLIENT_PORT); + printf(" -p, --packet-size Packet size in samples (default: %d)\n", DEFAULT_PACKET_SIZE); + printf(" -n, --channels Number of channels (default: %d)\n", DEFAULT_CHANNELS); + printf(" -v, --verbose Enable verbose output\n"); + printf(" -h, --help Show this help message\n"); + printf("\nExamples:\n"); + printf(" %s # Connect to localhost with defaults\n", program_name); + printf(" %s -s 192.168.1.100 --server-port 9000 # Connect to remote server\n", program_name); + printf(" %s -v -p 256 -c 1 # Verbose mode, smaller packets, mono\n", program_name); + printf("\nDescription:\n"); + printf(" This simulator acts like a PWAR client (e.g., Windows ASIO driver).\n"); + printf(" It receives audio packets from a PWAR server, processes them,\n"); + printf(" and sends them back, creating a loopback test environment.\n"); +} + +static int parse_arguments(int argc, char *argv[], client_config_t *cfg) { + // Set defaults + strcpy(cfg->server_ip, DEFAULT_SERVER_IP); + cfg->server_port = DEFAULT_SERVER_PORT; + cfg->client_port = DEFAULT_CLIENT_PORT; + cfg->channels = DEFAULT_CHANNELS; + cfg->packet_size = DEFAULT_PACKET_SIZE; + cfg->verbose = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return -1; + } else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--server") == 0) && i + 1 < argc) { + strcpy(cfg->server_ip, argv[++i]); + } else if (strcmp(argv[i], "--server-port") == 0 && i + 1 < argc) { + cfg->server_port = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--client-port") == 0) && i + 1 < argc) { + cfg->client_port = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--packet-size") == 0) && i + 1 < argc) { + cfg->packet_size = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--channels") == 0) && i + 1 < argc) { + cfg->channels = atoi(argv[++i]); + } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { + cfg->verbose = 1; + } else { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + print_usage(argv[0]); + return -1; + } + } + + // Validate configuration + if (cfg->server_port <= 0 || cfg->server_port > 65535) { + fprintf(stderr, "Invalid server port: %d\n", cfg->server_port); + return -1; + } + if (cfg->client_port <= 0 || cfg->client_port > 65535) { + fprintf(stderr, "Invalid client port: %d\n", cfg->client_port); + return -1; + } + if (cfg->channels < 1 || cfg->channels > 8) { + fprintf(stderr, "Invalid channel count: %d (must be 1-8)\n", cfg->channels); + return -1; + } + if (cfg->packet_size < 32 || cfg->packet_size > 4096) { + fprintf(stderr, "Invalid packet size: %d (must be 32-4096)\n", cfg->packet_size); + return -1; + } + + return 0; +} + +static void setup_recv_socket(int port) { + recv_sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (recv_sockfd < 0) { + perror("recv socket creation failed"); + exit(EXIT_FAILURE); + } + + // Set socket options for better performance + int rcvbuf = 1024 * 1024; + setsockopt(recv_sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + + // Set socket timeout to allow periodic checking of keep_running flag + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; // 100ms timeout + if (setsockopt(recv_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { + perror("Warning: Failed to set socket timeout"); + } + + struct sockaddr_in recv_addr; + memset(&recv_addr, 0, sizeof(recv_addr)); + recv_addr.sin_family = AF_INET; + recv_addr.sin_addr.s_addr = INADDR_ANY; + recv_addr.sin_port = htons(port); + + if (bind(recv_sockfd, (struct sockaddr *)&recv_addr, sizeof(recv_addr)) < 0) { + perror("recv socket bind failed"); + exit(EXIT_FAILURE); + } + + if (config.verbose) { + printf("[Client Simulator] Listening on port %d\n", port); + } +} + +static void setup_send_socket(const char *server_ip, int server_port) { + send_sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (send_sockfd < 0) { + perror("send socket creation failed"); + exit(EXIT_FAILURE); + } + + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(server_port); + + if (inet_pton(AF_INET, server_ip, &servaddr.sin_addr) <= 0) { + fprintf(stderr, "Invalid server IP address: %s\n", server_ip); + exit(EXIT_FAILURE); + } + + if (config.verbose) { + printf("[Client Simulator] Sending to %s:%d\n", server_ip, server_port); + } +} + +static void *receiver_thread(void *userdata) { + // Set real-time scheduling + struct sched_param sp = { .sched_priority = 90 }; + if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) != 0) { + perror("Warning: Failed to set SCHED_FIFO for receiver_thread"); + } + + pwar_packet_t packet; + pwar_packet_t response_packet; + uint64_t packets_processed = 0; + + printf("[Client Simulator] Receiver thread started\n"); + + while (keep_running) { + ssize_t n = recvfrom(recv_sockfd, &packet, sizeof(packet), 0, NULL, NULL); + + if (n == (ssize_t)sizeof(packet)) { + // Set Windows receive timestamp + packet.t2_windows_recv = latency_manager_timestamp_now(); + + // Copy input to output 1:1 (no audio processing) + response_packet = packet; // Copy the whole structure including samples + + // Set Windows send timestamp + response_packet.t3_windows_send = latency_manager_timestamp_now(); + + // Send response packet back to server + ssize_t sent = sendto(send_sockfd, &response_packet, sizeof(response_packet), 0, + (struct sockaddr *)&servaddr, sizeof(servaddr)); + if (sent < 0) { + perror("sendto failed"); + } + + packets_processed++; + if (config.verbose && packets_processed % 1000 == 0) { + printf("[Client Simulator] Processed %llu packets\n", + (unsigned long long)packets_processed); + } + } else if (n < 0) { + // Check if it's a timeout (expected) vs a real error + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Timeout is expected - just continue and check keep_running + continue; + } else if (keep_running) { + // Only print error if we're not shutting down + perror("recvfrom error"); + } + } + // If n == 0 or some other size, just continue + } + + printf("[Client Simulator] Receiver thread stopped\n"); + return NULL; +} + +static void signal_handler(int sig) { + printf("\n[Client Simulator] Received signal %d, shutting down...\n", sig); + keep_running = 0; +} + +int main(int argc, char *argv[]) { + printf("PWAR Client Simulator - Testing tool for PWAR protocol\n"); + printf("Simulates a PWAR client (like Windows ASIO driver)\n\n"); + + // Parse command line arguments + if (parse_arguments(argc, argv, &config) < 0) { + return 1; + } + + // Print configuration + printf("Configuration:\n"); + printf(" Server: %s:%d\n", config.server_ip, config.server_port); + printf(" Client port: %d\n", config.client_port); + printf(" Channels: %d\n", config.channels); + printf(" Packet size: %d samples\n", config.packet_size); + printf(" Verbose: %s\n", config.verbose ? "enabled" : "disabled"); + printf("\n"); + + // Set up signal handlers + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Set up networking + setup_recv_socket(config.client_port); + setup_send_socket(config.server_ip, config.server_port); + + // Start receiver thread + pthread_t recv_thread; + if (pthread_create(&recv_thread, NULL, receiver_thread, NULL) != 0) { + perror("Failed to create receiver thread"); + return 1; + } + + printf("[Client Simulator] Started successfully. Press Ctrl+C to stop.\n"); + printf("[Client Simulator] Waiting for audio packets from PWAR server...\n"); + + // Main loop + while (keep_running) { + struct timespec sleep_time = {0, 100000000}; // 100ms + nanosleep(&sleep_time, NULL); + } + + // Cleanup + printf("[Client Simulator] Shutting down...\n"); + keep_running = 0; + pthread_join(recv_thread, NULL); + + close(recv_sockfd); + close(send_sockfd); + + printf("[Client Simulator] Shutdown complete\n"); + return 0; +} diff --git a/linux/libpwar.c b/linux/libpwar.c index bc2a0eb..2cb1596 100644 --- a/linux/libpwar.c +++ b/linux/libpwar.c @@ -1,4 +1,10 @@ +// Core PWAR Library - Audio backend agnostic implementation +// Contains the core PWAR protocol logic without direct audio API dependencies + +#define _POSIX_C_SOURCE 199309L // Enable POSIX time functions + #include "libpwar.h" +#include "audio_backend.h" #include #include #include @@ -6,99 +12,88 @@ #include #include #include +#include #include #include #include #include #include -#include -#include -#include -#include -#include "latency_manager.h" - -#include "pwar_packet.h" -#include "pwar_router.h" -#include "pwar_rcv_buffer.h" +#include "../protocol/latency_manager.h" +#include "../protocol/pwar_packet.h" +#include "../protocol/pwar_ring_buffer.h" #define DEFAULT_STREAM_IP "192.168.66.3" #define DEFAULT_STREAM_PORT 8321 - #define MAX_BUFFER_SIZE 4096 #define NUM_CHANNELS 2 // Global data for GUI mode -static struct data *g_pwar_data = NULL; +static struct pwar_core_data *g_pwar_data = NULL; static pthread_t g_recv_thread; static int g_pwar_initialized = 0; static int g_pwar_running = 0; static pwar_config_t g_current_config; -struct data; - -struct port { - struct data *data; -}; - -struct data { - struct pw_main_loop *loop; - struct pw_filter *filter; - struct port *in_port; - struct port *left_out_port; - struct port *right_out_port; - float sine_phase; - uint8_t passthrough_test; // Add passthrough_test flag - uint8_t oneshot_mode; // Add oneshot_mode flag - uint32_t seq; +// Core PWAR data structure (audio backend agnostic) +struct pwar_core_data { + // Audio backend + audio_backend_t *audio_backend; + + // PWAR configuration + pwar_config_t config; + + // Network int sockfd; struct sockaddr_in servaddr; int recv_sockfd; - - pthread_mutex_t packet_mutex; - pthread_cond_t packet_cond; - pwar_packet_t latest_packet; - int packet_available; - - pwar_router_t linux_router; - pthread_mutex_t pwar_rcv_mutex; // Mutex for receive buffer - - uint32_t current_windows_buffer_size; // Current Windows buffer size in samples + + // PWAR protocol state + uint32_t seq; + + // Buffering for Windows packet accumulation + float *accumulation_buffer; // Buffer to accumulate device buffers + uint32_t accumulated_samples; // Number of samples currently accumulated + uint32_t packets_per_send; // How many device buffers per Windows packet + + uint32_t current_windows_buffer_size; + volatile int should_stop; }; -static void setup_recv_socket(struct data *data, int port); +// Forward declarations +static void setup_socket(struct pwar_core_data *data, const char *ip, int port); +static void setup_recv_socket(struct pwar_core_data *data, int port); static void *receiver_thread(void *userdata); +static void audio_process_callback(float *in, float *out_left, float *out_right, + uint32_t n_samples, void *userdata); +static void process_audio(struct pwar_core_data *data, float *in, uint32_t n_samples, + float *left_out, float *right_out); -static void setup_socket(struct data *data, const char *ip, int port); - -static void stream_buffer(float *samples, uint32_t n_samples, void *userdata); -static void on_process(void *userdata, struct spa_io_position *position); -static void do_quit(void *userdata, int signal_number); +static void setup_socket(struct pwar_core_data *data, const char *ip, int port) { + data->sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (data->sockfd < 0) { + perror("socket creation failed"); + exit(EXIT_FAILURE); + } + memset(&data->servaddr, 0, sizeof(data->servaddr)); + data->servaddr.sin_family = AF_INET; + data->servaddr.sin_port = htons(port); + data->servaddr.sin_addr.s_addr = inet_addr(ip); +} -// Extract common initialization logic -static int init_data_structure(struct data *data, const pwar_config_t *config); -static int create_pipewire_filter(struct data *data); - -// New GUI functions -int pwar_requires_restart(const pwar_config_t *old_config, const pwar_config_t *new_config); -int pwar_update_config(const pwar_config_t *config); -int pwar_init(const pwar_config_t *config); -int pwar_start(void); -int pwar_stop(void); -void pwar_cleanup(void); -int pwar_is_running(void); - -static void setup_recv_socket(struct data *data, int port) { +static void setup_recv_socket(struct pwar_core_data *data, int port) { data->recv_sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (data->recv_sockfd < 0) { perror("recv socket creation failed"); exit(EXIT_FAILURE); } + // Increase UDP receive buffer to 1MB to reduce risk of overrun int rcvbuf = 1024 * 1024; if (setsockopt(data->recv_sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0) { perror("setsockopt SO_RCVBUF failed"); } + struct sockaddr_in recv_addr; memset(&recv_addr, 0, sizeof(recv_addr)); recv_addr.sin_family = AF_INET; @@ -117,272 +112,174 @@ static void *receiver_thread(void *userdata) { perror("Warning: Failed to set SCHED_FIFO for receiver_thread"); } - struct data *data = (struct data *)userdata; - char recv_buffer[sizeof(pwar_packet_t) > sizeof(pwar_latency_info_t) ? sizeof(pwar_packet_t) : sizeof(pwar_latency_info_t)]; - float linux_output_buffers[NUM_CHANNELS * MAX_BUFFER_SIZE] = {0}; + struct pwar_core_data *data = (struct pwar_core_data *)userdata; + char recv_buffer[sizeof(pwar_packet_t)]; + float output_buffers[NUM_CHANNELS * MAX_BUFFER_SIZE] = {0}; + + // Set socket timeout to allow periodic checking of should_stop + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; // 100ms timeout + if (setsockopt(data->recv_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { + perror("Warning: Failed to set socket timeout"); + } - while (1) { + while (!data->should_stop) { ssize_t n = recvfrom(data->recv_sockfd, recv_buffer, sizeof(recv_buffer), 0, NULL, NULL); + if (n == (ssize_t)sizeof(pwar_packet_t)) { pwar_packet_t *packet = (pwar_packet_t *)recv_buffer; - latency_manager_process_packet_server(packet); - data->current_windows_buffer_size = packet->n_samples * packet->num_packets; - if (data->oneshot_mode) { - pthread_mutex_lock(&data->packet_mutex); - data->latest_packet = *packet; - data->packet_available = 1; - pthread_cond_signal(&data->packet_cond); - pthread_mutex_unlock(&data->packet_mutex); - } - else { - int samples_ready = pwar_router_process_packet(&data->linux_router, packet, linux_output_buffers, MAX_BUFFER_SIZE, NUM_CHANNELS); - if (samples_ready > 0) { - pthread_mutex_lock(&data->pwar_rcv_mutex); // Lock before buffer add - pwar_rcv_buffer_add_buffer(linux_output_buffers, samples_ready, NUM_CHANNELS); - pthread_mutex_unlock(&data->pwar_rcv_mutex); // Unlock after buffer add - } + latency_manager_process_packet(packet); + data->current_windows_buffer_size = packet->n_samples; + + pwar_ring_buffer_push(packet->samples, packet->n_samples, NUM_CHANNELS); + latency_manager_report_ring_buffer_fill_level(pwar_ring_buffer_get_available()); + } else if (n < 0) { + // Check if it's a timeout (expected) vs a real error + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // Timeout is expected - just continue and check should_stop + continue; + } else if (!data->should_stop) { + // Only print error if we're not shutting down + perror("recvfrom error"); } - } else if (n == (ssize_t)sizeof(pwar_latency_info_t)) { - pwar_latency_info_t *latency_info = (pwar_latency_info_t *)recv_buffer; - latency_manager_handle_latency_info(latency_info); } } return NULL; } -static void setup_socket(struct data *data, const char *ip, int port) { - data->sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (data->sockfd < 0) { - perror("socket creation failed"); - exit(EXIT_FAILURE); - } - memset(&data->servaddr, 0, sizeof(data->servaddr)); - data->servaddr.sin_family = AF_INET; - data->servaddr.sin_port = htons(port); - data->servaddr.sin_addr.s_addr = inet_addr(ip); -} - -static void stream_buffer(float *samples, uint32_t n_samples, void *userdata) { - struct data *data = (struct data *)userdata; - pwar_packet_t packet; - packet.seq = data->seq++; - packet.n_samples = n_samples; - packet.packet_index = 0; // Reset packet index for new packet, (as its oneshot mode) - packet.num_packets = 1; // Only one packet in oneshot mode - // Just stream the first channel for now.. FIXME: This should be updated to handle multiple channels properly in the future - memcpy(packet.samples[0], samples, n_samples * sizeof(float)); - - packet.timestamp = latency_manager_timestamp_now(); - packet.seq_timestamp = packet.timestamp; // Set seq_timestamp to the same value as timestamp - if (sendto(data->sockfd, &packet, sizeof(packet), 0, (struct sockaddr *)&data->servaddr, sizeof(data->servaddr)) < 0) { - perror("sendto failed"); +static void audio_process_callback(float *in, float *out_left, float *out_right, + uint32_t n_samples, void *userdata) { + struct pwar_core_data *data = (struct pwar_core_data *)userdata; + + if (data->config.passthrough_test) { + // Local passthrough test - just copy input to output + if (out_left) memcpy(out_left, in, n_samples * sizeof(float)); + if (out_right) memcpy(out_right, in, n_samples * sizeof(float)); + return; } -} -static void process_one_shot(void *userdata, float *in, uint32_t n_samples, float *left_out, float *right_out) { - struct data *data = (struct data *)userdata; - stream_buffer(in, n_samples, data); - int got_packet = 0; - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - ts.tv_nsec += 2 * 1000 * 1000; - if (ts.tv_nsec >= 1000000000) { - ts.tv_sec += 1; - ts.tv_nsec -= 1000000000; - } - pthread_mutex_lock(&data->packet_mutex); - while (!data->packet_available) { - // Wait for packet or timeout (no ping-pong) - int rc = pthread_cond_timedwait(&data->packet_cond, &data->packet_mutex, &ts); - if (rc == ETIMEDOUT) - break; - } - if (data->packet_available) { - if (left_out) - memcpy(left_out, data->latest_packet.samples[0], n_samples * sizeof(float)); - if (right_out) - memcpy(right_out, data->latest_packet.samples[1], n_samples * sizeof(float)); - got_packet = 1; - data->packet_available = 0; - } - pthread_mutex_unlock(&data->packet_mutex); - if (!got_packet) { - latency_manager_report_xrun(); - printf("\033[0;31m--- ERROR -- No valid packet received, outputting silence\n"); - printf("I wanted seq: %u and got seq: %lu\033[0m\n", data->seq - 1, data->latest_packet.seq); - if (left_out) - memset(left_out, 0, n_samples * sizeof(float)); - if (right_out) - memset(right_out, 0, n_samples * sizeof(float)); - } + process_audio(data, in, n_samples, out_left, out_right); } -static void process_ping_pong(void *userdata, float *in, uint32_t n_samples, float *left_out, float *right_out) { - struct data *data = (struct data *)userdata; - - // Create a packet for the input samples - pwar_packet_t packet; - packet.seq = data->seq++; - packet.n_samples = n_samples; - - // Just stream the first channel for now.. FIXME: This should be updated to handle multiple channels properly in the future - memcpy(packet.samples[0], in, n_samples * sizeof(float)); - - packet.timestamp = latency_manager_timestamp_now(); - packet.seq_timestamp = packet.timestamp; // Set seq_timestamp to the same value as timestamp - packet.num_packets = 1; - packet.packet_index = 0; - - /* Lock to prevent the response being received too soon */ - pthread_mutex_lock(&data->pwar_rcv_mutex); // Lock before get_chunk - - if (sendto(data->sockfd, &packet, sizeof(packet), 0, (struct sockaddr *)&data->servaddr, sizeof(data->servaddr)) < 0) { - perror("sendto failed"); - } - - float linux_rcv_buffers[NUM_CHANNELS * n_samples]; - memset(linux_rcv_buffers, 0, sizeof(linux_rcv_buffers)); - // Get the chunk from n-1 (ping-pong) - if (!pwar_rcv_get_chunk(linux_rcv_buffers, NUM_CHANNELS, n_samples)) { - printf("\033[0;31m--- ERROR -- No valid buffer ready, outputting silence\033[0m\n"); - latency_manager_report_xrun(); +static void process_audio(struct pwar_core_data *data, float *in, uint32_t n_samples, + float *left_out, float *right_out) { + // Accumulate input samples in interleaved format into the accumulation buffer + for (uint32_t i = 0; i < n_samples; i++) { + uint32_t buffer_idx = data->accumulated_samples + i; + data->accumulation_buffer[buffer_idx * NUM_CHANNELS + 0] = in[i]; // Left channel + data->accumulation_buffer[buffer_idx * NUM_CHANNELS + 1] = in[i]; // Right channel } - - pthread_mutex_unlock(&data->pwar_rcv_mutex); // Unlock after get_chunk - - if (left_out) - memcpy(left_out, linux_rcv_buffers, n_samples * sizeof(float)); - if (right_out) - memcpy(right_out, linux_rcv_buffers + n_samples, n_samples * sizeof(float)); -} - -static void on_process(void *userdata, struct spa_io_position *position) { - struct data *data = (struct data *)userdata; - float *in = pw_filter_get_dsp_buffer(data->in_port, position->clock.duration); - float *left_out = pw_filter_get_dsp_buffer(data->left_out_port, position->clock.duration); - float *right_out = pw_filter_get_dsp_buffer(data->right_out_port, position->clock.duration); - - uint32_t n_samples = position->clock.duration; - if (data->passthrough_test) { - if (left_out) - memcpy(left_out, in, n_samples * sizeof(float)); - if (right_out) - memcpy(right_out, in, n_samples * sizeof(float)); - return; - } - - if (data->oneshot_mode) { - // Use one-shot processing, i.e. Linux send, Windows process, Linux receive in one go - process_one_shot(data, in, n_samples, left_out, right_out); + + data->accumulated_samples += n_samples; + + // Check if we have accumulated enough samples for a Windows packet + if (data->accumulated_samples >= data->config.windows_packet_size) { + // Send accumulated packet to Windows + pwar_packet_t packet; + packet.n_samples = data->config.windows_packet_size; + + // Copy accumulated samples to packet + memcpy(packet.samples, data->accumulation_buffer, + data->config.windows_packet_size * NUM_CHANNELS * sizeof(float)); + + packet.t1_linux_send = latency_manager_timestamp_now(); + + if (sendto(data->sockfd, &packet, sizeof(packet), 0, + (struct sockaddr *)&data->servaddr, sizeof(data->servaddr)) < 0) { + perror("sendto failed"); + } + + // Reset accumulation buffer + data->accumulated_samples = 0; + memset(data->accumulation_buffer, 0, + data->config.windows_packet_size * NUM_CHANNELS * sizeof(float)); } - else { - // Use ping-pong processing, i.e. Linux send, Windows process, Linux receive in chunks - process_ping_pong(data, in, n_samples, left_out, right_out); + + // Get processed samples from ring buffer for current device buffer size + float rcv_buffers[NUM_CHANNELS * n_samples]; + memset(rcv_buffers, 0, sizeof(rcv_buffers)); + + pwar_ring_buffer_pop(rcv_buffers, n_samples, NUM_CHANNELS); + + // Convert from interleaved format to separate channel outputs + for (uint32_t i = 0; i < n_samples; i++) { + if (left_out) left_out[i] = rcv_buffers[i * NUM_CHANNELS + 0]; + if (right_out) right_out[i] = rcv_buffers[i * NUM_CHANNELS + 1]; } } -static const struct pw_filter_events filter_events = { - PW_VERSION_FILTER_EVENTS, - .process = on_process, -}; - -static void do_quit(void *userdata, int signal_number) { - struct data *data = (struct data *)userdata; - pw_main_loop_quit(data->loop); -} - -// Thread function to run PipeWire main loop for GUI mode -static void *pipewire_thread_func(void *userdata) { - struct data *data = (struct data *)userdata; - pw_main_loop_run(data->loop); - return NULL; -} - // Extract common initialization logic -static int init_data_structure(struct data *data, const pwar_config_t *config) { - memset(data, 0, sizeof(struct data)); +static int init_core_data(struct pwar_core_data *data, const pwar_config_t *config) { + memset(data, 0, sizeof(struct pwar_core_data)); + data->config = *config; setup_socket(data, config->stream_ip, config->stream_port); setup_recv_socket(data, DEFAULT_STREAM_PORT); - pthread_mutex_init(&data->packet_mutex, NULL); - pthread_cond_init(&data->packet_cond, NULL); - data->packet_available = 0; - pthread_mutex_init(&data->pwar_rcv_mutex, NULL); - data->passthrough_test = config->passthrough_test; - data->oneshot_mode = config->oneshot_mode; - data->sine_phase = 0.0f; - pwar_router_init(&data->linux_router, NUM_CHANNELS); + data->seq = 0; - return 0; -} - -static int create_pipewire_filter(struct data *data) { - const struct spa_pod *params[1]; - uint8_t buffer[1024]; - struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); - - data->filter = pw_filter_new_simple( - pw_main_loop_get_loop(data->loop), - "pwar", - pw_properties_new( - PW_KEY_MEDIA_TYPE, "Audio", - PW_KEY_MEDIA_CATEGORY, "Filter", - PW_KEY_MEDIA_ROLE, "DSP", - NULL), - &filter_events, - data); - - data->in_port = pw_filter_add_port(data->filter, - PW_DIRECTION_INPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(struct port), - pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit float mono audio", - PW_KEY_PORT_NAME, "input", - NULL), - NULL, 0); - - data->left_out_port = pw_filter_add_port(data->filter, - PW_DIRECTION_OUTPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(struct port), - pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit float mono audio", - PW_KEY_PORT_NAME, "output-left", - NULL), - NULL, 0); - - data->right_out_port = pw_filter_add_port(data->filter, - PW_DIRECTION_OUTPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(struct port), - pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit float mono audio", - PW_KEY_PORT_NAME, "output-right", - NULL), - NULL, 0); - - params[0] = spa_process_latency_build(&b, - SPA_PARAM_ProcessLatency, - &SPA_PROCESS_LATENCY_INFO_INIT( - .ns = 10 * SPA_NSEC_PER_MSEC - )); - - if (pw_filter_connect(data->filter, - PW_FILTER_FLAG_RT_PROCESS, - params, 1) < 0) { + // Calculate packets per send and allocate accumulation buffer + data->packets_per_send = config->windows_packet_size / config->device_buffer_size; + data->accumulated_samples = 0; + + // Allocate accumulation buffer for interleaved samples + size_t buffer_size = config->windows_packet_size * NUM_CHANNELS * sizeof(float); + data->accumulation_buffer = (float*)malloc(buffer_size); + if (!data->accumulation_buffer) { + fprintf(stderr, "Failed to allocate accumulation buffer\n"); return -1; } - + memset(data->accumulation_buffer, 0, buffer_size); + + // Initialize ring buffer with configured depth and Windows packet size + pwar_ring_buffer_init(config->ring_buffer_depth, NUM_CHANNELS, config->windows_packet_size); + + // Create appropriate audio backend using unified factory + if (!audio_backend_is_available(config->backend_type)) { + fprintf(stderr, "Audio backend type %d is not available (not compiled in)\n", config->backend_type); + free(data->accumulation_buffer); + return -1; + } + + data->audio_backend = audio_backend_create(config->backend_type); + + if (!data->audio_backend) { + fprintf(stderr, "Failed to create audio backend\n"); + free(data->accumulation_buffer); + return -1; + } + + // Initialize the audio backend + if (audio_backend_init(data->audio_backend, &config->audio_config, + audio_process_callback, data) < 0) { + fprintf(stderr, "Failed to initialize audio backend\n"); + audio_backend_cleanup(data->audio_backend); + data->audio_backend = NULL; + free(data->accumulation_buffer); + return -1; + } + + // Initialize latency manager with Windows packet size + latency_manager_init(config->audio_config.sample_rate, config->windows_packet_size, data->audio_backend->ops->get_latency(data->audio_backend)); + return 0; } -// New GUI functions +// Signal handler for CLI mode +static volatile int cli_keep_running = 1; +static void cli_sigint_handler(int sig) { + cli_keep_running = 0; +} + +// Public API Implementation int pwar_requires_restart(const pwar_config_t *old_config, const pwar_config_t *new_config) { - if (old_config->buffer_size != new_config->buffer_size || + if (old_config->device_buffer_size != new_config->device_buffer_size || + old_config->windows_packet_size != new_config->windows_packet_size || + old_config->ring_buffer_depth != new_config->ring_buffer_depth || strcmp(old_config->stream_ip, new_config->stream_ip) != 0 || - old_config->stream_port != new_config->stream_port) { + old_config->stream_port != new_config->stream_port || + old_config->backend_type != new_config->backend_type) { return 1; } return 0; @@ -398,8 +295,7 @@ int pwar_update_config(const pwar_config_t *config) { } // Apply runtime-changeable settings - g_pwar_data->passthrough_test = config->passthrough_test; - g_pwar_data->oneshot_mode = config->oneshot_mode; + g_pwar_data->config.passthrough_test = config->passthrough_test; g_current_config = *config; return 0; @@ -412,24 +308,20 @@ int pwar_init(const pwar_config_t *config) { g_current_config = *config; - char latency[32]; - snprintf(latency, sizeof(latency), "%d/48000", config->buffer_size); - setenv("PIPEWIRE_LATENCY", latency, 1); - - g_pwar_data = malloc(sizeof(struct data)); + g_pwar_data = malloc(sizeof(struct pwar_core_data)); if (!g_pwar_data) { return -1; } - if (init_data_structure(g_pwar_data, config) < 0) { + if (init_core_data(g_pwar_data, config) < 0) { free(g_pwar_data); g_pwar_data = NULL; return -1; } + // Start receiver thread + g_pwar_data->should_stop = 0; pthread_create(&g_recv_thread, NULL, receiver_thread, g_pwar_data); - pw_init(NULL, NULL); - g_pwar_data->loop = pw_main_loop_new(NULL); g_pwar_initialized = 1; return 0; @@ -440,21 +332,10 @@ int pwar_start(void) { return -1; } - // Create a new main loop for this start session - if (g_pwar_data->loop) { - pw_main_loop_destroy(g_pwar_data->loop); - } - g_pwar_data->loop = pw_main_loop_new(NULL); - - if (create_pipewire_filter(g_pwar_data) < 0) { + if (audio_backend_start(g_pwar_data->audio_backend) < 0) { return -1; } - // Start the PipeWire main loop in a separate thread for GUI mode - pthread_t pw_thread; - pthread_create(&pw_thread, NULL, pipewire_thread_func, g_pwar_data); - pthread_detach(pw_thread); // We don't need to join this thread - g_pwar_running = 1; return 0; } @@ -464,16 +345,7 @@ int pwar_stop(void) { return -1; } - // Signal the PipeWire main loop to quit - if (g_pwar_data->loop) { - pw_main_loop_quit(g_pwar_data->loop); - } - - if (g_pwar_data->filter) { - pw_filter_destroy(g_pwar_data->filter); - g_pwar_data->filter = NULL; - } - + audio_backend_stop(g_pwar_data->audio_backend); g_pwar_running = 0; return 0; } @@ -484,13 +356,14 @@ void pwar_cleanup(void) { } if (g_pwar_initialized) { + g_pwar_data->should_stop = 1; pthread_cancel(g_recv_thread); pthread_join(g_recv_thread, NULL); - if (g_pwar_data->loop) { - pw_main_loop_destroy(g_pwar_data->loop); + if (g_pwar_data->audio_backend) { + audio_backend_cleanup(g_pwar_data->audio_backend); + g_pwar_data->audio_backend = NULL; } - pw_deinit(); if (g_pwar_data->sockfd > 0) { close(g_pwar_data->sockfd); @@ -499,9 +372,12 @@ void pwar_cleanup(void) { close(g_pwar_data->recv_sockfd); } - pthread_mutex_destroy(&g_pwar_data->packet_mutex); - pthread_cond_destroy(&g_pwar_data->packet_cond); - pthread_mutex_destroy(&g_pwar_data->pwar_rcv_mutex); + if (g_pwar_data->accumulation_buffer) { + free(g_pwar_data->accumulation_buffer); + g_pwar_data->accumulation_buffer = NULL; + } + + pwar_ring_buffer_free(); free(g_pwar_data); g_pwar_data = NULL; @@ -513,35 +389,59 @@ int pwar_is_running(void) { return g_pwar_running; } - int pwar_cli_run(const pwar_config_t *config) { - char latency[32]; - snprintf(latency, sizeof(latency), "%d/48000", config->buffer_size); - setenv("PIPEWIRE_LATENCY", latency, 1); - - struct data data; + struct pwar_core_data data; pthread_t recv_thread; - // Use the shared initialization function - if (init_data_structure(&data, config) < 0) { + // Set up signal handler for CLI mode + signal(SIGINT, cli_sigint_handler); + signal(SIGTERM, cli_sigint_handler); + + // Initialize core data + if (init_core_data(&data, config) < 0) { return -1; } + // Start receiver thread + data.should_stop = 0; pthread_create(&recv_thread, NULL, receiver_thread, &data); - pw_init(NULL, NULL); - data.loop = pw_main_loop_new(NULL); - pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); - pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); - if (create_pipewire_filter(&data) < 0) { - fprintf(stderr, "can't connect\n"); + // Start audio backend + if (audio_backend_start(data.audio_backend) < 0) { + fprintf(stderr, "Failed to start audio backend\n"); + data.should_stop = 1; + pthread_join(recv_thread, NULL); + audio_backend_cleanup(data.audio_backend); return -1; } - pw_main_loop_run(data.loop); - pw_filter_destroy(data.filter); - pw_main_loop_destroy(data.loop); - pw_deinit(); + printf("PWAR CLI started with %s backend. Press Ctrl+C to stop.\n", + config->backend_type == AUDIO_BACKEND_ALSA ? "ALSA" : + config->backend_type == AUDIO_BACKEND_PIPEWIRE ? "PipeWire" : "Simulated"); + + // Wait for shutdown signal + while (cli_keep_running) { + struct timespec sleep_time = {0, 100000000}; // 100ms + nanosleep(&sleep_time, NULL); + } + + printf("\nShutting down PWAR CLI...\n"); + + // Cleanup + data.should_stop = 1; + audio_backend_stop(data.audio_backend); + pthread_join(recv_thread, NULL); + audio_backend_cleanup(data.audio_backend); + + if (data.sockfd > 0) close(data.sockfd); + if (data.recv_sockfd > 0) close(data.recv_sockfd); + + if (data.accumulation_buffer) { + free(data.accumulation_buffer); + } + + pwar_ring_buffer_free(); + return 0; } @@ -549,19 +449,10 @@ void pwar_get_latency_metrics(pwar_latency_metrics_t *metrics) { if (!metrics) return; if (g_pwar_initialized && g_pwar_running) { - latency_manager_get_current_metrics(metrics); + latency_manger_get_current_metrics(metrics); } else { // Return zeros if not running - metrics->audio_proc_min_ms = 0.0; - metrics->audio_proc_max_ms = 0.0; - metrics->audio_proc_avg_ms = 0.0; - metrics->jitter_min_ms = 0.0; - metrics->jitter_max_ms = 0.0; - metrics->jitter_avg_ms = 0.0; - metrics->rtt_min_ms = 0.0; - metrics->rtt_max_ms = 0.0; - metrics->rtt_avg_ms = 0.0; - metrics->xruns = 0; + memset(metrics, 0, sizeof(pwar_latency_metrics_t)); } } diff --git a/linux/libpwar.h b/linux/libpwar.h index 1f45da4..0c8191d 100644 --- a/linux/libpwar.h +++ b/linux/libpwar.h @@ -3,6 +3,7 @@ #include #include "../protocol/pwar_latency_types.h" +#include "audio_backend.h" #ifdef __cplusplus extern "C" { @@ -14,8 +15,11 @@ typedef struct { char stream_ip[PWAR_MAX_IP_LEN]; int stream_port; int passthrough_test; - int oneshot_mode; - int buffer_size; + int device_buffer_size; // Audio device buffer size in frames (32/64/128 etc.) + int windows_packet_size; // Windows packet buffer size in frames (64/128 etc.) + int ring_buffer_depth; // Depth of the ring buffer for audio processing + audio_backend_type_t backend_type; + audio_config_t audio_config; } pwar_config_t; int pwar_cli_run(const pwar_config_t *config); diff --git a/linux/pipewire_backend.c b/linux/pipewire_backend.c new file mode 100644 index 0000000..3e4bcb4 --- /dev/null +++ b/linux/pipewire_backend.c @@ -0,0 +1,273 @@ +// PipeWire Audio Backend for PWAR +// Provides PipeWire-specific audio interface implementation + +#include "audio_backend.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// PipeWire backend private data +typedef struct { + struct pw_main_loop *loop; + struct pw_filter *filter; + struct pw_filter_port *in_port; + struct pw_filter_port *left_out_port; + struct pw_filter_port *right_out_port; + + audio_config_t config; + pthread_t pw_thread; + volatile int should_stop; +} pipewire_backend_data_t; + +struct port { + struct data *data; +}; + +// Forward declaration +static void on_process(void *userdata, struct spa_io_position *position); + +static const struct pw_filter_events filter_events = { + PW_VERSION_FILTER_EVENTS, + .process = on_process, +}; + +static void on_process(void *userdata, struct spa_io_position *position) { + audio_backend_t *backend = (audio_backend_t *)userdata; + pipewire_backend_data_t *data = (pipewire_backend_data_t *)backend->private_data; + + float *in = pw_filter_get_dsp_buffer(data->in_port, position->clock.duration); + float *left_out = pw_filter_get_dsp_buffer(data->left_out_port, position->clock.duration); + float *right_out = pw_filter_get_dsp_buffer(data->right_out_port, position->clock.duration); + + uint32_t n_samples = position->clock.duration; + + // Call the PWAR processing callback + if (backend->callback) { + backend->callback(in, left_out, right_out, n_samples, backend->userdata); + } else { + // Fallback: output silence if no callback + if (left_out) memset(left_out, 0, n_samples * sizeof(float)); + if (right_out) memset(right_out, 0, n_samples * sizeof(float)); + } +} + +static void *pipewire_thread_func(void *userdata) { + audio_backend_t *backend = (audio_backend_t *)userdata; + pipewire_backend_data_t *data = (pipewire_backend_data_t *)backend->private_data; + + printf("Starting PipeWire audio processing thread.\n"); + pw_main_loop_run(data->loop); + printf("PipeWire audio processing thread stopped.\n"); + return NULL; +} + +static int pipewire_init(audio_backend_t *backend, const audio_config_t *config, + audio_process_callback_t callback, void *userdata) { + pipewire_backend_data_t *data = malloc(sizeof(pipewire_backend_data_t)); + if (!data) { + return -1; + } + + memset(data, 0, sizeof(pipewire_backend_data_t)); + data->config = *config; + backend->private_data = data; + backend->callback = callback; + backend->userdata = userdata; + + // Set PipeWire latency environment variable + char latency[32]; + snprintf(latency, sizeof(latency), "%d/%d", config->frames, config->sample_rate); + setenv("PIPEWIRE_LATENCY", latency, 1); + + // Initialize PipeWire + pw_init(NULL, NULL); + data->loop = pw_main_loop_new(NULL); + if (!data->loop) { + free(data); + return -1; + } + + return 0; +} + +static int pipewire_start(audio_backend_t *backend) { + pipewire_backend_data_t *data = (pipewire_backend_data_t *)backend->private_data; + if (!data || backend->running) { + return -1; + } + + // Create PipeWire filter + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + data->filter = pw_filter_new_simple( + pw_main_loop_get_loop(data->loop), + "pwar", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Filter", + PW_KEY_MEDIA_ROLE, "DSP", + NULL), + &filter_events, + backend); // Pass backend as userdata + + if (!data->filter) { + return -1; + } + + // Add input port + data->in_port = pw_filter_add_port(data->filter, + PW_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "input", + NULL), + NULL, 0); + + // Add output ports + data->left_out_port = pw_filter_add_port(data->filter, + PW_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "output-left", + NULL), + NULL, 0); + + data->right_out_port = pw_filter_add_port(data->filter, + PW_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "output-right", + NULL), + NULL, 0); + + // Set process latency parameter + params[0] = spa_process_latency_build(&b, + SPA_PARAM_ProcessLatency, + &SPA_PROCESS_LATENCY_INFO_INIT( + .ns = (uint64_t)data->config.frames * SPA_NSEC_PER_SEC / data->config.sample_rate + )); + + // Connect the filter + if (pw_filter_connect(data->filter, + PW_FILTER_FLAG_RT_PROCESS, + params, 1) < 0) { + pw_filter_destroy(data->filter); + data->filter = NULL; + return -1; + } + + // Start PipeWire main loop in separate thread + data->should_stop = 0; + if (pthread_create(&data->pw_thread, NULL, pipewire_thread_func, backend) != 0) { + pw_filter_destroy(data->filter); + data->filter = NULL; + return -1; + } + + backend->running = 1; + printf("PipeWire backend started successfully.\n"); + return 0; +} + +static int pipewire_stop(audio_backend_t *backend) { + pipewire_backend_data_t *data = (pipewire_backend_data_t *)backend->private_data; + if (!data || !backend->running) { + return -1; + } + + // Signal the PipeWire main loop to quit + data->should_stop = 1; + if (data->loop) { + pw_main_loop_quit(data->loop); + } + + // Wait for thread to finish + pthread_join(data->pw_thread, NULL); + + // Destroy filter + if (data->filter) { + pw_filter_destroy(data->filter); + data->filter = NULL; + } + + backend->running = 0; + printf("PipeWire backend stopped.\n"); + return 0; +} + +static void pipewire_cleanup(audio_backend_t *backend) { + pipewire_backend_data_t *data = (pipewire_backend_data_t *)backend->private_data; + if (!data) { + return; + } + + if (backend->running) { + pipewire_stop(backend); + } + + // Cleanup PipeWire resources + if (data->loop) { + pw_main_loop_destroy(data->loop); + } + + pw_deinit(); + + free(data); + backend->private_data = NULL; + printf("PipeWire backend cleaned up.\n"); +} + +static int pipewire_is_running(audio_backend_t *backend) { + return backend->running; +} + +static void pipewire_get_stats(audio_backend_t *backend, void *stats) { + // PipeWire doesn't provide the same level of statistics as ALSA + // This could be extended to provide PipeWire-specific metrics + (void)backend; + (void)stats; +} + +static float pipewire_get_latency(audio_backend_t *backend) { + pipewire_backend_data_t *data = (pipewire_backend_data_t *)backend->private_data; + if (!data || !data->config.sample_rate) return 0.0f; + + // Return latency based on the configured buffer size + // PipeWire uses quantum (frames) for latency calculation + return ((float)data->config.frames * 1000.0f) / (float)data->config.sample_rate; +} + +static const audio_backend_ops_t pipewire_ops = { + .init = pipewire_init, + .start = pipewire_start, + .stop = pipewire_stop, + .cleanup = pipewire_cleanup, + .is_running = pipewire_is_running, + .get_stats = pipewire_get_stats, + .get_latency = pipewire_get_latency +}; + +audio_backend_t* audio_backend_create_pipewire(void) { + audio_backend_t *backend = malloc(sizeof(audio_backend_t)); + if (!backend) { + return NULL; + } + + memset(backend, 0, sizeof(audio_backend_t)); + backend->ops = &pipewire_ops; + return backend; +} diff --git a/linux/pwar_cli.c b/linux/pwar_cli.c index 3ca0e3b..97dc940 100644 --- a/linux/pwar_cli.c +++ b/linux/pwar_cli.c @@ -1,54 +1,208 @@ -/* - * pwar_cli.c - CLI frontend for PipeWire <-> UDP streaming bridge (PWAR) - * - * (c) 2025 Philip K. Gisslow - * This file is part of the PipeWire ASIO Relay (PWAR) project. - */ +// PWAR CLI - Unified CLI application supporting both ALSA and PipeWire backends +// Uses the new unified PWAR architecture +// Compile: gcc -O2 -o pwar_cli_new pwar_cli_new.c libpwar_new.c alsa_backend.c pipewire_backend.c audio_backend.c -lasound -lpipewire-0.3 -lspa-0.2 -lm -lpthread #include #include #include +#include #include "libpwar.h" -#define DEFAULT_STREAM_IP "192.168.66.3" -#define DEFAULT_STREAM_PORT 8321 -#define DEFAULT_BUFFER_SIZE 64 +// Default configuration +#define DEFAULT_STREAM_IP "192.168.66.3" +#define DEFAULT_STREAM_PORT 8321 +#define DEFAULT_PASSTHROUGH_TEST 0 // 1 = local passthrough test +#define DEFAULT_DEVICE_BUFFER_SIZE 32 // Device buffer size in frames +#define DEFAULT_WINDOWS_PACKET_SIZE 64 // Windows packet buffer size in frames +#define DEFAULT_RING_BUFFER_DEPTH 2048 // Ring buffer depth in samples + +// Audio defaults +#define DEFAULT_SAMPLE_RATE 48000 +#define DEFAULT_FRAMES 32 // Legacy - same as device buffer size +#define DEFAULT_CHANNELS 2 + +// ALSA specific defaults +#define DEFAULT_PCM_DEVICE_PLAYBACK "hw:3,0" +#define DEFAULT_PCM_DEVICE_CAPTURE "hw:3,0" + +static void print_usage(const char *program_name) { + printf("Usage: %s [options]\n", program_name); + printf("Options:\n"); + printf(" --backend Audio backend: alsa or pipewire (default: pipewire)\n"); + printf(" -i, --ip Target IP address (default: %s)\n", DEFAULT_STREAM_IP); + printf(" --port Target port (default: %d)\n", DEFAULT_STREAM_PORT); + printf(" -t, --passthrough Enable passthrough test mode\n"); + printf(" -b, --device-buffer Device buffer size in frames (default: %d)\n", DEFAULT_DEVICE_BUFFER_SIZE); + printf(" -p, --packet-buffer Windows packet buffer size in frames (default: %d)\n", DEFAULT_WINDOWS_PACKET_SIZE); + printf(" -r, --rate Sample rate (default: %d)\n", DEFAULT_SAMPLE_RATE); + printf(" -d, --ring-depth Ring buffer depth in samples (default: %d)\n", DEFAULT_RING_BUFFER_DEPTH); + printf(" --capture-device ALSA capture device (ALSA only, default: %s)\n", DEFAULT_PCM_DEVICE_CAPTURE); + printf(" --playback-device ALSA playback device (ALSA only, default: %s)\n", DEFAULT_PCM_DEVICE_PLAYBACK); + printf(" -h, --help Show this help message\n"); + printf("\nBuffer size guidelines:\n"); + printf(" Device buffer: 32, 64, 128, 256 frames (lower = lower latency, higher CPU load)\n"); + printf(" Packet buffer: Must be multiple of device buffer (64, 128, 256, 512 frames)\n"); + printf("\nBackends:\n"); + printf(" alsa Use ALSA for audio I/O\n"); + printf(" pipewire Use PipeWire for audio I/O\n"); + printf(" simulated Use simulated audio for testing (no hardware needed)\n"); + printf("\nExamples:\n"); + printf(" %s # Use PipeWire with default settings\n", program_name); + printf(" %s --backend alsa -i 192.168.1.100 --port 9000 -b 64 -p 128\n", program_name); + printf(" %s --backend pipewire -b 32 -p 64\n", program_name); + printf(" %s --backend simulated --passthrough # Test mode without hardware\n", program_name); +} + +static audio_backend_type_t parse_backend(const char *backend_str) { + if (strcmp(backend_str, "alsa") == 0) { + return AUDIO_BACKEND_ALSA; + } else if (strcmp(backend_str, "pipewire") == 0) { + return AUDIO_BACKEND_PIPEWIRE; + } else if (strcmp(backend_str, "simulated") == 0) { + return AUDIO_BACKEND_SIMULATED; + } else { + return AUDIO_BACKEND_PIPEWIRE; // Default fallback + } +} + +static int parse_arguments(int argc, char *argv[], pwar_config_t *config) { + // Set defaults + strcpy(config->stream_ip, DEFAULT_STREAM_IP); + config->stream_port = DEFAULT_STREAM_PORT; + config->passthrough_test = DEFAULT_PASSTHROUGH_TEST; + config->device_buffer_size = DEFAULT_DEVICE_BUFFER_SIZE; + config->windows_packet_size = DEFAULT_WINDOWS_PACKET_SIZE; + config->ring_buffer_depth = DEFAULT_RING_BUFFER_DEPTH; + config->backend_type = AUDIO_BACKEND_PIPEWIRE; // Default to PipeWire + + // Audio config defaults + config->audio_config.device_playback = DEFAULT_PCM_DEVICE_PLAYBACK; + config->audio_config.device_capture = DEFAULT_PCM_DEVICE_CAPTURE; + config->audio_config.sample_rate = DEFAULT_SAMPLE_RATE; + config->audio_config.frames = DEFAULT_DEVICE_BUFFER_SIZE; // Use device buffer size + config->audio_config.playback_channels = DEFAULT_CHANNELS; + config->audio_config.capture_channels = DEFAULT_CHANNELS; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return -1; + } else if (strcmp(argv[i], "--backend") == 0 && i + 1 < argc) { + config->backend_type = parse_backend(argv[++i]); + } else if ((strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--ip") == 0) && i + 1 < argc) { + strcpy(config->stream_ip, argv[++i]); + } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + config->stream_port = atoi(argv[++i]); + } else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--passthrough") == 0) { + config->passthrough_test = 1; + } else if ((strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "--device-buffer") == 0) && i + 1 < argc) { + int device_buffer = atoi(argv[++i]); + config->device_buffer_size = device_buffer; + config->audio_config.frames = device_buffer; // Keep audio config in sync + } else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--packet-buffer") == 0) && i + 1 < argc) { + config->windows_packet_size = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--rate") == 0) && i + 1 < argc) { + config->audio_config.sample_rate = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--ring-depth") == 0) && i + 1 < argc) { + config->ring_buffer_depth = atoi(argv[++i]); + } else if (strcmp(argv[i], "--capture-device") == 0 && i + 1 < argc) { + config->audio_config.device_capture = argv[++i]; + } else if (strcmp(argv[i], "--playback-device") == 0 && i + 1 < argc) { + config->audio_config.device_playback = argv[++i]; + } else { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + print_usage(argv[0]); + return -1; + } + } + + // Validate that windows_packet_size is a multiple of device_buffer_size + if (config->windows_packet_size % config->device_buffer_size != 0) { + fprintf(stderr, "Error: Windows packet buffer size (%d) must be a multiple of device buffer size (%d)\n", + config->windows_packet_size, config->device_buffer_size); + return -1; + } + + return 0; +} int main(int argc, char *argv[]) { pwar_config_t config; - memset(&config, 0, sizeof(config)); - strncpy(config.stream_ip, DEFAULT_STREAM_IP, sizeof(config.stream_ip) - 1); - config.stream_port = DEFAULT_STREAM_PORT; - config.passthrough_test = 0; - config.oneshot_mode = 0; - config.buffer_size = DEFAULT_BUFFER_SIZE; - - for (int i = 1; i < argc; ++i) { - if ((strcmp(argv[i], "--ip") == 0 || strcmp(argv[i], "-i") == 0) && i + 1 < argc) { - strncpy(config.stream_ip, argv[++i], sizeof(config.stream_ip) - 1); - config.stream_ip[sizeof(config.stream_ip) - 1] = '\0'; - } else if ((strcmp(argv[i], "--port") == 0 || (strcmp(argv[i], "-p") == 0)) && i + 1 < argc) { - config.stream_port = atoi(argv[++i]); - } else if ((strcmp(argv[i], "--passthrough_test") == 0) || (strcmp(argv[i], "-pt") == 0)) { - config.passthrough_test = 1; - } else if ((strcmp(argv[i], "--oneshot") == 0)) { - config.oneshot_mode = 1; - } else if ((strcmp(argv[i], "--buffer_size") == 0 || strcmp(argv[i], "-b") == 0) && i + 1 < argc) { - config.buffer_size = atoi(argv[++i]); + + printf("PWAR CLI - Low-latency audio streaming with PWAR protocol\n"); + printf("Unified architecture supporting multiple audio backends\n\n"); + + // Parse command line arguments + if (parse_arguments(argc, argv, &config) < 0) { + return 1; + } + + // Validate backend availability + if (!audio_backend_is_available(config.backend_type)) { + const char *backend_name = "Unknown"; + if (config.backend_type == AUDIO_BACKEND_ALSA) backend_name = "ALSA"; + else if (config.backend_type == AUDIO_BACKEND_PIPEWIRE) backend_name = "PipeWire"; + else if (config.backend_type == AUDIO_BACKEND_SIMULATED) backend_name = "Simulated"; + + printf("Error: %s backend is not available (not compiled in)\n", backend_name); + printf("Available backends:\n"); + if (audio_backend_is_available(AUDIO_BACKEND_ALSA)) { + printf(" - ALSA\n"); + } + if (audio_backend_is_available(AUDIO_BACKEND_PIPEWIRE)) { + printf(" - PipeWire\n"); + } + if (audio_backend_is_available(AUDIO_BACKEND_SIMULATED)) { + printf(" - Simulated\n"); } + return 1; } - - printf("Starting PWAR with config:\n"); - printf(" Stream IP: %s\n", config.stream_ip); - printf(" Stream Port: %d\n", config.stream_port); - printf(" Passthrough Test: %s\n", config.passthrough_test ? "Enabled" : "Disabled"); - printf(" Oneshot Mode: %s\n", config.oneshot_mode ? "Enabled" : "Disabled"); - printf(" Buffer Size: %d\n", config.buffer_size); - - char latency[32]; - snprintf(latency, sizeof(latency), "%d/48000", config.buffer_size); - setenv("PIPEWIRE_LATENCY", latency, 1); - - int ret = pwar_cli_run(&config); - return ret; + + // Print configuration + printf("Configuration:\n"); + printf(" Target: %s:%d\n", config.stream_ip, config.stream_port); + printf(" Passthrough test: %s\n", config.passthrough_test ? "enabled" : "disabled"); + + const char *backend_name = "Unknown"; + if (config.backend_type == AUDIO_BACKEND_ALSA) backend_name = "ALSA"; + else if (config.backend_type == AUDIO_BACKEND_PIPEWIRE) backend_name = "PipeWire"; + else if (config.backend_type == AUDIO_BACKEND_SIMULATED) backend_name = "Simulated"; + printf(" Backend: %s\n", backend_name); + + printf(" Sample rate: %u Hz\n", config.audio_config.sample_rate); + printf(" Device buffer size: %u frames (%.2f ms)\n", + config.device_buffer_size, + (double)config.device_buffer_size * 1000.0 / config.audio_config.sample_rate); + printf(" Windows packet size: %u frames (%.2f ms)\n", + config.windows_packet_size, + (double)config.windows_packet_size * 1000.0 / config.audio_config.sample_rate); + printf(" Packets per send: %u device buffers\n", + config.windows_packet_size / config.device_buffer_size); + printf(" Ring buffer depth: %d samples (%.2f ms)\n", + config.ring_buffer_depth, + (double)config.ring_buffer_depth * 1000.0 / config.audio_config.sample_rate); + + if (config.backend_type == AUDIO_BACKEND_ALSA) { + printf(" Capture device: %s (%u channels)\n", + config.audio_config.device_capture, config.audio_config.capture_channels); + printf(" Playback device: %s (%u channels)\n", + config.audio_config.device_playback, config.audio_config.playback_channels); + } else if (config.backend_type == AUDIO_BACKEND_PIPEWIRE) { + printf(" Audio I/O: PipeWire filter with %u channels\n", config.audio_config.capture_channels); + } else if (config.backend_type == AUDIO_BACKEND_SIMULATED) { + printf(" Audio I/O: Simulated audio\n"); + + } + printf("\n"); + + // Run PWAR CLI + int result = pwar_cli_run(&config); + + if (result < 0) { + fprintf(stderr, "PWAR CLI failed to start\n"); + return 1; + } + + printf("PWAR CLI finished successfully\n"); + return 0; } diff --git a/linux/pwar_gui.qml b/linux/pwar_gui.qml index 3a8f2a4..10b8a6a 100644 --- a/linux/pwar_gui.qml +++ b/linux/pwar_gui.qml @@ -180,7 +180,7 @@ ApplicationWindow { ComboBox { id: bufferSizeCombo Layout.fillWidth: true - model: [64, 128] + model: [32, 64, 128] currentIndex: { var idx = model.indexOf(pwarController.bufferSize); return idx >= 0 ? idx : 0; @@ -193,14 +193,23 @@ ApplicationWindow { } Label { - text: "Oneshot Mode" + text: "Ring Buffer Depth" color: textPrimary font.bold: true } - CheckBox { - id: oneshotCheck - checked: pwarController.oneshotMode - onCheckedChanged: pwarController.oneshotMode = checked + ComboBox { + id: ringBufferDepthCombo + Layout.fillWidth: true + model: [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192] + currentIndex: { + var idx = model.indexOf(pwarController.ringBufferDepth); + return idx >= 0 ? idx : 2; // default to 2048 + } + onCurrentValueChanged: { + if (currentValue !== pwarController.ringBufferDepth) { + pwarController.ringBufferDepth = currentValue; + } + } } Label { @@ -436,29 +445,12 @@ ApplicationWindow { } Label { - text: "Audio Proc (ms)" - color: textPrimary - font.bold: true - } - Label { - text: Number(pwarController.audioProcMinMs || 0).toFixed(3) + "/" + - Number(pwarController.audioProcMaxMs || 0).toFixed(3) + "/" + - Number(pwarController.audioProcAvgMs || 0).toFixed(3) - color: orangeAccent - font.bold: true - Layout.fillWidth: true - Layout.leftMargin: statusValueLeftMargin - } - - Label { - text: "Jitter (ms)" + text: "Ring Buffer Avg (ms)" color: textPrimary font.bold: true } Label { - text: Number(pwarController.jitterMinMs || 0).toFixed(3) + "/" + - Number(pwarController.jitterMaxMs || 0).toFixed(3) + "/" + - Number(pwarController.jitterAvgMs || 0).toFixed(3) + text: Number(pwarController.ringBufferAvgMs || 0).toFixed(3) color: orangeAccent font.bold: true Layout.fillWidth: true @@ -481,7 +473,7 @@ ApplicationWindow { } Label { - text: "XRUNS (last 2s)" + text: "XRUNS" color: textPrimary font.bold: true } @@ -494,7 +486,7 @@ ApplicationWindow { } Label { - text: "ASIO Buffer Size" + text: "Chunk Size" color: textPrimary font.bold: true } diff --git a/linux/simulated_backend.c b/linux/simulated_backend.c new file mode 100644 index 0000000..c2b1721 --- /dev/null +++ b/linux/simulated_backend.c @@ -0,0 +1,308 @@ +// Simulated Audio Backend for PWAR +// Provides perfect timing simulation for testing without hardware + +#define _GNU_SOURCE +#include "audio_backend.h" +#include +#include +#include +#include +#include +#include +#include + +// Simulated backend private data +typedef struct { + pthread_t thread; + volatile int running; + audio_process_callback_t callback; + void *userdata; + + // Audio configuration + uint32_t sample_rate; + uint32_t frames; + uint32_t channels_in; + uint32_t channels_out; + + // Test signal generation + double phase; + double freq; // Low frequency for latency measurement + + // Timing statistics + uint64_t total_callbacks; + struct timespec start_time; + + uint64_t last_input_zero_cross; + uint64_t last_output_zero_cross; + float rtt; + + // RTT stats for last 2 seconds + float rtt_min; + float rtt_max; + double rtt_sum; + uint32_t rtt_count; + uint32_t discontinuities; +} simulated_backend_data_t; + +static uint64_t timespec_to_ns(const struct timespec *ts) { + return (uint64_t)ts->tv_sec * 1000000000ULL + ts->tv_nsec; +} + +// Get the current time in nanoseconds +static uint64_t get_current_time_ns() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return timespec_to_ns(&ts); +} + +static void perform_analysis(simulated_backend_data_t *data, float *input_buffer, float *output_left, float *output_right, uint32_t frames) { + // Print every value in left output buffer for debugging + static float last_input_sample = 0.0f; + static float last_output_sample = 0.0f; + + static double output_phase = 0.0; + const float epsilon = 1e-4f; // Acceptable error margin + + for (uint32_t i = 0; i < frames; i++) { + // Input zero crossing: could be used for sync, but we just track RTT as before + if (input_buffer[i] >= 0.0f && last_input_sample < 0.0f) { + data->last_input_zero_cross = get_current_time_ns(); + } + if (output_left[i] >= 0.0f && last_output_sample < 0.0f) { + output_phase = 0.0; // Reset output phase on zero crossing + data->last_output_zero_cross = get_current_time_ns(); + data->rtt = (data->last_output_zero_cross - data->last_input_zero_cross) / 1000000.0f; // in ms + + // Update RTT stats + if (data->rtt_count == 0) { + data->rtt_min = data->rtt_max = data->rtt; + data->rtt_sum = data->rtt; + } else { + if (data->rtt < data->rtt_min) data->rtt_min = data->rtt; + if (data->rtt > data->rtt_max) data->rtt_max = data->rtt; + data->rtt_sum += data->rtt; + } + data->rtt_count++; + } + + // For every sample, verify output_left matches expected sine value + double expected_sample = 0.3 * sin(2.0 * M_PI * output_phase); + if (fabsf(output_left[i] - expected_sample) > epsilon) { + data->discontinuities++; + } + + output_phase += data->freq / data->sample_rate; + if (output_phase >= 1.0) output_phase -= 1.0; + + last_input_sample = input_buffer[i]; + last_output_sample = output_left[i]; + } +} + +static void* simulated_thread(void* arg) { + audio_backend_t *backend = (audio_backend_t *)arg; + simulated_backend_data_t *data = (simulated_backend_data_t*)backend->private_data; + + printf("[Simulated Audio] Starting audio simulation thread\n"); + printf("[Simulated Audio] Sample rate: %u Hz, Buffer size: %u frames\n", + data->sample_rate, data->frames); + printf("[Simulated Audio] Test signal: %.1f Hz\n", data->freq); + + // Calculate precise timing for buffer delivery + uint64_t frame_time_ns = (uint64_t)data->frames * 1000000000ULL / data->sample_rate; + struct timespec sleep_time = { + .tv_sec = frame_time_ns / 1000000000ULL, + .tv_nsec = frame_time_ns % 1000000000ULL + }; + + printf("[Simulated Audio] Buffer interval: %.3f ms\n", frame_time_ns / 1000000.0); + + // Allocate buffers (single channel input) + float *input_buffer = calloc(data->frames, sizeof(float)); + float *output_left = calloc(data->frames, sizeof(float)); + float *output_right = calloc(data->frames, sizeof(float)); + + if (!input_buffer || !output_left || !output_right) { + printf("[Simulated Audio] Failed to allocate buffers\n"); + goto cleanup; + } + + // Record start time + clock_gettime(CLOCK_MONOTONIC, &data->start_time); + + // Initialize RTT stats + data->rtt_min = 0.0f; + data->rtt_max = 0.0f; + data->rtt_sum = 0.0; + data->rtt_count = 0; + + while (data->running) { + // Generate test input signal (single channel sine wave) + for (uint32_t i = 0; i < data->frames; i++) { + float sample = 0.3f * sinf(2.0f * M_PI * data->phase); + input_buffer[i] = sample; + data->phase += data->freq / data->sample_rate; + if (data->phase >= 1.0) data->phase -= 1.0; + } + + // Call the audio processing callback (PWAR protocol processing) + if (data->callback) { + data->callback(input_buffer, output_left, output_right, data->frames, data->userdata); + } + + data->total_callbacks++; + + perform_analysis(data, input_buffer, output_left, output_right, data->frames); + + // Print periodic RTT stats (every 2nd second) + if (data->total_callbacks % (2 * data->sample_rate / data->frames) == 0) { + float rtt_avg = (data->rtt_count > 0) ? (float)(data->rtt_sum / data->rtt_count) : 0.0f; + printf("[Simulated Audio]: AudioProc: RTT: min=%.3fms max=%.3fms avg=%.3fms\n", + data->rtt_min, data->rtt_max, rtt_avg); + + if (data->discontinuities > 0) { + printf("\033[1;31m[Simulated Audio] ERROR: Detected %u discontinuities in output signal over last 2 seconds\033[0m\n", data->discontinuities); + } + + // Reset stats for next interval + data->rtt_min = 0.0f; + data->rtt_max = 0.0f; + data->rtt_sum = 0.0; + data->rtt_count = 0; + data->discontinuities = 0; + } + + // Simulate precise hardware timing + nanosleep(&sleep_time, NULL); + } + +cleanup: + printf("[Simulated Audio] Stopping audio simulation thread\n"); + free(input_buffer); + free(output_left); + free(output_right); + return NULL; +} + +static int simulated_init(audio_backend_t *backend, const audio_config_t *config, + audio_process_callback_t callback, void *userdata) { + simulated_backend_data_t *data = calloc(1, sizeof(simulated_backend_data_t)); + if (!data) { + printf("[Simulated Audio] Failed to allocate backend data\n"); + return -1; + } + + data->callback = callback; + data->userdata = userdata; + data->sample_rate = config->sample_rate; + data->frames = config->frames; + data->channels_in = config->capture_channels; + data->channels_out = config->playback_channels; + data->running = 0; + data->total_callbacks = 0; + + // Test signal frequency (low frequency for latency measurement) + // At 10 Hz, zero crossings are ~100ms apart, good for measuring 0.8-30ms latency + data->freq = 10.0; // 10 Hz sine wave + data->phase = 0.0; + + backend->private_data = data; + backend->callback = callback; + backend->userdata = userdata; + + printf("[Simulated Audio] Backend initialized successfully\n"); + return 0; +} + +static int simulated_start(audio_backend_t *backend) { + simulated_backend_data_t *data = (simulated_backend_data_t*)backend->private_data; + if (!data) return -1; + + if (data->running) { + printf("[Simulated Audio] Already running\n"); + return 0; + } + + data->running = 1; + backend->running = 1; + + if (pthread_create(&data->thread, NULL, simulated_thread, backend) != 0) { + printf("[Simulated Audio] Failed to create audio thread\n"); + data->running = 0; + backend->running = 0; + return -1; + } + + printf("[Simulated Audio] Started successfully\n"); + return 0; +} + +static int simulated_stop(audio_backend_t *backend) { + simulated_backend_data_t *data = (simulated_backend_data_t*)backend->private_data; + if (!data || !data->running) return 0; + + printf("[Simulated Audio] Stopping...\n"); + data->running = 0; + backend->running = 0; + + pthread_join(data->thread, NULL); + printf("[Simulated Audio] Stopped successfully\n"); + return 0; +} + +static void simulated_cleanup(audio_backend_t *backend) { + simulated_backend_data_t *data = (simulated_backend_data_t*)backend->private_data; + if (!data) return; + + if (data->running) { + simulated_stop(backend); + } + + printf("[Simulated Audio] Cleaning up\n"); + free(data); + backend->private_data = NULL; +} + +static int simulated_is_running(audio_backend_t *backend) { + simulated_backend_data_t *data = (simulated_backend_data_t*)backend->private_data; + return data ? data->running : 0; +} + +static void simulated_get_stats(audio_backend_t *backend, void *stats) { + simulated_backend_data_t *data = (simulated_backend_data_t*)backend->private_data; + if (!data || !stats) return; + + // For now, just print some basic stats + printf("[Simulated Audio Stats] Total callbacks: %llu\n", + (unsigned long long)data->total_callbacks); +} + +static float simulated_get_latency(audio_backend_t *backend) { + simulated_backend_data_t *data = (simulated_backend_data_t *)backend->private_data; + if (!data || !data->sample_rate) return 0.0f; + return ((float)data->frames * 2.0f * 1000.0f) / (float)data->sample_rate; +} + +static const audio_backend_ops_t simulated_ops = { + .init = simulated_init, + .start = simulated_start, + .stop = simulated_stop, + .cleanup = simulated_cleanup, + .is_running = simulated_is_running, + .get_stats = simulated_get_stats, + .get_latency = simulated_get_latency +}; + +audio_backend_t* audio_backend_create_simulated(void) { + audio_backend_t *backend = calloc(1, sizeof(audio_backend_t)); + if (!backend) { + printf("[Simulated Audio] Failed to allocate backend\n"); + return NULL; + } + + backend->ops = &simulated_ops; + backend->private_data = NULL; + backend->running = 0; + + return backend; +} diff --git a/linux/test/integration_test.c b/linux/test/integration_test.c index 0e7bd5a..b1e2b53 100644 --- a/linux/test/integration_test.c +++ b/linux/test/integration_test.c @@ -133,7 +133,7 @@ void *test_thread_func(void *arg) { data->pid_pipewire = fork(); if (data->pid_pipewire == 0) { // Child process: exec pwar with arguments - //execl("build/pwar_cli", "pwar_cli", "--ip", "127.0.0.1", "--port", "8322", "--oneshot", "--buffer_size", "64", (char *)NULL); + //execl("build/pwar_cli", "pwar_cli", "--ip", "127.0.0.1", "--port", "8322", "--buffer_size", "64", (char *)NULL); execl("build/pwar_cli", "pwar_cli", "--ip", "127.0.0.1", "--port", "8322", "--buffer_size", "128", (char *)NULL); perror("execl pwar"); exit(1); diff --git a/linux/torture.c b/linux/torture.c deleted file mode 100644 index 4e9186b..0000000 --- a/linux/torture.c +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../protocol/pwar_packet.h" - -#define TORTURE_PORT 8321 -#define TORTURE_IP "192.168.66.3" - -static int recv_sockfd; -static pthread_mutex_t packet_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t packet_cond = PTHREAD_COND_INITIALIZER; -static pwar_packet_t latest_packet; -static int packet_available = 0; - -#define RT_STREAM_PACKET_FRAME_SIZE 128 // Define the frame size for the torture test - -static void setup_recv_socket(int port) { - recv_sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (recv_sockfd < 0) { - perror("recv socket creation failed"); - exit(EXIT_FAILURE); - } - int rcvbuf = 1024 * 1024; - setsockopt(recv_sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); - struct sockaddr_in recv_addr; - memset(&recv_addr, 0, sizeof(recv_addr)); - recv_addr.sin_family = AF_INET; - recv_addr.sin_addr.s_addr = INADDR_ANY; - recv_addr.sin_port = htons(port); - if (bind(recv_sockfd, (struct sockaddr *)&recv_addr, sizeof(recv_addr)) < 0) { - perror("recv socket bind failed"); - exit(EXIT_FAILURE); - } -} - -static void *receiver_thread(void *userdata) { - struct sched_param sp = { .sched_priority = 90 }; - pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp); - pwar_packet_t packet; - while (1) { - ssize_t n = recvfrom(recv_sockfd, &packet, sizeof(packet), 0, NULL, NULL); - if (n == (ssize_t)sizeof(packet)) { - pthread_mutex_lock(&packet_mutex); - latest_packet = packet; - packet_available = 1; - pthread_cond_signal(&packet_cond); - pthread_mutex_unlock(&packet_mutex); - //printf("[RECV] Got packet seq=%lu n_samples=%u\n", packet.seq, packet.n_samples); - } - } - return NULL; -} - -int main() { - int sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (sockfd < 0) { perror("socket"); exit(1); } - struct sockaddr_in servaddr; - memset(&servaddr, 0, sizeof(servaddr)); - servaddr.sin_family = AF_INET; - servaddr.sin_port = htons(TORTURE_PORT); - servaddr.sin_addr.s_addr = inet_addr(TORTURE_IP); - - setup_recv_socket(TORTURE_PORT); - pthread_t recv_thread; - pthread_create(&recv_thread, NULL, receiver_thread, NULL); - - uint64_t seq = 0; - while (1) { - pwar_packet_t packet; - packet.n_samples = RT_STREAM_PACKET_FRAME_SIZE; - packet.seq = seq++; - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - packet.timestamp = (uint64_t)ts.tv_sec * 1000000000 + ts.tv_nsec; - for (int i = 0; i < RT_STREAM_PACKET_FRAME_SIZE/2; ++i) { - packet.samples[0][i] = (float)i; - packet.samples[1][i] = (float)(RT_STREAM_PACKET_FRAME_SIZE - i); - } - ssize_t sent = sendto(sockfd, &packet, sizeof(packet), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); - if (sent != sizeof(packet)) { - perror("sendto"); - } else { - //printf("[SEND] Sent packet seq=%lu\n", packet.seq); - } - usleep(2600); // 2.6ms - } - return 0; -} diff --git a/linux/windows_sim.c b/linux/windows_sim.c deleted file mode 100644 index 653ea58..0000000 --- a/linux/windows_sim.c +++ /dev/null @@ -1,134 +0,0 @@ -/* - * windows_sim.c - PipeWire <-> UDP streaming bridge for PWAR - * - * (c) 2025 Philip K. Gisslow - * This file is part of the PipeWire ASIO Relay (PWAR) project. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../protocol/pwar_packet.h" -#include "../protocol/pwar_router.h" - -#include "latency_manager.h" - -#define DEFAULT_STREAM_IP "127.0.0.1" -#define DEFAULT_STREAM_PORT 8321 -#define SIM_PORT 8322 - -#define CHANNELS 2 -#define BUFFER_SIZE 512 - -static int recv_sockfd; -static pthread_mutex_t packet_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t packet_cond = PTHREAD_COND_INITIALIZER; -static pwar_packet_t latest_packet; -static int packet_available = 0; -static pwar_router_t router; -static struct sockaddr_in servaddr; -static int sockfd; - -static void setup_recv_socket(int port) { - recv_sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (recv_sockfd < 0) { - perror("recv socket creation failed"); - exit(EXIT_FAILURE); - } - int rcvbuf = 1024 * 1024; - setsockopt(recv_sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); - struct sockaddr_in recv_addr; - memset(&recv_addr, 0, sizeof(recv_addr)); - recv_addr.sin_family = AF_INET; - recv_addr.sin_addr.s_addr = INADDR_ANY; - recv_addr.sin_port = htons(port); - if (bind(recv_sockfd, (struct sockaddr *)&recv_addr, sizeof(recv_addr)) < 0) { - perror("recv socket bind failed"); - exit(EXIT_FAILURE); - } -} - -static void *receiver_thread(void *userdata) { - struct sched_param sp = { .sched_priority = 90 }; - pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp); - pwar_packet_t packet; - - pwar_packet_t output_packets[32]; - uint32_t packets_to_send = 0; - while (1) { - ssize_t n = recvfrom(recv_sockfd, &packet, sizeof(packet), 0, NULL, NULL); - if (n == (ssize_t)sizeof(packet)) { - pthread_mutex_lock(&packet_mutex); - latest_packet = packet; - packet_available = 1; - float output_buffers[CHANNELS * BUFFER_SIZE] = {0}; - uint32_t chunk_size = packet.n_samples; - packet.num_packets = BUFFER_SIZE / chunk_size; - latency_manager_process_packet_client(&packet); - int samples_ready = pwar_router_process_streaming_packet(&router, &packet, output_buffers, BUFFER_SIZE, CHANNELS); - if (samples_ready > 0) { - uint32_t seq = packet.seq; - - latency_manager_start_audio_cbk_begin(); - // Process the output buffers as needed - // Loop back. - // But first copy channel 0 to channel 1 for testing - for (uint32_t i = 0; i < samples_ready; ++i) - output_buffers[BUFFER_SIZE + i] = output_buffers[i]; - latency_manager_start_audio_cbk_end(); - - pwar_router_send_buffer(&router, chunk_size, output_buffers, samples_ready, CHANNELS, output_packets, 32, &packets_to_send); - - uint64_t timestamp = latency_manager_timestamp_now(); - // Set seq for all packets in this buffer - for (uint32_t i = 0; i < packets_to_send; ++i) { - output_packets[i].seq = seq; - output_packets[i].timestamp = timestamp; - } - for (uint32_t i = 0; i < packets_to_send; ++i) { - ssize_t sent = sendto(sockfd, &output_packets[i], sizeof(output_packets[i]), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); - if (sent < 0) { - perror("sendto failed"); - } - } - } - pwar_latency_info_t latency_info; - if (latency_manager_time_for_sending_latency_info(&latency_info)) { - ssize_t sent = sendto(sockfd, &latency_info, sizeof(latency_info), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); - if (sent < 0) { - perror("sendto latency info failed"); - } - } - pthread_cond_signal(&packet_cond); - pthread_mutex_unlock(&packet_mutex); - } - } - return NULL; -} - -int main() { - sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (sockfd < 0) { perror("socket"); exit(1); } - memset(&servaddr, 0, sizeof(servaddr)); - servaddr.sin_family = AF_INET; - servaddr.sin_port = htons(DEFAULT_STREAM_PORT); - servaddr.sin_addr.s_addr = inet_addr(DEFAULT_STREAM_IP); - - pwar_router_init(&router, CHANNELS); - - setup_recv_socket(SIM_PORT); - pthread_t recv_thread; - pthread_create(&recv_thread, NULL, receiver_thread, NULL); - - while (1) { - sleep(1); - } - return 0; -} \ No newline at end of file diff --git a/protocol/latency_manager.c b/protocol/latency_manager.c index 9b6d32e..f21a496 100644 --- a/protocol/latency_manager.c +++ b/protocol/latency_manager.c @@ -17,188 +17,177 @@ typedef struct { } latency_stat_t; static struct { - uint64_t last_latency_info_sent; // Timestamp of the last latency info sent + uint64_t last_windows_recv; + uint64_t last_linux_recv; - uint64_t last_local_packet_timestamp; // Timestamp of the last packet processed - uint64_t last_remote_packet_timestamp; // Timestamp of the last remote packet processed + float expected_interval_ms; + uint32_t sample_rate; - // ---- - uint64_t current_seq; // Current sequence number for packets - uint32_t current_index; - uint32_t num_packets; // Total number of packets in the current sequence + latency_stat_t rtt_stat; + latency_stat_t audio_proc_stat; + latency_stat_t windows_rcv_delta_stat; + latency_stat_t linux_rcv_delta_stat; + latency_stat_t ring_buffer_fill_level_stat; - uint64_t audio_ckb_start_timestamp; // Timestamp when the audio callback started - uint64_t audio_ckb_end_timestamp; // Timestamp when the audio callback ended + latency_stat_t rtt_stat_current; + latency_stat_t audio_proc_stat_current; + latency_stat_t windows_rcv_delta_stat_current; + latency_stat_t linux_rcv_delta_stat_current; + latency_stat_t ring_buffer_fill_level_stat_current; - // ---- - latency_stat_t audio_proc; // Statistics for audio processing - latency_stat_t network_jitter; // Statistics for network jitter - latency_stat_t round_trip_time; // Statistics for round trip time + int64_t clock_offset_sum; + uint32_t clock_offset_count; + int64_t current_clock_offset; - uint32_t xruns_2sec; // Number of xruns in the last 2 seconds - uint32_t xruns; + float audio_backend_latency_ms; + + uint64_t last_print_time; } internal = {0}; -void latency_manager_init() { +static void process_latency_stat(latency_stat_t *stat, uint64_t value) { + if (stat->count == 0 || value < stat->min) { + stat->min = value; + } + if (stat->count == 0 || value > stat->max) { + stat->max = value; + } + stat->total += value; + stat->count++; + stat->avg = stat->total / stat->count; } -void latency_manager_start_audio_cbk_begin() { - internal.audio_ckb_start_timestamp = latency_manager_timestamp_now(); -} -void latency_manager_start_audio_cbk_end() { - internal.audio_ckb_end_timestamp = latency_manager_timestamp_now(); - // Calculate the duration of the audio callback - uint64_t duration = internal.audio_ckb_end_timestamp - internal.audio_ckb_start_timestamp; - internal.audio_proc.total += duration; - internal.audio_proc.count++; - if (duration < internal.audio_proc.min || internal.audio_proc.count == 1) { - internal.audio_proc.min = duration; - } - if (duration > internal.audio_proc.max || internal.audio_proc.count == 1) { - internal.audio_proc.max = duration; - } +void latency_manager_init(uint32_t sample_rate, uint32_t buffer_size, float audio_backend_latency_ms) { + internal.expected_interval_ms = (buffer_size / (float)sample_rate) * 1000.0f; + internal.sample_rate = sample_rate; + internal.audio_backend_latency_ms = audio_backend_latency_ms; } -void latency_manager_process_packet_client(pwar_packet_t *packet) { - uint64_t packet_ts = packet->timestamp; - uint64_t nowNs = latency_manager_timestamp_now(); - uint64_t time_since_last_local_packet = nowNs - internal.last_local_packet_timestamp; - internal.last_local_packet_timestamp = nowNs; +void latency_manager_report_ring_buffer_fill_level(uint32_t fill_level) { + process_latency_stat(&internal.ring_buffer_fill_level_stat, fill_level); +} - uint64_t audio_ckb_interval = packet_ts - internal.last_remote_packet_timestamp; - internal.last_remote_packet_timestamp = packet_ts; +void calculate_windows_clock_offset(pwar_packet_t *packet) { + uint64_t estimated_rtt = internal.rtt_stat_current.avg; + + // Method 1: Linux->Windows direction + // t2 should occur at approximately t1 + one_way_delay + int64_t offset1 = ((int64_t)packet->t1_linux_send + (int64_t)(estimated_rtt / 2)) - (int64_t)packet->t2_windows_recv; + + // Method 2: Windows->Linux direction + // t3 should occur at approximately t4 - one_way_delay + int64_t offset2 = ((int64_t)packet->t4_linux_recv - (int64_t)(estimated_rtt / 2)) - (int64_t)packet->t3_windows_send; + + // Average the two estimates for better accuracy + int64_t combined_offset = (offset1 + offset2) / 2; + + internal.clock_offset_sum += combined_offset; + internal.clock_offset_count++; +} - // Jitter can be negative, log signed value - int64_t jitter = (int64_t)time_since_last_local_packet - (int64_t)audio_ckb_interval; - uint64_t abs_jitter = (jitter < 0) ? -jitter : jitter; - internal.network_jitter.total += abs_jitter; - internal.network_jitter.count++; - if (abs_jitter < internal.network_jitter.min || internal.network_jitter.count == 1) { - internal.network_jitter.min = abs_jitter; - } - if (abs_jitter > internal.network_jitter.max || internal.network_jitter.count == 1) { - internal.network_jitter.max = abs_jitter; - } +uint64_t convert_windows_to_linux_time(uint64_t windows_time) { + return windows_time + internal.current_clock_offset; } -void latency_manager_process_packet_server(pwar_packet_t *packet) { - if (packet->packet_index == packet->num_packets - 1) { - uint64_t round_trip_time = latency_manager_timestamp_now() - packet->seq_timestamp; - internal.round_trip_time.total += round_trip_time; - internal.round_trip_time.count++; - if (round_trip_time < internal.round_trip_time.min || internal.round_trip_time.count == 1) { - internal.round_trip_time.min = round_trip_time; - } - if (round_trip_time > internal.round_trip_time.max || internal.round_trip_time.count == 1) { - internal.round_trip_time.max = round_trip_time; - } +void latency_manager_check_for_abnormalities(uint64_t rtt, pwar_packet_t *packet) { + float rtt_ms = rtt / 1000000.0f; + if (rtt_ms > internal.expected_interval_ms) { + float linux_to_windows = (packet->t2_windows_recv - packet->t1_linux_send) / 1000000.0f; + float windows_to_linux = (packet->t4_linux_recv - packet->t3_windows_send) / 1000000.0f; + printf("\033[33m[PWAR][Warning]: High RTT detected: %.2fms (Linux->Windows: %.2fms, Windows->Linux: %.2fms)\033[0m\n", rtt_ms, linux_to_windows, windows_to_linux); } } +void latency_manager_process_packet(pwar_packet_t *packet) { + packet->t4_linux_recv = latency_manager_timestamp_now(); + calculate_windows_clock_offset(packet); -int latency_manager_time_for_sending_latency_info(pwar_latency_info_t *latency_info) { - // Send latency info every 2 seconds - uint64_t now = latency_manager_timestamp_now(); - if (now - internal.last_latency_info_sent >= 2 * 1000000000) { - internal.audio_proc.avg = (internal.audio_proc.count > 0) ? (internal.audio_proc.total / internal.audio_proc.count) : 0; - internal.network_jitter.avg = (internal.network_jitter.count > 0) ? (internal.network_jitter.total / internal.network_jitter.count) : 0; - - // Fill in audio_proc stats - latency_info->audio_proc_avg = internal.audio_proc.avg; - latency_info->audio_proc_min = internal.audio_proc.min; - latency_info->audio_proc_max = internal.audio_proc.max; + uint64_t rtt = packet->t4_linux_recv - packet->t1_linux_send; + uint64_t audio_proc = packet->t3_windows_send - packet->t2_windows_recv; - // Reset audio_proc - internal.audio_proc.min = UINT64_MAX; - internal.audio_proc.max = 0; - internal.audio_proc.total = 0; - internal.audio_proc.count = 0; + uint64_t windows_rcv_delta = packet->t2_windows_recv - internal.last_windows_recv; + uint64_t linux_rcv_delta = packet->t4_linux_recv - internal.last_linux_recv; + process_latency_stat(&internal.rtt_stat, rtt); + process_latency_stat(&internal.audio_proc_stat, audio_proc); + process_latency_stat(&internal.windows_rcv_delta_stat, windows_rcv_delta); + process_latency_stat(&internal.linux_rcv_delta_stat, linux_rcv_delta); - // Fill in network jitter stats - latency_info->jitter_avg = internal.network_jitter.avg; - latency_info->jitter_min = internal.network_jitter.min; - latency_info->jitter_max = internal.network_jitter.max; + internal.last_windows_recv = packet->t2_windows_recv; + internal.last_linux_recv = packet->t4_linux_recv; - // Reset network jitter - internal.network_jitter.min = UINT64_MAX; - internal.network_jitter.max = 0; - internal.network_jitter.total = 0; - internal.network_jitter.count = 0; + // Convert the clocks + packet->t2_windows_recv = convert_windows_to_linux_time(packet->t2_windows_recv); + packet->t3_windows_send = convert_windows_to_linux_time(packet->t3_windows_send); + latency_manager_check_for_abnormalities(rtt, packet); - internal.last_latency_info_sent = now; - return 1; // Indicate that latency info should be sent + // Print stats every 2 seconds + uint64_t current_time = latency_manager_timestamp_now(); + if (internal.last_print_time == 0) { + internal.last_print_time = current_time; } - return 0; -} - -void latency_manager_handle_latency_info(pwar_latency_info_t *latency_info) { - // Print all stats as ms in one streamlined line - internal.round_trip_time.avg = (internal.round_trip_time.count > 0) ? (internal.round_trip_time.total / internal.round_trip_time.count) : 0; - printf("[PWAR]: AudioProc: min=%.3fms max=%.3fms avg=%.3fms | Jitter: min=%.3fms max=%.3fms avg=%.3fms | RTT: min=%.3fms max=%.3fms avg=%.3fms\n", - latency_info->audio_proc_min / 1000000.0, - latency_info->audio_proc_max / 1000000.0, - latency_info->audio_proc_avg / 1000000.0, - latency_info->jitter_min / 1000000.0, - latency_info->jitter_max / 1000000.0, - latency_info->jitter_avg / 1000000.0, - internal.round_trip_time.min / 1000000.0, - internal.round_trip_time.max / 1000000.0, - internal.round_trip_time.avg / 1000000.0); - - internal.round_trip_time.min = UINT64_MAX; - internal.round_trip_time.max = 0; - internal.round_trip_time.total = 0; - internal.round_trip_time.count = 0; - - internal.audio_proc.min = latency_info->audio_proc_min; - internal.audio_proc.max = latency_info->audio_proc_max; - internal.audio_proc.avg = latency_info->audio_proc_avg; - - internal.network_jitter.min = latency_info->jitter_min; - internal.network_jitter.max = latency_info->jitter_max; - internal.network_jitter.avg = latency_info->jitter_avg; - - internal.xruns_2sec = internal.xruns; // Store xruns for the last 2 seconds - internal.xruns = 0; // Reset xruns count -} - -void latency_manager_get_current_metrics(pwar_latency_metrics_t *metrics) { - if (!metrics) return; - - // Calculate averages for current data - internal.audio_proc.avg = (internal.audio_proc.count > 0) ? (internal.audio_proc.total / internal.audio_proc.count) : 0; - internal.network_jitter.avg = (internal.network_jitter.count > 0) ? (internal.network_jitter.total / internal.network_jitter.count) : 0; - internal.round_trip_time.avg = (internal.round_trip_time.count > 0) ? (internal.round_trip_time.total / internal.round_trip_time.count) : 0; - - // Convert from nanoseconds to milliseconds - metrics->audio_proc_min_ms = internal.audio_proc.min / 1000000.0; - metrics->audio_proc_max_ms = internal.audio_proc.max / 1000000.0; - metrics->audio_proc_avg_ms = internal.audio_proc.avg / 1000000.0; - metrics->jitter_min_ms = internal.network_jitter.min / 1000000.0; - metrics->jitter_max_ms = internal.network_jitter.max / 1000000.0; - metrics->jitter_avg_ms = internal.network_jitter.avg / 1000000.0; - - metrics->rtt_min_ms = internal.round_trip_time.min / 1000000.0; - metrics->rtt_max_ms = internal.round_trip_time.max / 1000000.0; - metrics->rtt_avg_ms = internal.round_trip_time.avg / 1000000.0; - - if (internal.xruns_2sec == 0) { - if (internal.xruns > 1000) internal.xruns = 1000; - metrics->xruns = internal.xruns; // No xruns in the last 2 seconds - } else { - metrics->xruns = internal.xruns_2sec; // Return the xruns count from the last 2 seconds + uint64_t time_since_print = current_time - internal.last_print_time; + if (time_since_print >= 2000000000ULL) { // 2 seconds in nanoseconds + printf("[PWAR]: Audio RTT: min=%.2fms avg=%.2fms max=%.2fms | Net RTT: min=%.2fms avg=%.2fms max=%.2fms | AudioProc: min=%.2fms avg=%.2fms max=%.2fms | WinJitter: min=%.2fms avg=%.2fms max=%.2fms | LinuxJitter: min=%.2fms avg=%.2fms max=%.2fms\n", + ((internal.ring_buffer_fill_level_stat.min / (float)internal.sample_rate) * 1000.0f) + internal.audio_backend_latency_ms, + ((internal.ring_buffer_fill_level_stat.avg / (float)internal.sample_rate) * 1000.0f) + internal.audio_backend_latency_ms, + ((internal.ring_buffer_fill_level_stat.max / (float)internal.sample_rate) * 1000.0f) + internal.audio_backend_latency_ms, + internal.rtt_stat.min / 1000000.0, + internal.rtt_stat.avg / 1000000.0, + internal.rtt_stat.max / 1000000.0, + internal.audio_proc_stat.min / 1000000.0, + internal.audio_proc_stat.avg / 1000000.0, + internal.audio_proc_stat.max / 1000000.0, + internal.windows_rcv_delta_stat.min / 1000000.0, + internal.windows_rcv_delta_stat.avg / 1000000.0, + internal.windows_rcv_delta_stat.max / 1000000.0, + internal.linux_rcv_delta_stat.min / 1000000.0, + internal.linux_rcv_delta_stat.avg / 1000000.0, + internal.linux_rcv_delta_stat.max / 1000000.0); + + internal.ring_buffer_fill_level_stat_current = internal.ring_buffer_fill_level_stat; + internal.rtt_stat_current = internal.rtt_stat; + internal.audio_proc_stat_current = internal.audio_proc_stat; + internal.windows_rcv_delta_stat_current = internal.windows_rcv_delta_stat; + internal.linux_rcv_delta_stat_current = internal.linux_rcv_delta_stat; + + // Reset stats for next period + internal.rtt_stat = (latency_stat_t){0}; + internal.audio_proc_stat = (latency_stat_t){0}; + internal.windows_rcv_delta_stat = (latency_stat_t){0}; + internal.linux_rcv_delta_stat = (latency_stat_t){0}; + internal.ring_buffer_fill_level_stat = (latency_stat_t){0}; + + // Update current clock offset estimate + internal.current_clock_offset = internal.clock_offset_sum / (int64_t)internal.clock_offset_count; + internal.clock_offset_sum = 0; + internal.clock_offset_count = 0; + + internal.last_print_time = current_time; } } - -void latency_manager_report_xrun() { - internal.xruns++; +void latency_manger_get_current_metrics(pwar_latency_metrics_t *metrics) { + metrics->rtt_min_ms = internal.rtt_stat_current.min / 1000000.0; + metrics->rtt_max_ms = internal.rtt_stat_current.max / 1000000.0; + metrics->rtt_avg_ms = internal.rtt_stat_current.avg / 1000000.0; + metrics->audio_proc_min_ms = internal.audio_proc_stat_current.min / 1000000.0; + metrics->audio_proc_max_ms = internal.audio_proc_stat_current.max / 1000000.0; + metrics->audio_proc_avg_ms = internal.audio_proc_stat_current.avg / 1000000.0; + metrics->windows_jitter_min_ms = internal.windows_rcv_delta_stat_current.min / 1000000.0; + metrics->windows_jitter_max_ms = internal.windows_rcv_delta_stat_current.max / 1000000.0; + metrics->windows_jitter_avg_ms = internal.windows_rcv_delta_stat_current.avg / 1000000.0; + metrics->linux_jitter_min_ms = internal.linux_rcv_delta_stat_current.min / 1000000.0; + metrics->linux_jitter_max_ms = internal.linux_rcv_delta_stat_current.max / 1000000.0; + metrics->linux_jitter_avg_ms = internal.linux_rcv_delta_stat_current.avg / 1000000.0; + metrics->ring_buffer_min_ms = (internal.ring_buffer_fill_level_stat_current.min / (float)internal.sample_rate) * 1000.0f; + metrics->ring_buffer_max_ms = (internal.ring_buffer_fill_level_stat_current.max / (float)internal.sample_rate) * 1000.0f; + metrics->ring_buffer_avg_ms = (internal.ring_buffer_fill_level_stat_current.avg / (float)internal.sample_rate) * 1000.0f; + metrics->xruns = 0; // XRUNs tracking not implemented yet } uint64_t latency_manager_timestamp_now() { diff --git a/protocol/latency_manager.h b/protocol/latency_manager.h index af5e78f..68d917c 100644 --- a/protocol/latency_manager.h +++ b/protocol/latency_manager.h @@ -10,22 +10,14 @@ extern "C" { #include "pwar_packet.h" #include "pwar_latency_types.h" -void latency_manager_init(); +void latency_manager_init(uint32_t sample_rate, uint32_t buffer_size, float audio_backend_latency_ms); uint64_t latency_manager_timestamp_now(); -void latency_manager_process_packet_client(pwar_packet_t *packet); -void latency_manager_process_packet_server(pwar_packet_t *packet); +void latency_manager_process_packet(pwar_packet_t *packet); -void latency_manager_start_audio_cbk_begin(); -void latency_manager_start_audio_cbk_end(); +void latency_manager_report_ring_buffer_fill_level(uint32_t fill_level); -int latency_manager_time_for_sending_latency_info(pwar_latency_info_t *latency_info); -void latency_manager_handle_latency_info(pwar_latency_info_t *latency_info); - -// New function to get current metrics for GUI display -void latency_manager_get_current_metrics(pwar_latency_metrics_t *metrics); - -void latency_manager_report_xrun(); +void latency_manger_get_current_metrics(pwar_latency_metrics_t *metrics); #ifdef __cplusplus } diff --git a/protocol/pwar_latency_types.h b/protocol/pwar_latency_types.h index 1d08fac..feb958d 100644 --- a/protocol/pwar_latency_types.h +++ b/protocol/pwar_latency_types.h @@ -2,15 +2,30 @@ #define PWAR_LATENCY_TYPES typedef struct { - double audio_proc_min_ms; - double audio_proc_max_ms; - double audio_proc_avg_ms; - double jitter_min_ms; - double jitter_max_ms; - double jitter_avg_ms; + // Round-trip time double rtt_min_ms; double rtt_max_ms; double rtt_avg_ms; + + // Audio processing time + double audio_proc_min_ms; + double audio_proc_max_ms; + double audio_proc_avg_ms; + + // Windows receive delta (jitter) + double windows_jitter_min_ms; + double windows_jitter_max_ms; + double windows_jitter_avg_ms; + + // Linux receive delta (jitter) + double linux_jitter_min_ms; + double linux_jitter_max_ms; + double linux_jitter_avg_ms; + + // Ring buffer fill level (in milliseconds) + double ring_buffer_min_ms; + double ring_buffer_max_ms; + double ring_buffer_avg_ms; uint32_t xruns; } pwar_latency_metrics_t; diff --git a/protocol/pwar_packet.h b/protocol/pwar_packet.h index 4906ddf..90a85f4 100644 --- a/protocol/pwar_packet.h +++ b/protocol/pwar_packet.h @@ -11,42 +11,20 @@ #include #define PWAR_PACKET_MAX_CHUNK_SIZE 128 -#define PWAR_PACKET_MIN_CHUNK_SIZE 64 +#define PWAR_PACKET_MIN_CHUNK_SIZE 32 #define PWAR_CHANNELS 2 -/* - * NOTE: This packet structure is currently locked to 2 channels (PWAR_CHANNELS = 2) for simplicity and compatibility. - * The underlying protocol and codebase are designed to support an arbitrary number of channels in the future. - * - * When multi-channel support is implemented, this struct will be refactored to use a flat sample array with stride, - * rather than a fixed 2D array. This will allow for more flexible and efficient handling of any channel count. - * - * If you are developing for more than 2 channels, be aware that this is a planned area of development. - */ - typedef struct { uint16_t n_samples; // I.e. the current chunk size, must be <= PWAR_PACKET_MAX_CHUNK_SIZE - uint64_t seq; - /* For segmentation */ - uint32_t num_packets; // total number of packets in this sequence - uint32_t packet_index; // index of this packet in the sequence + /* 4-point timestamp system for comprehensive latency analysis */ + uint64_t t1_linux_send; + uint64_t t2_windows_recv; + uint64_t t3_windows_send; + uint64_t t4_linux_recv; - uint64_t seq_timestamp; - uint64_t timestamp; - - float samples[PWAR_CHANNELS][PWAR_PACKET_MAX_CHUNK_SIZE]; // interleaved samples + float samples[PWAR_CHANNELS * PWAR_PACKET_MAX_CHUNK_SIZE]; // interleaved samples } pwar_packet_t; -typedef struct { - uint32_t audio_proc_min; // Minimum processing time in nanoseconds - uint32_t audio_proc_max; // Maximum processing time in nanoseconds - uint32_t audio_proc_avg; // Average processing time in nanoseconds - - uint32_t jitter_min; // Minimum network jitter in nanoseconds - uint32_t jitter_max; // Maximum network jitter in nanoseconds - uint32_t jitter_avg; // Average network jitter in nanoseconds - -} pwar_latency_info_t; #endif /* PWAR_PACKET */ diff --git a/protocol/pwar_rcv_buffer.c b/protocol/pwar_rcv_buffer.c deleted file mode 100644 index c78f88d..0000000 --- a/protocol/pwar_rcv_buffer.c +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include "pwar_rcv_buffer.h" -#include - -#define PWAR_RCV_BUFFER_MAX_CHANNELS 16 -#define PWAR_RCV_BUFFER_MAX_SAMPLES 4096 -#define PWAR_RCV_BUFFER_CHUNK_SIZE 128 - -static struct { - float buffers[2][PWAR_RCV_BUFFER_MAX_CHANNELS][PWAR_RCV_BUFFER_MAX_SAMPLES]; - uint32_t n_samples[2]; - uint32_t channels; - uint32_t chunk_pos; - int buffer_ready[2]; - int ping_pong; // 0 or 1 -} rcv = {0}; - -int pwar_rcv_buffer_add_buffer(float *buffer, uint32_t n_samples, uint32_t channels) { - if (channels > PWAR_RCV_BUFFER_MAX_CHANNELS || n_samples > PWAR_RCV_BUFFER_MAX_SAMPLES) return -1; - int idx = rcv.ping_pong; - for (uint32_t ch = 0; ch < channels; ++ch) { - memcpy(rcv.buffers[idx][ch], &buffer[ch * n_samples], n_samples * sizeof(float)); - } - rcv.n_samples[idx] = n_samples; - rcv.channels = channels; - rcv.buffer_ready[idx] = 1; - return 0; -} - -int pwar_rcv_get_chunk(float *chunks, uint32_t channels, uint32_t chunk_size) { - int idx = !rcv.ping_pong; // read from the other buffer - if (!rcv.buffer_ready[idx] || channels > rcv.channels) { - // No buffer ready, output silence - for (uint32_t ch = 0; ch < channels; ++ch) { - memset(&chunks[ch * chunk_size], 0, chunk_size * sizeof(float)); - } - rcv.ping_pong = !rcv.ping_pong; // swap buffers - return 0; - } - uint32_t n_samples = rcv.n_samples[idx]; - uint32_t start = rcv.chunk_pos * chunk_size; - // Copy chunk - for (uint32_t ch = 0; ch < channels; ++ch) { - uint32_t remain = n_samples - start; - uint32_t to_copy = remain < chunk_size ? remain : chunk_size; - memcpy(&chunks[ch * chunk_size], &rcv.buffers[idx][ch][start], to_copy * sizeof(float)); - if (to_copy < chunk_size) { - memset(&chunks[ch * chunk_size + to_copy], 0, (chunk_size - to_copy) * sizeof(float)); - } - } - rcv.chunk_pos++; - // If this was the last chunk, mark buffer as consumed - if ((rcv.chunk_pos * chunk_size) >= n_samples) { - rcv.buffer_ready[idx] = 0; - rcv.chunk_pos = 0; - rcv.ping_pong = !rcv.ping_pong; // swap buffers - } - return 1; -} \ No newline at end of file diff --git a/protocol/pwar_rcv_buffer.h b/protocol/pwar_rcv_buffer.h deleted file mode 100644 index 089a4fe..0000000 --- a/protocol/pwar_rcv_buffer.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef PWAR_RCV_BUFFER -#define PWAR_RCV_BUFFER - -#include - -// No init needed, always static -// buffer: flat array, channel-major order: buffer[channel * n_samples + sample] -int pwar_rcv_buffer_add_buffer(float *buffer, uint32_t n_samples, uint32_t channels); -int pwar_rcv_get_chunk(float *chunks, uint32_t channels, uint32_t chunk_size); - -#endif /* PWAR_RCV_BUFFER */ diff --git a/protocol/pwar_ring_buffer.c b/protocol/pwar_ring_buffer.c new file mode 100644 index 0000000..562b104 --- /dev/null +++ b/protocol/pwar_ring_buffer.c @@ -0,0 +1,231 @@ +#include "pwar_ring_buffer.h" +#include +#include +#include +#include + + +static struct { + float *buffer; // Audio buffer + uint32_t depth; // Buffer depth in samples + uint32_t channels; // Number of audio channels + uint32_t expected_buffer_size; // Expected buffer size for prefill + uint32_t write_index; // Current write position + uint32_t read_index; // Current read position + uint32_t available; // Number of samples available to read + uint32_t overruns; // Count of overrun events + uint32_t underruns; // Count of underrun events + pthread_mutex_t mutex; // Thread safety +} ring_buffer = {0}; + + +static void prefill_buffer() { + if (ring_buffer.buffer == NULL) { + return; + } + + // Reset indices and fill with zeros up to depth - expected_buffer_size + // This leaves one expected_buffer_size worth of headroom for overruns + ring_buffer.write_index = 0; + ring_buffer.read_index = 0; + + // Prefill to depth - expected_buffer_size, leaving headroom + uint32_t prefill_samples = ring_buffer.depth - ring_buffer.expected_buffer_size; + for (uint32_t i = 0; i < prefill_samples; i++) { + uint32_t write_pos = ring_buffer.write_index * ring_buffer.channels; + for (uint32_t ch = 0; ch < ring_buffer.channels; ch++) { + ring_buffer.buffer[write_pos + ch] = 0.0f; + } + ring_buffer.write_index = (ring_buffer.write_index + 1) % ring_buffer.depth; + } + + ring_buffer.available = prefill_samples; // Buffer has prefill_samples available +} + + +void pwar_ring_buffer_init(uint32_t depth, uint32_t channels, uint32_t expected_buffer_size) { + // Free any existing buffer + if (ring_buffer.buffer != NULL) { + pwar_ring_buffer_free(); + } + + // Initialize the ring buffer structure with increased depth to handle overruns + ring_buffer.depth = depth + expected_buffer_size; + ring_buffer.channels = channels; + ring_buffer.expected_buffer_size = expected_buffer_size; + ring_buffer.write_index = 0; + ring_buffer.read_index = 0; + ring_buffer.available = 0; + ring_buffer.overruns = 0; + ring_buffer.underruns = 0; + + // Allocate buffer memory (total_depth * channels * sizeof(float)) + ring_buffer.buffer = (float*)calloc(ring_buffer.depth * channels, sizeof(float)); + if (ring_buffer.buffer == NULL) { + fprintf(stderr, "Error: Failed to allocate ring buffer memory\n"); + return; + } + + // Initialize mutex for thread safety + if (pthread_mutex_init(&ring_buffer.mutex, NULL) != 0) { + fprintf(stderr, "Error: Failed to initialize ring buffer mutex\n"); + free(ring_buffer.buffer); + ring_buffer.buffer = NULL; + return; + } + + prefill_buffer(); + printf("Ring buffer initialized: %d samples + %d buffer (%d total), %d channels\n", + depth, expected_buffer_size, ring_buffer.depth, channels); +} + +void pwar_ring_buffer_free() { + pthread_mutex_lock(&ring_buffer.mutex); + + if (ring_buffer.buffer != NULL) { + free(ring_buffer.buffer); + ring_buffer.buffer = NULL; + } + + ring_buffer.depth = 0; + ring_buffer.channels = 0; + ring_buffer.expected_buffer_size = 0; + ring_buffer.write_index = 0; + ring_buffer.read_index = 0; + ring_buffer.available = 0; + + pthread_mutex_unlock(&ring_buffer.mutex); + pthread_mutex_destroy(&ring_buffer.mutex); + + printf("Ring buffer freed\n"); +} + +int pwar_ring_buffer_push(float *buffer, uint32_t n_samples, uint32_t channels) { + if (ring_buffer.buffer == NULL || buffer == NULL) { + return -1; // Buffer not initialized or invalid input + } + + if (channels != ring_buffer.channels) { + fprintf(stderr, "Error: Channel count mismatch (%d vs %d)\n", channels, ring_buffer.channels); + return -1; + } + + pthread_mutex_lock(&ring_buffer.mutex); + + // Check for potential overrun + uint32_t samples_to_write = n_samples; + uint32_t free_space = ring_buffer.depth - ring_buffer.available; + + if (samples_to_write > free_space) { + // Overrun detected - we'll overwrite the oldest data + ring_buffer.overruns++; + + // Move read pointer forward to make space for new data + uint32_t samples_to_skip = samples_to_write - free_space; + ring_buffer.read_index = (ring_buffer.read_index + samples_to_skip) % ring_buffer.depth; + + printf("Warning: Ring buffer overrun detected. Skipped %d samples (total overruns: %d)\n", + samples_to_skip, ring_buffer.overruns); + } + + // Copy samples to the ring buffer + for (uint32_t i = 0; i < samples_to_write; i++) { + uint32_t write_pos = ring_buffer.write_index * ring_buffer.channels; + + // Copy all channels for this sample + for (uint32_t ch = 0; ch < ring_buffer.channels; ch++) { + ring_buffer.buffer[write_pos + ch] = buffer[i * channels + ch]; + } + + ring_buffer.write_index = (ring_buffer.write_index + 1) % ring_buffer.depth; + } + + // Update available count: if overrun occurred, buffer is now full + if (samples_to_write > free_space) { + ring_buffer.available = ring_buffer.depth; // Buffer is full after overrun + } else { + ring_buffer.available += samples_to_write; // Normal case + } + + pthread_mutex_unlock(&ring_buffer.mutex); + + return 1; // Success +} + +int pwar_ring_buffer_pop(float *samples, uint32_t n_samples, uint32_t channels) { + if (ring_buffer.buffer == NULL || samples == NULL) { + return -1; // Buffer not initialized or invalid output + } + + if (channels != ring_buffer.channels) { + fprintf(stderr, "Error: Channel count mismatch (%d vs %d)\n", channels, ring_buffer.channels); + return -1; + } + + pthread_mutex_lock(&ring_buffer.mutex); + + uint32_t samples_to_read = n_samples; + + // Check for underrun + if (samples_to_read > ring_buffer.available) { + ring_buffer.underruns++; + + printf("Warning: Ring buffer underrun detected. Requested %d, available %d (total underruns: %d)\n", + n_samples, ring_buffer.available, ring_buffer.underruns); + + // Fill output with zeros since we have no valid data + memset(samples, 0, n_samples * channels * sizeof(float)); + + // Reset the entire buffer with zeros for maximum protection against future underruns + prefill_buffer(); + + pthread_mutex_unlock(&ring_buffer.mutex); + return n_samples; // Return the requested number (filled with zeros) + } + + // Copy samples from the ring buffer + for (uint32_t i = 0; i < samples_to_read; i++) { + uint32_t read_pos = ring_buffer.read_index * ring_buffer.channels; + + // Copy all channels for this sample + for (uint32_t ch = 0; ch < ring_buffer.channels; ch++) { + samples[i * channels + ch] = ring_buffer.buffer[read_pos + ch]; + } + + ring_buffer.read_index = (ring_buffer.read_index + 1) % ring_buffer.depth; + } + + ring_buffer.available -= samples_to_read; + + pthread_mutex_unlock(&ring_buffer.mutex); + + return samples_to_read; // Return actual number of samples read +} + +uint32_t pwar_ring_buffer_get_available() { + pthread_mutex_lock(&ring_buffer.mutex); + uint32_t available = ring_buffer.available; + pthread_mutex_unlock(&ring_buffer.mutex); + return available; +} + +uint32_t pwar_ring_buffer_get_overruns() { + pthread_mutex_lock(&ring_buffer.mutex); + uint32_t overruns = ring_buffer.overruns; + pthread_mutex_unlock(&ring_buffer.mutex); + return overruns; +} + +uint32_t pwar_ring_buffer_get_underruns() { + pthread_mutex_lock(&ring_buffer.mutex); + uint32_t underruns = ring_buffer.underruns; + pthread_mutex_unlock(&ring_buffer.mutex); + return underruns; +} + +void pwar_ring_buffer_reset_stats() { + pthread_mutex_lock(&ring_buffer.mutex); + ring_buffer.overruns = 0; + ring_buffer.underruns = 0; + pthread_mutex_unlock(&ring_buffer.mutex); +} \ No newline at end of file diff --git a/protocol/pwar_ring_buffer.h b/protocol/pwar_ring_buffer.h new file mode 100644 index 0000000..daef37b --- /dev/null +++ b/protocol/pwar_ring_buffer.h @@ -0,0 +1,19 @@ +#ifndef PWAR_RING_BUFFER +#define PWAR_RING_BUFFER + +#include +#include + +void pwar_ring_buffer_init(uint32_t depth, uint32_t channels, uint32_t expected_buffer_size); +void pwar_ring_buffer_free(); + +int pwar_ring_buffer_push(float *buffer, uint32_t n_samples, uint32_t channels); +int pwar_ring_buffer_pop(float *samples, uint32_t n_samples, uint32_t channels); + +// Additional utility functions +uint32_t pwar_ring_buffer_get_available(); +uint32_t pwar_ring_buffer_get_overruns(); +uint32_t pwar_ring_buffer_get_underruns(); +void pwar_ring_buffer_reset_stats(); + +#endif /* PWAR_RING_BUFFER */ diff --git a/protocol/pwar_router.c b/protocol/pwar_router.c deleted file mode 100644 index 0fd5e24..0000000 --- a/protocol/pwar_router.c +++ /dev/null @@ -1,89 +0,0 @@ -/* - * pwar_router.c - PipeWire <-> UDP streaming bridge for PWAR - * - * (c) 2025 Philip K. Gisslow - * This file is part of the PipeWire ASIO Relay (PWAR) project. - */ - -#include "pwar_router.h" -#include "pwar_packet.h" -#include - -void pwar_router_init(pwar_router_t *router, uint32_t channel_count) { - router->channel_count = channel_count; - router->received_packets = 0; - // Use the correct size for packet_received array - const uint32_t max_packets = sizeof(router->packet_received) / sizeof(router->packet_received[0]); - for (uint32_t i = 0; i < max_packets; ++i) router->packet_received[i] = 0; - router->current_seq = (uint64_t)(-1); // Initialize to invalid seq -} - -int pwar_router_process_streaming_packet(pwar_router_t *router, pwar_packet_t *input_packet, float *output_buffers, uint32_t max_samples, uint32_t channel_count) { - int index = input_packet->seq - router->current_seq; - if (index >= 0 && index < input_packet->num_packets) { - input_packet->packet_index = index; - input_packet->seq = router->current_seq; - } - return pwar_router_process_packet(router, input_packet, output_buffers, max_samples, channel_count); -} - -int pwar_router_process_packet(pwar_router_t *router, pwar_packet_t *input_packet, float *output_buffers, uint32_t max_samples, uint32_t channel_count) { - if (!input_packet || !output_buffers) return -1; - if (input_packet->num_packets == 0 || input_packet->packet_index >= input_packet->num_packets) return -2; - if (input_packet->n_samples > PWAR_PACKET_MAX_CHUNK_SIZE) return -3; - // Reset state if new buffer sequence detected - if (input_packet->seq != router->current_seq) { - router->current_seq = input_packet->seq; - router->received_packets = 0; - router->seq_timestamp = input_packet->seq_timestamp; // Update the sequence timestamp - const uint32_t max_packets = sizeof(router->packet_received) / sizeof(router->packet_received[0]); - for (uint32_t i = 0; i < max_packets; ++i) router->packet_received[i] = 0; - } - if (!router->packet_received[input_packet->packet_index]) { - // Copy samples to internal buffer - uint32_t offset = input_packet->packet_index * input_packet->n_samples; - for (uint32_t ch = 0; ch < router->channel_count && ch < PWAR_CHANNELS; ++ch) { - for (uint32_t s = 0; s < input_packet->n_samples; ++s) { - router->buffers[ch][offset + s] = input_packet->samples[ch][s]; - } - } - router->packet_received[input_packet->packet_index] = 1; - router->received_packets++; - } - // Check if all packets for this buffer are received - if (router->received_packets == input_packet->num_packets) { - // Calculate total number of samples from packet info - uint32_t total_samples = (input_packet->num_packets - 1) * input_packet->n_samples + input_packet->n_samples; - uint32_t n_samples = total_samples < max_samples ? total_samples : max_samples; - for (uint32_t ch = 0; ch < channel_count && ch < router->channel_count; ++ch) { - memcpy(&output_buffers[ch * n_samples], router->buffers[ch], n_samples * sizeof(float)); - } - return n_samples; // Return number of samples ready - } - return 0; // Not ready yet -} - -// Returns 0 on success, -1 if not enough space in packets array, -2 if invalid arguments -int pwar_router_send_buffer(pwar_router_t *router, uint32_t chunk_size, float *samples, uint32_t n_samples, uint32_t channel_count, pwar_packet_t *packets, const uint32_t packet_count, uint32_t *packets_to_send) { - (void)router; // Unused in this implementation, but could be used for future enhancements - if (!samples || !packets || !packets_to_send || channel_count == 0 || n_samples == 0) return -2; - uint32_t total_packets = (n_samples + chunk_size - 1) / chunk_size; - if (total_packets > packet_count) { - *packets_to_send = 0; - return -1; // Not enough space in packets array - } - for (uint32_t p = 0; p < total_packets; ++p) { - uint32_t start = p * chunk_size; - uint32_t ns = (n_samples - start > chunk_size) ? chunk_size : (n_samples - start); - packets[p].packet_index = p; - packets[p].num_packets = total_packets; - packets[p].n_samples = ns; - packets[p].seq_timestamp = router->seq_timestamp; - for (uint32_t ch = 0; ch < channel_count; ++ch) { - memcpy(packets[p].samples[ch], &samples[ch * n_samples + start], ns * sizeof(float)); - } - } - *packets_to_send = total_packets; - return 1; -} - diff --git a/protocol/pwar_router.h b/protocol/pwar_router.h deleted file mode 100644 index 1d8f46e..0000000 --- a/protocol/pwar_router.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * pwar_router.h - PipeWire ASIO Relay (PWAR) project - * - * (c) 2025 Philip K. Gisslow - * This file is part of the PipeWire ASIO Relay (PWAR) project. - */ - - -#ifndef PWAR_ROUTER -#define PWAR_ROUTER - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include "pwar_packet.h" - -#define PWAR_ROUTER_MAX_CHANNELS 16 -#define PWAR_ROUTER_MAX_BUFFER_SIZE 4096 - -typedef struct { - uint32_t channel_count; - - float buffers[PWAR_ROUTER_MAX_CHANNELS][PWAR_ROUTER_MAX_BUFFER_SIZE]; // interleaved buffers for each channel - - // State for packet assembly - uint32_t received_packets; - uint8_t packet_received[PWAR_ROUTER_MAX_BUFFER_SIZE / PWAR_PACKET_MIN_CHUNK_SIZE]; - uint64_t current_seq; // Track current buffer sequence number - uint64_t seq_timestamp; // Timestamp for the current sequence -} pwar_router_t; - -void pwar_router_init(pwar_router_t *router, uint32_t channel_count); - -// Returns the number of samples ready when all packets have been processed, 0 if more packets are needed -// output_buffers: flat array, channel-major order: output_buffers[channel * n_samples + sample] -// max_samples: maximum number of samples per channel to write to output_buffers -int pwar_router_process_packet(pwar_router_t *router, pwar_packet_t *input_packet, float *output_buffers, uint32_t max_samples, uint32_t channel_count); - -int pwar_router_process_streaming_packet(pwar_router_t *router, pwar_packet_t *input_packet, float *output_buffers, uint32_t max_samples, uint32_t channel_count); - -// samples: flat array, channel-major order: samples[channel * n_samples + sample] -// n_samples: number of samples per channel -// channel_count: number of channels -// packets: output array for generated packets -// packet_count: size of the packets array -// packets_to_send: output, set to the number of packets generated from the input samples -int pwar_router_send_buffer(pwar_router_t *router, uint32_t chunk_size, float *samples, uint32_t n_samples, uint32_t channel_count, pwar_packet_t *packets, const uint32_t packet_count, uint32_t *packets_to_send); - -#ifdef __cplusplus -} -#endif -#endif /* PWAR_ROUTER */ diff --git a/protocol/test/CMakeLists.txt b/protocol/test/CMakeLists.txt index 2b2bea0..b305572 100644 --- a/protocol/test/CMakeLists.txt +++ b/protocol/test/CMakeLists.txt @@ -1,5 +1,10 @@ # CMakeLists.txt for protocol tests -cmake_minimum_required(VERSION 3.15) + +# Options to control which tests to build +option(BUILD_RCV_BUFFER_TEST "Build pwar_rcv_buffer_test" OFF) +option(BUILD_ROUTER_TEST "Build pwar_router_test" OFF) +option(BUILD_RING_BUFFER_TEST "Build pwar_ring_buffer_test" ON) +option(BUILD_SEND_RECEIVE_CHAIN_TEST "Build pwar_send_receive_chain_test" OFF) # Find Check testing framework (optional) find_package(PkgConfig QUIET) @@ -7,75 +12,87 @@ if(PkgConfig_FOUND) pkg_check_modules(CHECK check) endif() -# Math library +# Find required libraries find_library(MATH_LIB m) +find_package(Threads REQUIRED) -# Protocol sources +# Protocol sources - use absolute paths relative to project root +set(PROTOCOL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) set(PROTOCOL_SOURCES - ../pwar_router.c - ../pwar_rcv_buffer.c + ${PROTOCOL_DIR}/pwar_router.c + ${PROTOCOL_DIR}/pwar_rcv_buffer.c + ${PROTOCOL_DIR}/pwar_ring_buffer.c ) # Check if pwar_send_buffer.c exists (it's referenced in tests but may not exist yet) -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/../pwar_send_buffer.c) - list(APPEND PROTOCOL_SOURCES ../pwar_send_buffer.c) +if(EXISTS ${PROTOCOL_DIR}/pwar_send_buffer.c) + list(APPEND PROTOCOL_SOURCES ${PROTOCOL_DIR}/pwar_send_buffer.c) set(HAS_SEND_BUFFER TRUE) else() set(HAS_SEND_BUFFER FALSE) - message(WARNING "pwar_send_buffer.c not found - some tests may not build correctly") + message(STATUS "pwar_send_buffer.c not found - some tests may not build correctly") endif() # Include directories -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) - -# Protocol test executables -add_executable(pwar_rcv_buffer_test - pwar_rcv_buffer_test.c - ${PROTOCOL_SOURCES} -) - -target_link_libraries(pwar_rcv_buffer_test ${MATH_LIB}) - -if(CHECK_FOUND) - target_include_directories(pwar_rcv_buffer_test PRIVATE ${CHECK_INCLUDE_DIRS}) - target_link_libraries(pwar_rcv_buffer_test ${CHECK_LIBRARIES}) - target_compile_options(pwar_rcv_buffer_test PRIVATE ${CHECK_CFLAGS_OTHER}) +include_directories(${PROTOCOL_DIR}) + +# Common function to set up test targets +function(add_protocol_test TEST_NAME TEST_SOURCE) + add_executable(${TEST_NAME} ${TEST_SOURCE}) + + # Link libraries + target_link_libraries(${TEST_NAME} ${MATH_LIB} Threads::Threads) + + # Add Check support if available + if(CHECK_FOUND) + target_include_directories(${TEST_NAME} PRIVATE ${CHECK_INCLUDE_DIRS}) + target_link_libraries(${TEST_NAME} ${CHECK_LIBRARIES}) + target_compile_options(${TEST_NAME} PRIVATE ${CHECK_CFLAGS_OTHER}) + + # Add test to CTest + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + endif() +endfunction() + +# Protocol test executables - only build if enabled +if(BUILD_RCV_BUFFER_TEST) + add_protocol_test(pwar_rcv_buffer_test + "pwar_rcv_buffer_test.c;${PROTOCOL_SOURCES}") + message(STATUS "Building pwar_rcv_buffer_test") endif() -add_executable(pwar_router_test - pwar_router_test.c - ${PROTOCOL_SOURCES} -) - -target_link_libraries(pwar_router_test ${MATH_LIB}) - -if(CHECK_FOUND) - target_include_directories(pwar_router_test PRIVATE ${CHECK_INCLUDE_DIRS}) - target_link_libraries(pwar_router_test ${CHECK_LIBRARIES}) - target_compile_options(pwar_router_test PRIVATE ${CHECK_CFLAGS_OTHER}) +if(BUILD_RING_BUFFER_TEST) + add_protocol_test(pwar_ring_buffer_test + "pwar_ring_buffer_test.c;${PROTOCOL_DIR}/pwar_ring_buffer.c") + message(STATUS "Building pwar_ring_buffer_test") endif() -add_executable(pwar_send_receive_chain_test - pwar_send_receive_chain_test.c - ${PROTOCOL_SOURCES} -) - -target_link_libraries(pwar_send_receive_chain_test ${MATH_LIB}) - -if(CHECK_FOUND) - target_include_directories(pwar_send_receive_chain_test PRIVATE ${CHECK_INCLUDE_DIRS}) - target_link_libraries(pwar_send_receive_chain_test ${CHECK_LIBRARIES}) - target_compile_options(pwar_send_receive_chain_test PRIVATE ${CHECK_CFLAGS_OTHER}) +if(BUILD_ROUTER_TEST) + add_protocol_test(pwar_router_test + "pwar_router_test.c;${PROTOCOL_SOURCES}") + message(STATUS "Building pwar_router_test") endif() -# Only build send receive chain test if send buffer exists -if(NOT HAS_SEND_BUFFER) - set_target_properties(pwar_send_receive_chain_test PROPERTIES EXCLUDE_FROM_ALL TRUE) - message(STATUS "Excluding pwar_send_receive_chain_test from build due to missing pwar_send_buffer.c") +if(BUILD_SEND_RECEIVE_CHAIN_TEST AND HAS_SEND_BUFFER) + add_protocol_test(pwar_send_receive_chain_test + "pwar_send_receive_chain_test.c;${PROTOCOL_SOURCES}") + message(STATUS "Building pwar_send_receive_chain_test") +elif(BUILD_SEND_RECEIVE_CHAIN_TEST AND NOT HAS_SEND_BUFFER) + message(STATUS "Cannot build pwar_send_receive_chain_test - missing pwar_send_buffer.c") endif() +# Enable testing if(CHECK_FOUND) + enable_testing() message(STATUS "Check testing framework found - building protocol tests with Check support") + message(STATUS "Run tests with: ninja test (or ctest)") else() message(STATUS "Check testing framework not found - building protocol tests without Check support") endif() + +# Summary of what's being built +message(STATUS "Protocol test build summary:") +message(STATUS " RCV Buffer Test: ${BUILD_RCV_BUFFER_TEST}") +message(STATUS " Ring Buffer Test: ${BUILD_RING_BUFFER_TEST}") +message(STATUS " Router Test: ${BUILD_ROUTER_TEST}") +message(STATUS " Send/Receive Chain Test: ${BUILD_SEND_RECEIVE_CHAIN_TEST}") diff --git a/protocol/test/README.md b/protocol/test/README.md new file mode 100644 index 0000000..2a161a9 --- /dev/null +++ b/protocol/test/README.md @@ -0,0 +1,76 @@ +# Protocol Tests + +This directory contains unit tests for the PWAR protocol components. + +## Building Tests + +From the project root, use: + +```bash +# Configure with Ninja (only ring buffer test enabled by default) +cmake -G Ninja -S . -B build + +# Build all enabled tests +ninja -C build + +# Run tests (if Check framework is available) +ninja -C build test +# or +cd build && ctest +``` + +## Test Control Options + +You can control which tests are built by setting CMake options: + +```bash +# Enable/disable specific tests +cmake -G Ninja -S . -B build \ + -DBUILD_RING_BUFFER_TEST=ON \ + -DBUILD_RCV_BUFFER_TEST=OFF \ + -DBUILD_ROUTER_TEST=OFF \ + -DBUILD_SEND_RECEIVE_CHAIN_TEST=OFF + +# Build only the ring buffer test (default) +cmake -G Ninja -S . -B build + +# Enable all tests +cmake -G Ninja -S . -B build \ + -DBUILD_RCV_BUFFER_TEST=ON \ + -DBUILD_ROUTER_TEST=ON \ + -DBUILD_RING_BUFFER_TEST=ON \ + -DBUILD_SEND_RECEIVE_CHAIN_TEST=ON +``` + +## Available Tests + +- **pwar_ring_buffer_test**: Comprehensive unit test for the ring buffer implementation + - Tests basic push/pop operations + - Tests overrun/underrun behavior (critical for finding bugs) + - Tests edge cases like wrap-around, zero samples, channel mismatches + - Tests thread safety and statistics + +- **pwar_rcv_buffer_test**: Tests for the receive buffer +- **pwar_router_test**: Tests for the packet router +- **pwar_send_receive_chain_test**: End-to-end tests (requires pwar_send_buffer.c) + +## Running Individual Tests + +After building, you can run tests directly: + +```bash +# Run ring buffer test +./build/protocol/test/pwar_ring_buffer_test + +# Run with verbose output +./build/protocol/test/pwar_ring_buffer_test -v +``` + +## Dependencies + +- **Check framework** (optional but recommended): Provides structured unit testing + - Install on Ubuntu/Debian: `sudo apt install check libcheck-dev` + - Tests will build without Check but with limited functionality + +- **pthread**: Required for ring buffer tests (thread safety testing) +- **math library**: Required for floating-point operations diff --git a/protocol/test/pwar_rcv_buffer_test.c b/protocol/test/pwar_rcv_buffer_test.c deleted file mode 100644 index 051b4c9..0000000 --- a/protocol/test/pwar_rcv_buffer_test.c +++ /dev/null @@ -1,73 +0,0 @@ -#include -#include -#include -#include "../pwar_rcv_buffer.h" - -#define TEST_CHANNELS 2 -#define TEST_CHUNK_SIZE 128 -#define TEST_BUF_SIZE 512 - -// Helper to fill a buffer with predictable sample values for testing -static void fill_samples(float *samples, uint32_t channels, uint32_t n_samples, uint32_t stride, float value) { - for (uint32_t ch = 0; ch < channels; ++ch) { - for (uint32_t s = 0; s < n_samples; ++s) { - samples[ch * stride + s] = value + ch * 1000 + s; - } - } -} - -// Test: When the receive buffer is empty, pwar_rcv_get_chunk should return silence (all zeros) -START_TEST(test_rcv_buffer_silence_before_fill) -{ - float chunks[TEST_CHANNELS * TEST_CHUNK_SIZE] = {0}; - int ret = pwar_rcv_get_chunk(chunks, TEST_CHANNELS, TEST_CHUNK_SIZE); - ck_assert_int_eq(ret, 0); // Should indicate silence - for (int ch = 0; ch < TEST_CHANNELS; ++ch) - for (int s = 0; s < TEST_CHUNK_SIZE; ++s) - ck_assert_float_eq_tol(chunks[ch * TEST_CHUNK_SIZE + s], 0.0f, 0.0001f); -} -END_TEST - -// Test: Fill the buffer, read out all chunks, then verify silence after buffer is empty -START_TEST(test_rcv_buffer_fill_and_read) -{ - float buf[TEST_CHANNELS * TEST_BUF_SIZE]; - fill_samples(buf, TEST_CHANNELS, TEST_BUF_SIZE, TEST_BUF_SIZE, 1.0f); - pwar_rcv_buffer_add_buffer(buf, TEST_BUF_SIZE, TEST_CHANNELS, TEST_BUF_SIZE); - - float chunks[TEST_CHANNELS * TEST_CHUNK_SIZE]; - for (int i = 0; i < 4; ++i) { - int ret = pwar_rcv_get_chunk(chunks, TEST_CHANNELS, TEST_CHUNK_SIZE); - ck_assert_int_eq(ret, 1); // Should indicate data was read - for (int ch = 0; ch < TEST_CHANNELS; ++ch) - for (int s = 0; s < TEST_CHUNK_SIZE; ++s) - ck_assert_float_eq_tol(chunks[ch * TEST_CHUNK_SIZE + s], 1.0f + ch * 1000 + i * TEST_CHUNK_SIZE + s, 0.0001f); - } - int ret = pwar_rcv_get_chunk(chunks, TEST_CHANNELS, TEST_CHUNK_SIZE); - ck_assert_int_eq(ret, 0); - for (int ch = 0; ch < TEST_CHANNELS; ++ch) - for (int s = 0; s < TEST_CHUNK_SIZE; ++s) - ck_assert_float_eq_tol(chunks[ch * TEST_CHUNK_SIZE + s], 0.0f, 0.0001f); -} -END_TEST - -// Test suite setup -Suite *rcv_buffer_suite(void) { - Suite *s = suite_create("pwar_rcv_buffer"); - TCase *tc_core = tcase_create("Core"); - tcase_add_test(tc_core, test_rcv_buffer_silence_before_fill); - tcase_add_test(tc_core, test_rcv_buffer_fill_and_read); - suite_add_tcase(s, tc_core); - return s; -} - -// Main entry for running the test suite -int main(void) { - int number_failed; - Suite *s = rcv_buffer_suite(); - SRunner *sr = srunner_create(s); - srunner_run_all(sr, CK_NORMAL); - number_failed = srunner_ntests_failed(sr); - srunner_free(sr); - return (number_failed == 0) ? 0 : 1; -} diff --git a/protocol/test/pwar_ring_buffer_test.c b/protocol/test/pwar_ring_buffer_test.c new file mode 100644 index 0000000..9e82a69 --- /dev/null +++ b/protocol/test/pwar_ring_buffer_test.c @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include +#include +#include "../pwar_ring_buffer.h" + +#define TEST_CHANNELS 2 +#define TEST_DEPTH 1024 +#define TEST_EXPECTED_BUFFER_SIZE 256 + +// Helper function to fill buffer with predictable test data +static void fill_test_data(float *buffer, uint32_t channels, uint32_t n_samples, float base_value) { + for (uint32_t sample = 0; sample < n_samples; sample++) { + for (uint32_t ch = 0; ch < channels; ch++) { + buffer[sample * channels + ch] = base_value + sample + (ch * 1000.0f); + } + } +} + +// Helper function to verify test data +static void verify_test_data(float *buffer, uint32_t channels, uint32_t n_samples, float base_value, const char *context) { + for (uint32_t sample = 0; sample < n_samples; sample++) { + for (uint32_t ch = 0; ch < channels; ch++) { + float expected = base_value + sample + (ch * 1000.0f); + float actual = buffer[sample * channels + ch]; + ck_assert_msg(fabsf(actual - expected) < 0.0001f, + "%s: Sample mismatch at [%d][%d]: expected %f, got %f", + context, sample, ch, expected, actual); + } + } +} + +// Test basic initialization and cleanup +START_TEST(test_ring_buffer_init_free) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + // Check initial state + ck_assert_uint_eq(pwar_ring_buffer_get_overruns(), 0); + ck_assert_uint_eq(pwar_ring_buffer_get_underruns(), 0); + + uint32_t expected_prefill = TEST_DEPTH; + ck_assert_uint_eq(pwar_ring_buffer_get_available(), expected_prefill); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test basic push/pop functionality +START_TEST(test_ring_buffer_basic_push_pop) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + uint32_t test_samples = TEST_EXPECTED_BUFFER_SIZE; + float input_buffer[TEST_CHANNELS * test_samples]; + float prefill_buffer[TEST_CHANNELS * TEST_DEPTH]; // Separate buffer for prefill data + float output_buffer[TEST_CHANNELS * test_samples]; + + fill_test_data(input_buffer, TEST_CHANNELS, test_samples, 1000.0f); + + // Push data + int ret = pwar_ring_buffer_push(input_buffer, test_samples, TEST_CHANNELS); + ck_assert_int_eq(ret, 1); + + // Check available count increased + uint32_t expected_available = TEST_DEPTH + test_samples; + ck_assert_uint_eq(pwar_ring_buffer_get_available(), expected_available); + + // Pop prefill data first (should be zeros) + memset(prefill_buffer, 0xAA, sizeof(prefill_buffer)); // Fill with junk + ret = pwar_ring_buffer_pop(prefill_buffer, TEST_DEPTH, TEST_CHANNELS); + ck_assert_int_eq(ret, TEST_DEPTH); + + // Verify prefill was zeros + for (uint32_t i = 0; i < TEST_DEPTH * TEST_CHANNELS; i++) { + ck_assert_float_eq_tol(prefill_buffer[i], 0.0f, 0.0001f); + } + + // Now pop our test data + memset(output_buffer, 0xAA, sizeof(output_buffer)); + ret = pwar_ring_buffer_pop(output_buffer, test_samples, TEST_CHANNELS); + ck_assert_int_eq(ret, test_samples); + + verify_test_data(output_buffer, TEST_CHANNELS, test_samples, 1000.0f, "basic_push_pop"); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test underrun behavior - this is where bugs often hide! +START_TEST(test_ring_buffer_underrun) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + uint32_t initial_underruns = pwar_ring_buffer_get_underruns(); + + // Try to pop more data than available (including prefill) + uint32_t excessive_samples = TEST_DEPTH + 100; + float output_buffer[TEST_CHANNELS * excessive_samples]; + memset(output_buffer, 0xAA, sizeof(output_buffer)); // Fill with junk + + int ret = pwar_ring_buffer_pop(output_buffer, excessive_samples, TEST_CHANNELS); + ck_assert_int_eq(ret, excessive_samples); // Should return requested count + + // Verify underrun was detected + ck_assert_uint_gt(pwar_ring_buffer_get_underruns(), initial_underruns); + + // Verify output is all zeros (silence) + for (uint32_t i = 0; i < excessive_samples * TEST_CHANNELS; i++) { + ck_assert_float_eq_tol(output_buffer[i], 0.0f, 0.0001f); + } + + // After underrun, buffer should be prefilled again + uint32_t expected_available = TEST_DEPTH; + ck_assert_uint_eq(pwar_ring_buffer_get_available(), expected_available); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test multiple small underruns +START_TEST(test_ring_buffer_multiple_underruns) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + uint32_t small_chunk = TEST_EXPECTED_BUFFER_SIZE; + float output_buffer[TEST_CHANNELS * small_chunk]; + + // Consume all prefill data first + uint32_t prefill_samples = TEST_DEPTH; + float prefill_buffer[TEST_CHANNELS * prefill_samples]; + pwar_ring_buffer_pop(prefill_buffer, prefill_samples, TEST_CHANNELS); + + uint32_t initial_underruns = pwar_ring_buffer_get_underruns(); + + // Now trigger multiple underruns + for (int i = 0; i < 5; i++) { + memset(output_buffer, 0xAA, sizeof(output_buffer)); + int ret = pwar_ring_buffer_pop(output_buffer, small_chunk, TEST_CHANNELS); + ck_assert_int_eq(ret, small_chunk); + + // Each should be zeros + for (uint32_t j = 0; j < small_chunk * TEST_CHANNELS; j++) { + ck_assert_float_eq_tol(output_buffer[j], 0.0f, 0.0001f); + } + } + + // Should have detected multiple underruns + ck_assert_uint_gt(pwar_ring_buffer_get_underruns(), initial_underruns); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test overrun behavior - critical for finding bugs! +START_TEST(test_ring_buffer_overrun) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + uint32_t initial_overruns = pwar_ring_buffer_get_overruns(); + + // Fill buffer completely and then some more + uint32_t excessive_samples = TEST_DEPTH + 500; // More than total capacity + float input_buffer[TEST_CHANNELS * excessive_samples]; + fill_test_data(input_buffer, TEST_CHANNELS, excessive_samples, 2000.0f); + + int ret = pwar_ring_buffer_push(input_buffer, excessive_samples, TEST_CHANNELS); + ck_assert_int_eq(ret, 1); // Should succeed + + // Verify overrun was detected + ck_assert_uint_gt(pwar_ring_buffer_get_overruns(), initial_overruns); + + // Buffer should be full after overrun, and full means TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE due to safety margin + ck_assert_uint_eq(pwar_ring_buffer_get_available(), TEST_DEPTH+TEST_EXPECTED_BUFFER_SIZE); + + // When we pop data, we should get the LATEST data (oldest was discarded) + float output_buffer[TEST_CHANNELS * TEST_DEPTH]; + ret = pwar_ring_buffer_pop(output_buffer, TEST_DEPTH, TEST_CHANNELS); + ck_assert_int_eq(ret, TEST_DEPTH); + + // The data should be from the end of the input (newest data) + // Account for the actual buffer capacity including safety margin + uint32_t actual_buffer_capacity = TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE; + uint32_t offset = excessive_samples - actual_buffer_capacity; + verify_test_data(output_buffer, TEST_CHANNELS, TEST_DEPTH, 2000.0f + offset, "overrun_latest_data"); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test gradual overrun build-up +START_TEST(test_ring_buffer_gradual_overrun) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + uint32_t chunk_size = TEST_EXPECTED_BUFFER_SIZE; + float input_buffer[TEST_CHANNELS * chunk_size]; + + // Keep pushing without popping to gradually fill buffer + uint32_t total_pushed = 0; + for (int i = 0; i < 10; i++) { + fill_test_data(input_buffer, TEST_CHANNELS, chunk_size, i * 100.0f); + int ret = pwar_ring_buffer_push(input_buffer, chunk_size, TEST_CHANNELS); + ck_assert_int_eq(ret, 1); + total_pushed += chunk_size; + + // Check if overrun occurred + if (total_pushed > TEST_DEPTH) { + ck_assert_uint_gt(pwar_ring_buffer_get_overruns(), 0); + break; + } + } + + // Buffer should be at capacity (including safety margin) + ck_assert_uint_eq(pwar_ring_buffer_get_available(), TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test concurrent overrun and underrun scenarios +START_TEST(test_ring_buffer_mixed_overrun_underrun) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + // First cause an overrun - need to exceed actual buffer capacity (TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE) + uint32_t large_push = TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE + 100; // 1380 > 1280, will cause overrun + float input_buffer[TEST_CHANNELS * large_push]; + fill_test_data(input_buffer, TEST_CHANNELS, large_push, 3000.0f); + pwar_ring_buffer_push(input_buffer, large_push, TEST_CHANNELS); + + uint32_t overruns_after_push = pwar_ring_buffer_get_overruns(); + ck_assert_uint_gt(overruns_after_push, 0); + + // Now cause an underrun by popping more than available + // After overrun, buffer has TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE samples + uint32_t available_after_overrun = TEST_DEPTH + TEST_EXPECTED_BUFFER_SIZE; + uint32_t large_pop = available_after_overrun + 100; // Pop more than available + float output_buffer[TEST_CHANNELS * large_pop]; + pwar_ring_buffer_pop(output_buffer, large_pop, TEST_CHANNELS); + + uint32_t underruns_after_pop = pwar_ring_buffer_get_underruns(); + ck_assert_uint_gt(underruns_after_pop, 0); + + // After underrun, buffer should be prefilled again + uint32_t expected_available = TEST_DEPTH; + ck_assert_uint_eq(pwar_ring_buffer_get_available(), expected_available); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test channel count validation +START_TEST(test_ring_buffer_channel_mismatch) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + float buffer[3 * TEST_EXPECTED_BUFFER_SIZE]; // Wrong channel count + + // Push with wrong channel count should fail + int ret = pwar_ring_buffer_push(buffer, TEST_EXPECTED_BUFFER_SIZE, 3); + ck_assert_int_eq(ret, -1); + + // Pop with wrong channel count should fail + ret = pwar_ring_buffer_pop(buffer, TEST_EXPECTED_BUFFER_SIZE, 3); + ck_assert_int_eq(ret, -1); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test NULL pointer handling +START_TEST(test_ring_buffer_null_pointers) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + float buffer[TEST_CHANNELS * TEST_EXPECTED_BUFFER_SIZE]; + + // Push with NULL buffer should fail + int ret = pwar_ring_buffer_push(NULL, TEST_EXPECTED_BUFFER_SIZE, TEST_CHANNELS); + ck_assert_int_eq(ret, -1); + + // Pop with NULL buffer should fail + ret = pwar_ring_buffer_pop(NULL, TEST_EXPECTED_BUFFER_SIZE, TEST_CHANNELS); + ck_assert_int_eq(ret, -1); + + pwar_ring_buffer_free(); + + // Operations on uninitialized buffer should fail + ret = pwar_ring_buffer_push(buffer, TEST_EXPECTED_BUFFER_SIZE, TEST_CHANNELS); + ck_assert_int_eq(ret, -1); + + ret = pwar_ring_buffer_pop(buffer, TEST_EXPECTED_BUFFER_SIZE, TEST_CHANNELS); + ck_assert_int_eq(ret, -1); +} +END_TEST + +// Test statistics reset +START_TEST(test_ring_buffer_stats_reset) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + // Cause an overrun first + float large_buffer[TEST_CHANNELS * (TEST_DEPTH + 100)]; + pwar_ring_buffer_push(large_buffer, TEST_DEPTH + 100, TEST_CHANNELS); + ck_assert_uint_gt(pwar_ring_buffer_get_overruns(), 0); + + // Check how many samples are actually available after the overrun + uint32_t available_after_overrun = pwar_ring_buffer_get_available(); + + // Now cause an underrun by popping more than what's available + uint32_t samples_to_cause_underrun = available_after_overrun + 100; + float extra_large_buffer[TEST_CHANNELS * samples_to_cause_underrun]; + pwar_ring_buffer_pop(extra_large_buffer, samples_to_cause_underrun, TEST_CHANNELS); + + ck_assert_uint_gt(pwar_ring_buffer_get_overruns(), 0); + ck_assert_uint_gt(pwar_ring_buffer_get_underruns(), 0); + + // Reset stats + pwar_ring_buffer_reset_stats(); + + ck_assert_uint_eq(pwar_ring_buffer_get_overruns(), 0); + ck_assert_uint_eq(pwar_ring_buffer_get_underruns(), 0); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test edge case: zero samples +START_TEST(test_ring_buffer_zero_samples) +{ + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + float buffer[TEST_CHANNELS]; + + // Push zero samples should succeed but do nothing + int ret = pwar_ring_buffer_push(buffer, 0, TEST_CHANNELS); + ck_assert_int_eq(ret, 1); + + uint32_t available_before = pwar_ring_buffer_get_available(); + + // Pop zero samples should succeed but do nothing + ret = pwar_ring_buffer_pop(buffer, 0, TEST_CHANNELS); + ck_assert_int_eq(ret, 0); + + ck_assert_uint_eq(pwar_ring_buffer_get_available(), available_before); + + pwar_ring_buffer_free(); +} +END_TEST + +// Test re-initialization (free and init again) +START_TEST(test_ring_buffer_reinit) +{ + // First initialization + pwar_ring_buffer_init(TEST_DEPTH, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE); + + float buffer[TEST_CHANNELS * TEST_EXPECTED_BUFFER_SIZE]; + fill_test_data(buffer, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE, 1000.0f); + pwar_ring_buffer_push(buffer, TEST_EXPECTED_BUFFER_SIZE, TEST_CHANNELS); + + // Re-initialize with different parameters + pwar_ring_buffer_init(TEST_DEPTH/2, TEST_CHANNELS, TEST_EXPECTED_BUFFER_SIZE/2); + + // Should have new prefill amount + uint32_t expected_available = TEST_DEPTH/2; + ck_assert_uint_eq(pwar_ring_buffer_get_available(), expected_available); + + // Stats should be reset + ck_assert_uint_eq(pwar_ring_buffer_get_overruns(), 0); + ck_assert_uint_eq(pwar_ring_buffer_get_underruns(), 0); + + pwar_ring_buffer_free(); +} +END_TEST + +// Critical test: Verify wrap-around behavior doesn't corrupt data +START_TEST(test_ring_buffer_wrap_around_integrity) +{ + // Use smaller buffer to force wrap-around quickly + uint32_t small_depth = 100; + uint32_t small_expected = 20; + pwar_ring_buffer_init(small_depth, TEST_CHANNELS, small_expected); + + uint32_t chunk_size = small_expected; // Use expected buffer size for this test + float input_buffer[TEST_CHANNELS * chunk_size]; + float output_buffer[TEST_CHANNELS * chunk_size]; + + // Consume prefill first + float prefill_buffer[TEST_CHANNELS * small_depth]; + pwar_ring_buffer_pop(prefill_buffer, small_depth, TEST_CHANNELS); + + // Now do multiple push/pop cycles to force wrap-around + for (int cycle = 0; cycle < 10; cycle++) { + fill_test_data(input_buffer, TEST_CHANNELS, chunk_size, cycle * 1000.0f); + + int ret = pwar_ring_buffer_push(input_buffer, chunk_size, TEST_CHANNELS); + ck_assert_int_eq(ret, 1); + + memset(output_buffer, 0xAA, sizeof(output_buffer)); + ret = pwar_ring_buffer_pop(output_buffer, chunk_size, TEST_CHANNELS); + ck_assert_int_eq(ret, chunk_size); + + verify_test_data(output_buffer, TEST_CHANNELS, chunk_size, cycle * 1000.0f, "wrap_around"); + } + + pwar_ring_buffer_free(); +} +END_TEST + +// Test suite setup +Suite *ring_buffer_suite(void) { + Suite *s = suite_create("pwar_ring_buffer"); + + TCase *tc_core = tcase_create("Core"); + tcase_add_test(tc_core, test_ring_buffer_init_free); + tcase_add_test(tc_core, test_ring_buffer_basic_push_pop); + tcase_add_test(tc_core, test_ring_buffer_channel_mismatch); + tcase_add_test(tc_core, test_ring_buffer_null_pointers); + tcase_add_test(tc_core, test_ring_buffer_zero_samples); + tcase_add_test(tc_core, test_ring_buffer_reinit); + tcase_add_test(tc_core, test_ring_buffer_stats_reset); + suite_add_tcase(s, tc_core); + + TCase *tc_underrun = tcase_create("Underrun"); + tcase_add_test(tc_underrun, test_ring_buffer_underrun); + tcase_add_test(tc_underrun, test_ring_buffer_multiple_underruns); + suite_add_tcase(s, tc_underrun); + + TCase *tc_overrun = tcase_create("Overrun"); + tcase_add_test(tc_overrun, test_ring_buffer_overrun); + tcase_add_test(tc_overrun, test_ring_buffer_gradual_overrun); + suite_add_tcase(s, tc_overrun); + + TCase *tc_edge_cases = tcase_create("EdgeCases"); + tcase_add_test(tc_edge_cases, test_ring_buffer_mixed_overrun_underrun); + tcase_add_test(tc_edge_cases, test_ring_buffer_wrap_around_integrity); + suite_add_tcase(s, tc_edge_cases); + + return s; +} + +// Main entry for running the test suite +int main(void) { + int number_failed; + Suite *s = ring_buffer_suite(); + SRunner *sr = srunner_create(s); + + // Run all tests + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (number_failed == 0) ? 0 : 1; +} diff --git a/protocol/test/pwar_router_test.c b/protocol/test/pwar_router_test.c deleted file mode 100644 index f6135d9..0000000 --- a/protocol/test/pwar_router_test.c +++ /dev/null @@ -1,132 +0,0 @@ -#include -#include -#include "../pwar_router.h" - -static void fill_samples(float *samples, uint32_t channels, uint32_t n_samples, uint32_t stride, float value) { - for (uint32_t ch = 0; ch < channels; ++ch) { - for (uint32_t s = 0; s < n_samples; ++s) { - samples[ch * stride + s] = value + ch * 1000 + s; - } - } -} - -START_TEST(test_router_send_and_process) -{ - pwar_router_t router; - const uint32_t channels = 2; - const uint32_t n_samples = 256; - const uint32_t stride = n_samples; - pwar_router_init(&router, channels); - float samples[channels * n_samples]; - fill_samples(samples, channels, n_samples, stride, 1.0f); - pwar_packet_t packets[16]; - uint32_t packets_to_send = 0; - int ret = pwar_router_send_buffer(&router, samples, n_samples, channels, stride, packets, 16, &packets_to_send); - ck_assert_int_eq(ret, 1); - ck_assert_int_gt(packets_to_send, 0); - float output[channels * n_samples]; - for (uint32_t i = 0; i < packets_to_send; ++i) { - int ready = pwar_router_process_packet(&router, &packets[i], output, n_samples, channels, stride); - if (i < packets_to_send - 1) - ck_assert_int_eq(ready, 0); - else - ck_assert_int_eq(ready, 1); - } - for (uint32_t ch = 0; ch < channels; ++ch) { - for (uint32_t s = 0; s < n_samples; ++s) { - ck_assert_float_eq_tol(output[ch * stride + s], samples[ch * stride + s], 0.0001f); - } - } -} -END_TEST - -START_TEST(test_router_out_of_order) -{ - pwar_router_t router; - const uint32_t channels = 2; - const uint32_t n_samples = 1024; - const uint32_t stride = n_samples; - pwar_router_init(&router, channels); - float samples[channels * n_samples]; - fill_samples(samples, channels, n_samples, stride, 2.0f); - pwar_packet_t packets[16]; - uint32_t packets_to_send = 0; - int ret = pwar_router_send_buffer(&router, samples, n_samples, channels, stride, packets, 16, &packets_to_send); - ck_assert_int_eq(ret, 1); - ck_assert_int_gt(packets_to_send, 0); - float output[channels * n_samples]; - // Deliver packets out of order - uint32_t mid = packets_to_send / 2; - int ready = pwar_router_process_packet(&router, &packets[mid], output, n_samples, channels, stride); - ck_assert_int_eq(ready, 0); - for (uint32_t i = 0; i < packets_to_send; ++i) { - if (i == mid) continue; - ready = pwar_router_process_packet(&router, &packets[i], output, n_samples, channels, stride); - } - ck_assert_int_eq(ready, 1); - for (uint32_t ch = 0; ch < channels; ++ch) { - for (uint32_t s = 0; s < n_samples; ++s) { - ck_assert_float_eq_tol(output[ch * stride + s], samples[ch * stride + s], 0.0001f); - } - } -} -END_TEST - -START_TEST(test_router_multiple_seq) -{ - pwar_router_t router; - const uint32_t channels = 2; - const uint32_t n_samples = 1024; - const uint32_t stride = n_samples; - pwar_router_init(&router, channels); - float samples[channels * n_samples]; - float output[channels * n_samples]; - pwar_packet_t packets[16]; - uint32_t packets_to_send = 0; - uint64_t seq = 100; - for (int round = 0; round < 3; ++round, ++seq) { - fill_samples(samples, channels, n_samples, stride, 10.0f * (round + 1)); - int ret = pwar_router_send_buffer(&router, samples, n_samples, channels, stride, packets, 16, &packets_to_send); - ck_assert_int_eq(ret, 1); - ck_assert_int_gt(packets_to_send, 0); - // Set the seq for all packets in this round - for (uint32_t i = 0; i < packets_to_send; ++i) { - packets[i].seq = seq; - } - int ready = 0; - for (uint32_t i = 0; i < packets_to_send; ++i) { - ready = pwar_router_process_packet(&router, &packets[i], output, n_samples, channels, stride); - } - ck_assert_int_eq(ready, 1); - for (uint32_t ch = 0; ch < channels; ++ch) { - for (uint32_t s = 0; s < n_samples; ++s) { - ck_assert_float_eq_tol(output[ch * stride + s], samples[ch * stride + s], 0.0001f); - } - } - } -} -END_TEST - -Suite *pwar_router_suite(void) { - Suite *s; - TCase *tc_core; - s = suite_create("pwar_router"); - tc_core = tcase_create("Core"); - tcase_add_test(tc_core, test_router_send_and_process); - tcase_add_test(tc_core, test_router_out_of_order); - tcase_add_test(tc_core, test_router_multiple_seq); - suite_add_tcase(s, tc_core); - return s; -} - -int main(void) { - int number_failed; - Suite *s; - SRunner *sr; - s = pwar_router_suite(); - sr = srunner_create(s); - srunner_run_all(sr, CK_NORMAL); - number_failed = srunner_ntests_failed(sr); - srunner_free(sr); - return (number_failed == 0) ? 0 : 1; -} \ No newline at end of file diff --git a/protocol/test/pwar_send_receive_chain_test.c b/protocol/test/pwar_send_receive_chain_test.c deleted file mode 100644 index b63fbea..0000000 --- a/protocol/test/pwar_send_receive_chain_test.c +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Integration Test: Send-Receive Chain for PWAR Protocol - * - * This test simulates the full data flow of the PWAR protocol between two endpoints (e.g., Linux and Windows). - * It demonstrates how to use the main PWAR components together: - * - * - pwar_send_buffer: Buffers outgoing audio data in blocks/chunks. - * - pwar_router: Serializes buffered data into packets for network transfer, and deserializes received packets. - * - pwar_rcv_buffer: Buffers received audio data for consumption. - * - * Data Flow (as simulated in this test): - * - * 1. Application pushes audio data to pwar_send_buffer (Linux side). - * 2. When enough data is buffered, pwar_send_buffer provides a block to pwar_router. - * 3. pwar_router serializes the block into packets (to be sent over the network). - * 4. Each packet is assigned a sequence number (seq) that uniquely identifies the buffer transaction. - * All packets for a given buffer share the same seq value. The seq is incremented for each new buffer. - * 5. Packets are (in reality) sent over the network to the other endpoint (Windows side). - * 6. On the receiving side, pwar_router uses the seq field to correctly reassemble packets into the original buffer, - * regardless of arrival order or network reordering. - * 7. Reconstructed blocks are sent back (simulating a round-trip or echo for test purposes), again with a new seq value. - * 8. The original sender receives the returned packets, processes them with pwar_router, and adds the result to pwar_rcv_buffer. - * 9. Application reads output audio data from pwar_rcv_buffer. - * - * In production: - * - Steps 1, 2, and 9 are typically in the audio callback or main processing loop. - * - Steps 3, 4, and 6 are called when sending/receiving network packets. - * - Step 8 is called when a full block is reconstructed from received packets. - * - The seq field is critical for robust UDP streaming, ensuring correct buffer assembly even with out-of-order delivery. - * - * This test can be used as a reference for integrating the send/receive chain in your own application. - * - * To run the test: - * cd protocol/test && make && ./_out/pwar_send_receive_chain_test - */ - -#include -#include -#include -#include - -#include "../pwar_send_buffer.h" -#include "../pwar_router.h" -#include "../pwar_rcv_buffer.h" - -#define CHANNELS 2 -#define CHUNK_SIZE 128 -#define WIN_BUFFER 1024 - -// Helper to set up test sine wave arrays and fill them -static void fill_test_sine_wave(float *test_samples, uint32_t channels, uint32_t n_test_samples, float frequency, float sample_rate) { - for (uint32_t ch = 0; ch < channels; ++ch) { - for (uint32_t s = 0; s < n_test_samples; ++s) { - test_samples[ch * n_test_samples + s] = sinf(2 * M_PI * frequency * s / sample_rate); - } - } -} - -START_TEST(test_send_and_rcv) -{ - const uint32_t n_test_samples = 8192; - pwar_router_t linux_router; - pwar_router_t win_router; - - pwar_router_init(&win_router, CHANNELS); - - // Generate a test sine wave, 2 channels, 8192 samples, 48 kHz sample rate, 440 Hz frequency - float test_samples[CHANNELS * n_test_samples]; - fill_test_sine_wave(test_samples, CHANNELS, n_test_samples, 440.0f, 48000.0f); - - float result_samples[CHANNELS * n_test_samples]; - - // Initialize Linux side - pwar_router_init(&linux_router, CHANNELS); - pwar_send_buffer_init(CHANNELS, WIN_BUFFER); - // The rcv buffer doesnt need any initialization, it is a singleton API - - // Initialize Windows side - pwar_router_init(&win_router, CHANNELS); - - uint32_t seq = 0; - - // Loop over the test_samples in chunks - for (uint32_t start = 0; start < n_test_samples; start += CHUNK_SIZE) { - // Linux side: - float chunk_buf[CHANNELS * CHUNK_SIZE]; - for (uint32_t ch = 0; ch < CHANNELS; ++ch) { - for (uint32_t s = 0; s < CHUNK_SIZE; ++s) { - chunk_buf[ch * CHUNK_SIZE + s] = test_samples[ch * n_test_samples + start + s]; - } - } - pwar_send_buffer_push(chunk_buf, CHUNK_SIZE, CHANNELS, CHUNK_SIZE); - - if (pwar_send_buffer_ready()) { - float linux_send_samples[CHANNELS * WIN_BUFFER]; - uint32_t n_samples = 0; - pwar_send_buffer_get(linux_send_samples, &n_samples, WIN_BUFFER); - - pwar_packet_t packets[32] = {0}; - uint32_t packets_to_send = 0; - pwar_router_send_buffer(&linux_router, linux_send_samples, n_samples, CHANNELS, WIN_BUFFER, packets, 32, &packets_to_send); - seq++; - // Set seq for all packets in this buffer - for (uint32_t i = 0; i < packets_to_send; ++i) { - packets[i].seq = seq; - } - - // NETWORK DELAY - - // (Fake sending the packets to Windows side) - // Windows side (receiving): - for (uint32_t p = 0; p < packets_to_send; ++p) { - float win_output_buffers[CHANNELS * WIN_BUFFER] = {0}; - int r = pwar_router_process_packet(&win_router, &packets[p], win_output_buffers, WIN_BUFFER, CHANNELS, WIN_BUFFER); - - if (r == 1) { - // FAKE PROCESSING.. - // Packet ready and we have output to send to Linux side - pwar_router_send_buffer(&win_router, win_output_buffers, WIN_BUFFER, CHANNELS, WIN_BUFFER, packets, 32, &packets_to_send); - // Set seq for all packets in this buffer - for (uint32_t i = 0; i < packets_to_send; ++i) { - packets[i].seq = seq; - } - } - } - - // NETWORK DELAY - for (uint32_t p = 0; p < packets_to_send; ++p) { - float linux_output_buffers[CHANNELS * WIN_BUFFER] = {0}; - int r = pwar_router_process_packet(&linux_router, &packets[p], linux_output_buffers, WIN_BUFFER, CHANNELS, WIN_BUFFER); - if (r == 1) { - pwar_rcv_buffer_add_buffer(linux_output_buffers, WIN_BUFFER, CHANNELS, WIN_BUFFER); - } - } - } - - // Linux side: Get a chunk from the receive buffer - float linux_rcv_buffers[CHANNELS * CHUNK_SIZE] = {0}; - pwar_rcv_get_chunk(linux_rcv_buffers, CHANNELS, CHUNK_SIZE); - - // Push the received chunk to the result samples - for (uint32_t ch = 0; ch < CHANNELS; ++ch) { - for (uint32_t s = 0; s < CHUNK_SIZE; ++s) { - result_samples[ch * n_test_samples + start + s] = linux_rcv_buffers[ch * CHUNK_SIZE + s]; - } - } - } - - // The system delay is WIN_BUFFER (1024), but since we write each received chunk at 'start', - // the first real output chunk (containing delayed input) is written at start = WIN_BUFFER - CHUNK_SIZE (896). - // This is because the receive buffer outputs zeros until the delay is met, and then the first chunk of real data - // is written at the index corresponding to the chunk that caused the buffer to fill up. - // So, to compare input and output, we must use result_samples[0][s + WIN_BUFFER - CHUNK_SIZE] vs test_samples[0][s]. - // This is a common artifact in block-based streaming systems. - - uint32_t max_testable = n_test_samples - (WIN_BUFFER - CHUNK_SIZE); - for (uint32_t s = 0; s < max_testable; ++s) { - ck_assert_float_eq_tol( - result_samples[0 * n_test_samples + s + WIN_BUFFER - CHUNK_SIZE], - test_samples[0 * n_test_samples + s], - 0.0001f - ); - } -} -END_TEST - - -Suite *send_receive_chain_suite(void) { - Suite *s = suite_create("pwar_send_receive_chain"); - TCase *tc_core = tcase_create("Core"); - tcase_add_test(tc_core, test_send_and_rcv); - suite_add_tcase(s, tc_core); - return s; -} - -int main(void) { - int number_failed; - Suite *s = send_receive_chain_suite(); - SRunner *sr = srunner_create(s); - srunner_run_all(sr, CK_NORMAL); - number_failed = srunner_ntests_failed(sr); - srunner_free(sr); - return (number_failed == 0) ? 0 : 1; -} diff --git a/windows/asio/CMakeLists.txt b/windows/asio/CMakeLists.txt index 3cf493e..8a476bc 100644 --- a/windows/asio/CMakeLists.txt +++ b/windows/asio/CMakeLists.txt @@ -10,7 +10,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(PWARASIO_SOURCES pwarASIO.cpp pwarASIOLog.cpp - ../../../protocol/pwar_router.c ../../../protocol/latency_manager.c ../../../third_party/asiosdk/common/combase.cpp ../../../third_party/asiosdk/common/dllentry.cpp diff --git a/windows/asio/pwarASIO.cpp b/windows/asio/pwarASIO.cpp index 892ab4c..0ff6488 100644 --- a/windows/asio/pwarASIO.cpp +++ b/windows/asio/pwarASIO.cpp @@ -21,7 +21,6 @@ #include "pwarASIOLog.h" #include "../../protocol/pwar_packet.h" -#include "../../protocol/pwar_router.h" #include "../../protocol/latency_manager.h" #include @@ -371,8 +370,7 @@ ASIOError pwarASIO::createBuffers(ASIOBufferInfo* bufferInfos, long numChannels, return ASE_NoMemory; } this->callbacks = callbacks; - // Initialize the router with the number of output channels - pwar_router_init(&router, PWAR_MAX_CHANNELS); + input_buffers = new float[PWAR_MAX_CHANNELS * blockFrames]; output_buffers = new float[PWAR_MAX_CHANNELS * blockFrames]; @@ -489,148 +487,149 @@ ASIOError pwarASIO::outputReady() { return ASE_NotPresent; } -void pwarASIO::udp_packet_listener() { +void pwarASIO::udp_iocp_listener() { WSADATA wsaData; - SOCKET sockfd; - sockaddr_in servaddr{}, cliaddr{}; - int n; - socklen_t len; - char buffer[2048]; - - // --- Raise thread priority and register with MMCSS --- - SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); - DWORD mmcssTaskIndex = 0; - HANDLE mmcssHandle = AvSetMmThreadCharacteristicsA("Pro Audio", &mmcssTaskIndex); - // Debug: verify thread priority and MMCSS registration - int prio = GetThreadPriority(GetCurrentThread()); - if (prio != THREAD_PRIORITY_TIME_CRITICAL) { - pwarASIOLog::Send("Warning: Thread priority not set to TIME_CRITICAL!"); - } else { - pwarASIOLog::Send("Thread priority set to TIME_CRITICAL."); - } - if (!mmcssHandle) { - pwarASIOLog::Send("Warning: MMCSS registration failed!"); - } else { - pwarASIOLog::Send("MMCSS registration succeeded."); + SOCKET sockfd = INVALID_SOCKET; + sockaddr_in servaddr{}; + + // Set thread priority to high for better real-time performance + if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST)) { + pwarASIOLog::Send("Warning: Failed to set high thread priority"); } - // ----------------------------------------------------- if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) { - if (mmcssHandle) AvRevertMmThreadCharacteristics(mmcssHandle); + pwarASIOLog::Send("WSAStartup failed in UDP listener"); return; } - sockfd = socket(AF_INET, SOCK_DGRAM, 0); + + sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd == INVALID_SOCKET) { + pwarASIOLog::Send("recv socket creation failed"); WSACleanup(); - if (mmcssHandle) AvRevertMmThreadCharacteristics(mmcssHandle); return; } - // Set SO_RCVBUF to minimal size for low latency - int rcvbuf = 1024; // 1 KB - setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (const char*)&rcvbuf, sizeof(rcvbuf)); - // Disable UDP connection reset behavior - DWORD bytesReturned = 0; - BOOL bNewBehavior = FALSE; - WSAIoctl(sockfd, SIO_UDP_CONNRESET, &bNewBehavior, sizeof(bNewBehavior), NULL, 0, &bytesReturned, NULL, NULL); + + // Disable ICMP port unreachable messages + BOOL new_behavior = FALSE; + DWORD bytes_returned = 0; + WSAIoctl(sockfd, SIO_UDP_CONNRESET, &new_behavior, sizeof(new_behavior), + NULL, 0, &bytes_returned, NULL, NULL); + + // Set socket options for better performance + int rcvbuf = 1024 * 1024; + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (char*)&rcvbuf, sizeof(rcvbuf)) == SOCKET_ERROR) { + pwarASIOLog::Send("Warning: Failed to set receive buffer size"); + } + + // Set socket timeout to allow periodic checking of running flag + DWORD timeout = 100; // 100ms timeout + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)) == SOCKET_ERROR) { + pwarASIOLog::Send("Warning: Failed to set socket timeout"); + } + + // Bind socket memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = INADDR_ANY; servaddr.sin_port = htons(8321); + if (bind(sockfd, reinterpret_cast(&servaddr), sizeof(servaddr)) == SOCKET_ERROR) { + pwarASIOLog::Send("recv socket bind failed"); closesocket(sockfd); WSACleanup(); return; } - udpListenerRunning = true; - pwar_packet_t output_packets[32]; - uint32_t packets_to_send = 0; - while (udpListenerRunning) { - len = sizeof(cliaddr); - WSABUF wsaBuf; - wsaBuf.buf = buffer; - wsaBuf.len = sizeof(buffer); - DWORD bytesReceived = 0; - DWORD flags = 0; - int res = WSARecvFrom(sockfd, &wsaBuf, 1, &bytesReceived, &flags, reinterpret_cast(&cliaddr), &len, NULL, NULL); - if (res == 0 && bytesReceived >= sizeof(pwar_packet_t)) { - pwar_packet_t pkt; - memcpy(&pkt, buffer, sizeof(pwar_packet_t)); - - uint32_t chunk_size = pkt.n_samples; - pkt.num_packets = blockFrames / chunk_size; - latency_manager_process_packet_client(&pkt); - - int samples_ready = pwar_router_process_streaming_packet(&router, &pkt, input_buffers, blockFrames, PWAR_MAX_CHANNELS); - - if (started && (samples_ready > 0)) { - uint32_t seq = pkt.seq; - latency_manager_start_audio_cbk_begin(); - - // Do the ASIO things.. input in input_buffers - size_t to_copy = blockFrames; + pwarASIOLog::Send("UDP listener started on port 8321"); + udpListenerRunning = true; + + pwar_packet_t packet; + pwar_packet_t response_packet; + uint64_t packets_processed = 0; - for (long i = 0; i < activeInputs; ++i) { + while (udpListenerRunning) { + int n = recvfrom(sockfd, (char*)&packet, sizeof(packet), 0, NULL, NULL); + + if (n == sizeof(packet)) { + // Set Windows receive timestamp + packet.t2_windows_recv = latency_manager_timestamp_now(); + + if (started && callbacks) { + // Copy audio data from packet to ASIO input buffers + for (long i = 0; i < activeInputs && i < PWAR_CHANNELS; ++i) { float* dest = inputBuffers[i] + (toggle ? blockFrames : 0); - - // Copy the first input channel.. - memcpy(dest, input_buffers, to_copy * sizeof(float)); - - // Zero out the rest - for (size_t j = to_copy; j < blockFrames; ++j) + // Copy samples from packet (interleaved format: L, R, L, R, ...) + for (int j = 0; j < blockFrames && j < packet.n_samples; ++j) { + dest[j] = packet.samples[j * PWAR_CHANNELS + i]; + } + // Fill remaining with zeros if packet has fewer samples + for (int j = packet.n_samples; j < blockFrames; ++j) { dest[j] = 0.0f; + } } + samplePosition += blockFrames; + // Call ASIO buffer switch if (timeInfoMode) { bufferSwitchX(); } else { callbacks->bufferSwitch(toggle, ASIOFalse); } - latency_manager_start_audio_cbk_end(); - - float* outputSamplesCh1 = outputBuffers[0] + (toggle ? blockFrames : 0); - float* outputSamplesCh2 = outputBuffers[1] + (toggle ? blockFrames : 0); - - memcpy(output_buffers, outputSamplesCh1, blockFrames * sizeof(float)); - memcpy(output_buffers + blockFrames, outputSamplesCh2, blockFrames * sizeof(float)); - - // Send the result - pwar_router_send_buffer(&router, chunk_size, output_buffers, samples_ready, PWAR_MAX_CHANNELS, output_packets, 32, &packets_to_send); - - uint64_t timestamp = latency_manager_timestamp_now(); - for (uint32_t i = 0; i < packets_to_send; ++i) { - output_packets[i].seq = seq; - output_packets[i].timestamp = timestamp; - output(output_packets[i]); + // Copy ASIO output buffers to response packet + response_packet = packet; // Copy structure + response_packet.t3_windows_send = latency_manager_timestamp_now(); + + // Copy output samples to interleaved format (L, R, L, R, ...) + float* outputCh1 = outputBuffers[0] + (toggle ? blockFrames : 0); + float* outputCh2 = (activeOutputs > 1) ? outputBuffers[1] + (toggle ? blockFrames : 0) : outputCh1; + + for (int j = 0; j < blockFrames && j < response_packet.n_samples; ++j) { + response_packet.samples[j * PWAR_CHANNELS + 0] = outputCh1[j]; + response_packet.samples[j * PWAR_CHANNELS + 1] = outputCh2[j]; } - toggle = toggle ? 0 : 1; - - pwar_latency_info_t latency_info; - if (latency_manager_time_for_sending_latency_info(&latency_info)) { - // Send the latency info over the socket - if (udpSendSocket != INVALID_SOCKET) { - WSABUF buffer; - buffer.buf = reinterpret_cast(&latency_info); - buffer.len = sizeof(latency_info); - DWORD bytesSent = 0; - int flags = 0; - WSASendTo(udpSendSocket, &buffer, 1, &bytesSent, flags, - reinterpret_cast(&udpSendAddr), sizeof(udpSendAddr), NULL, NULL); + + // Send response packet back to server + int sent = sendto(udpSendSocket, (char*)&response_packet, sizeof(response_packet), 0, + (struct sockaddr *)&udpSendAddr, sizeof(udpSendAddr)); + if (sent == SOCKET_ERROR) { + int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK) { + char errMsg[256]; + sprintf(errMsg, "sendto failed: %d", error); + pwarASIOLog::Send(errMsg); } } + + toggle = toggle ? 0 : 1; + } + + packets_processed++; + } else if (n == SOCKET_ERROR) { + int error = WSAGetLastError(); + // Check if it's a timeout (expected) vs a real error + if (error == WSAETIMEDOUT || error == WSAEWOULDBLOCK) { + // Timeout is expected - just continue and check udpListenerRunning + continue; + } else if (udpListenerRunning) { + // Only print error if we're not shutting down + char errMsg[256]; + sprintf(errMsg, "recvfrom error: %d", error); + pwarASIOLog::Send(errMsg); } } } + closesocket(sockfd); WSACleanup(); + pwarASIOLog::Send("UDP listener stopped"); } void pwarASIO::startUdpListener() { if (!udpListenerRunning) { udpListenerRunning = true; - udpListenerThread = std::thread(&pwarASIO::udp_packet_listener, this); + udpListenerThread = std::thread(&pwarASIO::udp_iocp_listener, this); } } diff --git a/windows/asio/pwarASIO.h b/windows/asio/pwarASIO.h index 496d222..2f35e82 100644 --- a/windows/asio/pwarASIO.h +++ b/windows/asio/pwarASIO.h @@ -12,7 +12,6 @@ #include #include #include "../../protocol/pwar_packet.h" -#include "../../protocol/pwar_router.h" #include "rpc.h" #include "rpcndr.h" @@ -23,11 +22,11 @@ #include "combase.h" #include "iasiodrv.h" -constexpr int kMinBlockFrames = 64; +constexpr int kMinBlockFrames = 32; constexpr int kMaxBlockFrames = 2048; constexpr int kDefaultBlockFrames = 128; -constexpr int kBlockFramesGranularity = 64; -constexpr int kNumInputs = 1; +constexpr int kBlockFramesGranularity = 32; +constexpr int kNumInputs = 2; constexpr int kNumOutputs = 2; class pwarASIO : public IASIO, public CUnknown { @@ -68,10 +67,9 @@ class pwarASIO : public IASIO, public CUnknown { bool isValidBufferSize(long bufferSize) const; private: - pwar_router_t router; void output(const pwar_packet_t& packet); void bufferSwitchX(); - void udp_packet_listener(); + void udp_iocp_listener(); void startUdpListener(); void stopUdpListener(); void initUdpSender(); diff --git a/windows/simulator/CMakeLists.txt b/windows/simulator/CMakeLists.txt new file mode 100644 index 0000000..11fc459 --- /dev/null +++ b/windows/simulator/CMakeLists.txt @@ -0,0 +1,31 @@ +# CMakeLists.txt for PWAR Windows Client Simulator +cmake_minimum_required(VERSION 3.16) + +# Create the simulator executable +add_executable(pwar_client_simulator + client_simulator.cpp + ${CMAKE_SOURCE_DIR}/protocol/latency_manager.c +) + +# Include directories +target_include_directories(pwar_client_simulator PRIVATE + ${CMAKE_SOURCE_DIR}/protocol + ${CMAKE_SOURCE_DIR}/windows/asio +) + +# Link libraries +target_link_libraries(pwar_client_simulator + ws2_32 + avrt +) + +# Set properties +set_target_properties(pwar_client_simulator PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON +) + +# Add compile definitions +target_compile_definitions(pwar_client_simulator PRIVATE + _WIN32_WINNT=0x0601 +) diff --git a/windows/simulator/README.md b/windows/simulator/README.md new file mode 100644 index 0000000..b260bc9 --- /dev/null +++ b/windows/simulator/README.md @@ -0,0 +1,130 @@ +# PWAR Windows Client Simulator + +A standalone Windows application that replicates the PWAR ASIO driver's network mechanics for testing purposes. + +## Overview + +This simulator provides the same UDP networking, IOCP-based processing, and PWAR protocol handling as the actual `pwarASIO.cpp` driver, but as a standalone executable that doesn't require ASIO registration or a DAW. + +## Features + +- **Identical Network Stack**: Uses the same IOCP-based UDP listener as the ASIO driver +- **Real-time Priority**: Sets thread priority to TIME_CRITICAL and registers with MMCSS +- **PWAR Protocol**: Full support for PWAR packet processing and latency management +- **Configuration**: Supports config files (same format as ASIO driver) and command-line options +- **Statistics**: Shows packet processing rates and timing information + +## Building + +The simulator is built automatically as part of the Windows build: + +```cmd +cd build +cmake --build . --target pwar_client_simulator +``` + +The executable will be created at: `build/windows/simulator/pwar_client_simulator.exe` + +## Usage + +### Basic Usage +```cmd +# Run with defaults (connects to 192.168.66.2:8321) +pwar_client_simulator.exe + +# Connect to localhost +pwar_client_simulator.exe -s 127.0.0.1 + +# Use custom buffer size and enable verbose output +pwar_client_simulator.exe -b 256 -v +``` + +### Command Line Options + +| Option | Description | Default | +|--------|-------------|---------| +| `-s, --server ` | Server IP address | `192.168.66.2` | +| `-p, --port ` | Server port | `8321` | +| `-c, --client-port ` | Client listening port | `8321` | +| `-b, --buffer ` | Buffer size in samples | `512` | +| `-n, --channels ` | Number of channels | `2` | +| `-r, --rate ` | Sample rate | `48000` | +| `-f, --config ` | Config file path | `%USERPROFILE%\pwarASIO.cfg` | +| `-v, --verbose` | Enable verbose output | `false` | +| `-h, --help` | Show help message | - | + +### Configuration File + +The simulator reads the same config file as the ASIO driver: +- Default location: `%USERPROFILE%\pwarASIO.cfg` +- Format: `key=value` pairs + +Example config file: +``` +udp_send_ip=192.168.1.100 +``` + +## How It Works + +1. **Initialization**: Sets up UDP sockets, PWAR router, and audio buffers +2. **IOCP Listener**: High-performance async UDP packet reception (same as ASIO driver) +3. **Packet Processing**: Processes incoming PWAR packets using the same router logic +4. **Audio Simulation**: Copies input to output (simulates audio processing) +5. **Response**: Sends processed audio back to server with proper sequencing and timing + +## Use Cases + +- **Protocol Testing**: Test PWAR protocol changes without a DAW +- **Performance Testing**: Measure latency and throughput +- **Development**: Quick iteration during PWAR development +- **Debugging**: Isolate network issues from ASIO driver complexity +- **CI/CD**: Automated testing in build pipelines + +## Differences from ASIO Driver + +- No ASIO callbacks (simulated with simple audio processing) +- No COM/DirectShow registration required +- Standalone executable (no DLL) +- Command-line interface instead of DAW integration +- Additional statistics and verbose output + +## Example Session + +```cmd +> pwar_client_simulator.exe -v -s 127.0.0.1 +PWAR Windows Client Simulator - Standalone testing tool +Replicates ASIO driver network mechanics for testing + +PWAR Windows Client Simulator Configuration: + Server: 127.0.0.1:8321 + Client port: 8321 + Channels: 2 + Buffer size: 512 samples + Sample rate: 48000 Hz + Verbose: enabled + +UDP sender initialized, target: 127.0.0.1:8321 +UDP receiver bound to port 8321 +IOCP Thread priority set to TIME_CRITICAL. +IOCP MMCSS registration succeeded. +Started IOCP listener thread, waiting for packets... +Simulator started successfully. Press Ctrl+C to stop. +Waiting for audio packets from PWAR server... + +Processed 1000 packets in 1 seconds (sent: 1000) +Processed 2000 packets in 2 seconds (sent: 2000) +^C +Received signal 2, shutting down... + +Shutting down... +IOCP listener thread stopped + +Final Statistics: + Runtime: 2 seconds + Packets processed: 2048 + Packets sent: 2048 + Average rate: 1024.00 packets/sec + Sample position: 1048576 samples (21.85 seconds) + +Shutdown complete +``` diff --git a/windows/simulator/client_simulator.cpp b/windows/simulator/client_simulator.cpp new file mode 100644 index 0000000..aaa1788 --- /dev/null +++ b/windows/simulator/client_simulator.cpp @@ -0,0 +1,341 @@ +/* + * client_simulator.cpp - PWAR Client Simulator (Windows) + * + * Simulates a PWAR client (like Windows ASIO driver) for testing + * Receives audio from PWAR server, processes it, and sends it back + * + * (c) 2025 Philip K. Gisslow + * This file is part of the PipeWire ASIO Relay (PWAR) project. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "../../protocol/pwar_packet.h" +#include "../../protocol/latency_manager.h" +} + +#pragma comment(lib, "ws2_32.lib") + +#ifndef SIO_UDP_CONNRESET +#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR,12) +#endif + +// Default configuration (matching your existing setup) +#define DEFAULT_SERVER_IP "196.168.66.2" +#define DEFAULT_SERVER_PORT 8321 +#define DEFAULT_CLIENT_PORT 8321 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_PACKET_SIZE 512 + +// Configuration structure +typedef struct { + char server_ip[64]; + int server_port; + int client_port; + int channels; + int packet_size; + int verbose; +} client_config_t; + +// Global state +static SOCKET recv_sockfd = INVALID_SOCKET; +static SOCKET send_sockfd = INVALID_SOCKET; +static struct sockaddr_in servaddr; +static volatile int keep_running = 1; +static client_config_t config; + +static void print_usage(const char *program_name) { + printf("PWAR Client Simulator - Simulates a PWAR client for testing\n\n"); + printf("Usage: %s [options]\n", program_name); + printf("Options:\n"); + printf(" -s, --server Server IP address (default: %s)\n", DEFAULT_SERVER_IP); + printf(" --server-port Server port (default: %d)\n", DEFAULT_SERVER_PORT); + printf(" -c, --client-port Client listening port (default: %d)\n", DEFAULT_CLIENT_PORT); + printf(" -p, --packet-size Packet size in samples (default: %d)\n", DEFAULT_PACKET_SIZE); + printf(" -n, --channels Number of channels (default: %d)\n", DEFAULT_CHANNELS); + printf(" -v, --verbose Enable verbose output\n"); + printf(" -h, --help Show this help message\n"); + printf("\nExamples:\n"); + printf(" %s # Connect to localhost with defaults\n", program_name); + printf(" %s -s 192.168.1.100 --server-port 9000 # Connect to remote server\n", program_name); + printf(" %s -v -p 256 -c 1 # Verbose mode, smaller packets, mono\n", program_name); + printf("\nDescription:\n"); + printf(" This simulator acts like a PWAR client (e.g., Windows ASIO driver).\n"); + printf(" It receives audio packets from a PWAR server, processes them,\n"); + printf(" and sends them back, creating a loopback test environment.\n"); +} + +static int parse_arguments(int argc, char *argv[], client_config_t *cfg) { + // Set defaults + strcpy_s(cfg->server_ip, sizeof(cfg->server_ip), DEFAULT_SERVER_IP); + cfg->server_port = DEFAULT_SERVER_PORT; + cfg->client_port = DEFAULT_CLIENT_PORT; + cfg->channels = DEFAULT_CHANNELS; + cfg->packet_size = DEFAULT_PACKET_SIZE; + cfg->verbose = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return -1; + } else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--server") == 0) && i + 1 < argc) { + strcpy_s(cfg->server_ip, sizeof(cfg->server_ip), argv[++i]); + } else if (strcmp(argv[i], "--server-port") == 0 && i + 1 < argc) { + cfg->server_port = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--client-port") == 0) && i + 1 < argc) { + cfg->client_port = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--packet-size") == 0) && i + 1 < argc) { + cfg->packet_size = atoi(argv[++i]); + } else if ((strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--channels") == 0) && i + 1 < argc) { + cfg->channels = atoi(argv[++i]); + } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { + cfg->verbose = 1; + } else { + fprintf(stderr, "Unknown argument: %s\n", argv[i]); + print_usage(argv[0]); + return -1; + } + } + + // Validate configuration + if (cfg->server_port <= 0 || cfg->server_port > 65535) { + fprintf(stderr, "Invalid server port: %d\n", cfg->server_port); + return -1; + } + if (cfg->client_port <= 0 || cfg->client_port > 65535) { + fprintf(stderr, "Invalid client port: %d\n", cfg->client_port); + return -1; + } + if (cfg->channels < 1 || cfg->channels > 8) { + fprintf(stderr, "Invalid channel count: %d (must be 1-8)\n", cfg->channels); + return -1; + } + if (cfg->packet_size < 32 || cfg->packet_size > 4096) { + fprintf(stderr, "Invalid packet size: %d (must be 32-4096)\n", cfg->packet_size); + return -1; + } + + return 0; +} + +static void setup_recv_socket(int port) { + recv_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (recv_sockfd == INVALID_SOCKET) { + fprintf(stderr, "recv socket creation failed: %d\n", WSAGetLastError()); + exit(EXIT_FAILURE); + } + + // Disable ICMP port unreachable messages + BOOL new_behavior = FALSE; + DWORD bytes_returned = 0; + WSAIoctl(recv_sockfd, SIO_UDP_CONNRESET, &new_behavior, sizeof(new_behavior), + NULL, 0, &bytes_returned, NULL, NULL); + + // Set socket options for better performance + int rcvbuf = 1024 * 1024; + if (setsockopt(recv_sockfd, SOL_SOCKET, SO_RCVBUF, (char*)&rcvbuf, sizeof(rcvbuf)) == SOCKET_ERROR) { + fprintf(stderr, "Warning: Failed to set receive buffer size: %d\n", WSAGetLastError()); + } + + // Set socket timeout to allow periodic checking of keep_running flag + DWORD timeout = 100; // 100ms timeout + if (setsockopt(recv_sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)) == SOCKET_ERROR) { + fprintf(stderr, "Warning: Failed to set socket timeout: %d\n", WSAGetLastError()); + } + + struct sockaddr_in recv_addr; + memset(&recv_addr, 0, sizeof(recv_addr)); + recv_addr.sin_family = AF_INET; + recv_addr.sin_addr.s_addr = INADDR_ANY; + recv_addr.sin_port = htons((u_short)port); + + if (bind(recv_sockfd, (struct sockaddr *)&recv_addr, sizeof(recv_addr)) == SOCKET_ERROR) { + fprintf(stderr, "recv socket bind failed: %d\n", WSAGetLastError()); + exit(EXIT_FAILURE); + } + + if (config.verbose) { + printf("[Client Simulator] Listening on port %d\n", port); + } +} + +static void setup_send_socket(const char *server_ip, int server_port) { + send_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (send_sockfd == INVALID_SOCKET) { + fprintf(stderr, "send socket creation failed: %d\n", WSAGetLastError()); + exit(EXIT_FAILURE); + } + + // Disable ICMP port unreachable messages + BOOL new_behavior = FALSE; + DWORD bytes_returned = 0; + WSAIoctl(send_sockfd, SIO_UDP_CONNRESET, &new_behavior, sizeof(new_behavior), + NULL, 0, &bytes_returned, NULL, NULL); + + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons((u_short)server_port); + + if (inet_pton(AF_INET, server_ip, &servaddr.sin_addr) <= 0) { + fprintf(stderr, "Invalid server IP address: %s\n", server_ip); + exit(EXIT_FAILURE); + } + + if (config.verbose) { + printf("[Client Simulator] Sending to %s:%d\n", server_ip, server_port); + } +} + +static DWORD WINAPI receiver_thread(LPVOID userdata) { + // Set thread priority to high for better real-time performance + if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST)) { + fprintf(stderr, "Warning: Failed to set high thread priority: %lu\n", GetLastError()); + } + + pwar_packet_t packet; + pwar_packet_t response_packet; + uint64_t packets_processed = 0; + + printf("[Client Simulator] Receiver thread started\n"); + + while (keep_running) { + int n = recvfrom(recv_sockfd, (char*)&packet, sizeof(packet), 0, NULL, NULL); + + if (n == sizeof(packet)) { + // Set Windows receive timestamp + packet.t2_windows_recv = latency_manager_timestamp_now(); + + // Copy input to output 1:1 (no audio processing) + response_packet = packet; // Copy the whole structure including samples + + // Set Windows send timestamp + response_packet.t3_windows_send = latency_manager_timestamp_now(); + + // Send response packet back to server + int sent = sendto(send_sockfd, (char*)&response_packet, sizeof(response_packet), 0, + (struct sockaddr *)&servaddr, sizeof(servaddr)); + if (sent == SOCKET_ERROR) { + int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK) { + fprintf(stderr, "sendto failed: %d\n", error); + } + } + + packets_processed++; + if (config.verbose && packets_processed % 1000 == 0) { + printf("[Client Simulator] Processed %llu packets\n", + (unsigned long long)packets_processed); + } + } else if (n == SOCKET_ERROR) { + int error = WSAGetLastError(); + // Check if it's a timeout (expected) vs a real error + if (error == WSAETIMEDOUT || error == WSAEWOULDBLOCK) { + // Timeout is expected - just continue and check keep_running + continue; + } else if (keep_running) { + // Only print error if we're not shutting down + fprintf(stderr, "recvfrom error: %d\n", error); + } + } + // If n == 0 or some other size, just continue + } + + printf("[Client Simulator] Receiver thread stopped\n"); + return 0; +} + +static BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) { + switch (ctrl_type) { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_CLOSE_EVENT: + printf("\n[Client Simulator] Received shutdown signal, shutting down...\n"); + keep_running = 0; + return TRUE; + default: + return FALSE; + } +} + +int main(int argc, char *argv[]) { + printf("PWAR Client Simulator - Testing tool for PWAR protocol (Windows)\n"); + printf("Simulates a PWAR client (like Windows ASIO driver)\n\n"); + + // Initialize Winsock + WSADATA wsa_data; + int wsa_result = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (wsa_result != 0) { + fprintf(stderr, "WSAStartup failed: %d\n", wsa_result); + return 1; + } + + // Parse command line arguments + if (parse_arguments(argc, argv, &config) < 0) { + WSACleanup(); + return 1; + } + + // Print configuration + printf("Configuration:\n"); + printf(" Server: %s:%d\n", config.server_ip, config.server_port); + printf(" Client port: %d\n", config.client_port); + printf(" Channels: %d\n", config.channels); + printf(" Packet size: %d samples\n", config.packet_size); + printf(" Verbose: %s\n", config.verbose ? "enabled" : "disabled"); + printf("\n"); + + // Set up console control handler for graceful shutdown + if (!SetConsoleCtrlHandler(console_ctrl_handler, TRUE)) { + fprintf(stderr, "Warning: Failed to set console control handler\n"); + } + + // Set up networking + setup_recv_socket(config.client_port); + setup_send_socket(config.server_ip, config.server_port); + + // Start receiver thread + HANDLE recv_thread = CreateThread(NULL, 0, receiver_thread, NULL, 0, NULL); + if (recv_thread == NULL) { + fprintf(stderr, "Failed to create receiver thread: %lu\n", GetLastError()); + closesocket(recv_sockfd); + closesocket(send_sockfd); + WSACleanup(); + return 1; + } + + printf("[Client Simulator] Started successfully. Press Ctrl+C to stop.\n"); + printf("[Client Simulator] Waiting for audio packets from PWAR server...\n"); + + // Main loop + while (keep_running) { + Sleep(100); // 100ms + } + + // Cleanup + printf("[Client Simulator] Shutting down...\n"); + keep_running = 0; + WaitForSingleObject(recv_thread, 5000); // Wait up to 5 seconds for thread to finish + CloseHandle(recv_thread); + + if (recv_sockfd != INVALID_SOCKET) { + closesocket(recv_sockfd); + } + if (send_sockfd != INVALID_SOCKET) { + closesocket(send_sockfd); + } + + WSACleanup(); + + printf("[Client Simulator] Shutdown complete\n"); + return 0; +} \ No newline at end of file diff --git a/windows/torture/CMakeLists.txt b/windows/torture/CMakeLists.txt deleted file mode 100644 index 688d844..0000000 --- a/windows/torture/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -# CMakeLists.txt for torture test -add_executable(pwar_torture - torture_main.cpp -) -target_include_directories(pwar_torture PRIVATE - ${CMAKE_SOURCE_DIR}/protocol -) diff --git a/windows/torture/torture_main.cpp b/windows/torture/torture_main.cpp deleted file mode 100644 index 4e37046..0000000 --- a/windows/torture/torture_main.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// torture_main.cpp -// Simple torture test for socket thread logic -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../../protocol/pwar_packet.h" - -#pragma comment(lib, "ws2_32.lib") -#pragma comment(lib, "avrt.lib") -#pragma comment(lib, "winmm.lib") - -std::atomic running{true}; - -void udp_receive_thread() { - // Raise thread priority and register with MMCSS - // SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); - // DWORD mmcssTaskIndex = 0; - // HANDLE mmcssHandle = AvSetMmThreadCharacteristicsA("Pro Audio", &mmcssTaskIndex); - // int prio = GetThreadPriority(GetCurrentThread()); - // if (prio != THREAD_PRIORITY_TIME_CRITICAL) { - // std::cerr << "Warning: Thread priority not set to TIME_CRITICAL!\n"; - // } else { - // std::cout << "Thread priority set to TIME_CRITICAL.\n"; - // } - // if (!mmcssHandle) { - // std::cerr << "Warning: MMCSS registration failed!\n"; - // } else { - // std::cout << "MMCSS registration succeeded.\n"; - // } - - WSADATA wsaData; - SOCKET sockfd; - sockaddr_in servaddr{}, cliaddr{}; - int n; - socklen_t len; - char buffer[2048]; - - if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) { - std::cerr << "WSAStartup failed!\n"; - return; - } - sockfd = socket(AF_INET, SOCK_DGRAM, 0); - if (sockfd == INVALID_SOCKET) { - std::cerr << "Socket creation failed!\n"; - WSACleanup(); - return; - } - int rcvbuf = 1024; - setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (const char*)&rcvbuf, sizeof(rcvbuf)); - memset(&servaddr, 0, sizeof(servaddr)); - servaddr.sin_family = AF_INET; - servaddr.sin_addr.s_addr = INADDR_ANY; - servaddr.sin_port = htons(8321); - if (bind(sockfd, reinterpret_cast(&servaddr), sizeof(servaddr)) == SOCKET_ERROR) { - std::cerr << "Bind failed!\n"; - closesocket(sockfd); - WSACleanup(); - return; - } - std::cout << "Listening for UDP packets on port 8321...\n"; - - // Setup socket for sending responses - SOCKET send_sock = socket(AF_INET, SOCK_DGRAM, 0); - sockaddr_in dest_addr{}; - dest_addr.sin_family = AF_INET; - dest_addr.sin_port = htons(8321); - inet_pton(AF_INET, "192.168.66.2", &dest_addr.sin_addr); - - while (running) { - len = sizeof(cliaddr); - int bytesReceived = recvfrom(sockfd, buffer, sizeof(buffer), 0, reinterpret_cast(&cliaddr), &len); - if (bytesReceived >= (int)sizeof(pwar_packet_t)) { - pwar_packet_t pkt; - memcpy(&pkt, buffer, sizeof(pwar_packet_t)); - // Respond with the same seq - pwar_packet_t resp = pkt; - sendto(send_sock, reinterpret_cast(&resp), sizeof(resp), 0, - reinterpret_cast(&dest_addr), sizeof(dest_addr)); - } - } - closesocket(sockfd); - closesocket(send_sock); - WSACleanup(); - // if (mmcssHandle) AvRevertMmThreadCharacteristics(mmcssHandle); -} - -int main() { - // Set main thread to highest priority and MMCSS - // SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); - // DWORD mmcssTaskIndex = 0; - // HANDLE mmcssHandle = AvSetMmThreadCharacteristicsA("Pro Audio", &mmcssTaskIndex); - // int prio = GetThreadPriority(GetCurrentThread()); - // if (prio != THREAD_PRIORITY_TIME_CRITICAL) { - // std::cerr << "[Main] Warning: Thread priority not set to TIME_CRITICAL!\n"; - // } else { - // std::cout << "[Main] Thread priority set to TIME_CRITICAL.\n"; - // } - // if (!mmcssHandle) { - // std::cerr << "[Main] Warning: MMCSS registration failed!\n"; - // } else { - // std::cout << "[Main] MMCSS registration succeeded.\n"; - // } - - std::thread t(udp_receive_thread); - - std::cout << "Press Enter to quit...\n"; - std::cin.get(); - running = false; - t.join(); - timeEndPeriod(1); // Restore timer resolution - // if (mmcssHandle) AvRevertMmThreadCharacteristics(mmcssHandle); - return 0; -}