Polishing pass: correctness fixes, cross-platform paths, dev-tooling cleanup - #6
Merged
Conversation
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
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
… fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRqrPknqKw1D4gmCMRTXSg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/panmodifiers were no-ops offline.parse_dslpopulatedstep.gain_db/step.panbut the mixer only applied velocity —| gain -6dband| pan L100rendered identically to the plain pattern. Now applied (pan via a √2-normalised constant-power law, sopan 0is unchanged).| tunewas ignored for real WAV samples (synth voices only). Samples now repitch vari-speed, matching the synth kit and the docs.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 DIRwas 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 mutecrashed or mis-targeted on bad channels (rawIndexError, or negative-index wrap). Now a friendly out-of-range error.audx render "hh ax8"). Now a clear message, non-zero exit.audx.audiopackage 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/audxon Linux,~/Library/Application Support/audxon macOS,%APPDATA%\audxon Windows (was hardcoded to the macOS path everywhere).AUDX_CONFIG_DIR/AUDX_SAMPLES_DIR/AUDX_PROJECTS_DIRoverrides (with~expansion); the samples override is now one consistently-named variable.audxno longer creates directories as a side effect.Dev tooling & docs
ruff+mypytoolchain (they still drove uninstalledblack/isort/flake8, somake lint/formatand every hook failed).pattern-create, etc.) hidden from--help(still functional).NxMrhythm docs corrected to match behaviour (README, pattern-language.md and code comments disagreed); fixed two other drifted DSL examples.Verification
ruff check,mypy src/audx— cleanpytest— 236 passed · webtsc+vitest— 108 passed · studio bundle builds🤖 Generated with Claude Code