From 396d8917e047f110c2cab88dc3732d5ca5cc72be Mon Sep 17 00:00:00 2001 From: Mason Date: Thu, 1 Oct 2026 00:58:57 -0700 Subject: [PATCH 1/2] docs: ship run instructions, signal guide, and measured validation evidence --- README.md | 92 ++++++++++++++++++----- docs/ENGINEERING_DECISIONS.md | 65 ++++++++++++++++- docs/IMPLEMENTATION_PLAN.md | 2 + docs/PRD.md | 2 + docs/TECHNICAL_DESIGN.md | 2 + docs/USER_GUIDE.md | 132 +++++++++++++++++++++++++++++++++ docs/VALIDATION.md | 133 ++++++++++++++++++++++++++++++++++ src/app.js | 25 ++++++- src/style.css | 2 + 9 files changed, 430 insertions(+), 25 deletions(-) create mode 100644 docs/USER_GUIDE.md create mode 100644 docs/VALIDATION.md diff --git a/README.md b/README.md index ca37bc0..1de1f4e 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,84 @@ # SIGINTTool -A browser-based signal sketchpad: draw a waveform, see its mathematical function, and hear the result. Shape sounds with drawn pitch and volume envelopes, and distinguish each oscillator by its own consistent color. +Draw a signal. See its function. Hear it. -**Project status:** Product and technical planning. The application described below is not implemented yet. +SIGINTTool is a browser signal sketchpad with colored oscillators, drawn pitch and +volume envelopes, a persistent soundboard, and audio-clip modulation. Processing +and storage stay on your device. No account or server upload is required. -## Project documents +## Run it -- [Product requirements](docs/PRD.md) -- [Technical design](docs/TECHNICAL_DESIGN.md) -- [Implementation plan and issue dependencies](docs/IMPLEMENTATION_PLAN.md) -- [Delivery tracker #1](https://github.com/CurtisTechSolutions/SIGINTTool/issues/1) +Use Node.js 22, then: + +~~~sh +npm ci +npm run dev +~~~ + +Open **http://localhost:4173**. Click **Play** to enable browser audio. + +~~~sh +npm run check +npm test +npm run build +~~~ + +The build is a static site in **dist/**. Serve that directory from localhost or +HTTPS; opening index.html as a file will not provide the worker/audio environment. +GitHub Actions also saves a static-site artifact for each successful build. + +## What you can do -## Planned first release +- Draw with pencil, line, eraser, presets, snapping, zoom, numeric controls, and undo. +- Combine up to four named oscillators with persistent colors, phase, mute and solo. +- Draw pitch and volume independently, or create a finite amplitude timeline. +- Inspect sine fits, Fourier coefficients/errors, model overlays, output and spectrum. +- Save named soundboard pads with independent audio and editable snapshots. +- Import WAV/MP3, trim an excerpt, choose stereo conversion, and use it or a frozen + saved clip for volume-follow, ring, or pitch/varispeed modulation. +- Export mono WAV, source/modulated/rendered CSV, full coefficient JSON, and complete + standalone JavaScript/Python functions. +- Autosave locally; download JSON projects or self-contained asset-bearing ZIP bundles. -- Paint-like waveform editing, numeric controls, presets, and undo/redo. -- Periodic oscillators and direct finite-duration signals. -- Drawn pitch and volume envelopes with phase-continuous synthesis. -- Up to four independently editable oscillators with persistent colors, labels, mute/solo, and mono mixing. -- Readable sine fits, Fourier approximations with measured error, and general numerical functions for complex shapes. -- Browser audio, signal/spectrum inspection, WAV and data/function exports, and local project files. -- A named soundboard for saving sounds, replaying them from buttons, and reopening editable copies. -- Local WAV/MP3 imports or saved clips as modulation sources: volume follow, ring modulation, and pitch/varispeed. -- Self-contained project bundles that preserve the audio assets needed to edit and reproduce saved sounds. +Waveform horizontal position is **time** and vertical position is **signed amplitude**. +A spectrum uses **frequency** horizontally. Pitch and volume are separate time lanes. +Complex drawings have complete numerical functions; a compact sine expression is +explicitly an approximation. -Simple shapes may have compact formulas. Complex drawings retain complete numerical representations; the product will identify approximation and audio-processing differences explicitly. +## Limits and behavior + +Finite sounds and retained imported clips are 1 ms–10 seconds. Imports accept +mono/stereo WAV or MP3 up to 20 MiB and 60 seconds. Projects support eight audio assets +and 20,000 editable points. Export rates are 44.1 and 48 kHz. One modulation route is +active at a time; save its result as a clip to build another stage. + +Saved clips contain audio before monitor volume. Playback and WAV export apply monitor +volume once. Modulation sources are immutable copies: editing or removing their original +soundboard pad does not change dependent sounds. Browser storage can be cleared or run +out of space; download project backups for portability. + +Function exports include source evaluators and complete numerical tables for modulated +and rendered output. These tables reproduce the saved settings; editable changes to +effects require the project bundle and a new render. Clip varispeed changes timing as +well as pitch. + +## Documentation and validation + +- [User guide](docs/USER_GUIDE.md) +- [Implementation decisions](docs/ENGINEERING_DECISIONS.md) +- [Validation and measured limits](docs/VALIDATION.md) +- [Product requirements](docs/PRD.md) +- [Technical design](docs/TECHNICAL_DESIGN.md) +- [Implementation plan](docs/IMPLEMENTATION_PLAN.md) +- [Delivery tracker](https://github.com/CurtisTechSolutions/SIGINTTool/issues/1) +- [Contributing and test setup](CONTRIBUTING.md) -Implementation setup and build commands will be added as part of [foundation issue #2](https://github.com/CurtisTechSolutions/SIGINTTool/issues/2). +The app uses native browser modules, Canvas, Web Workers, Web Audio, and IndexedDB. +Its local MP3 decoder is pinned and includes [third-party notices](src/vendor/README.md). +CI covers numerical tests, generated JS/Python, Chromium/Firefox/WebKit workflows, and +real Safari on macOS. See the validation report for measured scope and remaining human +acceptance checks. ## License -See [LICENSE](LICENSE). +See [LICENSE](LICENSE). Vendored components retain their own licenses in src/vendor/. diff --git a/docs/ENGINEERING_DECISIONS.md b/docs/ENGINEERING_DECISIONS.md index d3f8629..331c63b 100644 --- a/docs/ENGINEERING_DECISIONS.md +++ b/docs/ENGINEERING_DECISIONS.md @@ -2,15 +2,15 @@ ## Browser-native modules -The application uses standard JavaScript modules, Canvas 2D, Web Workers, Web Audio, and IndexedDB. The planning documents recommended React/TypeScript as a starting option. This implementation chooses native modules because the workspace is primarily a canvas and a small set of controls, pure numerical code can run unchanged under Node, and the app can ship without third-party runtime code or a bundler. Modules separate the project contract, signal math, renderer, transport, storage, exports, and interface. +The application uses standard JavaScript modules, Canvas 2D, Web Workers, Web Audio, and IndexedDB. The planning documents recommended React/TypeScript as a starting option. This implementation chooses native modules because the workspace is primarily a canvas and a small set of controls, pure numerical code can run unchanged under Node, and the interface can ship without a UI framework or a bundler. A pinned, separately licensed WebAssembly decoder provides local MP3 support. Modules separate the project contract, signal math, renderer, transport, storage, exports, and interface. -Node 22 is the pinned development major. There are no application dependencies; the lockfile records this deliberately. Playwright 1.63.0 is pinned for CI browser checks. Its Chromium, Firefox, and WebKit tests do not substitute for actual Safari or a human usability study. +Node 22 is the pinned development major. There are no npm runtime dependencies; the MP3 distribution is vendored with notices and pinned source references. Playwright 1.63.0 is pinned for CI browser checks. Its Chromium, Firefox, and WebKit tests do not substitute for actual Safari or a human usability study. ## Build and release `npm ci`, `npm run check`, `npm test`, and `npm run build` validate and produce a static `dist/` directory. `npm run dev` serves the workspace at localhost:4173. CI runs on every PR and on main, and retains the static build artifact. The inherited Docker publishing workflow was removed because the application has no container deployment requirement. Release preparation is an explicit manual workflow; implementation merges do not publish releases or containers. -The initial foundation includes versioned bounds/validation and independent model fixtures. DSP quality and maximum-resource evidence are added with the renderer; foundation issue #2 stays open until that evidence exists. +The foundation includes versioned bounds/validation, independent numerical fixtures, a custom radix-2 FFT, measured filter response, and maximum-workload profiling. See VALIDATION.md for evidence and remaining release acceptance checks. ## Bounded audio decoding @@ -23,3 +23,62 @@ its pinned sources and licenses are included in src/vendor/README.md. Supported WAV inputs are integer PCM 8/16/24/32-bit or IEEE float32. Extensible/compressed WAV, free-format MP3, changing MP3 format midstream, and unrecognized trailing metadata are rejected with a clear error. These limits are deliberate first-release boundaries. + + +## Rendering and mathematical exports +The renderer is shared by preview, soundboard caching and exports. It runs in workers +at four times the destination sample rate, reconstructs clip samples with a 64-tap +windowed-sinc kernel, and applies a 257-tap Blackman low-pass before decimation. +The tested filter target is less than 0.5 dB loss at 0.4 × destination sample rate and +more than 60 dB rejection at 0.5 × destination sample rate. This is a measured filter +contract, not a promise that every arbitrarily discontinuous drawing is artifact-free. + +Periodic carriers use bandwidth-limited harmonic tables. Drawn log-linear pitch uses +exact segment integrals; the regular render grid advances through geometric increments, +with an analytic reset every 1,024 samples or at a knot to bound floating-point drift. +Clip-driven pitch/readhead changes use trapezoidal integration. Carrier DC is removed +before drawn gain; generated post-modulation DC is reported, not subtracted afterward. +Headroom is one global attenuation over the completed finite clip. Monitor gain is +outside the cached render and applied once at playback or monitored export. + +Finite clip edges use up to 5 ms fades, including loop restarts. This preserves duration +but can make a short dip at a repeated seam. Modulator availability has bounded ramps; +clip looping uses periodic sinc reconstruction, with no implied time stretching. +Band-limited table selection can change high-harmonic content during a pitch sweep. + +Source function exports contain full source/model tables, coefficient arrays, envelope +knots and phase-prefix segments. They retain source DC and precede clip modulation. +Modulated and rendered function exports carry complete numerical result tables. This +is an intentional, stateless definition of the fixed result: arbitrary-time queries +interpolate those tables and need no hidden follower/readhead state. They do not +re-synthesize modified effect settings. The separate project bundle preserves all +PCM assets and editable routing for that purpose. + +## Runtime and persistence boundaries +Worker cancellation terminates the current computation. Playback uses generation +checks and one shared AudioContext; a stopped or obsolete job cannot start audio later. +Saved clips retain pre-monitor PCM and an independent project snapshot atomically. +Immutable content-derived audio IDs preserve dependencies after source-pad updates +or removal. The in-memory prepared-pad cache is bounded to 8 MiB; library listing reads +metadata. Current-project buffers are pruned when their references change. + +Browser storage is local to its origin/profile and can be cleared or exhaust quota. +Failed transactions preserve the previous saved state. Removed sounds retain recovery +data for Undo; this release favors preserving referenced audio over automatic garbage +collection, so repeated updates/removals can leave unused records until browser data +is cleared after backing up projects. + +Archive format version 1 uses standard ZIP STORE entries only. Import validates +directory/header consistency, byte/entry limits, paths, CRCs, PCM hashes, versions, +finite sample bounds and complete project references before changing state. + +## Automation and release scope +Each implementation slice was a separate reviewed diff/PR, merged only after its +required CI jobs passed. The repository does not enable GitHub's automatic-merge flag; +the authorized merge operation was performed directly after verifying each PR head. +No production deployment, package publication, or scheduled background task is implied. + +Real Safari tests use Apple's bundled safaridriver on a macOS runner. This is separate +from Playwright WebKit. [Apple's setup](https://developer.apple.com/documentation/safari-developer-tools/macos-enabling-webdriver) +documents enabling WebDriver for this test environment. Human listening, screen-reader +sessions and the five-person formative study remain explicit release acceptance work. diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index b985c6d..8fb6ef7 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,7 @@ # SIGINTTool implementation plan +> Implementation is now available on `main`. These documents retain the agreed requirements and original design recommendations. Actual choices, supported boundaries and evidence are in [ENGINEERING_DECISIONS.md](ENGINEERING_DECISIONS.md) and [VALIDATION.md](VALIDATION.md); use [USER_GUIDE.md](USER_GUIDE.md) to run the app. + **Status:** Proposed first-release backlog; implementation has not started in this planning change. **Date:** 2026-09-30 **Tracking issue:** [#1 — Product delivery plan](https://github.com/CurtisTechSolutions/SIGINTTool/issues/1) diff --git a/docs/PRD.md b/docs/PRD.md index ba5127d..9bc4078 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,5 +1,7 @@ # SIGINTTool — Draw a signal, see its function, hear its sound +> Implementation is now available on `main`. These documents retain the agreed requirements and original design recommendations. Actual choices, supported boundaries and evidence are in [ENGINEERING_DECISIONS.md](ENGINEERING_DECISIONS.md) and [VALIDATION.md](VALIDATION.md); use [USER_GUIDE.md](USER_GUIDE.md) to run the app. + **Status:** Proposed product requirements; no application has been implemented. **Date:** 2026-09-30 **Repository:** [CurtisTechSolutions/SIGINTTool](https://github.com/CurtisTechSolutions/SIGINTTool) diff --git a/docs/TECHNICAL_DESIGN.md b/docs/TECHNICAL_DESIGN.md index 9b4fdb3..8fd3911 100644 --- a/docs/TECHNICAL_DESIGN.md +++ b/docs/TECHNICAL_DESIGN.md @@ -1,5 +1,7 @@ # SIGINTTool technical design +> Implementation is now available on `main`. These documents retain the agreed requirements and original design recommendations. Actual choices, supported boundaries and evidence are in [ENGINEERING_DECISIONS.md](ENGINEERING_DECISIONS.md) and [VALIDATION.md](VALIDATION.md); use [USER_GUIDE.md](USER_GUIDE.md) to run the app. + **Status:** Proposed architecture for discussion, not implemented or benchmarked. **Date:** 2026-09-30 **Product contract:** [PRD](PRD.md) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..7b99c54 --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,132 @@ +# Using SIGINTTool + +## Draw and listen + +1. Start in Waveform. Draw one repeating cycle with Pencil or Line. Eraser writes + silence over the selected interval. Later strokes replace earlier strokes. +2. Set repetition frequency in Hz. The horizontal axis remains time: a 1,000 Hz + cycle spans 1 ms. The selected oscillator defines the displayed cycle width; + other oscillators overlay the same physical time range. +3. Press Play. Monitor changes listening volume; Escape or Stop stops playback. + Start and end edges are faded over up to 5 ms to reduce clicks. +4. Give the sound a name and use Save as new sound. + +Waveform height is signed amplitude, from −1 to +1. Moving the entire waveform up +adds a DC offset; it is not the same as increasing volume. Source math retains that +offset. The audio renderer removes oscillator DC before drawn gain and modulation. + +Pencil and Line support pointer/touch input. Numeric point controls provide time/value +entry without drawing. Ctrl/Cmd+Z undoes; Shift+Ctrl/Cmd+Z redoes. Space on the page +background plays/stops. Controls and pads use native keyboard activation. + +## Oscillators and envelopes + +Each oscillator has a persistent color and name. The selected line is solid and thicker; +other lines are dashed. Plot Hide only changes visibility. Mute removes audio; if any +voice is soloed, only unmuted soloed voices are included. + +Envelopes adds two independent lanes for the selected oscillator: +pitch in Hz on a logarithmic vertical scale, and volume as linear gain from 0 to 1. +Both horizontal axes show composition time. Frequency is integrated into phase, so +a pitch sweep does not reset the waveform at envelope knots. + +Waveform mode plays fixed-frequency cycles. Timeline instead plays one finite amplitude +drawing directly. A slowly varying Timeline can be below hearing range. Its duration +stretches the whole drawing; it does not act as a pitch envelope. + +## Functions and inspection + +Drawing is a piecewise-linear sample table. Sine fit is the strongest candidate among +the supported first 64 harmonics, with an error score. Fourier approximation keeps the +chosen harmonic count. The source is preserved when either approximation is selected. + +The white dotted model overlay compares the selected sine or Fourier model with the +original drawing. The error values compare against a dense 4× source grid. The audio +output and spectrum describe the last completed render. The spectrum uses a Hann +window, up to 8,192 samples, linear magnitude, and a horizontal frequency axis in Hz. + +Audio can differ from the original drawing: harmonics above the playable bandwidth +are omitted, carrier DC is removed, the result is low-pass filtered, loud mixes receive +one global attenuation, and finite edges are faded. The inspector reports these steps. +Monitor is a separate final volume control. + +## Soundboard and backups + +Save as new sound renders a snapshot and stores both its editable project and its +pre-monitor audio in a single IndexedDB transaction. Open creates an editable copy. +Editing that copy leaves the saved pad intact until Update opened sound succeeds. + +Pads play exclusively. Rename, reorder, Remove, and Undo remove manage the board. +Recover previous draft restores the working draft displaced by an Open operation. +Current edits autosave after a short delay; the status line shows when that succeeds. + +Download project saves JSON when there are no audio assets, or a .sigint.zip bundle +with all required PCM when assets are present. Open project validates the whole file +before changing the editor. Opening and restoring never starts audio. + +## Use one sound to shape another + +Upload WAV or MP3, inspect the preview, choose a 1 ms–10 second excerpt, and choose +Average L+R, Left, or Right. Stereo averaging can intentionally cancel opposite channels. +Nothing is uploaded. Cancel terminates the decoder worker. + +Choose an imported clip or a soundboard clip as the modulator. A saved clip is captured +by immutable audio identity, before monitor volume. It is independent of the original +pad's future updates/removal and does not recursively run that pad's project. + +Select a carrier: current mix/Timeline, one named oscillator, or another audio clip. + +- **Volume follower:** follows rectified sample amplitude with attack/release smoothing, + then blends between unchanged carrier and that gain envelope. +- **Ring:** multiplies signed carrier and modulator samples, with dry/wet depth. +- **Pitch:** uses a semitone depth. For oscillators it integrates effective frequency. + For clips/current rendered mix it integrates a readhead: varispeed changes pitch + and timing together. + +Offset delays the modulator. Its loop and the carrier's loop are separate controls. +Outside a finite modulator the effect depth returns to zero through bounded transitions; +silence inside the active clip still follows the chosen effect. A finite carrier is +zero after its end unless its loop is selected. + +Audition modulator explicitly plays the source through the shared transport. Numeric +source statistics and pitch-rate bounds supplement the waveform. Compare without clip +modulation restarts from the same initial phase/readhead; it does not crossfade between +unrelated positions. + +## Exports + +Choose 44.1 or 48 kHz. The renderer recomputes output for that rate. + +- WAV: mono 16-bit PCM, complete conditioned output with monitor gain applied once. +- CSV Source: source models plus drawn envelopes, before clip modulation/conditioning. +- CSV Modulated: numerical route output before final filter, attenuation and edge fades. +- CSV Rendered: conditioned, monitored output. +- JSON: complete source/model data, coefficients, units and settings. +- JavaScript/Python: complete data and source/oscillator/modulated/rendered functions; + no packages or companion assets needed to evaluate the exported settings. + +For JS, import signal and call signal(timeSeconds, "rendered"). For Python, import +signal from the downloaded module and use the same arguments. Default "source" retains +source DC and integrates pitch analytically. oscillator(t, voiceId) exposes an individual +periodic voice (including negative-time wrap in Waveform mode). + +The modulated/rendered functions are full numerical tables with linear interpolation +and zero outside the finite interval. They do not claim a compact symbolic solution +or reconstruct audio above their sampling rate. The project bundle is the editable +definition for re-rendering changed effects. + +Selected-oscillator exports isolate that voice and include an effect only when routed +to that oscillator. Mix and external-carrier effects belong to the Current sound scope. +Exports use a captured snapshot and never mutate the editor. Cancel export stops its worker. + +## Recovery and boundaries + +If browser audio cannot start, check the browser's audio permission and output device, +then activate Play again. Unsupported files show an error while keeping the current +project. WAV accepts uncompressed integer PCM or float32; some extensible WAV and unusual +MP3 metadata/format variants require conversion first. + +If local storage is unavailable or full, keep the current work with Download project. +The app does not claim a successful save before the transaction completes. There is no +cloud synchronization, live microphone processing, multitrack sequencer, stereo output, +time-preserving pitch shift, or production deployment in this version. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md new file mode 100644 index 0000000..edb69f5 --- /dev/null +++ b/docs/VALIDATION.md @@ -0,0 +1,133 @@ +# Validation report + +Implementation evidence is attached to [PR #29](https://github.com/CurtisTechSolutions/SIGINTTool/pull/29) +at commit f14b36b1fcf0691280d0ba45da027312eaa90e2b, from +[CI run 36814330501](https://github.com/CurtisTechSolutions/SIGINTTool/actions/runs/36814330501). +Later PRs rerun the same suite. This report distinguishes implemented behavior, +automated evidence, measured performance and remaining human release acceptance. + +## Reproduce + +~~~sh +npm ci +npm run check +node scripts/fixtures.mjs +npm test +node --expose-gc scripts/benchmark.mjs +npm run build +npm install --no-save --package-lock=false playwright@1.63.0 axe-core@4.10.3 +npx playwright install --with-deps chromium firefox webkit +npm run test:browser +~~~ + +Codec fixture generation requires ffmpeg. Linux browser tests need an audio device; +CI starts a PulseAudio null sink. Real Safari uses a macOS runner: + +~~~sh +sudo safaridriver --enable +python3 tests/safari.py +~~~ + +The collaboration host could not start shell processes, so builds and tests ran in +GitHub Actions from committed branches. No local test execution or production +deployment is claimed. + +## Numerical and export coverage + +35 tests passed with no skipped tests in the recorded run. Fixtures cover: + +- Ordered drawing strokes, backward/vertical strokes, erase, half-open sampling, + periodic wrap, finite ends, smoothing/resolution contracts and malformed projects. +- FFT normalization, signs, DC and Nyquist scaling, sine fitting, Fourier error, + known-frequency spectra and stable log-linear pitch integration. +- Independent oscillator phase cancellation, mute/solo, zero gain, global headroom, + ring sum/difference tones, high-frequency rejection, constant octave pitch, + varispeed carrier EOF/looping and volume-follower timing. +- Maximum source resolution, an independent analytic logarithmic sweep, sinc rate + conversion and FIR pass/stop bands. +- WAV headers/quantization, ZIP CRC/path/bounds, immutable PCM bundle round trips, + stereo conversion, truncated codec input, actual generated WAV/MP3 decoding. +- Standalone JavaScript and Python source/modulated/rendered evaluation, including + negative-time periodic wrap, finite out-of-domain behavior, phase prefixes, + monitor applied once and selected-voice isolation. + +The decimator has less than 0.5 dB loss at 0.4 times destination sample rate and more +than 60 dB rejection at 0.5 times destination sample rate. A 14 kHz carrier multiplied +by 13 kHz retains the 1 kHz difference while rejecting the aliased 21 kHz sum. +These are explicit fixtures, not a claim of exhaustive alias-free arbitrary modulation. + +## Browser and storage coverage + +| Environment | Recorded coverage | +| --- | --- | +| Chrome for Testing / headless Chromium 153.0.8010.12, Linux | Full integrated browser suite, axe WCAG 2 A/AA and 2.1 AA tags, 390 px responsive layout, soundboard stress | +| Firefox 155.0, Linux | Full integrated browser suite | +| Playwright WebKit 26.6, Linux | Full integrated browser suite | +| Safari 26.6.2, macOS 26.6.2 arm64 | Actual Safari WebDriver: WAV/MP3 workers, trim/conversion, ring audio at 48 kHz, Stop, soundboard snapshot isolation | + +The integrated suite draws and edits voices/envelopes, exercises undo/redo, renders +and stops audio, saves/reloads independent snapshots, imports both codecs, routes +effects to a voice distinct from editor selection, downloads WAV/functions, deletes +an original source pad, saves the dependent sound, and reopens its asset-bearing bundle +in a fresh browser context. Reload/Open never starts audio automatically. + +Recovery tests verify that malformed project import and simulated quota failure +preserve work, import cancellation terminates its worker, and a stopped pending render +cannot start later. No axe violations were reported for the tagged rules. This does not +replace manual screen-reader or human keyboard/usability review. + +The capacity fixture stores 31 distinct ten-second clips alongside a saved sound. +Listing 32 pads performs **zero audio-buffer reads**. After 100 prepared pad play/Stop +cycles and a stopped pending editor render, **zero source nodes remain**. Measured +prepared-pad p95 latency was **8.4 ms**, including the tested UI path. + +## Performance and memory + +Recorded baseline: Node 22.23.3, Linux 6.17.0-1022-azure, a shared GitHub-hosted +2-vCPU AMD EPYC 7763 runner with approximately 7.8 GiB memory. + +The workload uses four 16,384-sample saw carriers, drawn pitch/gain, ten seconds at +48 kHz, a full-length clip, mix-target pitch modulation at ±24 semitones, and both +source/carrier looping. Each result has 480,000 samples with peak at or below 0.95. + +| Measurement | Result | +| --- | --- | +| Periodic analysis p95, 20 runs | 40.24 ms | +| Render minimum / maximum, 10 runs | 1.51 s / 2.74 s | +| Render median | 1.55 s | +| Render empirical p95 (nearest rank, 10 runs) | 2.74 s | +| Sampled post-render typed-array memory maximum | 42.94 MiB | +| Whole Node process RSS maximum | 175.18 MiB | + +Optimizations retained the 4× render and filter quality: contiguous sinc reads avoid +per-tap wrapping, exact kernel phases avoid redundant interpolation, geometric pitch +increments avoid per-sample exponentials, symmetric FIR evaluation shares work, and +workers avoid unnecessary timer clamping. Direct AbortSignal callers still yield. + +**The original proposed two-second p95 render target is not met on this shared runner.** +It remains an open release qualification item in #16; it has not been silently weakened +or treated as a passing performance assertion. The worker keeps the interface usable +and can be canceled while a larger sound prepares. + +Post-render array-buffer sampling is not a peak browser-heap measurement. The renderer +checks an owned-scratch estimate before allocation, prepared pad PCM is capped at +8 MiB, imports preflight decoded frames/bytes, and projects cap asset counts/sizes. +The 128 MiB application-wide budget still needs a full browser/decoder peak-memory +profile; whole-process RSS includes Node/runtime memory and is not equated with that +budget. Arbitrary heavy recordings and unbounded modulation are outside this version. + +## Remaining release acceptance + +Implementation and automated checks are available on main. The following have not +been represented as completed: + +- A stable-hardware full-browser profile of maximum rendering and peak memory, + plus drawing-event latency and decoder-internal peak overhead. +- Branded stable Chrome/Edge checks with ordinary hardware audio devices, and human + listening across devices. Playwright Chromium is identified by its actual build. +- Manual keyboard and screen-reader sessions, and the planned five-person formative + test (four of five complete draw → hear → find function within two minutes). + +Track these in [#15](https://github.com/CurtisTechSolutions/SIGINTTool/issues/15) and +[#16](https://github.com/CurtisTechSolutions/SIGINTTool/issues/16). +The parent release tracker remains open until the acceptance evidence is complete. diff --git a/src/app.js b/src/app.js index 8c46cf2..2220458 100644 --- a/src/app.js +++ b/src/app.js @@ -18,9 +18,10 @@ export function element(tag,attrs={},...children){ return node; } const history=new History(project()),renderer=new Renderer(),changes=new Set(),cache=new Map(); +let playbackClock=null,playbackFrame=0; let playing=false,playingTag='editor',intent=0,lastRender=null,bypass=false,renderQueue=Promise.resolve(); const view={modelOverlay:false,tool:'pencil',zoom:1,pan:0,snap:false,only:false,hidden:new Set()}; -const transport=new Transport((state,tag)=>{playing=state==='playing';playingTag=tag||playingTag;$('#play').textContent=playing?'↻ Restart':'▶ Play';$('#play').setAttribute('aria-label',playing?'Restart playback':'Play sound');$('#transport-state').textContent=playing?'Playing '+(tag==='editor'?'current sound':tag):'Stopped';}); +const transport=new Transport((state,tag,details)=>{playing=state==='playing';playingTag=tag||playingTag;playbackClock=playing?{...details,loop:tag==='editor'&&app.project.loop}:null;cancelAnimationFrame(playbackFrame);updatePlayhead();$('#play').textContent=playing?'↻ Restart':'▶ Play';$('#play').setAttribute('aria-label',playing?'Restart playback':'Play sound');$('#transport-state').textContent=playing?'Playing '+(tag==='editor'?'current sound':tag):'Stopped';}); export const app={ get project(){return history.current;}, retainedAssetIds(){return [...new Set([history.current,...history.past,...history.future].flatMap(p=>p.assets))];},get lastRender(){return lastRender;},get playbackRevision(){return intent;},assets:{}, @@ -61,7 +62,15 @@ export const app={ } }; -$('#app').innerHTML="
◒ SIGINTSignal sketchpad
Stopped
\n
\n

