drawbar 0.7.0

Your Nord's sounds, in a window: browse, edit and send programs, samples and pianos, in the browser or on the desktop
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//! How much room a folder has, what is in it, and what the queue would put there.
//!
//! Every figure here is the instrument's own: a [`Status`] entry counted in its
//! partition's unit, and the [`AllocationUnit`] that says what one of those units is
//! worth in bytes. No capacity constant lives in this app.

use eframe::egui;
use nord_usb::wire::{AllocationUnit, Status};
use nord_usb::ObjectClass;

use crate::app::{accent, warn};
use crate::device::DeviceState;
use crate::queue::Queue;
use crate::workspace::Workspace;

/// The trough's height, wherever a meter is drawn.
pub const TROUGH: f32 = 5.0;

/// The point past which a fill stops reading as room and starts reading as a warning.
const CROWDED: f32 = 0.9;

/// How full one class's partition is, and what the queue would add to it.
///
/// ⚠️ Counted in the partition's own unit, never in bytes: a slot-addressed class counts
/// items and a library counts blocks of [`AllocationUnit`] net bytes each.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Meter {
    pub used: u64,
    pub total: u64,
    /// What the queue would add. An entry replacing what is in its slot adds nothing.
    pub queued: u64,
}

impl Meter {
    /// The share of the partition its contents take.
    pub fn filled(&self) -> f32 {
        match self.total {
            0 => 0.0,
            total => (self.used as f32 / total as f32).clamp(0.0, 1.0),
        }
    }

    /// The share the queue would add, cut to whatever is left of the trough.
    pub fn incoming(&self) -> f32 {
        match self.total {
            0 => 0.0,
            total => (self.queued as f32 / total as f32).clamp(0.0, 1.0 - self.filled()),
        }
    }

    /// Whether the fill has passed the point where it is a warning rather than a figure.
    pub fn crowded(&self) -> bool {
        self.filled() > CROWDED
    }
}

/// Whether a class's partition is one that fills: counted in bytes, or divided into
/// more than one bank.
///
/// ⚠️ What is left is a single bank of fixed slots. Its meter would say what its own
/// heading already says in figures, and a bar is a claim about room running out where
/// nothing ever runs out but the count.
fn fills(class: ObjectClass, unit: Option<AllocationUnit>, banks: usize) -> bool {
    (class.is_library() && unit.is_some()) || banks > 1
}

/// What a class's partition holds and what is on its way to it, or nothing for a class
/// whose counters have not been read or whose partition does not fill.
///
/// ⚠️ A partition reporting a total of nothing has no meter either: nothing can be
/// written there and nothing is counted, so a full-width empty trough would be a
/// measurement of a thing that does not divide.
pub fn meter(
    class: ObjectClass,
    inventory: &[Status],
    unit: Option<AllocationUnit>,
    banks: usize,
    queue: &Queue,
    workspace: &Workspace,
) -> Option<Meter> {
    if !fills(class, unit, banks) {
        return None;
    }
    let status = inventory.iter().find(|status| status.class == class)?;
    if status.total() == 0 {
        return None;
    }
    let (used, total) = match status.slots() {
        Some(slots) => (u64::from(status.count), u64::from(slots)),
        None => (u64::from(status.used), status.total()),
    };
    Some(Meter {
        used,
        total,
        queued: incoming(class, status.slots().is_some(), unit, queue, workspace),
    })
}

/// What the queue would add to a class, in the unit that class's meter counts in.
///
/// ⚠️ An entry that replaces what is in its slot adds nothing — the write frees what it
/// overwrites. A slot-addressed class counts the slots that would fill; a library counts
/// the blocks its bodies occupy, which nothing can work out until the partition has
/// reported its allocation unit.
fn incoming(
    class: ObjectClass,
    by_slot: bool,
    unit: Option<AllocationUnit>,
    queue: &Queue,
    workspace: &Workspace,
) -> u64 {
    queue
        .entries()
        .iter()
        .filter(|held| held.class == class && held.replaces.occupant().is_none())
        .filter_map(|held| match by_slot {
            true => Some(1),
            false => {
                let bytes = workspace.get(held.id)?.bytes.len();
                unit?.blocks_for(bytes).ok().map(u64::from)
            }
        })
        .sum()
}

