From ee1011915a1436154d59e6cdd02cd5e40d288985 Mon Sep 17 00:00:00 2001 From: JakimPL Date: Sun, 4 Oct 2026 23:43:03 +0200 Subject: [PATCH 1/5] Fixed: the converter's status lines and stage names reading the language file once at construction --- .../logic/main/converter/messages.py | 57 +++++++++++++------ .../logic/main/converter/test_messages.py | 22 ++++++- .../logic/main/converter/texts.py | 9 ++- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/sampletones_application/logic/main/converter/messages.py b/src/sampletones_application/logic/main/converter/messages.py index 04565dd04..eaf3fce74 100644 --- a/src/sampletones_application/logic/main/converter/messages.py +++ b/src/sampletones_application/logic/main/converter/messages.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Dict, Final, Tuple +from typing import Final, Tuple from sampletones_application.categories.estimate import time_estimation from sampletones_application.categories.manager import LanguageManager @@ -21,20 +21,30 @@ class ConverterMessages: def __init__(self, language_manager: LanguageManager) -> None: self._language_manager = language_manager - self.idle: str = language_manager["main.converter.message.status_idle"] - self.waiting: str = language_manager["main.converter.message.status_waiting"] - self._generating_library: str = language_manager["main.converter.message.status_generating_library"] - self._updating_library: str = language_manager["main.converter.message.status_updating_library"] - self.canceling: str = language_manager["main.converter.message.status_canceling"] - self.canceled: str = language_manager["main.converter.message.status_canceled"] - self.completed: str = language_manager["main.converter.message.status_reconstruction_completed"] - self.failed: str = language_manager["main.converter.message.status_error"] - self._stages: Dict[ReconstructionStage, str] = { - ReconstructionStage.LOADING: language_manager["main.converter.message.stage_loading"], - ReconstructionStage.MATCHING: language_manager["main.converter.message.stage_matching"], - ReconstructionStage.DECODING: language_manager["main.converter.message.stage_decoding"], - ReconstructionStage.GATHERING: language_manager["main.converter.message.stage_gathering"], - } + + @property + def idle(self) -> str: + return self._language_manager["main.converter.message.status_idle"] + + @property + def waiting(self) -> str: + return self._language_manager["main.converter.message.status_waiting"] + + @property + def canceling(self) -> str: + return self._language_manager["main.converter.message.status_canceling"] + + @property + def canceled(self) -> str: + return self._language_manager["main.converter.message.status_canceled"] + + @property + def completed(self) -> str: + return self._language_manager["main.converter.message.status_reconstruction_completed"] + + @property + def failed(self) -> str: + return self._language_manager["main.converter.message.status_error"] def progress_text(self, progress: ServiceProgress[ConversionItem]) -> str: """What the run is doing, how far it has come, and how long it has left. @@ -54,9 +64,9 @@ def preparing_library(self, state: LibraryState) -> str: """The line a run shows while its library is prepared: an update of a library another version built, and a generation of a missing one.""" if state is LibraryState.OUTDATED: - return self._updating_library + return self._language_manager["main.converter.message.status_updating_library"] - return self._generating_library + return self._language_manager["main.converter.message.status_generating_library"] def action_label( self, @@ -104,7 +114,18 @@ def _stage_text(self, progress: ServiceProgress[ConversionItem]) -> str: return "" return self._language_manager["main.converter.template.stage_template"].format( - stage=self._stages[step.stage], + stage=self._stage_name(step.stage), completed=step.completed, total=step.total, ) + + def _stage_name(self, stage: ReconstructionStage) -> str: + match stage: + case ReconstructionStage.LOADING: + return self._language_manager["main.converter.message.stage_loading"] + case ReconstructionStage.MATCHING: + return self._language_manager["main.converter.message.stage_matching"] + case ReconstructionStage.DECODING: + return self._language_manager["main.converter.message.stage_decoding"] + case ReconstructionStage.GATHERING: + return self._language_manager["main.converter.message.stage_gathering"] diff --git a/tests/unit/sampletones_application/logic/main/converter/test_messages.py b/tests/unit/sampletones_application/logic/main/converter/test_messages.py index 1c998d225..426663192 100644 --- a/tests/unit/sampletones_application/logic/main/converter/test_messages.py +++ b/tests/unit/sampletones_application/logic/main/converter/test_messages.py @@ -13,7 +13,7 @@ from sampletones_core.reconstructions.stage import ReconstructionStage from tests.suite.base import BaseTestSuite from tests.suite.case import BaseRegularTestCase -from tests.unit.sampletones_application.logic.main.converter.texts import TEXTS, messages +from tests.unit.sampletones_application.logic.main.converter.texts import TEXTS, messages, messages_reading FRAMES: Final[int] = 1100 @@ -66,6 +66,26 @@ def test_the_status_names_the_stage_and_its_counts(self) -> None: assert messages().progress_text(progress) == "Reconstructing kick... - matching 412/1100" +class TestTextAtThePointOfUse(BaseTestSuite): + """Every phrase reads the language file when it is shown, so a language chosen mid-run reaches + the next reading.""" + + def test_a_status_line_reads_the_text_standing_when_shown(self) -> None: + texts = dict(TEXTS) + converter_messages = messages_reading(texts) + texts["main.converter.message.status_idle"] = "Ready" + + assert converter_messages.idle == "Ready" + + def test_a_stage_reads_the_text_standing_when_shown(self) -> None: + texts = dict(TEXTS) + converter_messages = messages_reading(texts) + texts["main.converter.message.stage_matching"] = "comparing" + progress = _progress(0, 1, item=_item("kick", _step(ReconstructionStage.MATCHING, 412))) + + assert converter_messages.progress_text(progress) == "Reconstructing kick... - comparing 412/1100" + + class TestActionLabel(BaseTestSuite): """The button says what the run writes. diff --git a/tests/unit/sampletones_application/logic/main/converter/texts.py b/tests/unit/sampletones_application/logic/main/converter/texts.py index 5a2327831..909594af0 100644 --- a/tests/unit/sampletones_application/logic/main/converter/texts.py +++ b/tests/unit/sampletones_application/logic/main/converter/texts.py @@ -1,4 +1,4 @@ -from typing import Dict, Final +from typing import Dict, Final, Mapping from sampletones_application.logic.main.converter.messages import ConverterMessages from tests.suite.language import FakeLanguageManager @@ -23,4 +23,9 @@ def messages() -> ConverterMessages: """The converter's phrases over a language manager stating the ones a test reads.""" - return ConverterMessages(FakeLanguageManager(TEXTS)) # type: ignore[arg-type] + return messages_reading(TEXTS) + + +def messages_reading(texts: Mapping[str, str]) -> ConverterMessages: + """The converter's phrases over a language manager answering from ``texts`` as they stand.""" + return ConverterMessages(FakeLanguageManager(texts)) # type: ignore[arg-type] From 63a1b2eeb203429cdbc8d85f8b318a48c7fd5ded Mon Sep 17 00:00:00 2001 From: JakimPL Date: Sun, 4 Oct 2026 23:44:48 +0200 Subject: [PATCH 2/5] Renamed: the tabs' busy-state nudges to follow_busy_state --- src/sampletones_application/application.py | 6 ++-- .../coordinators/tabs/instructions.py | 2 +- .../coordinators/tabs/main.py | 7 ++++- .../sampletones_application/test_busy_lock.py | 29 ++++++++++++++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/sampletones_application/application.py b/src/sampletones_application/application.py index e66a1c211..b4750c639 100644 --- a/src/sampletones_application/application.py +++ b/src/sampletones_application/application.py @@ -1116,7 +1116,7 @@ def _refresh_busy_state(self) -> None: panel reads the live ``_is_operation_active`` state for itself; this only nudges them to re-apply, so the busy truth lives in one place. The menu follows the same edge, since what grays an entry offering another such operation is one already running.""" - self._instructions_tab.refresh_generate_button() + self._instructions_tab.follow_busy_state() self._update_menu() def _on_dialog_activity_changed(self) -> None: @@ -1127,7 +1127,7 @@ def _on_dialog_activity_changed(self) -> None: converter's own view, and the menu entries that would start another exclusive operation. """ self._refresh_busy_state() - self._main_tab.refresh_converter_view() + self._main_tab.follow_busy_state() def _on_library_operation_changed(self) -> None: """Responds to a library generation starting or finishing: refreshes the cross-tab action @@ -1135,7 +1135,7 @@ def _on_library_operation_changed(self) -> None: operation. The converter's own view changes refresh only the action buttons, so this extra converter refresh fires solely on library edges and stays clear of a refresh loop.""" self._refresh_busy_state() - self._main_tab.refresh_converter_view() + self._main_tab.follow_busy_state() def _export_reconstruction_wav_dialog(self) -> None: if self._reconstruction_coordinator.check_loaded(): diff --git a/src/sampletones_application/coordinators/tabs/instructions.py b/src/sampletones_application/coordinators/tabs/instructions.py index 4ba5ce47a..3b6c36e76 100644 --- a/src/sampletones_application/coordinators/tabs/instructions.py +++ b/src/sampletones_application/coordinators/tabs/instructions.py @@ -528,7 +528,7 @@ def _ask_before_exit(self, proceed: VoidCallback, decline: VoidCallback) -> None on_cancel=decline, ) - def refresh_generate_button(self) -> None: + def follow_busy_state(self) -> None: self._library_panel.refresh_action_buttons() @property diff --git a/src/sampletones_application/coordinators/tabs/main.py b/src/sampletones_application/coordinators/tabs/main.py index ddec77448..6a8945ab9 100644 --- a/src/sampletones_application/coordinators/tabs/main.py +++ b/src/sampletones_application/coordinators/tabs/main.py @@ -827,7 +827,12 @@ def _ask_before_exit(self, proceed: VoidCallback, decline: VoidCallback) -> None def is_converter_panel_visible(self) -> bool: return self._converter_panel.is_visible() - def refresh_converter_view(self) -> None: + def follow_busy_state(self) -> None: + """Re-applies the converter's view to the busy state after another operation starts or ends. + + The converter's own view changes already reach the busy state through + ``on_busy_state_changed``, so this answers the edges of other operations alone. + """ self._converter_logic.refresh_view() def _take_up_path(self, path: Path) -> None: diff --git a/tests/unit/sampletones_application/test_busy_lock.py b/tests/unit/sampletones_application/test_busy_lock.py index ae7649bf9..52f994c2c 100644 --- a/tests/unit/sampletones_application/test_busy_lock.py +++ b/tests/unit/sampletones_application/test_busy_lock.py @@ -1,12 +1,18 @@ +from dataclasses import dataclass from pathlib import Path +from typing import Callable from unittest.mock import MagicMock +import pytest + from sampletones_application.application import Application from sampletones_application.services.export.kind import ExportKind from sampletones_application.services.export.success import ExportSuccess from sampletones_application.services.result import ServiceProgress, ServiceStarted from sampletones_core.exporters.skipped import NO_SKIPPED_ROWS from sampletones_core.exports.stage import ExportStage +from tests.suite.base import BaseTestSuite +from tests.suite.case import BaseRegularTestCase NOTHING_MEASURED: int = 0 BYTES_SO_FAR: int = 812 @@ -102,14 +108,29 @@ class TestBusyRefreshPropagation: def test_refresh_nudges_both_tabs(self) -> None: application = _application() application._refresh_busy_state() - application._instructions_tab.refresh_generate_button.assert_called_once_with() + application._instructions_tab.follow_busy_state.assert_called_once_with() def test_refresh_reaches_the_menu(self) -> None: application = _application() application._refresh_busy_state() application._update_menu.assert_called_once_with() - def test_a_dialog_edge_refreshes_the_converter_view(self) -> None: + +class TestOtherOperationEdges(BaseTestSuite): + """The edge of an operation outside the converter reaches the converter's view, which hears of + its own edges by itself.""" + + @dataclass(frozen=True, kw_only=True) + class TestCase(BaseRegularTestCase): + edge: Callable[[Application], None] + + test_cases = ( + TestCase(label="a_dialog_edge", edge=Application._on_dialog_activity_changed), + TestCase(label="a_library_edge", edge=Application._on_library_operation_changed), + ) + + @pytest.mark.parametrize("test_case", test_cases, ids=lambda test_case: test_case.label) + def test_the_edge_reaches_the_converter_view(self, test_case: TestCase) -> None: application = _application() - application._on_dialog_activity_changed() - application._main_tab.refresh_converter_view.assert_called_once_with() + test_case.edge(application) + application._main_tab.follow_busy_state.assert_called_once_with() From a95413553a7c964eeec05aad756b1fa7d32b1727 Mon Sep 17 00:00:00 2001 From: JakimPL Date: Sun, 4 Oct 2026 23:48:55 +0200 Subject: [PATCH 3/5] Added: the axiom that white and dark noise sound on the noise channel with every channel offered under every spectrum method --- .../reconstruction/test_noise_matching.py | 92 +++++++++++++++---- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/tests/integration/reconstruction/test_noise_matching.py b/tests/integration/reconstruction/test_noise_matching.py index 1969a910e..702d4b776 100644 --- a/tests/integration/reconstruction/test_noise_matching.py +++ b/tests/integration/reconstruction/test_noise_matching.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, Final, List +from typing import Any, Callable, Dict, Final, List import numpy as np import pytest @@ -22,15 +22,20 @@ from sampletones_core.reconstructions import Reconstructor from sampletones_core.reconstructions.reconstructor.stems.configs.config import StemsConfig from sampletones_core.reconstructions.reconstructor.stems.configs.settings import StemSettings +from sampletones_tools.synthesis.oscillators.walk_noise import WalkNoiseOscillator from tests.suite.analysis import analyzed_config +from tests.suite.base import BaseTestSuite +from tests.suite.case import BaseRegularTestCase NOISE_ONLY: Final[List[ChannelName]] = [ChannelName.NOISE] -EVERY_CHANNEL: Final[List[ChannelName]] = [ChannelName.PULSE1, ChannelName.TRIANGLE, ChannelName.NOISE] +EVERY_CHANNEL: Final[List[ChannelName]] = ChannelName.items() +LIBRARY_CHANNELS: Final[List[ChannelName]] = [ChannelName.PULSE1, ChannelName.TRIANGLE, ChannelName.NOISE] TONAL_PITCH: Final[int] = 69 TONAL_NEIGHBORHOOD: Final[range] = range(TONAL_PITCH - 2, TONAL_PITCH + 3) -WHITE_NOISE_SEED: Final[int] = 11 +NOISE_RECORDING_SEED: Final[int] = 11 WHITE_NOISE_DEVIATION: Final[float] = 0.3 -WHITE_NOISE_SECONDS: Final[float] = 1.0 +NOISE_SECONDS: Final[float] = 1.0 +DARK_NOISE_PEAK: Final[float] = 0.9 EDGE_FRAMES: Final[int] = 1 QUIET_VOLUMES: Final[range] = range(1, 4) LOUD_VOLUMES: Final[range] = range(12, 16) @@ -44,12 +49,21 @@ HISS: Final[slice] = slice(BURST_FRAMES + GAP_FRAMES, BURST_FRAMES + GAP_FRAMES + HISS_FRAMES) +NoiseBuilder = Callable[[np.ndarray, np.random.Generator], np.ndarray] + + @dataclass(frozen=True) class Converted: hiss_frames: np.ndarray instructions: List[InstructionUnion] +@dataclass(frozen=True) +class Analysis: + config: Config + library: InstructionLibrary + + def _config(method: SpectrumMethod) -> Config: return analyzed_config(method, gamma=Config().library.transformation_gamma) @@ -90,12 +104,16 @@ def _burst_then_hiss(path: Path, config: Config) -> Path: def _every_channel_library(config: Config) -> InstructionLibrary: - """Every noise instruction beside the notes around one pitch, so a tonal answer stands within reach.""" + """Every noise instruction beside the notes around one pitch, so a tonal answer stands within reach. + + Both pulse channels play the pulse instructions, so the first pulse channel's instructions + serve the second. + """ window = Window.from_config(config) extractor = get_feature_extractor(config, window) data: Dict[InstructionUnion, InstructionLibraryFragment[Any]] = {} - for channel_name, generator in get_generators_by_channels(config, EVERY_CHANNEL).items(): + for channel_name, generator in get_generators_by_channels(config, LIBRARY_CHANNELS).items(): for instruction in generator.get_possible_instructions(): if channel_name != ChannelName.NOISE and instruction.on and instruction.pitch not in TONAL_NEIGHBORHOOD: continue @@ -107,12 +125,18 @@ def _every_channel_library(config: Config) -> InstructionLibrary: return library -def _white_noise(path: Path, config: Config) -> Path: - sample_rate = config.library.sample_rate - count = int(sample_rate * WHITE_NOISE_SECONDS) - audio = np.random.default_rng(WHITE_NOISE_SEED).normal(0.0, WHITE_NOISE_DEVIATION, count) +def _white_noise(time: np.ndarray, generator: np.random.Generator) -> np.ndarray: + return generator.normal(0.0, WHITE_NOISE_DEVIATION, time.shape[0]) + + +def _dark_noise(time: np.ndarray, generator: np.random.Generator) -> np.ndarray: + return DARK_NOISE_PEAK * WalkNoiseOscillator(kind="walk_noise").render(time, generator=generator) - write_wave(path, sample_rate, audio) + +def _noise_recording(path: Path, config: Config, noise: NoiseBuilder) -> Path: + sample_rate = config.library.sample_rate + time = np.arange(int(sample_rate * NOISE_SECONDS)) / sample_rate + write_wave(path, sample_rate, noise(time, np.random.default_rng(NOISE_RECORDING_SEED))) return path @@ -142,15 +166,43 @@ def converted( ) -class TestWhiteNoise: - def test_white_noise_sounds_on_the_noise_channel(self, tmp_path: Path) -> None: - """ - White noise resembles the noise channel's contribution on average, so the noise channel - carries it through. - """ - config = Config() - path = _white_noise(tmp_path / "white.wav", config) - reconstructor = Reconstructor(config, frozenset(EVERY_CHANNEL), library=_every_channel_library(config)) +@pytest.fixture( + scope="module", + params=list(SpectrumMethod), + ids=lambda method: method.value, +) +def every_channel_analysis(request: pytest.FixtureRequest) -> Analysis: + """Every channel's library under one spectrum method, shared by every noise reconstructed with it.""" + config = _config(request.param) + return Analysis(config=config, library=_every_channel_library(config)) + + +class TestNoiseSoundsOnTheNoiseChannel(BaseTestSuite): + """ + A recording built from noise reconstructs with the noise channel sounding, with every channel + offered and under every spectrum method. Noise resembles the noise channel's contribution on + average, so the noise channel carries it through, whether the noise is white or dark. + """ + + @dataclass(frozen=True, kw_only=True) + class TestCase(BaseRegularTestCase): + noise: NoiseBuilder + + test_cases = ( + TestCase(label="white", noise=_white_noise), + TestCase(label="dark", noise=_dark_noise), + ) + + @pytest.mark.parametrize("test_case", test_cases, ids=lambda test_case: test_case.label) + def test_the_noise_channel_sounds( + self, + test_case: TestCase, + every_channel_analysis: Analysis, + tmp_path: Path, + ) -> None: + config = every_channel_analysis.config + path = _noise_recording(tmp_path / f"{test_case.label}.wav", config, test_case.noise) + reconstructor = Reconstructor(config, frozenset(EVERY_CHANNEL), library=every_channel_analysis.library) reconstruction = reconstructor.reconstruct( [path], StemsConfig.single_entry(StemSettings(channels=EVERY_CHANNEL, bends=[])) From 7ccfdc241bb8e1076dc1a840cb653387fa64f638 Mon Sep 17 00:00:00 2001 From: JakimPL Date: Mon, 5 Oct 2026 00:08:57 +0200 Subject: [PATCH 4/5] Reworked: the bugs and to-dos ledger to the entries still owed --- docs/development/bugs-and-todos.md | 138 +++++++++-------------------- docs/tools/calibration.md | 3 +- 2 files changed, 44 insertions(+), 97 deletions(-) diff --git a/docs/development/bugs-and-todos.md b/docs/development/bugs-and-todos.md index 07a9aa1e5..2bf8b4367 100644 --- a/docs/development/bugs-and-todos.md +++ b/docs/development/bugs-and-todos.md @@ -13,9 +13,8 @@ behind. Each entry says what is owed and why, and the code and the change that c * Keyboard navigation of the converter's list of gathered recordings: a cursor the arrow keys move, `Home` and `End`, and folders opened and closed from the keyboard. * Waveform LOD for zooming -* Drag and drop -* Multiple Reconstruction views -* In-project sample selection in Reconstruction view +* Drag and drop of browser nodes onto views, such as a reconstruction onto Samples. +* Tabs for several reconstructions in the Reconstruction view. * Application installation progress bar ### Tracker @@ -24,91 +23,49 @@ The first two entries are the settings an imported `.fti` reports as left behind [FamiTracker export](../formats/famitracker.md#c-reading-an-instrument-file)). Each one closed is a dimension the import starts carrying. -* Release points. A note-off cuts the channel today. A release segment needs the playback walk, the NSF - driver and `NoteOff` to gain one. -* Arpeggio modes. A sequence's `setting` byte says absolute. Fixed, relative and scheme need an enum of - their own, and scheme needs the item bit-packing FamiTracker gives it. -* The sample column reads the sample still playing from the top of each frame, so a frame's first rows - take no transpose or volume while playback still carries the previous frame's sample. -* A tracker export moves a whole contour by one written note, so where a transpose carries part of it - outside pitches 24–119, the ticks in-app playback clamps sound unclamped in the tracker. A clamped table - (Bitphase) or arpeggio (FamiTracker) per such transposition would make them exact. -* A FamiTracker transpose row moving a note more than fifteen semitones, or a shared pattern's cell that - frames reach needing different slides, is written without its slide and reported. A second effect - column, or a pattern cloned per frame, would carry it. -* A FamiTracker module's pulse level can sound a step away from in-app playback. FamiTracker rounds the - product of the two levels down and keeps the quietest level where that comes out silent, while the app - and Bitphase round it to the nearest step. -* A FamiTracker module places the longer rows of an uneven tempo by FamiTracker's own running count. - A compatibility setting writing a speed effect per row at tempo 150 would make it play the app's groove. +* Release points. A note-off cuts the channel today, so a release segment has nowhere to play. +* Arpeggio modes. Fixed, relative and scheme arpeggios import as absolute offsets today. +* Compatibility options for the FamiTracker export, so a module plays as the app does where FamiTracker's + own rules differ, such as how it rounds a pulse level and where it places the longer rows of an uneven + tempo. ### Workflow -* Waveform construction preview for single-file conversion -* Picking several rows of the converter's list at once, so a group leaves or settles in one gesture. The - widget family already draws a multi-pick reading, built for the mix chooser. A converter pick needs one - with no ceiling, and every gesture the list offers one row must reach each picked row. -* Selection operations on a reconstruction -* Reconstruction trimming -* Marking broken files where the browsers list them, so fewer notices reach the reader. A reconstruction - that fails to load, one whose recordings are missing and a recording that cannot be read could stand in a - warning or an error color. Today every attempt to open or play one raises its notice, once per attempt, - and a mark would tell the reader before they try. +* Waveform construction preview: a waveform drawn from the partial reconstruction while the converter + reconstructs a single file. +* Picking several rows of the converter's list at once, so a group leaves or settles in one gesture. +* Selecting parts of a reconstruction's waveform to crop, trim, cut and paste them along with their + instructions. +* Marking broken files where the browsers list them: a reconstruction that fails to load, one whose + recordings are missing and a recording that cannot be read could stand in a warning or an error color, + so the reader learns before trying to open or play one. ### Features * In-application guide/tutorial * Language selector -* Reading only the bins the refinement asks for. The pitch reading transforms every bin the spectrum - covers and uses a handful of them per frame. On a CPU build one transform is a tenth or more of a short - conversion. Restricting the kernel to the rows the chosen notes name is a slice. The care is that the - union of harmonic bins over a whole stream is wider than any one frame's. -* Keeping what a stopped folder scan found. Stopping a long walk drops the recordings met so far, while the - gathering already answers with them. Handing that list on would let a reader stop a long walk and keep - the count they watched climb. It needs the empty branch of the read reworked beside it, since a stop - before the first recording is a different answer from a folder with none. -* Calibrating the pitch refinement. The refinement's confidence threshold, change weight and window are - chosen by hand, and [the calibration](../tools/calibration.md) could measure them beside the criterion - blend. The change weight is the one with an audible trade-off: it decides how large a one-frame - excursion the walk follows and how large it absorbs, which is vibrato against jitter. +* Reading only the bins the refinement asks for. The pitch reading transforms every bin the spectrum covers + and uses a handful per frame, which is a tenth or more of a short conversion on a CPU build. +* Keeping the recordings a stopped folder scan has found, so stopping a long walk keeps the count the reader + watched climb. +* Calibrating the pitch refinement. Its confidence threshold, change weight and window are chosen by hand, + and [the calibration](../tools/calibration.md) could measure them. The change weight trades vibrato + against jitter. ### Technical -* Leading the calibration report with `mr-loudness-dB`. The report lists `mr-auditory-dB` first, which - reads silence as closer to a tone than any render. The order changes once by-ear ratings of a sweep hold - the loudness-weighted referee at ρ ≥ 0.6 in every category. A listening round scored `polyphony-chord` - 6–7 dB better on a render that dropped the noise channel entirely, so the bar is unmet. -* An axiom that a recording built with noise reconstructs with the noise channel sounding. The corpus - knows which items were synthesized from noise, and the render records hold the per-channel timelines. - Such a test would fence the criterion against noise deafness without a listening round, as - `referee/test_axioms.py` does for the referees. -* API documentation -* Code documentation -* Screen scenarios on Windows and macOS. They run on Linux alone: a scenario's pointer and keys reach the - application through X11, and whether DearPyGui opens its window on GitHub's Windows and macOS runners is - unverified. A run there starts from a spike that opens the application on each runner and presses one - control through the callback a click runs. -* What a build makes of the configuration and the session state an older one left behind. Neither has a - version, so neither travels an upgrade chain or has an archived corpus. A `state.yaml` naming a panel - that has since gone, or a configuration missing a setting added since, is read by whatever each loader - happens to do with it. -* The element enums that outlived their keys. An element enum is named only where a `_label(element)` - helper takes one, and the language-keys check expands such a helper over the whole enum, so a member no - call names is reached all the same. Spelling those keys literally at the call site makes each entry - exactly checkable and retires the enums that remain. -* Respecting FamiTracker limits at the writers. A target format's ceilings belong to the code that writes - it, and today only the sequence-length ceiling follows that rule. The instrument, sequence and pattern - counts should follow it too, so a project past one of them is reported to the reader and not refused by - the writer. +* Screen scenarios on Windows and macOS. They run on Linux alone, and whether DearPyGui opens its window on + GitHub's Windows and macOS runners is unverified. +* An upgrade step and an archived corpus for the configuration and the session state. Both load through + recovery, which drops what no longer validates, and neither has a version an upgrade could start from. +* The element enums that outlived their keys. The language-keys check expands a `_label(element)` helper + over its whole enum, so a member no call names is reached all the same. Spelling the keys at the call + site makes each entry exactly checkable. * Per-tab undo routing * A history of its own for a standalone reconstruction document, one loaded from disk and not opened as a - project sample. The engine is session-scoped to a project, so an edit to such a document is undoable - nowhere. Giving it a stack reuses the same engine ([undo](application/undo.md)). Until then, an edit that - silences a channel or lets a recording go is reversible only by reloading the file. -* In-application console + project sample. An edit to such a document is undoable nowhere ([undo](application/undo.md)), so an edit + that silences a channel or lets a recording go is reversible only by reloading the file. * Improve performance of the browser's favorite scan of the entire tree per click -* The Command key on macOS. The macOS scheme and the text-field rule read Command as Super, and whether - DearPyGui reports it as Super there or swaps it with Ctrl is unverified on a Mac. ## Architecture @@ -124,31 +81,22 @@ currently out of line. An entry leaves when the code meets the contract again. * `application.py` and `ui/elements/tree/tree.py` each hold several concerns in one module, past the size at which the sequencer panels were divided into subpackages. Each divides the same way: a module per concern, with the class that stays holding the collaborators and the public surface. -* The Main tab's public surface renames several calls on its way to a panel (`refresh_converter_view`, - `is_converter_panel_visible`, `refresh_browser`). Those are the tab's own face, and the inbound callbacks - the Coordinators contract governs are forwarded as they stand. The open question is whether the face - needs those names at all, or whether the application should ask for the thing and not for the refresh of - it. -* `dpg_get_item_parent` catches `Exception`, where the Error Handling Policy leaves the broad catch to a - service's top-level task wrapper. Its docstring says why, and the catch narrows the day DearPyGui raises - a type of its own. -* Every fixture in the Main tab coordinator's tests builds the coordinator through `__new__` and fills its - privates by hand, so those cases describe a method and not the wired object. A case reading the - coordinator's own behavior cannot see a hook left unset. Building the object in that file closes the - gap. +* The tab coordinators' and the application's unit tests build their object through `__new__` and fill its + private fields by hand, so a case describes a method and not the wired object, and a hook left unset goes + unseen. * Which recordings a mix is built from is decided in `ui/` until **Add** is pressed. The chooser holds the pick as its own set and asks the view model how a gesture moves it, so a projection computes state transitions. The logic layer hears only the answer. Principles 3 and 4 put that machine in `logic/`, with the chooser drawing what a view model says and reporting the gesture. Moving it is a phase and not a patch, because the dialog drives the pick today. -* `ConverterMessages` reads the strings it shows a reader once, at construction, where principle 8 has text - resolve at the point of use. The stage names and status lines are cached as fields, and the run's - templates are read live. The fix is to read each key where it is used and let the manager answer. -* Every gesture in the converter re-derives the whole setup. A gesture hands `ConverterLogic._rewrite` a - state whose recordings are new objects, so the rows and the batch entries are read from it cold, and the - garbage collector's own share falls inside them. On a very large folder that is long enough to feel as a - pause before a widget is touched, and all of it repeats work, since one recording changed. The answer is - to hold the rows against the gathering that produced them and derive entries for the recordings a gesture - moved. +* Every gesture in the converter re-derives the whole setup, so a gesture costs fifteen times as much on + 10,000 recordings as on 1,000, much of it in garbage collection. A gesture hands + `ConverterLogic._rewrite` a state whose recordings are new objects, and the rows and the batch entries + are read from it cold. Holding the rows against the gathering that produced them, and deriving entries + for the recordings a gesture moved, closes it. ## Bugs + +* The Sample column shows no sample on a frame's first rows, since its reading starts over at each frame. It + offers no transpose or volume there, while playback applies them to the sample the previous frame left + sounding. diff --git a/docs/tools/calibration.md b/docs/tools/calibration.md index 66414df5a..b9b0d75c6 100644 --- a/docs/tools/calibration.md +++ b/docs/tools/calibration.md @@ -201,5 +201,4 @@ closer to a sine than silence is". Rankings it is known to get wrong are recorde ### Which referee leads The report lists `mr-auditory-dB` first. `mr-loudness-dB` takes the lead once by-ear ratings of a sweep -of renders agree with its scores. [Bugs and to-dos](../development/bugs-and-todos.md) tracks the bar -that agreement must reach. +of renders agree with its scores. From b40461b2fc273e47987fad20289913adbdeb9be3 Mon Sep 17 00:00:00 2001 From: JakimPL Date: Mon, 5 Oct 2026 00:37:49 +0200 Subject: [PATCH 5/5] Reworded: the ledger entries a cold read found unclear --- docs/development/bugs-and-todos.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/development/bugs-and-todos.md b/docs/development/bugs-and-todos.md index 2bf8b4367..72dc40e34 100644 --- a/docs/development/bugs-and-todos.md +++ b/docs/development/bugs-and-todos.md @@ -9,9 +9,8 @@ behind. Each entry says what is owed and why, and the code and the change that c ### Navigation * Interface scale -* Tree navigation using keys -* Keyboard navigation of the converter's list of gathered recordings: a cursor the arrow keys move, `Home` - and `End`, and folders opened and closed from the keyboard. +* Keyboard navigation of the trees, and of the converter's list of gathered recordings: a cursor the arrow + keys move, `Home` and `End`, and folders opened and closed from the keyboard. * Waveform LOD for zooming * Drag and drop of browser nodes onto views, such as a reconstruction onto Samples. * Tabs for several reconstructions in the Reconstruction view. @@ -44,23 +43,25 @@ dimension the import starts carrying. * In-application guide/tutorial * Language selector -* Reading only the bins the refinement asks for. The pitch reading transforms every bin the spectrum covers - and uses a handful per frame, which is a tenth or more of a short conversion on a CPU build. +* Reading only the bins the refinement asks for. The pitch refinement bends notes by reading the audio's + spectrum, and it transforms every frequency bin the spectrum covers while using a handful per frame, + which is a tenth or more of a short conversion on a CPU build. * Keeping the recordings a stopped folder scan has found, so stopping a long walk keeps the count the reader watched climb. -* Calibrating the pitch refinement. Its confidence threshold, change weight and window are chosen by hand, - and [the calibration](../tools/calibration.md) could measure them. The change weight trades vibrato - against jitter. +* Calibrating the pitch refinement. The settings that decide how a pitch reading bends a note (a + confidence threshold, a change weight and a window) are chosen by hand, and + [the calibration](../tools/calibration.md) could measure them. The change weight trades vibrato against + jitter. ### Technical * Screen scenarios on Windows and macOS. They run on Linux alone, and whether DearPyGui opens its window on GitHub's Windows and macOS runners is unverified. -* An upgrade step and an archived corpus for the configuration and the session state. Both load through - recovery, which drops what no longer validates, and neither has a version an upgrade could start from. -* The element enums that outlived their keys. The language-keys check expands a `_label(element)` helper - over its whole enum, so a member no call names is reached all the same. Spelling the keys at the call - site makes each entry exactly checkable. +* An upgrade path for the configuration and the session state. A file an older build left behind is read + as the loader happens to, and no archived files of older builds test it. +* The element enums that outlived their keys. The language-keys check treats a key named through an + element enum as the whole enum, so a key no call uses goes unnoticed. Spelling each key at its call site + makes the check exact. * Per-tab undo routing * A history of its own for a standalone reconstruction document, one loaded from disk and not opened as a project sample. An edit to such a document is undoable nowhere ([undo](application/undo.md)), so an edit