DRAW → UNDERSTAND → HEAR

Your next sound starts with a line.

\n
\n\n
\n
\n
\n
Waveform · signed amplitude
\n
+1
0
−1
\n
Time →
\n\n
1×
\n
\n
Edit exact points without drawing

Waveform: −1…1. Pitch: 20…20,000 Hz. Volume: 0…1. Undo restores every edit.

\n
\n\n
\n
Draw on the canvas, then press Play. Sound stays on this device.
\n
\n
"; +$('#app').innerHTML="
◒ SIGINTSignal sketchpad
Stopped
\n
\n

DRAW → UNDERSTAND → HEAR

Your next sound starts with a line.

\n
\n\n
\n
\n
\n
Waveform · signed amplitude
\n
+1
0
−1
\n
Time →
\n\n
1×
\n
\n
Edit exact points without drawing

Waveform: −1…1. Pitch: 20…20,000 Hz. Volume: 0…1. Undo restores every edit.

\n
\n\n
\n
Draw on the canvas, then press Play. Sound stays on this device.
\n\n
\n
"; +function updatePlayhead(){ + const root=$('#playhead');if(!root)return;root.hidden=!playbackClock; + if(!playbackClock)return; + const elapsed=Math.max(0,transport.context.currentTime-playbackClock.start),T=playbackClock.duration; + const position=playbackClock.loop?elapsed%T:Math.min(T,elapsed); + $('#playback-progress').value=position/T;$('#playback-time').textContent=position.toFixed(2)+' / '+T.toFixed(2)+' s'; + playbackFrame=requestAnimationFrame(updatePlayhead); +} function selected(){return app.project.voices.find(v=>v.id===app.project.selectedId);} function data(v){ const old=cache.get(v.id);if(old?.voice===v)return old; @@ -188,12 +197,24 @@ function refresh(){ if(active)document.querySelector('[data-key="'+CSS.escape(active)+'"]')?.focus(); requestAnimationFrame(draw); } +const guide=element('details',{class:'quick-guide'},element('summary',{},'Start here · draw, listen, save'),element('ol',{}, + element('li',{},'Waveform draws one repeating cycle. Left to right is time; up and down is signed amplitude. Closer cycles in the same time mean higher frequency.'), + element('li',{},'Envelopes add separate pitch (Hz) and volume (gain) lanes over the sound duration. Timeline plays the drawn amplitude directly and may be sub-audible when slowly varying.'), + element('li',{},'Play starts browser audio. Monitor controls listening/export volume. Escape stops; Ctrl/Cmd+Z undoes. Numeric point controls work without a pointer.'), + element('li',{},'Name the sound and Save as new. Open edits a copy; Update opened sound explicitly replaces the saved version. Download a project for a portable backup.'), + element('li',{},'Upload a WAV/MP3 or choose a saved clip to modulate another sound. Ring uses signed samples; Volume follows loudness; clip Pitch also changes playback timing.') +));document.querySelector('.workspace-heading').after(guide); $('#play').onclick=()=>app.play();$('#stop').onclick=()=>app.stop(); $('#monitor').oninput=e=>{transport.setVolume(Number(e.target.value));$('#monitor-value').textContent=Math.round(e.target.value*100)+'%';}; $('#monitor').onchange=e=>app.edit(p=>{p.monitor=Number(e.target.value);}); $('#project-name').onchange=e=>app.edit(p=>{p.name=e.target.value;}); $('#add-voice').onclick=()=>app.edit(p=>{if(p.voices.length===4)return;const v=voice(p.voices.length);v.color=[0,1,2,3].find(c=>!p.voices.some(v=>v.color===c));p.voices.push(v);p.selectedId=v.id;}); document.querySelectorAll('[data-mode]').forEach(b=>{b.onclick=()=>app.edit(p=>{p.mode=b.dataset.mode;if(p.mode==='timeline'&&p.modulation.target==='oscillator')p.modulation.target='mix';});}); +document.querySelectorAll('[data-mode]').forEach((b,index)=>{b.onkeydown=e=>{ + const buttons=[...document.querySelectorAll('[data-mode]')];let next=index; + if(e.key==='ArrowRight')next=(index+1)%buttons.length;else if(e.key==='ArrowLeft')next=(index+buttons.length-1)%buttons.length;else if(e.key==='Home')next=0;else if(e.key==='End')next=buttons.length-1;else return; + e.preventDefault();buttons[next].click();buttons[next].focus(); +};}); document.querySelectorAll('[data-tool]').forEach(b=>{b.onclick=()=>{view.tool=b.dataset.tool;refresh();};}); $('#undo').onclick=()=>{if(history.undo())changed();};$('#redo').onclick=()=>{if(history.redo())changed();}; $('#clear').onclick=()=>app.edit(p=>{if(p.mode==='timeline')p.timeline.strokes=[];else p.voices.find(v=>v.id===p.selectedId).waveform.strokes=[];}); diff --git a/src/style.css b/src/style.css index 5665f52..2419614 100644 --- a/src/style.css +++ b/src/style.css @@ -11,4 +11,6 @@ :root{--surface:#101925;--border:#2c384c}.mix-legend{color:#dce4f2}a:focus-visible,summary:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,canvas:focus-visible{outline:3px solid #8ee9ff;outline-offset:3px} +#playhead{display:flex;align-items:center;gap:12px;font:11px ui-monospace,monospace;color:var(--muted);margin:8px 0}#playhead progress{width:100%;accent-color:var(--accent)}#playhead output{white-space:nowrap}.quick-guide{border:1px solid var(--line);border-radius:8px;padding:10px 16px;margin-bottom:18px;color:#bdcadc;font-size:12px}.quick-guide summary{cursor:pointer}.quick-guide li{margin:8px 0;max-width:1100px} + .control-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.control-grid>label,.voice{min-width:0}.route-formula{overflow-wrap:anywhere}@media(max-width:700px){.control-grid,.voices-panel #voices{grid-template-columns:repeat(2,minmax(0,1fr))}.control-grid{padding:12px 0}.asset-row .hint{white-space:normal}.extra-panel h2{overflow-wrap:anywhere}} From df217c70098cb8065eb10f1d6ebe7f5d7464eee2 Mon Sep 17 00:00:00 2001 From: Mason Date: Thu, 1 Oct 2026 01:02:21 -0700 Subject: [PATCH 2/2] fix: explain pitch continuity and validate carrier selection before activation --- docs/ENGINEERING_DECISIONS.md | 2 ++ src/modulation-ui.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ENGINEERING_DECISIONS.md b/docs/ENGINEERING_DECISIONS.md index 331c63b..40341b4 100644 --- a/docs/ENGINEERING_DECISIONS.md +++ b/docs/ENGINEERING_DECISIONS.md @@ -82,3 +82,5 @@ Real Safari tests use Apple's bundled safaridriver on a macOS runner. This is se from Playwright WebKit. [Apple's setup](https://developer.apple.com/documentation/safari-developer-tools/macos-enabling-webdriver) documents enabling WebDriver for this test environment. Human listening, screen-reader sessions and the five-person formative study remain explicit release acceptance work. + +Native oscillators and AudioParam automation were evaluated as a simpler path, but a shared finite numerical renderer was selected to make arbitrary drawings, clip varispeed, exports and frozen soundboard results reproducible through one implementation. Web Audio schedules the completed buffer; a real-time AudioWorklet and continuously connected modulation graph are unnecessary for this finite first release. diff --git a/src/modulation-ui.js b/src/modulation-ui.js index 5f77a70..d59e721 100644 --- a/src/modulation-ui.js +++ b/src/modulation-ui.js @@ -42,7 +42,7 @@ export function installModulation(app){ const options=[['','Choose a clip…'],...available.entries()]; const set=(key,value)=>app.edit(p=>{p.modulation[key]=value;}); grid.replaceChildren( - field('Enable modulation',check('mod-enabled',m.enabled,v=>{if(v&&!m.sourceId){app.status('Choose a modulator clip first.',true);refresh();return;}set('enabled',v);})), + field('Enable modulation',check('mod-enabled',m.enabled,v=>{if(v&&(!m.sourceId||(m.target==='asset'&&!m.carrierId))){app.status('Choose both the modulator and its carrier before enabling this route.',true);refresh();return;}set('enabled',v);})), field('Modulator clip',select('mod-source',options,m.sourceId,id=>attachAudio(id,'sourceId'))), field('Effect',select('mod-mode',[['volume','Volume follower'],['ring','Ring modulation'],['pitch','Pitch / varispeed']],m.mode,value=>set('mode',value))), field('Apply to',select('mod-target',[['mix','Current sound / mix'],...(p.mode==='timeline'?[]:[['oscillator','One oscillator']]),['asset','Another audio clip']],m.target,value=>{ @@ -63,7 +63,7 @@ export function installModulation(app){ :m.mode==='ring'?'y(t) = carrier(t) × [1 − d(t) + d(t) × modulator(t)]' :m.target==='oscillator'?'f(t) = basePitch(t) × 2^(D(t) × modulator(t) / 12); phase = integral of frequency' :'readhead(t) = integral of 2^(D(t) × modulator(t) / 12); y(t) = carrier(readhead(t)). Pitch and timing change together.'; - if(!m.sourceLoop)formula.textContent+=' Outside the modulator clip: unchanged carrier.'; + if(!m.sourceLoop)formula.textContent+=m.mode==='pitch'?' Outside the modulator clip: base pitch/rate resumes; accumulated phase and position are preserved.':' Outside the modulator clip: unchanged carrier.'; audition.disabled=!m.sourceId; if(m.sourceId){ const a=app.assets[m.sourceId]||await app.store.get('audio',m.sourceId);if(token!==refreshToken)return;