Skip to content

Auto-settings engine: convert(auto=...) planner + scorer - #38

Open
yumiaura wants to merge 1 commit into
mainfrom
feat/auto-engine
Open

Auto-settings engine: convert(auto=...) planner + scorer#38
yumiaura wants to merge 1 commit into
mainfrom
feat/auto-engine

Conversation

@yumiaura

Copy link
Copy Markdown
Owner

Summary

Phase A of the --auto feature (engine only; the CLI flag + docs land in the
stacked Phase B PR). Adds convert(auto=True), which picks source, method,
voices, transpose and duty from the song's own features instead of the user
guessing.

Why hybrid, not "render everything and keep what passes": the two validators
are not on one scale — validate_audio (chords/band/nes) has 4 weak checks that
almost everything clears; validate_melody (lead/pitch) has 9 strict ones; and
alias/clip are ~0% for every mode by construction. A scored search across families
would therefore always favour chords/band/nes over lead. So:

  • Cross-family choice is rules on cheap content features — estimate_polyphony
    picks lead vs a fuller arrangement, drum_density picks chords vs a drum-backed
    band, and a dense steady beat upgrades band to the beat-quantised nes
    (choose_voices).
  • transpose is a uniform octave shift centring the median note on the ringtone
    register (choose_transpose) — pitch classes and harmony survive.
  • duty is the only scored micro-search (score_render): it changes timbre and
    aliasing but not the arrangement, so its candidates are comparable within the
    chosen family. 0.25 is tried first so it wins ties and output stays deterministic.

Refactor: the four per-branch render bodies move verbatim into a disk-free
render_events() helper, so the planner renders several duty candidates off one
Demucs separation + one basic-pitch transcription and keeps the best; convert()
writes the winner once. Arrangement settings now take None as a "not set"
sentinel — under auto the planner fills the unset ones (explicit values always
win); without auto they fall back to module defaults, so existing callers and the
current CLI are unchanged.

Test plan

  • convert("input.mp3", auto=True) selects instrumental/nes, centred register
    (transpose +24), E-minor key; all 4 quality checks pass.
  • Determinism: two auto runs are byte-identical (d5a2632…).
  • Override: auto=True, voices="chords" keeps chords, still auto-picks the rest.
  • Non-auto default render unchanged (chords, all checks ok).
  • python -c "import audio8bit.cli" smoke-imports (CI parity).
  • CI build + smoke-import green.

Introduce convert(auto=True): the pipeline picks source, method, voices,
transpose and duty from the song's own features instead of the user guessing.
No CLI surface yet - this is the engine; the --auto flag and docs follow.

Why a hybrid rather than "render everything and keep what passes": the two
validators are not on one scale (validate_audio has 4 weak checks that almost
everything clears; validate_melody has 9 strict ones; alias/clip are ~0% for
every mode by construction). So a scored search across families would always
favour chords/band/nes over lead. Instead:

- Cross-family choice is made by rules on cheap content features - polyphony
  (estimate_polyphony) picks lead vs a fuller arrangement, drum density
  (drum_density) picks chords vs a drum-backed band, and a dense steady beat
  upgrades band to the beat-quantised nes (choose_voices).
- transpose is a uniform octave shift centring the median note on the ringtone
  register (choose_transpose), so pitch classes and harmony survive.
- Only the duty cycle is a scored micro-search (score_render): it changes timbre
  and aliasing but not the arrangement, so its candidates are comparable within
  the chosen family. 0.25 is tried first so it wins score ties and output stays
  deterministic.

Refactor: the four per-branch render bodies move verbatim into a disk-free
render_events() helper returning (samples_u8, quality_ok, report_lines), so the
planner can render several duty candidates off one shared Demucs separation and
one basic-pitch transcription and keep only the best. convert() writes the
winner once. The arrangement settings now take None as a "not set" sentinel:
under auto the planner fills the unset ones (explicit values always win); without
auto an unset value falls back to its module default, so existing callers and the
current CLI behave exactly as before.

Verified on the instrumental test track: auto selects instrumental/nes with a
centred register and E-minor key, all quality checks pass, two runs are
byte-identical (deterministic), an explicit --voices chords is honoured while the
rest is still auto-picked, and the non-auto default render is unchanged.
@yumiaura yumiaura mentioned this pull request Jul 12, 2026
6 tasks
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.

1 participant