/// What is left in a class's partition, in bytes where the allocation unit says what a
/// unit is worth and in bare units where nothing has.
pub fn free_space(
    class: ObjectClass,
    inventory: &[Status],
    unit: Option<AllocationUnit>,
) -> Option<String> {
    let status = inventory.iter().find(|status| status.class == class)?;
    let (free, total) = (status.available(), status.total());
    Some(match unit {
        Some(unit) => format!(
            "{} free of {}",
            measure(free.saturating_mul(u64::from(unit.get()))),
            measure(total.saturating_mul(u64::from(unit.get()))),
        ),
        None => format!("{free} of {total} free, in units this folder counts in"),
    })
}

/// What is left in a class's partition, in bytes.
///
/// ⚠️ `None` until the partition has reported its allocation unit: free space is a
/// count of units, and nothing turns one into bytes without it.
pub fn free_bytes(class: ObjectClass, device: &DeviceState) -> Option<u64> {
    let unit = device.allocation_unit(class)?;
    let status = device
        .inventory
        .iter()
        .find(|status| status.class == class)?;
    Some(status.available().saturating_mul(u64::from(unit.get())))
}

/// The one thing in the queue that most nearly does not fit, and whether it does.
pub fn constraint(queue: &Queue, workspace: &Workspace, device: &DeviceState) -> Option<String> {
    let (name, bytes, class) = queue
        .entries()
        .iter()
        .filter_map(|held| {
            let entity = workspace.get(held.id)?;
            Some((entity.name.clone(), entity.bytes.len() as u64, held.class))
        })
        .max_by_key(|(_, bytes, _)| *bytes)?;
    let free = free_bytes(class, device)?;
    let verdict = match bytes <= free {
        true => "it fits",
        false => "it does not fit",
    };
    Some(format!(
        "{name} is {} and {} is free — {verdict}.",
        measure(bytes),
        measure(free)
    ))
}

/// A size in the widest unit that leaves a figure worth reading.
pub fn measure(bytes: u64) -> String {
    let (figure, unit) = scaled(bytes, bytes);
    format!("{figure} {unit}")
}

/// A part and the whole it is out of: `121/500 B`, `184.0/192.0 MB`.
///
/// ⚠️ One unit for the pair, taken from the whole. A part given its own unit would read
/// smaller than the total it sits under, and the two figures would no longer compare.
pub fn measure_out_of(part: u64, whole: u64) -> String {
    let (part, unit) = scaled(part, whole);
    let (whole, _) = scaled(whole, whole);
    format!("{part}/{whole} {unit}")
}

/// `bytes` written in the unit a size of `scale` deserves, and that unit's name.
fn scaled(bytes: u64, scale: u64) -> (String, &'static str) {
    const K: f64 = 1024.0;
    let held = bytes as f64;
    let scale = scale as f64;
    if scale < K {
        return (bytes.to_string(), "B");
    }
    if scale < K * K {
        return (format!("{:.1}", held / K), "kB");
    }
    (format!("{:.1}", held / (K * K)), "MB")
}

