Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d9e98f2
Updated: CHANGELOG
JakimPL Oct 10, 2026
ecc5690
Fixed: stem mute state carried onto a newly opened reconstruction
JakimPL Oct 10, 2026
5ff0096
Extended: the bend tooltip
JakimPL Oct 10, 2026
9f043c1
Redrawn: the tracker caret under its character and with the row's scroll
JakimPL Oct 10, 2026
0e83937
Added: a meter for what an edit's landing costs, headless and on screen
JakimPL Oct 10, 2026
c984868
Retuned: the tracker's palettes and fades for readable text under eve…
JakimPL Oct 10, 2026
1605527
Extended: the on-screen meter with a held burst and a single landing
JakimPL Oct 10, 2026
0b2f5a0
Redrawn: the guide pictures the tracker and the disabled shade changed
JakimPL Oct 10, 2026
e84d4c8
Rendered: the edited channel and the mix on the regeneration worker
JakimPL Oct 10, 2026
0b8cfb0
Fixed: the single-landing reading keeps the frames after the landing
JakimPL Oct 10, 2026
02c95c4
Changed: the long-document readings wait on the waveform's fade
JakimPL Oct 10, 2026
d136f4c
Updated: the typing scenario for the entry's typing color
JakimPL Oct 10, 2026
74aa309
Merge pull request #91 from JakimPL/redesign
JakimPL Oct 10, 2026
29e735a
Read: whether a channel stands by from its volume envelope
JakimPL Oct 10, 2026
3795860
Read: the waveform once per gesture, and the kept renders where nothi…
JakimPL Oct 10, 2026
216e2a6
Rendered: what a reader hears on the regeneration worker
JakimPL Oct 10, 2026
a313934
Painted: ownership one series per recording, repainted where it changed
JakimPL Oct 11, 2026
7f1d65a
Exported: the envelopes on the regeneration worker, installed at the …
JakimPL Oct 11, 2026
3544145
Kept: each waveform's reduction to the display's points by the array'…
JakimPL Oct 11, 2026
42eef96
Measured: a landing against the document's cold render, and a tick ag…
JakimPL Oct 11, 2026
f2c062b
Memoized: each stream's, each owner record's and each pattern's diges…
JakimPL Oct 11, 2026
167383b
Named: what the waveform's fade waits for
JakimPL Oct 11, 2026
3238d66
Owed: a WAV export to the worker where the cache keeps no sound for t…
JakimPL Oct 11, 2026
eded83c
Merge pull request #92 from JakimPL/showing
JakimPL Oct 11, 2026
50df3ae
Carried: the reader's choice onto the rebuilt record on the worker
JakimPL Oct 11, 2026
53b329c
Rendered: the sequencer's voice preview on a regeneration worker
JakimPL Oct 11, 2026
cf95ef2
Merge pull request #93 from JakimPL/hearing
JakimPL Oct 11, 2026
a0c6a60
Removed: the render cache's own rendering, every sound now a worker's…
JakimPL Oct 11, 2026
fcfb855
Answered: silence of the document's own length, a landing kept whole,…
JakimPL Oct 11, 2026
9c65616
Owed: one showing merged across gestures, and the envelopes to the wo…
JakimPL Oct 11, 2026
9dd50a6
Forgotten: a wanted preview when playback starts, and the trees' auto…
JakimPL Oct 11, 2026
ede8bd2
Mixed: the original of the recordings heard on the worker, kept besid…
JakimPL Oct 11, 2026
e1bc351
Witnessed: the fade and the edit on screen, the landing's cost in the…
JakimPL Oct 11, 2026
168141b
Simplified: one memo in the history, required inputs, and the docs to…
JakimPL Oct 11, 2026
59b035d
Answered: the fresh review's findings, a change waiting behind the ti…
JakimPL Oct 11, 2026
8aa0752
Updated: bugs and todos
JakimPL Oct 12, 2026
f87a0ab
Merge pull request #94 from JakimPL/closing
JakimPL Oct 12, 2026
79c0387
Fixed: the waveform update contract, its waveform taken by position
JakimPL Oct 12, 2026
245aa2d
Answered: the instruments panel's words while a document's envelopes …
JakimPL Oct 12, 2026
a310006
Held: every press of a burst to the double-click time, a late one a s…
JakimPL Oct 12, 2026
badad6c
Kept: the instruments panel's last envelopes
JakimPL Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Optimized reconstruction size.
* Optimized algorithm memory usage.
* Improved FamiTracker/Bitphase compatibility.
* Supported CUDA for local installation if available.
* Bumped the reconstruction data- version to `2.2`, with backward compatibility for `2.1`.
* Bumped the project data version to `1.1`, with backward compatibility.
* Bumped the library data version to `2.1`.
Expand Down
6 changes: 6 additions & 0 deletions automation/application/running.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def language(self) -> LanguageManager:
"""The application's language manager, which turns a language key into the words a user reads."""
return self._application.language_manager

