beqforge 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {beqforge-0.2.2 → beqforge-0.2.3}/PKG-INFO +1 -1
- {beqforge-0.2.2 → beqforge-0.2.3}/TODO.md +25 -23
- {beqforge-0.2.2 → beqforge-0.2.3}/beq_common/__init__.py +1 -1
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/AGENTS.md +5 -1
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/pipeline.py +26 -3
- beqforge-0.2.3/plans/texture-yardstick-sub-band.md +215 -0
- beqforge-0.2.3/plans/texture-yardstick-sub-band.patch +154 -0
- beqforge-0.2.3/tests/test_design_ripple_band.py +56 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.dockerignore +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.github/dependabot.yml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.github/workflows/build-designer-image.yml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.github/workflows/build-device-check.yml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.github/workflows/build-executable.yml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.github/workflows/dependabot-auto-merge.yaml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.github/workflows/tests.yml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.gitignore +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/.gitignore +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/beqforge.iml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/editor.xml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/jsonSchemas.xml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/misc.xml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/modules.xml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/.idea/vcs.xml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/AGENTS.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/CLAUDE.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/LICENCE.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/README.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beq_common/AGENTS.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beq_common/biquad.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beq_common/provenance.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beq_common/publication.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beq_common/types.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/__init__.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/accept.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/beqd.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/biquad.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/cache.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/charts.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/cli.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/design.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/designer.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/diagnose.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/explain.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/extraction.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/filters.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/harness.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/identify.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/material.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/record.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/reference.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/rolloff.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge/verify.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge-device-check.spec +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge.spec +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/AGENTS.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/__init__.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/analyse.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/audio.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/catalogue.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/cli.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/coefficients.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/engines.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/evidence.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/manifest.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/measurement.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/profiles.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/qualification.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/report.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/transactions.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqforge_device_check/verify.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/AGENTS.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/__init__.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/cache.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/cli.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/core.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/data/seed-manifest.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/beqoptimiser/data/seed.json.gz +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/device-check-examples/simulation.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/device-check.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/README.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/assets/example-1.png +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/assets/example-2.png +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/entry-results.json.gz +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser-report/statistics.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/docs/optimiser.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/evidence_validation.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/negative_corpus.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/packaging/designer/Dockerfile +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/packaging/designer/smoke.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/packaging/device-check/NOTICE.txt +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/F2-catalogue-filter-optimisation.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/F2-device-precision-validation.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/F2-package-consolidation.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/R2a-server-stage-cache.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/done-baseline-evidence.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/done-design-and-pipeline.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/research-design-decisions.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/plans/test-portability-and-runtime.md +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/pyproject.toml +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/__init__.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/conftest.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/fixtures/optimiser_seed_entry.json +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_cli.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_accept.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_cache.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_cache_stores.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_channel_evidence.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_charts.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_corpus.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_design.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_designer.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_designer_server.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_device_rate.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_diagnose.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_evidence.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_evidence_excess.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_exclusions.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_explain.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_extraction.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_filters.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_frozen_cache.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_guard.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_harness.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_headroom.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_hum.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_identify.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_jacobian.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_parallel.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_pipeline.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_playback.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_playback_headroom.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_publication.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_record.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_record_fingerprint.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_reference.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_references.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_rolloff.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_strategy_config.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_timings.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_design_verify.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_designer_image.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_designer_server_startup.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_analyse.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_catalogue.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_catalogue_predict.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_cli.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_engines.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_evidence.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_examples.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_manifest.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_measurement.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_qualification.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_transactions.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_device_check_verify.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_extract_layout.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_optimiser.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_optimiser_cache.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_optimiser_report.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tests/test_package_boundaries.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/__init__.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/build_optimiser_seed.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/design_beq.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/designer_server.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/a1_sweep.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/compare_records.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/compare_verdicts.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/inject_variants.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p14_compare.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p14_jacobian.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p14_run_pipeline.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p14_surrogate.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p18_compare_greedy.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p18_compare_seeded.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p18_compare_structure.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p18_greedy.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p18_seeded.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p18_structure.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/p20_smooth_drift.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/probe.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/refit_probe.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/request_timing.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/score_injected.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/experiments/t8_floor_rules.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/extract.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/ledger_template.html +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/negative_corpus.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/optimiser_catalogue_report.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/prepare_device_check_helper.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/render_ledger.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/replay.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/seed_optimiser_cache.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/smoke_test_device_check.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/smoke_test_exe.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/smoke_test_install.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/summarise.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/tools/validate_evidence.py +0 -0
- {beqforge-0.2.2 → beqforge-0.2.3}/uv.lock +0 -0
|
@@ -18,29 +18,31 @@ are retained so records and code comments remain traceable. Completed items are
|
|
|
18
18
|
| Priority | ID | Status | Value, work and blocker or reason for parking |
|
|
19
19
|
| --- | --- | --- | --- |
|
|
20
20
|
| 1 | Playback contract | open | Reconcile the supplied bass-management model with the model actually used for targets, verification and clipping. Only `lpf_fs` is consumed today; check the ignored fields before claiming the listener's clipping cost. Caller propagation is an external subtask. |
|
|
21
|
-
| 2 |
|
|
22
|
-
| 3 |
|
|
23
|
-
| 4 |
|
|
24
|
-
| 5 | Steep-filter
|
|
25
|
-
| 6 |
|
|
26
|
-
| 7 |
|
|
27
|
-
| 8 |
|
|
28
|
-
| 9 |
|
|
29
|
-
| 10 |
|
|
30
|
-
| 11 |
|
|
31
|
-
| 12 |
|
|
32
|
-
| 13 |
|
|
33
|
-
| 14 |
|
|
34
|
-
| 15 |
|
|
35
|
-
| 16 |
|
|
36
|
-
| 17 |
|
|
37
|
-
| 18 |
|
|
38
|
-
| 19 |
|
|
39
|
-
| 20 |
|
|
40
|
-
| 21 |
|
|
41
|
-
| 22 |
|
|
42
|
-
| 23 |
|
|
43
|
-
| 24 |
|
|
21
|
+
| 2 | Texture yardstick band | open (E10 only) | Implemented: the mix's ripple is measured about a cubic over the unchanged band, so the LFE knee is trend. 9 (2009), Beauty and the Beast, Crazy, Stupid, Love and Despicable Me 3 now accept; Carrie stays declined. Probe and protocol unchanged; corpus 0/45 gate pass. Neither width-normalised form separated `broadband/5`. The degree was chosen on corpus and library data, so it needs the fresh E10 validation before it counts as settled; see [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
|
|
22
|
+
| 3 | E10 | open | Predeclare fresh content-goal validation before further tuning or a default change. Existing seeds and corpus have repeatedly informed development; writing the protocol needs no new audio. |
|
|
23
|
+
| 4 | E1 | open | Add record-only scene-held-out verification: evidence independent of the material that generated the target. A gate needs a separate demonstrated benefit. |
|
|
24
|
+
| 5 | Steep-filter scoring | open | Implement coloured/codec-like floors and recipe-specific truth bands under E10; inspect shape failures and published-ceiling excess. Existing titles can supply programme texture. |
|
|
25
|
+
| 6 | Steep-filter default | blocked | Recovery improves substantially with `--content-edge`, but changing the default needs E10, the scoring above, listening judgements and more independent real steep titles. Safe House already works with the default; keep the option off meanwhile. |
|
|
26
|
+
| 7 | F2 / device behaviour | in progress; first hardware pilot measured | Coefficient quantisation does not establish hardware accuracy. Limit changes need measured internal arithmetic/noise/limit cycles on a named device and a capture setup; the [measurement plan](plans/F2-device-precision-validation.md) specifies Python sweep/capture, optional REW cross-checks, software controls, single-entry/full-catalogue analysis and a standalone cross-platform executable. The `beqforge-device-check` preview includes frozen manifests, capture/recovery, CLI-only miniDSP controls (minidsp-rs 0.1.12, batched through `-f`) with float32 transport attribution, staged qualification/convergence, uncertainty-aware filter checks, entry/unique-cascade frequency distributions and matched-device deltas. First verified pilot on a miniDSP 2x4 HD (USB loopback, 96 kHz): the device plays its float32 coefficients as predicted (worst 0.0074 dB, 2-200 Hz), while float32 coefficients themselves put 18.8 dB of error into a 5 Hz Q 6 peak; see the plan, section 12. First catalogue sample (50 cascades): 49 play as predicted on every bin (worst 0.0082 dB); the 50th was clipped by the device and is to be remeasured (F2a, plan section 13). Next: the `grid` and `boundary` suites on hardware to map where float32 coefficients stop representing low, high-Q filters. No Q-limit change yet. |
|
|
27
|
+
| 8 | Validation coverage (former TODO 10) | blocked | Broad real-programme generalisation needs the missing shapes and independently characterised paired releases listed below. Sparse titles exist; their absolute truth and wider sparse coverage remain missing. |
|
|
28
|
+
| 9 | Identification | blocked | Generalising `N(f)` and its fit band needs independent, varied source material, especially paired releases. Sensitivity experiments can proceed; programme-only uncertainty cannot identify the cause of a droop. |
|
|
29
|
+
| 10 | E4 | parked | No measured failure has been attributed to isolated ceiling holes. Reopen if unevenness or E9 excess is traced to such bins; smoothing may only reduce the raw licence. |
|
|
30
|
+
| 11 | F4 follow-up | parked | Both fitter-objective selection spikes lost passes/true positives. Judge several fits only with a demonstrated usable alternative discarded before `_judge`; a smaller residual is insufficient evidence. |
|
|
31
|
+
| 12 | F3 follow-up | parked | A fifth section moved no verdict when tried and costs more fitting. Reopen if an evidence-supported target has a demonstrated usable fit beyond four sections and no usable fit within four. |
|
|
32
|
+
| 13 | T2 | parked | The steep-variant audit found no swept cap binding a weak winner. Derive caps only when a binding cap demonstrably prevents a better supported result. |
|
|
33
|
+
| 14 | Residual (former TODO 5) | open | Correct the reported target error and its actual measurement band without changing fit selection. Primarily audit accuracy; any escalation/objective change is a separate, higher-impact decision. Non-parametric reporting is already allowed upstream. |
|
|
34
|
+
| 15 | Authored features | parked | Manual `--exclude` works; no validated automatic detector exists. Reopen with real labelled features and negatives showing that detection preserves legitimate bass. |
|
|
35
|
+
| 16 | Dynamic processing (former TODO 11) | parked | No suspected real case has scoped a useful tripwire. Reopen when compression/limiting demonstrably compromises this correction; non-LTI processing alone does not prove every BEQ is invalid. |
|
|
36
|
+
| 17 | Tilted passband reference (goal/corpus follow-up) | parked | No real in-sub-band case needs a sloping reference yet; Dossier's abstention was decided. Reopen with one. Goal-aware corpus labels belong to the open E10 protocol work. Nearest so far: 13 Going on 30's mix tilts about −7 dB/oct above 50 Hz (bass is coherent across channels, ~200 Hz is not) and finds its plateau only through the one-sided fallback. That reference reads a 36 dB deficit below 25 Hz; each channel's own plateau reads 30-34 dB. A reference built from the channels would be 2-5 dB less generous there (see plans/done-design-and-pipeline.md, 11 October 2026). |
|
|
37
|
+
| 18 | Mixed-channel policy | parked | `channel_scope` is currently an unpopulated optional response field, not a computed `mixed` finding awaiting a gate. Reopen with a case where existing per-channel restoration and common pricing produce an unsuitable mix correction. |
|
|
38
|
+
| 19 | Fraction dial (former TODO 7) | parked | No user/title has needed partial licensed restoration as a preference; tilt and tolerance already ship. Reopen with a concrete requested outcome, then price and validate it like every target. |
|
|
39
|
+
| 20 | Protective corner (former TODO 9) | parked | The corner is already derived from boost cap and implied order, floored by `lowest_frequency_hz`. No case requires an independent override or different fallback alignment; reopen with one. |
|
|
40
|
+
| 21 | Device configuration on the wire | parked | The current contract deliberately omits device preference; server flags already configure realisation. Reopen if one server must serve different device formats/rates per request; then agree an upstream extension. Hardware validity remains F2. |
|
|
41
|
+
| 22 | Catalogue comparison (former TODO 8) | parked | A disagreement detector has no demonstrated accuracy benefit. Reopen with an independent discrepancy to investigate; authored filters remain comparisons, never calibration targets. |
|
|
42
|
+
| 23 | Performance | parked | Further reuse changes cost, not filter accuracy. Reopen after measuring repeated numerical identities or material remaining analysis cost; missed-fit search belongs to F3/F4 instead. |
|
|
43
|
+
| 24 | Chart peak bias | parked | Deliberate catalogue-compatible per-bin maximum; it affects presentation only. Reopen if a reader needs an alternative percentile view; keep it distinct from the 95th-percentile evidence envelope. |
|
|
44
|
+
| 25 | Documentation citations | open | Remove or repoint orphan references and stale descriptions of shipped behaviour. Low filter impact, but prevents future changes being based on obsolete assumptions. |
|
|
45
|
+
| 26 | Texture yardstick retune (library review) | parked | The cubic (row 2) keeps the 200 Hz band only because the sub-band cap admitted one synthetic negative, `broadband/5`: chance unevenness in noise bursts, at a 2.6 dB boost. With 45 gated cases a single case moves the gate's upper bound from 0.079 to 0.118. Carrie is still declined (0.85x about a cubic, 1.33x with the cap). Reopen when the beqdesigner library review (~1,290 titles) is complete. Then retune against its declines: the band edge, the trend degree, and whether the gate weights a false acceptance by its boost. The library holds no known negative, so it can expose wrong declines but not measure false acceptances. Any retune still needs the corpus or a fresh E10 set for that, ideally with negatives built from real programme texture (row 5). See [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
|
|
44
46
|
|
|
45
47
|
## Review evidence
|
|
46
48
|
|
|
@@ -333,7 +333,11 @@ least `min_judge_octaves` above the floor — no fixed top (historical review T6
|
|
|
333
333
|
(`pipeline.passband_ripple_db`) is left alone: it abstains with `within_programme_ripple`
|
|
334
334
|
before any strategy runs. That ripple is the title's own yardstick, so there is no constant.
|
|
335
335
|
Crest to trough rather than a one-sided dip, because the reference sits on the crests. This
|
|
336
|
-
is what took the negative corpus from 4 false acceptances in 45 to 0.
|
|
336
|
+
is what took the negative corpus from 4 false acceptances in 45 to 0. The mix's ripple is
|
|
337
|
+
taken about a cubic in log frequency, so an LFE-led mix's knee above the crossover is trend,
|
|
338
|
+
not texture; each channel's is taken about a line. Don't shorten the band instead:
|
|
339
|
+
crest to trough shrinks with bin count, and the 1x boundary was calibrated on the full band
|
|
340
|
+
([plans/texture-yardstick-sub-band.md](../plans/texture-yardstick-sub-band.md)).
|
|
337
341
|
* **Steep filters: the default declines them; `judge_from_content_edge` is an opt-in that
|
|
338
342
|
recovers them and lifts some noise.** Below a steep filter's edge the priced target already
|
|
339
343
|
falls into the quiet floor with the programme — contrast bounds it — and the shape clauses
|
|
@@ -772,6 +772,9 @@ MAINS_HZ = (50.0, 60.0)
|
|
|
772
772
|
# inside the 50 Hz spacing so it never reaches the next harmonic
|
|
773
773
|
HUM_NOTCH_HALF_WIDTH_HZ = 2.5
|
|
774
774
|
HUM_SURROUND_HALF_WIDTH_HZ = 10.0
|
|
775
|
+
# the mix's texture is measured about a cubic in log frequency, so an LFE-led mix's knee above
|
|
776
|
+
# the crossover is trend, not ripple (plans/texture-yardstick-sub-band.md)
|
|
777
|
+
MIX_TEXTURE_TREND_DEGREE = 3
|
|
775
778
|
|
|
776
779
|
|
|
777
780
|
def mains_hum_notches(
|
|
@@ -815,7 +818,11 @@ def mains_hum_notches(
|
|
|
815
818
|
|
|
816
819
|
|
|
817
820
|
def passband_ripple_db(
|
|
818
|
-
material: Material,
|
|
821
|
+
material: Material,
|
|
822
|
+
params: "PipelineParams",
|
|
823
|
+
noise_floor_hz: float = math.nan,
|
|
824
|
+
*,
|
|
825
|
+
trend_degree: int = 1,
|
|
819
826
|
) -> tuple[float, tuple[float, float]] | None:
|
|
820
827
|
"""The programme's own texture where nothing is missing: crest-to-trough, dB, and where.
|
|
821
828
|
|
|
@@ -823,6 +830,17 @@ def passband_ripple_db(
|
|
|
823
830
|
correction — the top of the judged band to the top of the analysis band — as the spread of
|
|
824
831
|
the curve about its own log-frequency trend, so a tilted passband is not read as ripple.
|
|
825
832
|
|
|
833
|
+
The trend is a polynomial of `trend_degree` in log frequency. The mix's texture blocker
|
|
834
|
+
asks for a cubic (`MIX_TEXTURE_TREND_DEGREE`). In an LFE-led mix the level falls away
|
|
835
|
+
where the LFE channel's content ends (60-100 Hz), a knee a straight line cannot remove, so
|
|
836
|
+
it read as ripple and withheld a real deficit: 9 (2009) measured 8.8 dB of "ripple" against
|
|
837
|
+
a 6.2 dB shortfall, 2.2 dB about a cubic. The band is not shortened instead. Crest to
|
|
838
|
+
trough shrinks with the number of bins even on noise, so ending it at the crossover lowered
|
|
839
|
+
every yardstick and admitted a broadband negative on the corpus. A cubic keeps the band the
|
|
840
|
+
1x boundary was calibrated on and leaves the negatives where they were: at most 0.76x about
|
|
841
|
+
a cubic against 0.77x about a line, with every real and filtered title at 1.08x and up.
|
|
842
|
+
Each channel's own rule (`channels_missing_low_end`) keeps the line.
|
|
843
|
+
|
|
826
844
|
Crest to trough, not a one-sided dip, because the reference sits near the 90th percentile
|
|
827
845
|
of the spectrum, on the crests: a shortfall measured from it spans the whole swing. On the
|
|
828
846
|
negative corpus (IMPROVEMENT_PLAN E2) every unfiltered title's low-end deficit came in
|
|
@@ -847,7 +865,7 @@ def passband_ripple_db(
|
|
|
847
865
|
return None
|
|
848
866
|
smooth = smooth_unexcluded(response, freqs, bands, 15, levels=True)[band]
|
|
849
867
|
octaves = np.log2(freqs[band])
|
|
850
|
-
about = smooth - np.polyval(np.polyfit(octaves, smooth,
|
|
868
|
+
about = smooth - np.polyval(np.polyfit(octaves, smooth, trend_degree), octaves)
|
|
851
869
|
return float(np.max(about) - np.min(about)), (low, high)
|
|
852
870
|
|
|
853
871
|
|
|
@@ -1550,7 +1568,12 @@ def analyse(
|
|
|
1550
1568
|
f"programme (tracking stops at {diagnosis.noise_floor_hz:.1f} Hz), so there "
|
|
1551
1569
|
"is no bass passband to restore towards; restoration withheld"
|
|
1552
1570
|
)
|
|
1553
|
-
texture = passband_ripple_db(
|
|
1571
|
+
texture = passband_ripple_db(
|
|
1572
|
+
material,
|
|
1573
|
+
params,
|
|
1574
|
+
diagnosis.noise_floor_hz,
|
|
1575
|
+
trend_degree=MIX_TEXTURE_TREND_DEGREE,
|
|
1576
|
+
)
|
|
1554
1577
|
low_end = low_end_deficit_db(material, params, diagnosis.noise_floor_hz)
|
|
1555
1578
|
if texture is not None and low_end is not None:
|
|
1556
1579
|
ripple, (edge_low, edge_high) = texture
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Texture yardstick: measure it in the band the sub plays
|
|
2
|
+
|
|
3
|
+
**Status: implemented (17 October 2026), as a cubic detrend over the unchanged band, not as
|
|
4
|
+
either width-normalised measure below.** See [Outcome](#outcome-17-october-2026). The rest of
|
|
5
|
+
this document is the original handoff, from the beqdesigner library design run (October
|
|
6
|
+
2026), where every decline of the full JRiver library (~1,290 films) is being reviewed. The
|
|
7
|
+
first two attempts, which only shorten the band, are in
|
|
8
|
+
[texture-yardstick-sub-band.patch](texture-yardstick-sub-band.patch); it applies to `main`
|
|
9
|
+
at `fd9ae22`. It fails the corpus gate, so it was not committed.
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
`pipeline.passband_ripple_db` measures the programme's texture as the crest-to-trough swing
|
|
14
|
+
of the smoothed mean spectrum about its own linear log-frequency trend. It runs from the top
|
|
15
|
+
of the judged band to the **top of the analysis band** (`diagnose.band_hz[1]`, 200 Hz). If the
|
|
16
|
+
deepest low-end shortfall is no bigger than that swing, the title is declined
|
|
17
|
+
`within_programme_ripple`. `channels_missing_low_end` uses the same function for each
|
|
18
|
+
channel.
|
|
19
|
+
|
|
20
|
+
Above the sub crossover (80 Hz), the mix's shape is not texture the sub plays. In an
|
|
21
|
+
LFE-led 5.1 mix the level falls away where the LFE content ends (about 60-100 Hz). A linear
|
|
22
|
+
detrend cannot remove that knee, so it is counted as ripple and the yardstick gets bigger.
|
|
23
|
+
The rule's 1x boundary was calibrated on the negative corpus, which is two-channel synthetic
|
|
24
|
+
material with no LFE knee. That is why calibration did not catch this.
|
|
25
|
+
|
|
26
|
+
## Evidence
|
|
27
|
+
|
|
28
|
+
**9 (2009)**, 5.1 DTS-HD, LFE carries 77% of the plateau. Local rerun at v0.2.1, with the
|
|
29
|
+
material digest matching the deployed request:
|
|
30
|
+
|
|
31
|
+
| Yardstick band | Ripple | Shortfall | Outcome |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| 23-200 Hz (current) | 8.8 dB | 6.2 dB | declined `within_programme_ripple` |
|
|
34
|
+
| 23-80 Hz (crossover) | 5.1 dB | 6.2 dB | accepts `flatten`: LowShelf 13.0 Hz +6.4 dB Q 0.72, confidence 0.97 |
|
|
35
|
+
|
|
36
|
+
- The community BEQ (aron7awol) is 2x LowShelf 10 Hz +3.2 dB + LowShelf 27 Hz +2 dB: the same
|
|
37
|
+
kind and size of correction.
|
|
38
|
+
- The level-invariance evidence supports a real filter: the loudness strata agree down to
|
|
39
|
+
4.15 Hz, and content tracks all the way down.
|
|
40
|
+
|
|
41
|
+
Other titles declined this way in the run (shortfall vs the ripple capped at the crossover):
|
|
42
|
+
|
|
43
|
+
| Title | Shortfall | Capped ripple |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| Beauty and the Beast (1991) | 11.7 dB | 9.74 dB |
|
|
46
|
+
| Carrie (1976) | 8.4 dB | 6.32 dB |
|
|
47
|
+
| Crazy, Stupid, Love (2011) | 10.8 dB | 4.06 dB |
|
|
48
|
+
| Despicable Me 3 (2017) | 4.3 dB | 3.1 dB |
|
|
49
|
+
|
|
50
|
+
Battle: Los Angeles and Changeling are not affected: their capped ripple still exceeds their
|
|
51
|
+
shortfall.
|
|
52
|
+
|
|
53
|
+
## What was tried (the patch)
|
|
54
|
+
|
|
55
|
+
1. **Capping both rules at the sub edge** (`min(crossover_hz, bus_lowpass_hz)`, the edge the
|
|
56
|
+
plateau blocker already uses). Corpus: 1 false acceptance in 45 gated (`broadband/5`,
|
|
57
|
+
`counterfactual/25dB`). The gate's upper bound is 0.118, above 0.100: **fail**. Through the
|
|
58
|
+
per-channel rule, counterfactual targets also grew on Bugonia, Caught Stealing (peak
|
|
59
|
+
4.3 -> 12.3 dB) and Incredible Hulk.
|
|
60
|
+
2. **Capping the mix rule only** (`in_sub_band=True`; each channel keeps the whole passband).
|
|
61
|
+
The probe is unchanged on all 11 baseline titles, and the protocol is identical on both
|
|
62
|
+
seeds. 9, Beauty and the Beast and Carrie are all accepted:
|
|
63
|
+
- 9: LowShelf 13.0 Hz +6.4 dB
|
|
64
|
+
- Beauty and the Beast: LowShelf 13.8 Hz +2.4 dB, LowShelf 35.1 Hz +1.0 dB, PeakingEQ 24.4 Hz +4.6 dB
|
|
65
|
+
- Carrie: LowShelf 6.5 Hz +3.7 dB
|
|
66
|
+
|
|
67
|
+
The corpus still fails: `flatten` now accepts `broadband/5` (peak boost 2.6 dB), 1/45,
|
|
68
|
+
upper bound 0.118.
|
|
69
|
+
|
|
70
|
+
**Why the gate fails:** crest-to-trough is an extreme-value statistic, so it shrinks as the
|
|
71
|
+
band holds fewer bins, even on pure noise. The 1x boundary was earned with the 200 Hz band.
|
|
72
|
+
Shortening the band therefore lowers every title's yardstick, the genuine negatives'
|
|
73
|
+
included. Fixing the band is really a recalibration.
|
|
74
|
+
|
|
75
|
+
## Wanted: a width-normalised measure
|
|
76
|
+
|
|
77
|
+
Measure texture with a statistic that does not depend on how wide the band is. Then the band
|
|
78
|
+
can end at the sub edge without moving the 1x boundary the corpus set. Two candidate forms;
|
|
79
|
+
the owner of beqforge chooses:
|
|
80
|
+
|
|
81
|
+
- **A scaled robust spread.** Compute the residual about the trend as now, then take a spread
|
|
82
|
+
that converges with bin count instead of growing with it (standard deviation, or an
|
|
83
|
+
inter-percentile range such as 5th-95th). Scale it by the ratio of crest-to-trough to that
|
|
84
|
+
spread, measured over the full 200 Hz band on the calibration titles. Then, on the band the
|
|
85
|
+
rule was calibrated on, the yardstick stays in units of what the corpus measured, while a
|
|
86
|
+
shorter band no longer shrinks it just by being shorter.
|
|
87
|
+
- **A fixed-width window.** Take crest-to-trough over a sliding window of fixed octave width,
|
|
88
|
+
inside the judged-top-to-sub-edge band (the largest or the median of the windows), so every
|
|
89
|
+
title is measured over the same width. Choose the window from the corpus, and fall back to
|
|
90
|
+
the current measure where the band is narrower than one window.
|
|
91
|
+
|
|
92
|
+
Either way, keep what the docstring already relies on: the reference sits on the crests, so
|
|
93
|
+
the yardstick must still span the whole swing a shortfall is measured across; hum notches stay
|
|
94
|
+
excluded (`mains_hum_notches` still searches the whole passband, because below 80 Hz only one
|
|
95
|
+
harmonic remains); and when the judged band already reaches past the sub edge, the analysis
|
|
96
|
+
band is kept.
|
|
97
|
+
|
|
98
|
+
Decide separately whether the per-channel rule moves too. The first attempt shows that a
|
|
99
|
+
smaller yardstick there judged more channels short, so leaving it alone is the safer
|
|
100
|
+
default.
|
|
101
|
+
|
|
102
|
+
## Completion criteria
|
|
103
|
+
|
|
104
|
+
- `tools/negative_corpus.py`: false acceptances 0/45, gate pass, with `broadband/5` declined.
|
|
105
|
+
- `tools/validate_evidence.py` and the protocol: unchanged on both seeds; the probe unchanged
|
|
106
|
+
on the 11 baseline titles, or each change explained.
|
|
107
|
+
- 9 (2009) accepted with a correction of the size above. Beauty and the Beast and Carrie
|
|
108
|
+
accepted, or the reason they are not recorded.
|
|
109
|
+
- If the measure needs any constant fitted to the corpus, the fresh validation E10 asks for
|
|
110
|
+
applies before it becomes the default.
|
|
111
|
+
|
|
112
|
+
## Repro
|
|
113
|
+
|
|
114
|
+
The library run's audio and scripts are on the beq share (`/media/beq`):
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
python -I /media/beq/triage/scripts/ripple_counterfactual.py <beqforge checkout> \
|
|
118
|
+
"/media/beq/work/9 - audio 1" \
|
|
119
|
+
c942feb4b5f22f680bb4fc8fc484d63afe92c4dc796b1c26701a07a5b93c5739 <out dir>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
It reruns the title as deployed, then with the yardstick capped at the crossover, and checks
|
|
123
|
+
the material digest against the deployed request. The patch's
|
|
124
|
+
`tests/test_design_ripple_band.py` has a synthetic LFE-led source (noise with a knee at 70
|
|
125
|
+
Hz) that is a reasonable unit test for any replacement.
|
|
126
|
+
|
|
127
|
+
## Outcome (17 October 2026)
|
|
128
|
+
|
|
129
|
+
### Neither width-normalised form separates the corpus
|
|
130
|
+
|
|
131
|
+
Both forms were scored offline before any code changed. Each candidate measured the same
|
|
132
|
+
residual as `passband_ripple_db`, over the full band and capped at the sub edge, on all 63
|
|
133
|
+
corpus cases (seeds 1-9), the 11 baseline titles, the 43 variants and the 8 library titles
|
|
134
|
+
above. A scaled spread took the corpus median of crest-to-trough/spread over the full band as
|
|
135
|
+
its scale. Shortfall/yardstick, where above 1 is accepted:
|
|
136
|
+
|
|
137
|
+
| Sub-band yardstick | Worst gated negative | 9 | Beauty and the Beast | Despicable Me 3 |
|
|
138
|
+
| --- | --- | --- | --- | --- |
|
|
139
|
+
| crest-to-trough (the patch) | 1.11 (`sparse_dialogue/9`)\* | 1.22 | 1.20 | 1.39 |
|
|
140
|
+
| 4.79 x standard deviation | 1.05 (`broadband/5`)\* | 1.15 | 1.10 | 1.09 |
|
|
141
|
+
| 1.48 x 5th-95th percentile | 1.06 (`broadband/5`)\* | 1.33 | 0.99 | 1.12 |
|
|
142
|
+
| largest ¾-octave window | 1.11 (`sparse_dialogue/9`)\* | 1.14 | 1.13 | 1.30 |
|
|
143
|
+
|
|
144
|
+
\* Excluding `sparse_dialogue/5`, at 3.2-5.2x on every one. Its judged top is 77 Hz, so its
|
|
145
|
+
sub band is 0.06 octaves wide. A sub-band yardstick also needs a minimum width, which neither
|
|
146
|
+
form had.
|
|
147
|
+
|
|
148
|
+
The handoff's premise is only half right. `broadband/5`'s sub band is smoother in fact, not
|
|
149
|
+
just shorter: its standard deviation falls from 0.9 to 0.6 dB, and so does every
|
|
150
|
+
width-invariant statistic. A shorter band also lets the linear trend take up more of the slow
|
|
151
|
+
structure. No statistic over the sub band left a margin worth calling a separation.
|
|
152
|
+
|
|
153
|
+
### What was built: a cubic trend over the same band
|
|
154
|
+
|
|
155
|
+
The knee is a trend problem, not a band problem, so the band stays where the 1x boundary was
|
|
156
|
+
calibrated (judged top to `diagnose.band_hz[1]`), and only the mix's detrend changes.
|
|
157
|
+
`passband_ripple_db` takes `trend_degree` (1 by default). The mix's texture blocker passes
|
|
158
|
+
`MIX_TEXTURE_TREND_DEGREE` = 3. `channels_missing_low_end` keeps the line, as the handoff
|
|
159
|
+
advised.
|
|
160
|
+
|
|
161
|
+
The same study, full band, mix rule:
|
|
162
|
+
|
|
163
|
+
| Trend | Worst gated negative | Lowest real/filtered/variant | 9 | B&tB | CSL | DM3 | Carrie |
|
|
164
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
165
|
+
| line (before) | 0.77 | 1.01 (Incredible Hulk) | 0.71 | 0.98 | 0.98 | 0.62 | 0.82 |
|
|
166
|
+
| quadratic | 0.82 | 1.05 (Bugonia) | 1.21 | 1.00 | 1.06 | 1.31 | 0.86 |
|
|
167
|
+
| hinge at 80 Hz | 0.77 | 1.05 (Bugonia) | 1.00 | 1.01 | 1.08 | 1.14 | 0.87 |
|
|
168
|
+
| **cubic** | **0.76** | **1.08 (Bugonia)** | **2.79** | **1.11** | **1.06** | **1.43** | **0.85** |
|
|
169
|
+
|
|
170
|
+
The cubic is the only trend that widens the separation at both ends. Over all 125 items, it
|
|
171
|
+
changes the texture verdict on exactly four: 9, Beauty and the Beast (1991), Crazy, Stupid,
|
|
172
|
+
Love and Despicable Me 3. A cubic can raise a yardstick as well as lower it (`sparse_dialogue/9`
|
|
173
|
+
measures 5.54 dB about it against 5.24 about a line), but no verdict went that way. Battle: Los
|
|
174
|
+
Angeles (0.28) and Changeling (0.89) stay declined.
|
|
175
|
+
|
|
176
|
+
### Regression check
|
|
177
|
+
|
|
178
|
+
* **Probe** (11 baseline titles, default params): unchanged on every title.
|
|
179
|
+
* **Protocol**, seeds 101 and 947, before in a HEAD worktree (`a4b4d06`, the code of `fd9ae22`) and after: every decision,
|
|
180
|
+
score and failure identical. The only difference is the ripple figure quoted in two
|
|
181
|
+
`within_programme_ripple` notes per seed.
|
|
182
|
+
* **Negative corpus**, seeds 1-9: 0/45 false acceptances on the gated shapes, upper bound
|
|
183
|
+
0.079 against 0.100, **pass**. All of `broadband/*` abstains. Ungated: `natural_droop` 4/9,
|
|
184
|
+
`filtered` 7/9 true positives, median recovery 1.27 dB RMS. The committed
|
|
185
|
+
`negative_corpus.json` is from an older revision (`4c6d5c3`), and 7 cases differ from it:
|
|
186
|
+
5 in recovery or peak boost on accepted cascades, and `natural_droop/7`, now abstaining.
|
|
187
|
+
None is this change: no corpus case's texture verdict moves under the cubic, and the
|
|
188
|
+
blocker is all the change touches. The committed file was not refreshed.
|
|
189
|
+
* **Library titles**, real `designer.design` runs with the deployed request shape (bass
|
|
190
|
+
management `lpf_fs` 80):
|
|
191
|
+
|
|
192
|
+
| Title | Material digest | Accepted | Confidence |
|
|
193
|
+
| --- | --- | --- | --- |
|
|
194
|
+
| 9 (2009) | `c942feb4…`, matches the deployed request | `flatten`: LowShelf 13.0 Hz +6.4 dB Q 0.72 | 0.97 |
|
|
195
|
+
| Beauty and the Beast (1991) | `8d98789b…` | LowShelf 13.8 Hz +2.4 dB, LowShelf 35.1 Hz +1.0 dB, PeakingEQ 24.4 Hz +4.6 dB | 0.95 |
|
|
196
|
+
| Crazy, Stupid, Love | `df2456f6…` | `flatten`: LowShelf 34.5 Hz +10.0 dB Q 0.79, PeakingEQ 28.6 Hz −7.6 dB Q 2.07 | 0.93 |
|
|
197
|
+
| Despicable Me 3 | `7cf1e523…` | LowShelf 28.2 Hz +1.6 dB, LowShelf 15.0 Hz +1.0 dB | 0.96 |
|
|
198
|
+
|
|
199
|
+
9 is the correction the handoff expected, and Beauty and the Beast is the cascade the
|
|
200
|
+
patch's second attempt found. Crazy, Stupid, Love's cascade looks odd until its corrected
|
|
201
|
+
curve is checked: the average goes from a steep fall below about 25 Hz to flat at about
|
|
202
|
+
−72 dB down to about 5 Hz. The cut shapes the shelf; it does not fight it.
|
|
203
|
+
* **Carrie stays declined**: an 8.4 dB shortfall against 9.9 dB of ripple about a cubic
|
|
204
|
+
(10.3 about a line). Its judged band starts at 10 Hz, so its passband runs 4.3 octaves,
|
|
205
|
+
and the texture there is broadband, not a knee. Shortening the band would accept it. Nothing
|
|
206
|
+
measured here licenses doing that.
|
|
207
|
+
|
|
208
|
+
### Caveat: the degree is a choice made on this data
|
|
209
|
+
|
|
210
|
+
Degree 3 was picked from three degrees and a hinge after seeing the corpus and the library
|
|
211
|
+
ratios. That is a modelling choice informed by the corpus, so the completion criterion's E10
|
|
212
|
+
clause applies. Fresh validation should precede treating it as settled. The hinge at the
|
|
213
|
+
crossover is the one variant with no free choice (its bend is the playback crossover). It
|
|
214
|
+
leaves every corpus ratio as it was, but puts 9 at exactly 1.00x, so it does not fix the
|
|
215
|
+
reported title.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
diff --git a/beqforge/pipeline.py b/beqforge/pipeline.py
|
|
2
|
+
index 699762d..6a6b345 100644
|
|
3
|
+
--- a/beqforge/pipeline.py
|
|
4
|
+
+++ b/beqforge/pipeline.py
|
|
5
|
+
@@ -814,8 +814,21 @@ def mains_hum_notches(
|
|
6
|
+
return ()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
+def _sub_feed_edge_hz(params: "PipelineParams") -> float:
|
|
10
|
+
+ """The top of the band the sub plays: the lower of the crossover and the sub-bus low-pass."""
|
|
11
|
+
+ return min(
|
|
12
|
+
+ corner
|
|
13
|
+
+ for corner in (params.playback.crossover_hz, params.playback.bus_lowpass_hz)
|
|
14
|
+
+ if corner is not None
|
|
15
|
+
+ )
|
|
16
|
+
+
|
|
17
|
+
+
|
|
18
|
+
def passband_ripple_db(
|
|
19
|
+
- material: Material, params: "PipelineParams", noise_floor_hz: float = math.nan
|
|
20
|
+
+ material: Material,
|
|
21
|
+
+ params: "PipelineParams",
|
|
22
|
+
+ noise_floor_hz: float = math.nan,
|
|
23
|
+
+ *,
|
|
24
|
+
+ in_sub_band: bool = False,
|
|
25
|
+
) -> tuple[float, tuple[float, float]] | None:
|
|
26
|
+
"""The programme's own texture where nothing is missing: crest-to-trough, dB, and where.
|
|
27
|
+
|
|
28
|
+
@@ -823,6 +836,16 @@ def passband_ripple_db(
|
|
29
|
+
correction — the top of the judged band to the top of the analysis band — as the spread of
|
|
30
|
+
the curve about its own log-frequency trend, so a tilted passband is not read as ripple.
|
|
31
|
+
|
|
32
|
+
+ `in_sub_band` ends that passband at the sub feed's low-pass (`_sub_feed_edge_hz`) unless the
|
|
33
|
+
+ judged band already reaches past it. The mix's texture blocker asks for it: above the sub
|
|
34
|
+
+ edge the mix's shape is not texture the sub plays. In an LFE-led mix the level falls away
|
|
35
|
+
+ where the LFE channel's content ends, a knee a linear trend cannot remove, which read as
|
|
36
|
+
+ ripple and withheld a real deficit (9, 2009: 8.8 dB of "ripple" over 23-200 Hz against a
|
|
37
|
+
+ 6.2 dB shortfall, 5.1 dB up to the 80 Hz crossover). Each channel's own rule
|
|
38
|
+
+ (`channels_missing_low_end`) keeps the whole passband: there the smaller yardstick judged
|
|
39
|
+
+ more channels short, grew counterfactual targets (Caught Stealing 4.3 -> 12.3 dB) and
|
|
40
|
+
+ admitted a broadband negative on the corpus.
|
|
41
|
+
+
|
|
42
|
+
Crest to trough, not a one-sided dip, because the reference sits near the 90th percentile
|
|
43
|
+
of the spectrum, on the crests: a shortfall measured from it spans the whole swing. On the
|
|
44
|
+
negative corpus (IMPROVEMENT_PLAN E2) every unfiltered title's low-end deficit came in
|
|
45
|
+
@@ -841,7 +864,11 @@ def passband_ripple_db(
|
|
46
|
+
_judged_top_hz(anchor, params, noise_floor_hz),
|
|
47
|
+
params.diagnose.band_hz[1],
|
|
48
|
+
)
|
|
49
|
+
+ # a hum comb is recognised by its harmonics, so it is looked for over the whole passband
|
|
50
|
+
+ # even where the ripple is then measured on less of it: below 80 Hz only one is left
|
|
51
|
+
bands = (*params.exclude_bands_hz, *mains_hum_notches(freqs, response, low, high))
|
|
52
|
+
+ if in_sub_band and low < _sub_feed_edge_hz(params):
|
|
53
|
+
+ high = min(high, _sub_feed_edge_hz(params))
|
|
54
|
+
band = (freqs >= low) & (freqs <= high) & unexcluded(freqs, bands)
|
|
55
|
+
if band.sum() < 8:
|
|
56
|
+
return None
|
|
57
|
+
@@ -1537,11 +1564,7 @@ def analyse(
|
|
58
|
+
# (Black Bag with LR4 @ 30 injected — plateau 50-83 Hz gone, content tracking to
|
|
59
|
+
# 9 Hz), and that low end is content to restore, not an absence of bass. NaN means
|
|
60
|
+
# every band tracked.
|
|
61
|
+
- sub_edge = min(
|
|
62
|
+
- corner
|
|
63
|
+
- for corner in (params.playback.crossover_hz, params.playback.bus_lowpass_hz)
|
|
64
|
+
- if corner is not None
|
|
65
|
+
- )
|
|
66
|
+
+ sub_edge = _sub_feed_edge_hz(params)
|
|
67
|
+
if region[0] >= sub_edge and diagnosis.noise_floor_hz >= sub_edge:
|
|
68
|
+
blockers.append(
|
|
69
|
+
"no usable contiguous mix plateau in the band the sub plays: the mix's only "
|
|
70
|
+
@@ -1550,7 +1573,9 @@ def analyse(
|
|
71
|
+
f"programme (tracking stops at {diagnosis.noise_floor_hz:.1f} Hz), so there "
|
|
72
|
+
"is no bass passband to restore towards; restoration withheld"
|
|
73
|
+
)
|
|
74
|
+
- texture = passband_ripple_db(material, params, diagnosis.noise_floor_hz)
|
|
75
|
+
+ texture = passband_ripple_db(
|
|
76
|
+
+ material, params, diagnosis.noise_floor_hz, in_sub_band=True
|
|
77
|
+
+ )
|
|
78
|
+
low_end = low_end_deficit_db(material, params, diagnosis.noise_floor_hz)
|
|
79
|
+
if texture is not None and low_end is not None:
|
|
80
|
+
ripple, (edge_low, edge_high) = texture
|
|
81
|
+
diff --git a/tests/test_design_ripple_band.py b/tests/test_design_ripple_band.py
|
|
82
|
+
new file mode 100644
|
|
83
|
+
index 0000000..c0559b9
|
|
84
|
+
--- /dev/null
|
|
85
|
+
+++ b/tests/test_design_ripple_band.py
|
|
86
|
+
@@ -0,0 +1,68 @@
|
|
87
|
+
+"""The mix's texture yardstick is measured in the band the sub plays (9, 2009, 2026-10)."""
|
|
88
|
+
+
|
|
89
|
+
+import numpy as np
|
|
90
|
+
+from scipy import signal
|
|
91
|
+
+
|
|
92
|
+
+from beqforge.material import Material
|
|
93
|
+
+from beqforge.pipeline import PipelineParams, passband_ripple_db
|
|
94
|
+
+
|
|
95
|
+
+FS = 1000
|
|
96
|
+
+
|
|
97
|
+
+
|
|
98
|
+
+def _lfe_led(
|
|
99
|
+
+ high_pass_hz: float = 20.0, knee_hz: float = 70.0, seed: int = 5
|
|
100
|
+
+) -> Material:
|
|
101
|
+
+ """Noise high-passed at `high_pass_hz` whose level falls away above `knee_hz`.
|
|
102
|
+
+
|
|
103
|
+
+ The knee stands in for an LFE-led mix, where the level drops where the LFE channel's content
|
|
104
|
+
+ ends: real programme shape, but not texture the sub plays.
|
|
105
|
+
+ """
|
|
106
|
+
+ rng = np.random.default_rng(seed)
|
|
107
|
+
+ noise = rng.standard_normal(240 * FS)
|
|
108
|
+
+ bass = signal.sosfilt(
|
|
109
|
+
+ signal.butter(4, knee_hz, btype="low", fs=FS, output="sos"),
|
|
110
|
+
+ signal.sosfilt(
|
|
111
|
+
+ signal.butter(4, high_pass_hz, btype="high", fs=FS, output="sos"), noise
|
|
112
|
+
+ ),
|
|
113
|
+
+ )
|
|
114
|
+
+ mains = 0.05 * signal.sosfilt(
|
|
115
|
+
+ signal.butter(4, high_pass_hz, btype="high", fs=FS, output="sos"),
|
|
116
|
+
+ rng.standard_normal(noise.size),
|
|
117
|
+
+ )
|
|
118
|
+
+ return Material("lfe-led", FS, 0.01 * (bass + mains), {}, "complete_programme")
|
|
119
|
+
+
|
|
120
|
+
+
|
|
121
|
+
+def test_the_ripple_is_measured_up_to_the_sub_edge():
|
|
122
|
+
+ params = PipelineParams()
|
|
123
|
+
+ ripple = passband_ripple_db(_lfe_led(), params, in_sub_band=True)
|
|
124
|
+
+ assert ripple is not None
|
|
125
|
+
+ assert ripple[1][1] == params.playback.crossover_hz
|
|
126
|
+
+
|
|
127
|
+
+
|
|
128
|
+
+def test_a_knee_above_the_sub_edge_is_not_read_as_texture():
|
|
129
|
+
+ material = _lfe_led()
|
|
130
|
+
+ params = PipelineParams()
|
|
131
|
+
+ in_sub_band = passband_ripple_db(material, params, in_sub_band=True)
|
|
132
|
+
+ # what the yardstick is over the whole passband, as each channel's own rule measures it
|
|
133
|
+
+ to_band_top = passband_ripple_db(material, params)
|
|
134
|
+
+ assert in_sub_band is not None and to_band_top is not None
|
|
135
|
+
+ assert to_band_top[1][1] == params.diagnose.band_hz[1]
|
|
136
|
+
+ assert in_sub_band[0] < to_band_top[0] - 3.0
|
|
137
|
+
+
|
|
138
|
+
+
|
|
139
|
+
+def test_a_judged_band_past_the_sub_edge_keeps_the_analysis_band():
|
|
140
|
+
+ # a passband that only starts above the crossover: there is nothing below the sub edge to
|
|
141
|
+
+ # measure, so the yardstick is the one it always was
|
|
142
|
+
+ params = PipelineParams()
|
|
143
|
+
+ ripple = passband_ripple_db(
|
|
144
|
+
+ _lfe_led(high_pass_hz=110.0, knee_hz=400.0), params, in_sub_band=True
|
|
145
|
+
+ )
|
|
146
|
+
+ assert ripple is None or ripple[1][1] == params.diagnose.band_hz[1]
|
|
147
|
+
+
|
|
148
|
+
+
|
|
149
|
+
+def test_a_channel_s_own_rule_keeps_the_whole_passband():
|
|
150
|
+
+ # the per-channel texture rule: a smaller yardstick there judged more channels short and
|
|
151
|
+
+ # admitted a broadband negative on the corpus
|
|
152
|
+
+ params = PipelineParams()
|
|
153
|
+
+ ripple = passband_ripple_db(_lfe_led(), params)
|
|
154
|
+
+ assert ripple is not None and ripple[1][1] == params.diagnose.band_hz[1]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The mix's texture yardstick is measured about a cubic, not a line (9, 2009, 2026-10)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import signal
|
|
5
|
+
|
|
6
|
+
from beqforge.material import Material
|
|
7
|
+
from beqforge.pipeline import (
|
|
8
|
+
MIX_TEXTURE_TREND_DEGREE,
|
|
9
|
+
PipelineParams,
|
|
10
|
+
passband_ripple_db,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
FS = 1000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _programme(knee_hz: float | None, seed: int = 5) -> Material:
|
|
17
|
+
"""Noise high-passed at 20 Hz whose level falls away above `knee_hz`, if given.
|
|
18
|
+
|
|
19
|
+
The knee stands in for an LFE-led mix, where the level drops where the LFE channel's content
|
|
20
|
+
ends: real programme shape, but not texture the sub plays.
|
|
21
|
+
"""
|
|
22
|
+
rng = np.random.default_rng(seed)
|
|
23
|
+
high_pass = signal.butter(4, 20.0, btype="high", fs=FS, output="sos")
|
|
24
|
+
bass = signal.sosfilt(high_pass, rng.standard_normal(240 * FS))
|
|
25
|
+
if knee_hz is not None:
|
|
26
|
+
bass = signal.sosfilt(
|
|
27
|
+
signal.butter(4, knee_hz, btype="low", fs=FS, output="sos"), bass
|
|
28
|
+
)
|
|
29
|
+
mains = 0.05 * signal.sosfilt(high_pass, rng.standard_normal(bass.size))
|
|
30
|
+
return Material("programme", FS, 0.01 * (bass + mains), {}, "complete_programme")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_a_knee_above_the_crossover_is_trend_not_texture():
|
|
34
|
+
material, params = _programme(70.0), PipelineParams()
|
|
35
|
+
line = passband_ripple_db(material, params)
|
|
36
|
+
cubic = passband_ripple_db(material, params, trend_degree=MIX_TEXTURE_TREND_DEGREE)
|
|
37
|
+
assert line is not None and cubic is not None
|
|
38
|
+
assert cubic[0] < line[0] - 3.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_the_band_is_the_one_the_boundary_was_calibrated_on():
|
|
42
|
+
params = PipelineParams()
|
|
43
|
+
cubic = passband_ripple_db(
|
|
44
|
+
_programme(70.0), params, trend_degree=MIX_TEXTURE_TREND_DEGREE
|
|
45
|
+
)
|
|
46
|
+
assert cubic is not None and cubic[1][1] == params.diagnose.band_hz[1]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_programme_without_a_knee_keeps_its_texture():
|
|
50
|
+
# the cubic must not explain away texture a line already measured: on broadband noise
|
|
51
|
+
# the two agree
|
|
52
|
+
material, params = _programme(None), PipelineParams()
|
|
53
|
+
line = passband_ripple_db(material, params)
|
|
54
|
+
cubic = passband_ripple_db(material, params, trend_degree=MIX_TEXTURE_TREND_DEGREE)
|
|
55
|
+
assert line is not None and cubic is not None
|
|
56
|
+
assert abs(cubic[0] - line[0]) < 0.25 * line[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|