Skip to content
51 changes: 46 additions & 5 deletions automation/steps/reconstructions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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
Expand Down Expand Up @@ -86,13 +88,31 @@ def expect_open(screen: Screen, path: Path) -> None:
screen.expect(screen.sound_settled, bool, description=f"the sound of {path.name} landed")


def reconstruction_shade(screen: Screen) -> Optional[str]:
"""The theme the waveform's reconstruction line wears, which the fade swaps while its sound is on its way."""
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
return screen.theme_of(waveform.series_tag(screen.words(WAVEFORM_RECONSTRUCTION)))
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 wait_for_the_fade_to_end(screen: Screen, unfaded: Optional[str]) -> None:
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),
Expand All @@ -102,6 +122,27 @@ def wait_for_the_fade_to_end(screen: Screen, unfaded: Optional[str]) -> None:
)


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(
screen: Screen,
*,
Expand Down
1 change: 0 additions & 1 deletion automation/vocabulary/graphs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Final

WAVEFORM_REGENERATING: Final[str] = "global.graph.message.waveform_regenerating"
WAVEFORM_READING: Final[str] = "global.graph.message.waveform_reading"
WAVEFORM_RECONSTRUCTION: Final[str] = "global.graph.label.waveform_reconstruction"
1 change: 1 addition & 0 deletions automation/vocabulary/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

PAUSE: Final[str] = "global.menu.label.item_playback_pause"
PLAY: Final[str] = "global.menu.label.item_playback_play"
PLAY_VOICE: Final[str] = "global.context.label.play"
RESUME: Final[str] = "global.menu.label.item_playback_resume"
2 changes: 1 addition & 1 deletion docs/development/application/playback.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Two terms recur. A **source** is what plays one kind of audio: a reconstruction,

An instrument's audition is a preview of that kind. It reads the generator chosen on the instruments card, takes the note from the keyboard's two octaves above the octave the tracker types in, and renders the voice through the same two steps a tracker row takes: the step from the instrument's own pitch to the note, at full volume. The plot card draws the same rendering at the pitch the instrument stands at, so what is seen and what is heard name one generator.

**A preview sounds the frames it renders.** An instrument sounds the envelopes it has, and a sample previews the frames its reconstruction recorded. Both sound exactly what they carry, because a recording's drive matters only while the conversion runs. A sample's preview sounds the render the render cache keeps, or the one a worker of its own lands a moment later, so the render thread computes no sample for it; a newer click replaces a preview still on its way, and the voice heard is the last one asked for.
**A preview sounds the frames it renders.** An instrument sounds the envelopes it has, and a sample previews the frames its reconstruction recorded. Both sound exactly what they carry, because a recording's drive matters only while the conversion runs. A sample's preview sounds the render the render cache keeps, or the one a worker of its own lands a moment later, so the render thread computes no sample for it; a newer click replaces a preview still on its way, and playback the reader starts forgets one, so the voice heard is the last one asked for and a song started meanwhile keeps the device. A reconstruction a browser tree plays is read from disk and previewed the same way.

**A voice sounded on its own runs to its release, or to the length it is offered.** A row holds a voice for as long as the pattern asks. An audition and the voice list's preview have no row behind them, so each has a span of its own. A volume dimension that ends at silence releases the voice, and the sound stops there. A voice that circles from a loop point never reaches a last item, so it sounds for the ticks `AUDITION_TICKS` offers it. One count serves both spans, so the plot card draws exactly the frames the keyboard sounds.

Expand Down
45 changes: 21 additions & 24 deletions docs/development/application/reconstruction-edits.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ rebuild renders. The worker takes the renders the cache holds for the document w
audio of every channel in play, the mix of what the reader hears, and the envelopes each channel exports
for the recordings heard; the landing keeps the audio in the cache and installs the envelopes where the
instruments panel reads them before the tab reads either. A change to what is heard whose sound the
cache keeps none of is a step of the same line, rendered by the same worker. Every rule below keeps the document as though each change had
landed before the next one was made.
cache keeps none of is a step of the same line, rendered by the same worker. Every rule below keeps the
document as though each change had landed before the next one was made.

---

Expand Down Expand Up @@ -76,29 +76,26 @@ batch started is retuned again from what the sample now holds.

## A change to what is heard is a step

A box on the stems card or above the waveform changes the reader's listening, which the stems card and
the ribbon answer at once. The waveform and the audio are read from the render cache under the hearing
they sound, and the envelopes the instruments panel draws are the ones the worker exported for the
recordings heard; where the cache keeps no sound for the hearing, or the envelopes drawn were exported
for another choice of recordings, the panel asks for a listening step, which renders the hearing of the
document as it then stands and lands like a rebuild. The reader's choice travels onto a new record by one
rule, written in core and read by the worker and the listening state alike: a recording keeps what was
chosen for it where it keeps holding frames, is heard wherever it reaches for the first time, and the row
an edit's new frames offer joins what is heard where it appears, as a channel coming into play joins the
channels on. The worker carries the hearing onto the document it rebuilt before rendering and exporting,
so the landing installs sound and envelopes for the choice the listening then makes. One listening
waiting answers every choice made before it is taken, and a listening asked for during a rebuild reads
the rebuilt document. A freshly opened document is shown the same way, so its channels
render on the worker too. The showing the panel owes happens once the sound lands, and a hearing the
cache already answers is shown at once. A WAV export is a reading of what plays like a showing: it
writes the kept sound at once, and where the cache keeps none it is owed beside the showing and written
once the worker's reading lands, for what the reader hears then.
A box on the stems card or above the waveform changes the reader's listening. The stems card and the
ribbon answer at once. The waveform, the audio and the envelopes the instruments panel draws are read
from what the worker landed for the hearing. Where the screen holds none of it, the panel owes the showing
and asks for a listening step, which renders the hearing of the document as it then stands and lands like
a rebuild. The reader's choice travels onto a new record by one rule, written in core and read by the
worker and the listening state alike (`carried_hearing`), so the sound and envelopes a landing installs
are the ones the listening then reads. One listening waiting answers every choice made before it is
taken. A freshly opened document, a removal, a retune, an undo and a save-as are shown the same way,
their sound and envelopes landed by a listening. Every gesture that reads the sound joins what the panel
owes, so a flip of the source or a retune during a showing on its way settles with it. A WAV export is a
reading of what plays like a showing: it writes the kept sound at once, or waits beside the showing and
is written for what the reader hears once the sound lands. A document put away before that, or a reading
that fails, drops the export and tells the reader so. A channel change reaching its turn while the
envelopes of what is heard are on their way waits behind a listening of that choice.

## The waveform fades while the line is busy

The waveform is faded from the moment a step waits or runs until the last one lands. A reader sees the
The waveform is faded from the moment a step waits or runs until the last one lands, so a reader sees the
document is being rewritten for the whole span, however many steps it takes. The status bar names what
the fade waits for: an edit on its way reads "Regenerating reconstruction...", and a change to what is
heard, or a document just opened, reads "Preparing the sound...". The line reports the kind of work it
is busy with, so a hand-over from an edit to a reading of the sound changes the words under the same
fade.
the fade waits for: `global.graph.message.waveform_regenerating` for an edit on its way, and
`global.graph.message.waveform_reading` for a change to what is heard or a document just opened. The line
reports the kind of work it is busy with, so a hand-over from an edit to a reading of the sound changes
the words under the same fade.
21 changes: 11 additions & 10 deletions docs/development/application/render-thread.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ budget (`scheduling.queue_budget_seconds`), so a large backlog spreads across fr
continues. Every background result reaches UI state this way. Applying one to UI state directly from the
worker thread is forbidden.

A result carries what the landing installs. The regeneration renders the edited channel's audio on its
worker, the mix of what the reader hears and the envelopes each channel exports for the recordings heard,
and hands them back inside its result; the landing keeps the audio in the render cache and installs the
envelopes on the render thread. A reading the cache cannot answer, such as a recording
left out for the first time or a freshly opened document, is asked of the same worker and lands the same
way. What a worker produces for the interface therefore reaches what the interface reads through the
queue alone, and the render thread computes no sample while the worker renders. A document previewed
elsewhere, such as a voice clicked in the sequencer, reads the same cache and asks a worker of its own
for what the cache lacks (`DocumentPreviews` in `logic/shared/previews.py`), and sounds at the landing;
the cache's writers are the landings, every one of them on the render thread.
A result carries what the landing installs. The regeneration worker renders every sound the
Reconstructions tab draws or plays: each channel's audio, the mix of what the reader hears, the mix of the
recordings heard, which the waveform draws as the original, and the envelopes each channel exports for the
recordings heard. It hands them back inside its result. The landing keeps the audio in the render cache
and installs the envelopes, on the render thread. A reading the cache leaves unanswered is asked of the
same worker and lands the same way. What a worker produces for the interface therefore reaches what the
interface reads through the queue alone, and the render thread computes no sample while the worker
renders. A document previewed elsewhere reads the same cache and asks a worker of its own for what the
cache lacks (`DocumentPreviews` in `logic/shared/previews.py`), and sounds at the landing; a
reconstruction file a browser tree plays is read from disk and previewed the same way. The render cache
keeps what the landings adopt, so its writers are the landings, every one of them on the render thread.

A logic object hearing a worker's report, such as a library generation or the audio device's position,
posts its own handler to the queue the same way. The logic layer reaches the queue, and `utils/gui`
Expand Down
2 changes: 1 addition & 1 deletion docs/development/application/stems.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The reconstruction tab's Stems card turns the recorded assignment into a listene

### What the code guarantees

A filtered mix keeps every array at its unfiltered length, so it aligns with the unfiltered one sample for sample. The filter zeroes the unselected frames per channel before mixing (`filter_approximations`), and a channel every owner of which is heard keeps its render as it is, which is exact because a resting frame renders as silence already; a document heard whole therefore reads the kept renders and computes nothing. The original mix covers the recordings of the stems heard on any channel, and it is carried across an edit that keeps every recording. It is silence while no stem is heard, and there is none while no recording loaded. A surface paints a lane as one series per recording, traced over the stretches that recording holds, so a lane costs as many items as it has owners however many times the document divides, and a box on the stems card repaints as a theme bound again per recording. The listening state holds the channels each stem is heard on and the channels kept on, carries both onto a new record by the core's one rule (`carried_hearing`, which the regeneration worker applies to the document it rebuilds), and the panel logic re-answers the stems view model, the waveform and the audio data whenever either choice changes. The waveform and the audio are read from the render cache under the hearing they sound, and the envelopes the instruments panel draws are the ones the regeneration worker exported for the recordings heard; a hearing the cache keeps no sound for, or no envelopes for, is rendered on that worker as a step of the open document's line ([Editing the open reconstruction](reconstruction-edits.md)), and the showing waits for it, so a box computes no sample and reads no frame on the render thread. A reconstruction that records one source presents a single row for its recording, and one that records no source shows the card's empty state.
A filtered mix keeps every array at its unfiltered length, so it aligns with the unfiltered one sample for sample. The filter zeroes the unselected frames per channel before mixing (`filter_approximations`). A channel every owner of which is heard keeps its render as it is, which is exact because a resting frame renders as silence already. A document heard whole therefore reads the kept renders as they are. The original mix covers the recordings of the stems heard on any channel. It is a worker's render like the rest, kept under the recordings it was mixed from, so every document holding those recordings reads one mix. It is silence while no stem is heard, and there is none while no recording loaded. A surface paints a lane as one series per recording, traced over the stretches that recording holds, so a lane costs as many items as it has owners however many times the document divides. A box on the stems card repaints as a theme bound again per recording. The listening state holds the channels each stem is heard on and the channels kept on. It carries both onto a new record by the core's one rule (`carried_hearing`), which the regeneration worker applies to the document it rebuilds. The panel logic re-answers the stems view model, the waveform and the audio data whenever either choice changes. The waveform and the audio are read from the render cache under the hearing they sound, and the envelopes the instruments panel draws are the ones the regeneration worker exported for the recordings heard. A hearing the cache keeps no sound for, or no envelopes for, is rendered on that worker as a step of the open document's line ([Editing the open reconstruction](reconstruction-edits.md)), and the showing waits for it, so a box computes no sample and reads no frame on the render thread. A reconstruction that records one source presents a single row for its recording, and one that records no source shows the card's empty state.

Removal runs through `without_stem`, which returns a fresh reconstruction holding what the rules in [Editing a stems reconstruction](#editing-a-stems-reconstruction) leave. A confirmed removal is a step of the open document's edits, taken after the edits made before it ([Editing the open reconstruction](reconstruction-edits.md)), and it lands as a `ReconstructionEdit`, the payload both a regenerated instrument and a removed recording travel as. One path therefore rebinds the open document and records the edit against the project history. They differ in what the instruments panel does. A regenerated instrument carries the envelopes the panel's own edit wrote, so the panel keeps drawing them. A removal redraws every channel from the document it leaves.

Expand Down
2 changes: 1 addition & 1 deletion docs/development/application/undo.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ A reconstruction edit follows the same rule. `RegenerationService` rebuilds one

**The types enforce the split.** A value's model is frozen, so a write in place raises where it is made. A core test walks every class a project reaches (`tests/unit/sampletones_core/project/test_values.py`). It requires each class to be a declared shell or a frozen value made of values. The history audit (`tests/suite/history/`) replays gestures against a model of the stack. After every step, it checks each stored entry against the state it recorded.

**Derived data belongs to no entry.** A document's sound is rendered from its instructions. The application keeps every render in one `RenderCache`, which the composition root builds and the render thread fills with what the regeneration worker hands back: the channels and the mix of a rebuilt document, and the mix of what the reader hears of it. The sequencer's preview mix alone is rendered cold on its first read. A render is keyed by the identity of a channel's instructions and by the setup they sound under. It goes when those instructions go, and a byte budget bounds the rest. History therefore keeps instructions only, and documents that share a channel share its render.
**Derived data belongs to no entry.** A document's sound is rendered from its instructions. The application keeps every render in one `RenderCache`, which the composition root builds and the render thread fills with what the regeneration workers hand back: the channels and the mix of a rebuilt document, the mix of what the reader hears of it, and the whole of a voice previewed in the sequencer. The cache renders nothing itself. A render is keyed by the identity of a channel's instructions and by the setup they sound under. It goes when those instructions go, and a byte budget bounds the rest. History therefore keeps instructions only, and documents that share a channel share its render.

## The open voice across a restore

Expand Down
Loading
Loading