Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions automation/application/running.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ def language(self) -> LanguageManager:
return self._application.language_manager

def sound_settled(self) -> bool:
"""Whether the open reconstruction's sound stands landed, with no rebuild or listening on its way."""
return not self._application.reconstruction_rewrites.is_busy
"""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:
Expand Down
3 changes: 2 additions & 1 deletion automation/holds/regeneration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class RegenerationHold:
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. 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. The hold lets
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 Down
3 changes: 2 additions & 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 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 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 Expand Up @@ -153,6 +153,7 @@ The manager's own playback is held to the same rule. A stop waits a while for it
| What the reader is told when a sound fails | `PlaybackFailurePresenter` (`coordinators/playback/failures.py`) |
| The boundary that hands a transport command's failure to the presenter | `GuardedPlayer` (`coordinators/playback/guard.py`) |
| The sequencer's mute set, its mask, and solo | `SequencerChannelsLogic` (`logic/sequencer/channels.py`) |
| A whole document's preview, read from the render cache or rendered on a worker of its own | `DocumentPreviews` (`logic/shared/previews.py`) |
| Row mixing, and the mask it pulls while rendering | `RowSynthesizer` (`logic/sequencer/playback/synthesizer/`) |
| The values a note starts from and a channel holds between frames | `ChannelPerformance` (`sampletones_core/performance/state.py`) |
| What a row's voice and pitch start, and what they move | `apply_row` and `note_step` (`sampletones_core/performance/rows.py`) |
Expand Down
12 changes: 8 additions & 4 deletions docs/development/application/reconstruction-edits.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,14 @@ the ribbon answer at once. The waveform and the audio are read from the render c
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. An edit whose new frames offer a row of their own
moves the choice the listening makes on the new record, so its landing reads the envelopes on the spot. 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
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
Expand Down
5 changes: 4 additions & 1 deletion docs/development/application/render-thread.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ and hands them back inside its result; the landing keeps the audio in the render
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.
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 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, 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`), 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.

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/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Runs long operations (file conversion, waveform regeneration, export, playback s
- `_emit` posts the result to `CallbackQueue`, which puts every handler on the render thread (principle 6).
- Result types are a tagged union, so a subscriber matches exhaustively. A long operation reports through `ServiceStarted`, `ServiceProgress`, `ServiceIntermediate`, `ServiceSuccess`, `ServiceError` and `ServiceCanceled`, and a service whose reports carry more, such as the playhead or the request they answer, declares a union of its own in its `result.py`.
- A service is one subpackage holding `service.py` and `result.py`, so its implementation and the contract its subscribers type against are reached separately. The generic contracts every service reports through are in `services/result.py`. `ServiceProgress.fraction` is the one reading a bar draws. See [`progress.md`](progress.md).
- A service imports nothing from `logic/`, `view_model/`, `ui/`, `coordinators/` or `config/`, which the boundary check holds. What a service is handed must be safe on its worker: a value, a kernel it drives alone for the span of a job (`RowSynthesizerProtocol`), or a store built for concurrent use, each named as a Protocol in the service's own subpackage and proven by a case. What a job produces for the interface travels in its result, so the landing installs it on the render thread; `RenderedDocument` carries the audio its worker rendered.
- A service imports nothing from `logic/`, `view_model/`, `ui/`, `coordinators/` or `config/`, which the boundary check holds. What a service is handed must be safe on its worker: a value, a kernel it drives alone for the span of a job (`RowSynthesizerProtocol`), or a store built for concurrent use, each named as a Protocol in the service's own subpackage and proven by a case. What a job produces for the interface travels in its result, so the landing installs it on the render thread; `RenderedDocument` carries the audio its worker rendered. A service instance serves one client's line: a result reaches every subscriber of the instance that made it and names no sender, so a second line, such as the sequencer's previews beside the tab's rewrites, takes an instance of its own.

---

Expand Down
7 changes: 3 additions & 4 deletions docs/development/bugs-and-todos.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ dimension the import starts carrying.
entry down to the frames the edit changed ([undo](application/undo.md)).
* What the Reconstructions tab still computes on the render thread. The regeneration worker renders,
mixes and exports what the reader hears, and the landing installs it ([Editing the open
reconstruction](application/reconstruction-edits.md)); two readings stay on the render thread. The
sequencer's preview of a sample mixes the sample cold on its first read, since it reads the render
cache directly. The original mix of the recordings heard is mixed again on the render thread whenever a
recording is left out, which is a few milliseconds per recording on a two-minute document.
reconstruction](application/reconstruction-edits.md)); one reading stays on the render thread. The
original mix of the recordings heard is mixed again on the render thread whenever a recording is
left out, which is a few milliseconds per recording on a two-minute document.
* Improve performance of the browser's favorite scan of the entire tree per click
* The `fft` and `logfft` analysis floors. Their window spans two cycles of the pulse's lowest note, so the
triangle's lowest octave reaches them by its harmonics alone. Covering it doubles their window and
Expand Down
11 changes: 10 additions & 1 deletion src/sampletones_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
)
from sampletones_application.logic.reconstruction.rewrites.steps import RateChange
from sampletones_application.logic.render import SongRenderLogic
from sampletones_application.logic.shared.previews import DocumentPreviews
from sampletones_application.logic.shared.renders import RenderCache
from sampletones_application.parameters import (
InstructionsTabParameters,
Expand Down Expand Up @@ -291,6 +292,9 @@ def __init__(
self.reconstruction_manager,
self.regeneration_service,
)
self.preview_service: RegenerationService = RegenerationService(priority=_priority)
self.document_previews: DocumentPreviews = DocumentPreviews(self.renders, self.preview_service)
self.document_previews.on_failed = self._report_preview_failure
self.export_service: ExportService = ExportService(priority=_priority)
self.export_service.subscribe(self._on_export_activity)
self.render_service: SongRenderService = SongRenderService(priority=_priority)
Expand Down Expand Up @@ -593,7 +597,7 @@ def __init__(
browser_manager=self.browser_manager,
project_controller=self.project_controller,
history=self.history,
renders=self.renders,
previews=self.document_previews,
original_audio_locator=self._original_audio_locator,
instrument_exports=self._instrument_exports,
tab_active=self._is_sequencer_tab_current,
Expand Down Expand Up @@ -1109,6 +1113,11 @@ def _is_operation_active(self) -> bool:
or self.export_service.is_running()
)

def _report_preview_failure(self, exception: Exception) -> None:
"""Tells the reader a voice's preview failed to render, the way a rebuild's failure is told."""
logger.error_with_traceback(exception, "A preview's render failed")
self.dialogs.show_error(exception)

def _on_export_activity(self, result: ExportResult) -> None:
"""Follows an export claiming the application and handing it back.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
)
from sampletones_application.logic.sequencer.voices import SequencerVoicesLogic
from sampletones_application.logic.shared.file_playback import FilePlayback
from sampletones_application.logic.shared.renders import RenderCache
from sampletones_application.logic.shared.previews import DocumentPreviews
from sampletones_application.logic.shared.tree import TreeLogic
from sampletones_application.parameters.sequencer import SequencerTabParameters
from sampletones_application.services.song_player.service import SongPlayerService
Expand Down Expand Up @@ -119,7 +119,7 @@ def __init__(
browser_manager: BrowserManager,
project_controller: ProjectController,
history: HistoryManager,
renders: RenderCache,
previews: DocumentPreviews,
original_audio_locator: OriginalAudioLocator,
instrument_exports: InstrumentExportCoordinator,
*,
Expand Down Expand Up @@ -195,7 +195,7 @@ def __init__(
session_manager,
audio_device_manager,
scheduling=layout.scheduling,
renders=renders,
previews=previews,
)
self._sequencer_channels_logic: SequencerChannelsLogic = SequencerChannelsLogic()
self._song_player_logic: SongPlayerLogic = SongPlayerLogic(
Expand Down
Loading
Loading