def sound_settled(self) -> bool:
"""Whether every sound asked for stands landed: the open reconstruction's rebuilds and listenings, and a
voice's preview on its way to the sequencer.
"""
return not (self._application.reconstruction_rewrites.is_busy or self._application.document_previews.is_busy)

@property
def shortcuts(self) -> ShortcutManager:
"""The application's shortcut manager, which says what keys each action has under the scheme in
Expand Down
26 changes: 26 additions & 0 deletions automation/dearpygui/gestures/pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ def _press_where_it_stands(
)
if running and count > SINGLE_PRESS:
self._require_double_click(imgui_button, releases, double_clicks)
if running and count > DOUBLE_PRESS:
self._require_one_burst(imgui_button, releases)

def _counts(self, imgui_button: int) -> Tuple[int, int]:
"""The releases and the double-clicks the witness counted of ``imgui_button`` so far."""
Expand All @@ -226,6 +228,30 @@ def _counts(self, imgui_button: int) -> Tuple[int, int]:
self._witness.double_clicks(imgui_button),
)

def _require_one_burst(
self,
imgui_button: int,
releases: int,
) -> None:
"""Checks that every press after the double-click landed within Dear ImGui's double-click time of the one
before, which is what makes the presses one burst to a plot.

A plot takes a press that follows a double-click within that time as one more click of the
burst, and a later one as a click of its own, so a frame drawn slowly between two presses
turns a triple click into a double-click and a click.

Raises:
SlowFramesError: If a press of the burst came later than that after the one before it.
"""
times = self._bridge.ask(lambda: self._witness.button_release_times(imgui_button))[releases:]
for earlier, later in zip(times[DOUBLE_PRESS - 1 :], times[DOUBLE_PRESS:]):
took = later - earlier
if took >= IMGUI_DOUBLE_CLICK_SECONDS:
raise SlowFramesError(
f"Dear ImGui read a press of the burst as a click of its own: it came {took:.2f} s after "
f"the one before, past the {IMGUI_DOUBLE_CLICK_SECONDS} s double-click time"
)

def _require_double_click(
self,
imgui_button: int,
Expand Down
12 changes: 12 additions & 0 deletions automation/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
SIZE_SEPARATOR: Final[str] = "x"
ARTIFACTS_VARIABLE: Final[str] = "SAMPLETONES_SCREENS_ARTIFACTS"
REPORT_VARIABLE: Final[str] = "SAMPLETONES_SCREENS_REPORT"
REPEATS_VARIABLE: Final[str] = "SAMPLETONES_SCREENS_REPEATS"
SINGLE_RUN: Final[int] = 1
STRICT_HISTORY_VARIABLE: Final[str] = f"{SAMPLETONES_ENV_PREFIX}STRICT_HISTORY"
STRICT_HISTORY: Final[str] = "true"
NO_INPUT_METHOD: Final[str] = "@im=none"
Expand Down Expand Up @@ -111,6 +113,16 @@ def homes_root(environment: Mapping[str, str]) -> Path:
return Path(stated) if stated is not None else Path(tempfile.gettempdir())


def repeats(environment: Mapping[str, str]) -> int:
"""How many times a scenario written to be tried over runs: once, which ``SAMPLETONES_SCREENS_REPEATS`` raises.

A scenario that holds the application to a clean exit under a race is run many times in one
pass to be believed, and once in every other pass.
"""
stated = environment.get(REPEATS_VARIABLE)
return int(stated) if stated is not None else SINGLE_RUN


@dataclass(frozen=True)
class ScenarioFolders:
"""Where one scenario's run keeps its files: the home its application lives in, and what the run keeps.
Expand Down
29 changes: 14 additions & 15 deletions automation/holds/regeneration.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import threading
from typing import AbstractSet, Optional
from typing import Optional

import pytest

