Skip to content

Polishing pass: correctness fixes, cross-platform paths, dev-tooling cleanup - #6

Merged
chrisschouk merged 12 commits into
mainfrom
claude/polishing-work-hmwkoo
Jul 19, 2026
Merged

Polishing pass: correctness fixes, cross-platform paths, dev-tooling cleanup#6
chrisschouk merged 12 commits into
mainfrom
claude/polishing-work-hmwkoo

Conversation

@chrisschouk

@chrisschouk chrisschouk commented Jul 18, 2026

Copy link
Copy Markdown
Owner

A round of polishing across the CLI, renderer, DSL and dev tooling. Every change ships with tests; ruff, mypy, the Python suite (236 tests) and the web parity suite (108 tests) are all green.

Correctness fixes

  • gain / pan modifiers were no-ops offline. parse_dsl populated step.gain_db/step.pan but the mixer only applied velocity — | gain -6db and | pan L100 rendered identically to the plain pattern. Now applied (pan via a √2-normalised constant-power law, so pan 0 is unchanged).
  • | tune was ignored for real WAV samples (synth voices only). Samples now repitch vari-speed, matching the synth kit and the docs.
  • Euclidean rhythms never hit the downbeat. e(4,16) landed on 3,7,11,15 instead of 0,4,8,12. Fixed to the standard convention; hit counts unchanged. Mirrored in the browser DSL port with regenerated golden fixtures.
  • launch/open --samples DIR was silently ignored — it set an env var read too late. Now points the engine's sample library at the requested directory.
  • audx mix set / audx mute crashed or mis-targeted on bad channels (raw IndexError, or negative-index wrap). Now a friendly out-of-range error.
  • Malformed rhythm specs dumped a traceback (audx render "hh ax8"). Now a clear message, non-zero exit.
  • Standalone audx.audio package DSP (not yet wired in): the voice envelope crashed on any sound (mono envelope × interleaved stereo buffer), constant-power pan centred hard-left, long ramps overran short buffers, and the mixer ignored solo. All fixed with new tests.

Cross-platform & config

  • Config directory follows each platform's convention — XDG ~/.config/audx on Linux, ~/Library/Application Support/audx on macOS, %APPDATA%\audx on Windows (was hardcoded to the macOS path everywhere).
  • AUDX_CONFIG_DIR / AUDX_SAMPLES_DIR / AUDX_PROJECTS_DIR overrides (with ~ expansion); the samples override is now one consistently-named variable.
  • Importing audx no longer creates directories as a side effect.

Dev tooling & docs

  • Makefile and pre-commit config aligned to the real ruff + mypy toolchain (they still drove uninstalled black/isort/flake8, so make lint/format and every hook failed).
  • Deprecated flat CLI aliases (pattern-create, etc.) hidden from --help (still functional).
  • NxM rhythm docs corrected to match behaviour (README, pattern-language.md and code comments disagreed); fixed two other drifted DSL examples.
  • README test-count refreshed; CHANGELOG updated throughout.

Verification

  • ruff check, mypy src/audx — clean
  • pytest — 236 passed · web tsc + vitest — 108 passed · studio bundle builds

🤖 Generated with Claude Code

claude added 12 commits July 18, 2026 15:53
audx placed its config directory at ~/Library/Application Support/audx on
every OS, which is a macOS-only convention — on Linux and Windows that path
is wrong. Resolve config, samples and projects directories per platform:
XDG (~/.config/audx) on Linux, Application Support on macOS, and %APPDATA%
on Windows, each overridable via AUDX_CONFIG_DIR / AUDX_SAMPLES_DIR /
AUDX_PROJECTS_DIR with ~ expansion.

Also reconcile the samples override: `launch` wrote AUDX_SAMPLES_DIR while
`ai similar` read AUDX_SAMPLES, so the variable was silently ignored — both
now use AUDX_SAMPLES_DIR.

Importing audx no longer creates directories as a side effect; every writer
already makes its parent directory on demand.

Add tests covering per-platform resolution, env overrides, tilde expansion
and the no-side-effects-on-import contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
The flat command names kept for backwards compatibility (`pattern-create`,
`patterns-list`, `samples-index`, `samples-list`, `projects-list`) had no
docstrings, so they rendered as blank rows in `audx --help` and cluttered the
top-level command list alongside the documented subcommand form
(`audx pattern create`, …).

Mark them `hidden=True` and give each a docstring pointing at its replacement.
They still work for anyone who scripted against them; they just no longer show
up in help, leaving the subcommand groups as the single documented surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
The `--samples` option set the AUDX_SAMPLES_DIR environment variable at command
time, but config.SAMPLES_DIR is resolved once at import — before launch runs —
so the value was never read, and DAWApp only stored the path without using it.
The TUI resolves samples through the process-global library, so the option was
silently ignored.

