Trace of the Windows Port Session
===================================

This is a trace of how the coding agent helped port sb-send.c
from Linux-only to cross-platform (Linux + Windows).

Session start: user asked to port sb-send to Windows.

Phase 1: Exploration and Planning
---------------------------------

Agent explored the project structure. Found:
  - sb-send.c: 1326 lines, single-file C program
  - Makefile: gcc on Linux, ALSA optional
  - Dependencies: OpenSSL (MD5), opus, pthreads, ALSA (optional)
  - Protocol: hand-rolled AOO/SonoBus protocol over UDP/TCP

Found pre-built opus libraries:
  - /home/ground/tmp/opus-winx64/lib/libopus.a (pe-x86-64)
  - /home/ground/tmp/opus-winarm64/lib/libopus.a (pe-arm-wince, WRONG)
  - /home/ground/tmp/opus-1.5.2 (source, could build from source)

Found cross-compiler:
  - llvm-mingw at /home/ground/tmp/toolchains/
  - x86_64-w64-mingw32-clang and aarch64-w64-mingw32-clang

Built opus from source for Windows aarch64:
  - cmake with -DCMAKE_SYSTEM_NAME=Windows
  - Verified output: COFF-ARM64, IMAGE_FILE_MACHINE_ARM64

Phase 2: Initial Port
---------------------

Wrote the complete Windows port of sb-send.c:
  - Replaced OpenSSL with hardcoded MD5_EMPTY constant
  - Added Winsock2 for networking (closesocket, WSAStartup)
  - Added WASAPI for audio capture (IMMDeviceEnumerator, IAudioClient)
  - Added DirectShow-style device listing (--list option)
  - Kept pthreads via winpthreads from llvm-mingw
  - Added platform macros: sock_t, SOCK_INVALID, CLOSE_SOCKET
  - Signal handling: SetConsoleCtrlHandler on Windows
  - Sleep/usleep: Sleep() on Windows, usleep() on Linux

Updated Makefile with cross-compilation targets:
  - make sb-send-win64.exe
  - make sb-send-winarm64.exe

Both targets compiled clean with zero warnings.

Phase 3: First Bug Fix - WASAPI Thread Argument
------------------------------------------------

User reported: WASAPI mode exits immediately, stdin works.

Found bug: wasapi_reader_thread received &g_ring (StdinRing*)
but cast it to AppState* to access WASAPI handles. This read
garbage memory and crashed immediately.

Fix: Created WasapiThreadArg struct, passed both state and ring
to the thread. Later simplified to pass state and access g_ring
as a global.

Phase 4: Stdin Binary Mode
---------------------------

User reported: stdin mode hangs but no audio heard.

Found bug: Windows CRT does text-mode translation on stdin.
Binary PCM data was being corrupted by LF-to-CRLF conversion.

Fix: Added _setmode(_fileno(stdin), _O_BINARY) at start of
Windows stdin_reader_thread.

Phase 5: Banner printf Bug
----------------------------

Found dangling "" argument in the printf for the startup banner.
Restructured the ternary to be self-contained per platform.

Phase 6: select(0) Login Timeout
---------------------------------

User reported: login timeout when running Windows binary.

Found bug: All select() calls used 0 as the first argument (nfds).
On Linux, nfds must be highest fd + 1. On Windows it is ignored.

Fix: Changed all select(0, ...) to select(sock + 1, ...).

Phase 7: Remove -P Flag and compute_md5
-----------------------------------------

User requested: remove password support (keep default empty hash).

Removed:
  - -P flag from getopt
  - -P from help text
  - compute_md5 function (dead code)

Kept MD5_EMPTY constant for default password hash.

Phase 8: MD5 Implementation Bug
--------------------------------

While removing compute_md5, discovered the MD5 implementation had
two bugs:
  1. Wrong rotation amounts (single value per round instead of
     cycling through 4 values)
  2. Wrong output format (big-endian hex instead of LE byte order)

Verified against RFC 1321 test vectors:
  MD5("") = D41D8CD98F00B204E9800998ECF8427E (correct)
  MD5("abc") = 900150983CD24FB0D6963F7D28E17F72 (correct)

Then removed compute_md5 entirely since -P was removed.

Phase 9: Channel Conversion Fix
---------------------------------

User reported: -c 1 with WASAPI opens with 2 channels.

Found bug: When driver rejects 1ch format, fallback set
state->channels to device's channel count (2). Opus encoder
then encoded stereo instead of mono.