/// The trough, what the folder holds, and what the queue would add to it.
///
/// ⚠️ The bar takes the tone and the readout beside it keeps its own ink: accent on the
/// panel measures 4.1:1, which carries as a bar and fails as 11 px text.
pub fn bar(ui: &mut egui::Ui, meter: Meter) {
    let (rect, _) = ui.allocate_exact_size(
        egui::vec2(ui.available_width(), TROUGH),
        egui::Sense::hover(),
    );
    let visuals = ui.visuals().clone();
    let painter = ui.painter().clone();
    painter.rect_filled(rect, 1.0, visuals.extreme_bg_color);

    let filled = rect.width() * meter.filled();
    let tone = match meter.crowded() {
        true => warn(&visuals),
        false => accent(&visuals),
    };
    if filled > 0.0 {
        painter.rect_filled(
            egui::Rect::from_min_size(rect.min, egui::vec2(filled, rect.height())),
            1.0,
            tone,
        );
    }
    let incoming = rect.width() * meter.incoming();
    if incoming > 0.0 {
        painter.rect_filled(
            egui::Rect::from_min_size(
                egui::pos2(rect.left() + filled, rect.top()),
                egui::vec2(incoming, rect.height()),
            ),
            1.0,
            warn(&visuals),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::device::{pretend_allocation_unit, Device};
    use crate::log::Log;
    use crate::queue::enqueue;
    use crate::workspace::{Fresh, Origin};
    use nord_usb::Location;

    fn status(class: ObjectClass, count: u32, free: u32, used: u32) -> Status {
        Status {
            class,
            count,
            free,
            used,
            dirty: 0,
            spare: 0,
        }
    }

    fn at(slot: u32) -> Location {
        Location { bank: 0, slot }
    }

    /// A slot-addressed folder counts items, and what is queued for a slot nothing holds
    /// is a slot that would fill.
    #[test]
    fn a_slot_folder_meters_items_and_counts_only_what_would_fill_a_slot() {
        let ctx = egui::Context::default();
        let mut workspace = Workspace::new(ctx.clone());
        let mut device = Device::new(ctx);
        let mut log = Log::default();
        let class = ObjectClass::Program;
        let bytes = {
            let id = workspace.create(Fresh::Program, &mut log).unwrap();
            let held = workspace.get(id).unwrap().bytes.clone();
            workspace.remove(id, &mut log);
            held
        };
        // 100 of 400 slots, each program costing 121 bytes of the partition's count.
        let inventory = [status(class, 100, 300 * 121, 100 * 121)];
        // Two vacant destinations and one that is taken.
        device.pretend_scanned(class, 1, &["", "", "Africa Split"]);

        let mut queue = Queue::default();
        for slot in 0..3 {
            let id = workspace.ingest(
                format!("sound {slot}"),
                Origin::Fresh,
                bytes.clone(),
                &mut log,
            );
            enqueue(
                &workspace,
                &mut device,
                &mut queue,
                &mut log,
                id,
                class,
                at(slot),
            );
        }

        let held =
            meter(class, &inventory, None, 4, &queue, &workspace).expect("the class was read");
        assert_eq!(held.used, 100);
        assert_eq!(held.total, 400);
        assert_eq!(held.queued, 2, "the third replaces what is there");
        assert_eq!(held.filled(), 0.25);
        assert_eq!(held.incoming(), 2.0 / 400.0);
        assert!(!held.crowded());
    }

    /// A library counts blocks of its partition's allocation unit, and until that unit
    /// has arrived nothing can say how much of a block anything occupies.
    #[test]
    fn a_library_meters_blocks_and_has_no_meter_at_all_without_its_unit() {
        let ctx = egui::Context::default();
        let mut workspace = Workspace::new(ctx.clone());
        let mut device = Device::new(ctx);
        let mut log = Log::default();
        let class = ObjectClass::Sample;
        let inventory = [status(class, 84, 64, 1472)];
        device.pretend_scanned(class, 1, &[""]);

        let id = workspace.ingest("a sample".into(), Origin::Fresh, vec![0; 300_000], &mut log);
        let mut queue = Queue::default();
        enqueue(
            &workspace,
            &mut device,
            &mut queue,
            &mut log,
            id,
            class,
            at(0),
        );

        assert_eq!(meter(class, &inventory, None, 1, &queue, &workspace), None);

        let unit = pretend_allocation_unit(class, 131_064);
        let known = meter(class, &inventory, Some(unit), 1, &queue, &workspace).unwrap();
        assert_eq!(known.used, 1472);
        assert_eq!(known.total, 1536);
        // 300 000 / 131 064 = 2.29, and a partial block still costs a whole one.
        assert_eq!(known.queued, 3);
        assert!(known.crowded(), "1472 of 1536 is past nine tenths");
    }

    /// ⚠️ A folder whose partition reports a total of nothing is not an empty folder:
    /// nothing can be written there and nothing is counted, so it gets no meter rather
    /// than an empty one.
    #[test]
    fn a_partition_that_counts_nothing_at_all_has_no_meter() {
        let ctx = egui::Context::default();
        let workspace = Workspace::new(ctx);
        let queue = Queue::default();
        let class = ObjectClass::Piano;
        let unit = Some(pretend_allocation_unit(class, 261_632));
        let inventory = [
            status(class, 0, 0, 0),
            status(ObjectClass::Program, 1, 9, 1),
        ];

        let drawn = |class, unit, banks| meter(class, &inventory, unit, banks, &queue, &workspace);
        assert_eq!(drawn(class, unit, 1), None);
        assert!(drawn(ObjectClass::Program, None, 4).is_some());
    }

    /// A meter is for a partition that can fill: one counted in bytes, or one divided
    /// into more than one bank. A single bank of fixed slots has none — its heading
    /// already says the count, and nothing there runs out but slots.
    #[test]
    fn only_a_partition_that_can_fill_gets_a_meter() {
        let ctx = egui::Context::default();
        let mut device = Device::new(ctx.clone());
        let workspace = Workspace::new(ctx);
        let queue = Queue::default();
        let (one, many, library) = (ObjectClass::Live, ObjectClass::Program, ObjectClass::Sample);
        let inventory = [
            status(one, 1, 4, 1),
            status(many, 100, 300, 100),
            status(library, 84, 64, 1472),
        ];
        device.pretend_geometry(one, &[("Live", 5)]);
        device.pretend_geometry(many, &[("1", 50), ("2", 50), ("3", 50), ("4", 50)]);
        device.pretend_geometry(library, &[("Samp Lib", 1)]);
        let unit = |class| Some(pretend_allocation_unit(class, 1));
        let drawn = |class, unit| {
            meter(
                class,
                &inventory,
                unit,
                device.state.banks(class),
                &queue,
                &workspace,
            )
            .is_some()
        };

        assert!(!drawn(one, unit(one)), "one bank of fixed slots");
        assert!(drawn(many, unit(many)), "more than one bank");
        assert!(drawn(many, None), "a bank division needs no unit");
        assert!(
            drawn(library, unit(library)),
            "one bank, and counted in bytes"
        );
        assert!(!drawn(library, None), "nothing counts bytes without a unit");
    }

    /// The queued segment never runs past the end of the trough, whatever is waiting.
    #[test]
    fn the_queued_segment_stops_at_the_end_of_the_trough() {
        let full = Meter {
            used: 380,
            total: 400,
            queued: 500,
        };
        assert_eq!(full.filled(), 0.95);
        assert!((full.filled() + full.incoming() - 1.0).abs() < f32::EPSILON);
        // A class whose counters say nothing divides into nothing.
        let unread = Meter {
            used: 0,
            total: 0,
            queued: 4,
        };
        assert_eq!((unread.filled(), unread.incoming()), (0.0, 0.0));
    }

    /// The sentence names the largest thing waiting, its size, the room left, and
    /// whether one goes into the other — and says nothing at all with an empty queue.
    #[test]
    fn the_binding_constraint_is_the_largest_thing_waiting_against_the_room_left() {
        let ctx = egui::Context::default();
        let mut workspace = Workspace::new(ctx.clone());
        let mut device = Device::new(ctx);
        let mut log = Log::default();
        let class = ObjectClass::Sample;
        let mut queue = Queue::default();
        assert_eq!(constraint(&queue, &workspace, &device.state), None);

        device.pretend_scanned(class, 1, &["", ""]);
        device.state.inventory.push(status(class, 84, 64, 1472));
        for (name, size, slot) in [("small", 4096, 0), ("Grand", 5_347_738, 1)] {
            let id = workspace.ingest(name.into(), Origin::Fresh, vec![0; size], &mut log);
            enqueue(
                &workspace,
                &mut device,
                &mut queue,
                &mut log,
                id,
                class,
                at(slot),
            );
        }
        // Nothing says what a block is worth, so nothing says whether anything fits.
        assert_eq!(constraint(&queue, &workspace, &device.state), None);

        // 64 blocks of 131 064 bytes is 8.0 MB, and 5 347 738 bytes is 5.1 MB.
        device.pretend_partitions(&crate::device::ELECTRO5);
        assert_eq!(
            constraint(&queue, &workspace, &device.state).as_deref(),
            Some("Grand is 5.1 MB and 8.0 MB is free — it fits.")
        );

        device.state.inventory.clear();
        device.state.inventory.push(status(class, 84, 8, 1528));
        assert!(constraint(&queue, &workspace, &device.state)
            .is_some_and(|said| said.ends_with("it does not fit.")));
    }

    /// What is left reads in bytes once the partition has said what a unit is worth, and
    /// in the partition's own units before that.
    #[test]
    fn free_space_reads_in_bytes_only_once_the_allocation_unit_has_arrived() {
        let class = ObjectClass::Sample;
        let inventory = [status(class, 84, 64, 1472)];
        assert_eq!(
            free_space(class, &inventory, None).as_deref(),
            Some("64 of 1536 free, in units this folder counts in")
        );
        assert_eq!(
            free_space(
                class,
                &inventory,
                Some(pretend_allocation_unit(class, 131_064))
            )
            .as_deref(),
            Some("8.0 MB free of 192.0 MB")
        );
        assert_eq!(free_space(ObjectClass::Program, &inventory, None), None);
    }
}