from sampletones_application.services.regeneration.jobs import ListeningJob, RenderJob
from sampletones_application.services.regeneration.service import (
RegenerationService,
)
from sampletones_application.services.result import ServiceError
from sampletones_core.constants.enums import ChannelName
from sampletones_core.exporters import Features
from sampletones_core.reconstructions import Reconstruction


class RegenerationHold:
"""Holds every rebuild of an edited channel on its worker before it computes, until the scenario releases
it.

The rebuild waits where the real one runs, so the application draws and answers while the edit
is on its way, and once released the real rebuild computes the edit's result. The hold lets
is on its way, and once released the real rebuild computes the edit's result. A rendering of
what the reader hears, which the same worker runs on opening a document and on a box ticked,
goes through at once, so the document stands drawn while the rebuilds wait; a voice's preview
in the sequencer is a listening on a worker of its own and passes through the same way. The hold lets
every rebuild through from its release on, until the scenario holds again. A scenario standing
for a rebuild that breaks lets the held rebuilds go as that failure instead, which the service
reports the way it reports a rebuild that raised.
Expand All @@ -31,21 +32,19 @@ def __init__(self) -> None:

def install(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Replaces the service's rebuild with one that waits for the release before it computes."""
rebuild = RegenerationService._run # pylint: disable=protected-access

def held(
service: RegenerationService,
reconstruction: Reconstruction,
channel_name: ChannelName,
features: Features,
heard: AbstractSet[int],
) -> None:
run = RegenerationService._run # pylint: disable=protected-access

def held(service: RegenerationService, job: RenderJob) -> None:
if isinstance(job, ListeningJob):
run(service, job)
return

self._wait()
if self._failure is not None:
service._emit(ServiceError(exception=self._failure)) # pylint: disable=protected-access
return

rebuild(service, reconstruction, channel_name, features, heard)
run(service, job)

monkeypatch.setattr(RegenerationService, "_run", held)