Fix: Added wasapi_channels field to AppState. Never modify
state->channels. When wasapi_channels != state->channels,
downmix in audio thread (sum all channels, divide by count).

Phase 10: Ring Buffer Changed to Float
---------------------------------------

Went back and forth between direct WASAPI capture and ring
buffer approach. Direct capture had worse dropouts than ring
buffer because WASAPI delivers data in bursts.

Final solution:
  - Changed StdinRing from int16_t to float
  - WASAPI reader puts float samples directly (no conversion)
  - Stdin reader converts int16 to float before ring buffer
  - Audio thread reads float directly (no conversion)
  - WASAPI prefill reduced from 1s to 200ms

Phase 11: Format Spam Fix
--------------------------

User reported: multiple sb-send instances spam each other with
format requests every second.

Found bug: Receiving /aoo/src/*/format triggered sending format
back. A sends format to B, B responds with format, A responds
back, forever.

Fix: Removed the format-on-format handler entirely. Format is
only sent on invite (new connection or sink change).

Phase 12: Uninvite for Sender-Only Mode
-----------------------------------------

User wanted sb-send to tell peers "don't send audio to me"
since it is sender-only.

Added:
  - build_uninvite_msg() function
  - After sending format in invite handler, also send uninvite
    to source 0 (the dummy source)

Phase 13: Uninvite Targeting Fix
---------------------------------

Uninvite was sent to /aoo/src/<their_sink_id>/uninvite instead
of /aoo/src/0/uninvite. The wrong source id caused the remote
peer to silently drop the message.

Fix: Changed to always target source 0 (the dummy source).

Phase 14: Sink=0 Reconnection Bug
-----------------------------------

User reported: "Peer test connected: sink=0 src=0" on
reconnection. The invite_count / INVITE_MAX_RETRIES cap was
added but the sink=0 persisted.

Found bug: Uninvite handler was zeroing remote_sink_id and
our_source_id. When maintenance loop retried the invite, it
used our_source_id=0.

Fix: Uninvite handler now only sets connected=false and
format_sent=false. IDs are preserved for reconnection.

Phase 15: Mutex Audit
-----------------------

User requested complete locking review.

Found bugs:
  1. p->format_sent = true written OUTSIDE mutex (line 621)
     Race condition with audio_thread read and uninvite write.
     Fix: Moved inside mutex.

  2. server_thread maintenance loop used state->peer_count
     after unlock-relock. Could read stale count.
     Fix: Snapshot peer_count into local int before loop.

Phase 16: Windows Non-Blocking Socket Bug
------------------------------------------

User reported: Windows sends no uninvites, Linux works fine.

Found root cause: udp_thread set FIONBIO on Windows UDP socket,
making BOTH sendto and recvfrom non-blocking. On Linux,
MSG_DONTWAIT only affects recvfrom per-call. sendto remains
blocking.

When audio_thread, server_thread, and invite handler all send
UDP packets rapidly, the kernel buffer fills momentarily.
sendto returns WSAEWOULDBLOCK and uninvite is silently dropped.

Fix: Removed ioctlsocket(FIONBIO) entirely. The code already
uses select() before recvfrom, so non-blocking recv is not
needed.

Phase 17: Documentation
-------------------------

Wrote doc/story.txt (section XIII): First-person narrative of
the Windows port, covering all the bugs found and fixed.

Wrote doc/windows.txt: Build instructions for cross-compiling
from Linux using llvm-mingw and pre-built opus.

Final State
-----------

Files changed:
  - sb-send.c: 1814 lines (was 1326)
  - Makefile: 45 lines (was 25)
  - doc/story.txt: 150 lines (new)
  - doc/windows.txt: 147 lines (new)

Build targets:
  - make all (Linux ALSA)
  - make stdin (Linux stdin)
  - make sb-send-win64.exe (Windows x86-64, 817K static)
  - make sb-send-winarm64.exe (Windows ARM64, 731K static)

Bugs found and fixed:
  1. WASAPI thread wrong argument type
  2. Stdin binary mode missing on Windows
  3. select(0) login timeout on Linux
  4. MD5 rotation amounts wrong
  5. MD5 output format wrong (LE vs BE)
  6. Channel conversion overwriting state->channels
  7. Float ring buffer needed for WASAPI (no int16 roundtrip)
  8. Format message ping-pong between sb-send instances
  9. Uninvite targeting wrong source id
  10. Sink=0 reconnection from zeroing IDs on uninvite
  11. format_sent race condition (write outside mutex)
  12. server_thread stale peer_count after unlock-relock
  13. FIONBIO making sendto non-blocking on Windows