Add sampler.set_sample_library_root() and call it from `launch`, pointing the
global library the audio engine reads through at the requested directory. `open`
delegates to `launch`, so both commands now honour `--samples`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
The Makefile and .pre-commit-config.yaml still drove black, isort and flake8 —
none of which are project dependencies, so `make lint`, `make format` and every
pre-commit hook failed, and their formatting rules conflicted with the ruff
config the project actually uses (CI, README and CONTRIBUTING all standardise on
`ruff check` + `mypy`).

Point both at ruff: `make lint` runs `ruff check` + `mypy`, `make format` runs
`ruff check --fix`, `make build` uses `uv build`, and pre-commit uses the
official ruff hook (lint + autofix) plus the generic whitespace/EOF/YAML hooks.
No autoformatter is introduced (the tree is not ruff-format-clean and the
project doesn't use one).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
…errors

Three correctness fixes surfaced while auditing the render and CLI paths:

- **gain/pan modifiers were no-ops offline.** `parse_dsl` populated `step.gain_db`
  and `step.pan`, but the offline mixer only applied velocity, so documented
  modifiers like `| gain -6db` and `| pan L100` rendered bit-identically to the
  plain pattern. Apply gain in `_mix_clips`, and pan with a √2-normalised
  constant-power law so `pan 0` is identical to the un-panned path.
- **`mix set` / `mute` crashed or silently wrapped on bad channels.** They indexed
  the engine's numpy channel arrays directly, so an out-of-range channel raised a
  raw IndexError traceback and a negative one wrapped to the wrong channel. Add a
  bounds check with a friendly error.
- **Malformed rhythm specs dumped a traceback.** `hh ax8` hit an unguarded
  `int()`; `_beats_for_spec` now raises a descriptive error and `audx render`
  reports it cleanly and exits non-zero.

Re-synced the browser playground's bundled pattern.py copy; golden-vector
fixtures are unchanged since valid-input parsing is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
…or fixes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
`NxM` (e.g. `16x8`) places N evenly-spaced hits across the bar — the first
number is the hit count and the second is currently unused. The docs described
several conflicting things: the README said "8 hats over 16 steps", the code
comment said "8 hits across 16 steps", and pattern-language.md said "12 steps
per 8-beat bar". None matched the code, which produces N hits.

Correct all three to describe the real behaviour, and while in pattern-language.md
fix two more drifted examples: `snare 2/8` is beats 2 and 4 (not 2 and 6), and
replace the non-functional `bass synth C2 | arp 1/8` line (arp/synth aren't real
ops — it just made one hit) with a real melodic example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
`tune_semitones` was only applied on the synth-voice branch of `_voice_audio`;
when a step resolved to a real WAV the modifier was silently dropped. Add a
vari-speed `_repitch` helper (resample by the pitch ratio, like a hardware
sampler) and apply it to sample playback after sample-rate conversion, matching
how the synth kit and the docs ("transposes by resampling") already describe it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
…ckage

The src/audx/audio/ engine package isn't wired into the CLI/TUI yet and had no
tests, so several DSP bugs had gone unnoticed:

- Voice.process applied the mono envelope to the interleaved stereo buffer
  (`out *= env`), which broadcast-mismatched on every non-empty block — the
  voice crashed the moment it produced sound. Apply the envelope per channel.
- Constant-power pan used `cos(pan*π/4)`/`sin(pan*π/4)` over pan ∈ [-1, 1], so a
  centred voice played hard-left and a left pan inverted the right channel's
  polarity. Map pan onto the [0, π/2] quarter-circle first.
- _compute_envelope multiplied a full-length attack/release ramp into a
  buffer-length slice, raising a broadcast error when the ramp was longer than
  the block. Clip the ramp to the available frames.
- Mixer.process never honoured Channel.solo, so soloing a channel did nothing.
  Gate non-soloed channels when any solo is active.

Add tests covering all four.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
`_euclidean_grid` advanced its accumulator before testing the threshold, so the
first pulse landed late and step 0 was never set — e.g. `e(4,16)` hit 3,7,11,15
with no downbeat, contrary to the standard Euclidean convention every reference
implementation follows. Switch to the Bresenham change-detection form
(`(i*pulses)//steps`), which distributes pulses just as evenly but always places
one on step 0. Hit counts are unchanged, and a 0-pulse guard keeps `e(0,n)`
silent.

Mirror the fix in the browser DSL port (web/src/dsl.ts), regenerate the golden
fixtures, re-sync the bundled pattern.py and rebuild the studio bundle so the
Python parser, the TS port and the fixtures stay in lockstep (all 108 web
parity tests pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
@chrisschouk chrisschouk changed the title fix: resolve config paths per-platform instead of hardcoding macOS Polishing pass: correctness fixes, cross-platform paths, dev-tooling cleanup Jul 18, 2026
@chrisschouk
chrisschouk merged commit 1d65970 into main Jul 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants