Skip to main content

max / audiofiles

Describe the sample forge Six shapes: the window, the sample, the three sections and the two controls a section is made of. One screen rather than several, which is what the header already argued: a run in flight is a property of the subject, so it greys the controls rather than answering a different screen. Three helpers took a container and handed it back. `live` took an Act to grey it, `dial` took a Field to give it an address, and both are settings in a body now. `knob_route` returned an Action, which is a type no shape can declare; it was one line building an address the description now says where it uses it. The chop parameters are an optional number and an optional choice rather than a match with three arms. At most one of each in every method, and the number goes before the choice in the one method that has both.
Author: Max Johnson <me@maxj.phd> · 2026-09-04 21:30 UTC
Signed with PGP, not checked
Commit: 0db40223f43474af601eab19d1b29c467718ce5b
Parent: fb4a317
1 file changed, +219 insertions, -200 deletions
@@ -86,11 +86,9 @@
86 86 //! screen should not carry a description of a screen that has not been built.
87 87 //! The shipped window may keep it; there is nothing to port.
88 88
89 + use quasi_declare::declare;
89 90 use quasi_router::layout::Tone;
90 - use quasi_router::{
91 - Act, Action, Choice, Field, Node, RegionKind, Request, Response, RouteError, Router, Screen,
92 - Slot,
93 - };
91 + use quasi_router::{Action, Choice, Node, Request, Response, RouteError, Router};
94 92
95 93 use super::{Chop, DeviceChoice, Forging, Knob, Panels};
96 94
@@ -118,7 +116,7 @@
118 116
119 117 /// `GET /forge`
120 118 fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
121 - Ok(screen(state).into())
119 + Ok(showing(state))
122 120 }
123 121
124 122 /// `POST /forge/slice`
@@ -135,7 +133,7 @@
135 133 .unwrap_or_default();
136 134 let how = Chop::from_key(name).ok_or_else(|| RouteError::not_found("no such chop method"))?;
137 135 state.forge.slice_by(how);
138 - Ok(screen(state).into())
136 + Ok(showing(state))
139 137 }
140 138
141 139 /// `POST /forge/set/{knob}`
@@ -153,14 +151,14 @@
153 151 .or_else(|| request.payload.get(Node::SELECTED))
154 152 .unwrap_or_default();
155 153 state.forge.turn(knob, value);
156 - Ok(screen(state).into())
154 + Ok(showing(state))
157 155 }
158 156
159 157 /// `POST /forge/preview`
160 158 fn preview(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
161 159 forging(state)?;
162 160 state.forge.preview();
163 - Ok(screen(state).into())
161 + Ok(showing(state))
164 162 }
165 163
166 164 /// `POST /forge/chop`
@@ -175,7 +173,7 @@
175 173 return Err(RouteError::not_found("preview the slices first"));
176 174 }
177 175 state.forge.chop();
178 - Ok(screen(state).into())
176 + Ok(showing(state))
179 177 }
180 178
181 179 /// `POST /forge/device`
@@ -194,7 +192,7 @@
194 192 return Err(RouteError::not_found("no such device profile"));
195 193 }
196 194 state.forge.choose_device(chosen);
197 - Ok(screen(state).into())
195 + Ok(showing(state))
198 196 }
199 197
200 198 /// `POST /forge/conform`
@@ -204,7 +202,7 @@
204 202 return Err(RouteError::not_found("choose a device first"));
205 203 }
206 204 state.forge.conform();
207 - Ok(screen(state).into())
205 + Ok(showing(state))
208 206 }
209 207
210 208 /// `POST /forge/trim`
@@ -218,7 +216,7 @@
218 216 return Err(RouteError::not_found("choose two or more samples"));
219 217 }
220 218 state.forge.trim_silence();
221 - Ok(screen(state).into())
219 + Ok(showing(state))
222 220 }
223 221
224 222 /// The sample in the forge, refusing every write when there is none.
@@ -229,93 +227,209 @@
229 227 .ok_or_else(|| RouteError::not_found("no sample is in the forge"))
230 228 }
231 229
232 - /// The window.
233 - fn screen(state: &Panels<'_>) -> Screen {
234 - let body = match state.forge.forging() {
235 - Some(forging) => loaded(&forging),
236 - None => Slot::new(BODY, RegionKind::Pane).with(Node::empty(
237 - "Select a sample and open the forge to chop, conform, or batch-process it.",
238 - )),
239 - };
240 - Screen::sidebar_content("Sample Forge").with(body)
230 + /// The window, read and then described.
231 + pub(super) fn showing(state: &Panels<'_>) -> Response {
232 + Response::from(screen(&read(state)))
233 + }
234 +
235 + /// What the window draws, read off the app.
236 + struct Forge {
237 + /// The sample in the forge, while one is in it.
238 + loaded: Option<Loaded>,
241 239 }
242 240
243 241 /// A sample, and the three things that can be made out of it.
244 - fn loaded(forging: &Forging) -> Slot {
245 - let mut body = Slot::new(BODY, RegionKind::Pane)
246 - .with(Node::page(forging.name.clone()))
247 - .with(Node::text(format!("{} Hz", forging.rate)));
248 -
249 - // Said rather than drawn as a spinner beside a separator: what a reader
250 - // needs from it is that every control below is currently inert, and the
251 - // controls say that themselves through `State::Disabled`.
252 - if forging.busy {
253 - body = body.with(Node::banner(Tone::Info, "Working..."));
254 - }
255 -
256 - body = body.with(Node::Region(chopping(forging)));
257 - body = body.with(Node::Region(conforming(forging)));
258 - body.with(Node::Region(batching(forging)))
242 + struct Loaded {
243 + /// What the sample is called.
244 + name: String,
245 + /// What it was recorded at.
246 + rate: String,
247 + /// Whether a chop or a conform is in flight.
248 + ///
249 + /// Said rather than drawn as a spinner beside a separator: what a reader
250 + /// needs from it is that every control below is currently inert, and the
251 + /// controls say that themselves through `State::Disabled`.
252 + busy: bool,
253 + /// Slicing one sample into several.
254 + chop: Chopping,
255 + /// The device profiles a conform could target, while there are any.
256 + conform: Option<Conform>,
257 + /// Batch trimming, while enough samples are chosen for it to mean anything.
258 + batch: Option<Batch>,
259 259 }
260 260
261 261 /// Slicing one sample into several.
262 - fn chopping(forging: &Forging) -> Slot {
263 - // The section heading became the field's label: one name for one question,
264 - // rather than a heading above a control that could not say what it asked.
265 - let mut group = Slot::new("forge-chop", RegionKind::Group).with(Node::Field(Box::new(
266 - Field::radio(
267 - HOW,
268 - "Chop",
269 - Chop::ALL
270 - .into_iter()
271 - .map(|how| Choice::new(how.as_str(), how.label()))
272 - .collect(),
273 - )
274 - .value(forging.how.as_str())
275 - .writes(Action::post("/forge/slice")),
276 - )));
262 + struct Chopping {
263 + /// The method chosen, as the picker's value.
264 + how: &'static str,
265 + /// Every method it could be.
266 + methods: Vec<Choice>,
267 + /// The number the chosen method reads, where it reads one.
268 + ///
269 + /// Only the parameters the chosen method reads are here at all, which is the
270 + /// shipped window's own `match` and the settings screen's line: a control
271 + /// that cannot be used is worse than one that is not there. At most one of
272 + /// each, and the number goes before the choice in the one method that has
273 + /// both.
274 + number: Option<Dial>,
275 + /// The choice it reads, where it reads one.
276 + choice: Option<Strip>,
277 + /// What the commit reads.
278 + ///
279 + /// The count on the label, which is the correction the shipped button made
280 + /// to itself: a commit says its blast radius before it is pressed.
281 + go: String,
282 + /// Whether the commit is dead, for want of a preview or for a run in flight.
283 + stuck: bool,
284 + /// Whether it is dead for want of a preview specifically.
285 + ///
286 + /// The fifth consumer of `quasi:vocabulary:disabled-reason`: the shipped
287 + /// button says the reason to a pointer and nothing else can, so it is a line
288 + /// of its own beside the control.
289 + unpreviewed: bool,
290 + }
277 291
278 - // Only the parameters the chosen method reads, which is the shipped
279 - // window's own `match` and the settings screen's line: a control that
280 - // cannot be used is worse than one that is not there.
281 - group = match forging.how {
282 - Chop::Transient => group.with(dial(
283 - forging,
284 - Field::range(Knob::Sensitivity.as_str(), "Sensitivity", "0", "1")
285 - .step("0.01")
286 - .value(format!("{:.2}", forging.sensitivity)),
287 - )),
288 - Chop::Equal => group.with(strip(
289 - Knob::Divisions,
290 - "Slices",
291 - &forging.divisions.to_string(),
292 - [2_usize, 4, 8, 16, 32].map(|n| (n.to_string(), n.to_string())),
293 - )),
294 - Chop::Bpm => group
295 - .with(dial(
296 - forging,
297 - Field::range(Knob::Bpm.as_str(), "BPM", "20", "300")
298 - .step("0.5")
299 - .value(format!("{:.1}", forging.bpm)),
300 - ))
301 - .with(strip(
302 - Knob::Subdivisions,
303 - "Subdivision",
304 - &forging.subdivisions.to_string(),
305 - [("1", "1/4"), ("2", "1/8"), ("4", "1/16")]
306 - .map(|(value, label)| (value.to_owned(), label.to_owned())),
307 - )),
292 + /// A number a control turns.
293 + struct Dial {
294 + /// The name it submits under, which is also what its address is built from.
295 + name: &'static str,
296 + /// What the question is called.
297 + label: &'static str,
298 + /// The ends of the slider.
299 + min: &'static str,
300 + max: &'static str,
301 + /// The granularity it moves in.
302 + step: &'static str,
303 + /// The value showing, written at the precision the control uses.
304 + value: String,
305 + /// What the number is measured in, where it is measured in anything.
306 + unit: Option<&'static str>,
307 + }
308 +
309 + /// A handful of values that do not fold away, asked as a question.
310 + ///
311 + /// Five slice counts and three subdivisions are one choice each, written
312 + /// immediately. Being a field is what gives them a label; a bare strip of
313 + /// options had to borrow one from a section heading above.
314 + struct Strip {
315 + /// The name it submits under, which is also what its address is built from.
316 + name: &'static str,
317 + /// What the question is called.
318 + label: &'static str,
319 + /// The value showing.
320 + chosen: String,
321 + /// What it could be.
322 + options: Vec<Choice>,
323 + }
324 +
325 + /// Making one sample fit a piece of hardware.
326 + struct Conform {
327 + /// The profiles, as the picker's options.
328 + devices: Vec<Choice>,
329 + /// Which is chosen, empty for none.
330 + chosen: String,
331 + /// Whether the commit is dead: nothing chosen, or a run in flight.
332 + stuck: bool,
333 + }
334 +
335 + /// The one operation here that is about the selection rather than the sample.
336 + struct Batch {
337 + /// The level below which batch trim treats audio as silence.
338 + threshold: Dial,
339 + /// What the button reads.
340 + go: String,
341 + }
342 +
343 + /// What the window draws, read off the app.
344 + fn read(state: &Panels<'_>) -> Forge {
345 + Forge {
346 + loaded: state.forge.forging().map(|forging| Loaded {
347 + name: forging.name.clone(),
348 + rate: format!("{} Hz", forging.rate),
349 + busy: forging.busy,
350 + chop: chopping_read(&forging),
351 + conform: (!forging.devices.is_empty()).then(|| Conform {
352 + devices: forging
353 + .devices
354 + .iter()
355 + .map(|device| Choice::new(device.name.clone(), describe(device)))
356 + .collect(),
357 + chosen: forging.device.clone().unwrap_or_default(),
358 + stuck: forging.device.is_none() || forging.busy,
359 + }),
360 + batch: (forging.chosen >= 2).then(|| Batch {
361 + threshold: Dial {
362 + name: Knob::Threshold.as_str(),
363 + label: "Threshold",
364 + min: "-96",
365 + max: "-20",
366 + step: "1",
367 + value: format!("{:.0}", forging.threshold_db),
368 + unit: Some("dBFS"),
369 + },
370 + go: format!("Trim silence on {} samples", forging.chosen),
371 + }),
372 + }),
373 + }
374 + }
375 +
376 + /// The chop section, read off the sample.
377 + fn chopping_read(forging: &Forging) -> Chopping {
378 + let (number, choice) = match forging.how {
379 + Chop::Transient => (
380 + Some(Dial {
381 + name: Knob::Sensitivity.as_str(),
382 + label: "Sensitivity",
383 + min: "0",
384 + max: "1",
385 + step: "0.01",
386 + value: format!("{:.2}", forging.sensitivity),
387 + unit: None,
388 + }),
389 + None,
390 + ),
391 + Chop::Equal => (
392 + None,
393 + Some(Strip {
394 + name: Knob::Divisions.as_str(),
395 + label: "Slices",
396 + chosen: forging.divisions.to_string(),
397 + options: [2_usize, 4, 8, 16, 32]
398 + .into_iter()
399 + .map(|n| Choice::new(n.to_string(), n.to_string()))
400 + .collect(),
401 + }),
402 + ),
403 + Chop::Bpm => (
404 + Some(Dial {
405 + name: Knob::Bpm.as_str(),
406 + label: "BPM",
407 + min: "20",
408 + max: "300",
409 + step: "0.5",
410 + value: format!("{:.1}", forging.bpm),
411 + unit: None,
412 + }),
413 + Some(Strip {
414 + name: Knob::Subdivisions.as_str(),
415 + label: "Subdivision",
416 + chosen: forging.subdivisions.to_string(),
417 + options: [("1", "1/4"), ("2", "1/8"), ("4", "1/16")]
418 + .into_iter()
419 + .map(|(value, label)| Choice::new(value, label))
420 + .collect(),
421 + }),
422 + ),
308 423 };
309 -
310 - group = group.with(Node::Act(live(
311 - forging,
312 - Act::new("Preview slices", Action::post("/forge/preview")),
313 - )));
314 -
315 - // The count on the label, which is the correction the shipped button made to
316 - // itself: a commit says its blast radius before it is pressed.
317 - let mut go = Act::new(
318 - if forging.slices == 0 {
424 + Chopping {
425 + how: forging.how.as_str(),
426 + methods: Chop::ALL
427 + .into_iter()
428 + .map(|how| Choice::new(how.as_str(), how.label()))
429 + .collect(),
430 + number,
431 + choice,
432 + go: if forging.slices == 0 {
319 433 "Chop".to_owned()
320 434 } else {
321 435 format!(
@@ -324,89 +438,9 @@
324 438 if forging.slices == 1 { "" } else { "s" }
325 439 )
326 440 },
327 - Action::post("/forge/chop"),
328 - );
329 - if forging.slices == 0 {
330 - // The fifth consumer of `quasi:vocabulary:disabled-reason`: the shipped
331 - // button says this to a pointer and nothing else can.
332 - group = group.with(Node::text(
333 - "Preview the slices first to see how many will be created.",
334 - ));
335 - go = go.disabled();
336 - } else {
337 - go = live(forging, go);
441 + stuck: forging.slices == 0 || forging.busy,
442 + unpreviewed: forging.slices == 0,
338 443 }
339 -
340 - group.with(Node::Act(go)).with(Node::text(
341 - "Slices are written into a new folder beside this sample.",
342 - ))
343 - }
344 -
345 - /// Making one sample fit a piece of hardware.
346 - fn conforming(forging: &Forging) -> Slot {
347 - let group = Slot::new("forge-conform", RegionKind::Group);
348 -
349 - if forging.devices.is_empty() {
350 - return group
351 - .with(Node::section("Conform for device"))
352 - .with(Node::empty("No device profiles available."));
353 - }
354 -
355 - let mut field = Field::select(
356 - DEVICE,
357 - "Conform for device",
358 - forging
359 - .devices
360 - .iter()
361 - .map(|device| Choice::new(device.name.clone(), describe(device)))
362 - .collect(),
363 - )
364 - .writes(Action::post("/forge/device"));
365 - // The instruction is the picker's ghost text rather than a disabled button's
366 - // job, which is the call the shipped screen already made: a select with
367 - // nothing chosen reads as an empty box, and the greyed control beside it is
368 - // the wrong place to explain that.
369 - field.placeholder = Some("Select device...".to_owned());
370 - field.value = Some(forging.device.clone().unwrap_or_default());
371 -
372 - let mut go = Act::new("Conform", Action::post("/forge/conform"));
373 - if forging.device.is_none() {
374 - go = go.disabled();
375 - } else {
376 - go = live(forging, go);
377 - }
378 -
379 - group
380 - .with(Node::Field(Box::new(field)))
381 - .with(Node::Act(go))
382 - .with(Node::text(
383 - "Resamples and converts bit depth to match the device, as a new sample.",
384 - ))
385 - }
386 -
387 - /// The one operation here that is about the selection rather than the sample.
388 - fn batching(forging: &Forging) -> Slot {
389 - let group = Slot::new("forge-batch", RegionKind::Group).with(Node::section("Batch"));
390 -
391 - if forging.chosen < 2 {
392 - return group.with(Node::empty("Select 2+ samples to batch trim silence."));
393 - }
394 -
395 - group
396 - .with(Node::Field(Box::new(
397 - Field::range(Knob::Threshold.as_str(), "Threshold", "-96", "-20")
398 - .unit("dBFS")
399 - .step("1")
400 - .value(format!("{:.0}", forging.threshold_db))
401 - .writes(knob_route(Knob::Threshold)),
402 - )))
403 - .with(Node::Act(live(
404 - forging,
405 - Act::new(
406 - format!("Trim silence on {} samples", forging.chosen),
407 - Action::post("/forge/trim"),
408 - ),
409 - )))
410 444 }
411 445
412 446 /// What a device profile says about itself, as one option.
@@ -418,43 +452,148 @@
418 452 }
419 453 }
420 454
421 - /// A number the slicing reads, live unless a run is in flight.
422 - fn dial(forging: &Forging, field: Field) -> Node {
423 - let _ = forging;
424 - let name = field.name.clone();
425 - Node::Field(Box::new(
426 - field.writes(Action::post(format!("/forge/set/{name}"))),
427 - ))
455 + declare! {
456 + /// The window.
457 + ///
458 + /// One shape rather than several, which is the header's argument: the
459 + /// shipped window keeps drawing every control while a chop or a conform is
460 + /// in flight and greys them, because the sample is still the subject and
461 + /// nothing has been arrived at.
462 + shape screen(forge: &Forge) -> Screen;
463 +
464 + screen sidebar_content "Sample Forge" {
465 + include loaded(forge);
466 + }
428 467 }
429 468
430 - /// A handful of values that do not fold away, asked as a question.
431 - ///
432 - /// Five slice counts and three subdivisions are one choice each, written
433 - /// immediately. Being a field is what gives them a label; a bare strip of
434 - /// options had to borrow one from a section heading above.
435 - fn strip(
436 - knob: Knob,
437 - label: &str,
438 - chosen: &str,
439 - options: impl IntoIterator<Item = (String, String)>,
440 - ) -> Node {
441 - let options = options
442 - .into_iter()
443 - .map(|(value, label)| Choice::new(value, label))
444 - .collect();
445 - Node::Field(Box::new(
446 - Field::radio(knob.as_str(), label, options)
447 - .value(chosen)
448 - .writes(knob_route(knob)),
449 - ))
Lines truncated