Expand Down
1 change: 1 addition & 0 deletions automation/plugin/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def screen_fixture(
window_manager=window_manager,
language=screen_application.language,
shortcuts=screen_application.shortcuts,
sound_settled=screen_application.sound_settled,
dialogs=screen_boundaries.dialogs,
errors=screen_boundaries.errors,
file_manager=screen_boundaries.file_manager,
Expand Down
10 changes: 10 additions & 0 deletions automation/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def __init__(
window_manager: WindowManager,
language: LanguageManager,
shortcuts: ShortcutManager,
sound_settled: Callable[[], bool],
dialogs: ScriptedFileDialogs,
errors: ErrorRecords,
file_manager: FileManagerStandIn,
Expand All @@ -118,6 +119,7 @@ def __init__(
self._window_manager = window_manager
self._render_thread = render_thread
self._shortcuts = shortcuts
self._sound_settled = sound_settled
self._dialogs = dialogs
self._errors = errors
self._file_manager = file_manager
Expand All @@ -143,6 +145,10 @@ def __init__(
self.no_output_notice = Notice(bridge, hand, TAG_GLOBAL_DIALOG_NO_AUDIO_OUTPUT)
self.no_project_notice = Notice(bridge, hand, TAG_GLOBAL_DIALOG_NO_PROJECT_OPEN)

def sound_settled(self) -> bool:
"""Whether the open reconstruction's sound stands landed, which a box or an edit sends to the worker."""
return bool(self.bridge.ask(self._sound_settled))

def expect(
self,
reading: Callable[[], ReadingT],
Expand Down Expand Up @@ -186,6 +192,10 @@ def status(self) -> str:
"""What the status bar along the bottom of the window says."""
return self.bridge.ask(lambda: read_label(STATUS_BAR))

def status_drawn(self) -> str:
"""What the status bar says, read on the render thread itself, which a recording takes after every frame."""
return read_label(STATUS_BAR)

def shows_text(self, words: str) -> bool:
"""Whether text reading ``words`` was drawn in the last frame, such as a tooltip standing open."""
return self.bridge.ask(lambda: read_visible_text(words))
Expand Down
80 changes: 77 additions & 3 deletions automation/steps/reconstructions.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import operator
from functools import partial
from pathlib import Path
from typing import Final, List, Optional
from typing import Callable, Final, List, Optional, Tuple

from automation.dearpygui.items.colors import read_theme
from automation.dearpygui.items.types import Item
from automation.holds.regeneration import RegenerationHold
from automation.screen import Screen
from automation.views.bar_graph import BarGraph
from automation.vocabulary.graphs import WAVEFORM_RECONSTRUCTION
from sampletones_application.categories.hierarchy import Tab
from sampletones_application.utils.gui.shortcuts.ids import ShortcutId
from sampletones_core.constants.enums import ChannelName, FeatureKey
from sampletones_core.instructions import PulseInstruction
from sampletones_core.reconstructions import Reconstruction
from sampletones_shared.constants.symbols import TITLE_SEPARATOR
from sampletones_shared.paths.user import RECONSTRUCTIONS_DIRECTORY

FADE_TIMEOUT_SECONDS: Final[float] = 120.0

APPLICATION_NAME: Final[str] = "global.dialog.title.main_window"
UNTITLED: Final[str] = "global.dialog.label.untitled"
BY_CONFIGURATION: Final[str] = "global.browser.label.by_configuration"
Expand Down Expand Up @@ -57,7 +64,7 @@ def reconstruction_row(screen: Screen, path: Path) -> Item:
if not browser.is_open(above):
browser.open_by_click(above)
screen.expect(
lambda: browser.is_open(above),
partial(browser.is_open, above),
bool,
description=f"{browser.label(above)} open",
)
Expand All @@ -72,12 +79,68 @@ def load_from_the_browser(screen: Screen, path: Path) -> None:


def expect_open(screen: Screen, path: Path) -> None:
"""Waits until the reconstruction stored at ``path`` is the one open."""
"""Waits until the reconstruction stored at ``path`` is the one open and its sound has landed."""
screen.expect(
lambda: screen.reconstructions.shows_open(path),
bool,
description=f"{path.name} open",
)
screen.expect(screen.sound_settled, bool, description=f"the sound of {path.name} landed")


def reconstruction_shade(screen: Screen) -> str:
"""The theme the waveform's reconstruction line wears, which the fade swaps while its sound is on its way.

Raises:
AssertionError: If the line wears no theme, which would leave a fade nothing to swap.
"""
waveform = screen.reconstructions.waveform
shade = screen.theme_of(waveform.series_tag(screen.words(WAVEFORM_RECONSTRUCTION)))
assert shade is not None, "the reconstruction line wears no theme"
return shade


def shade_reading(screen: Screen) -> Callable[[], Optional[str]]:
"""A reading of the shade the reconstruction line wears, for a frame recording to take after every frame.

The reading runs on the render thread, so it reads the theme directly.
"""
tag = screen.reconstructions.waveform.series_tag(screen.words(WAVEFORM_RECONSTRUCTION))
return partial(read_theme, tag)


def wait_for_the_fade_to_end(
screen: Screen,
unfaded: str,
) -> None:
"""Waits until the reconstruction line wears ``unfaded`` again, which is when the last sound has landed."""
screen.bridge.expect(
lambda: reconstruction_shade(screen),
unfaded.__eq__,
description="the fade ended",
timeout=FADE_TIMEOUT_SECONDS,
)


def drag_item_to(
graph: BarGraph,
standing: List[int],
item: int,
*,
end: float,
bounds: Tuple[int, int],
) -> Tuple[int, List[int]]:
"""Drags the bar of ``item`` to ``end`` and returns the item the plot moved and the items expected after.

The item the plot puts under the release takes the value there, brought within ``bounds``, and
every other item stands as it did.
"""
release = graph.drag_item(item, start=standing[item], end=end)
index, value = graph.item_under(release)
low, high = bounds
expected = [*standing]
expected[index] = min(max(round(value), low), high)
return index, expected


def edit_envelope(
Expand Down Expand Up @@ -223,3 +286,14 @@ def raise_the_first_level_while_held(screen: Screen, hold: RegenerationHold, cha
def edited_title(screen: Screen, document: Path) -> str:
"""The window title while the open reconstruction ``document`` carries unsaved changes."""
return titled(screen, marked(document.name, unsaved=True))


def leave_letting_it_go(screen: Screen) -> None:
"""Presses Exit, confirms the question about unsaved changes and waits for the application to close."""
prompt = screen.reconstructions.unsaved_prompt
screen.press_shortcut(ShortcutId.EXIT)
screen.expect(prompt.is_shown, bool, description="the question about leaving")

prompt.confirm()

assert screen.wait_for_exit()
Loading
Loading