feat(patina): rebuild resonance detector, simplify controls, new look
Check and Windows preview / check (patina) (push) Successful in 1m48s
Check and Windows preview / check (rivet) (push) Successful in 1m26s
Check and Windows preview / previews (push) Successful in 55s
Independent plugin release / plan (push) Successful in 7s
Check and Windows preview / windows (push) Successful in 1m41s
Independent plugin release / build (push) Successful in 2m28s
Independent plugin release / publish (push) Successful in 7s

Patina 0.2.0. The old detector compared raw FFT bins with a one-octave
dB mean, so every harmonic of a tonal sound looked like a resonance and
real harsh bumps were barely touched. The new detector:

- averages power over each log band and smooths it over time
- joins harmonic tops into an envelope, smoothed by Sharpness
- compares it with a robust, tilt-aware local line fit that ignores
  empty regions and cannot be lifted by the bump itself

Six overlapping controls become Depth, Sharpness and Selectivity; focus
points now read "up cuts more, down protects". The analysis window is
halved to 2048 samples (about 43 ms delay). Behaviour tests cover plain
harmonic tones, resonant bumps, narrow rings, level independence,
flicker on noise and high sample rates.

Both editors get a dark metallic look with #FF0160 as the single
accent colour; screenshots and docs are updated. Projects saved with
Patina 0.1 load the new controls at their defaults.
This commit is contained in:
nvrl committed 2026-11-06 01:38:17 +01:00
1 parent 15853e61ac
commit 17c6fe3ab9
25 files changed
+1074 -539

No files matched your search

+3 -3
View File
@@ -1,4 +1,4 @@
use super::{egui, report, start, EditorState, LOCK, MUTED, NOTE};
use super::{egui, report, start, EditorState, LOCK, MUTED, NOTE, ROLE, SURFACE};
use rivet_app::{Output, Shared};
use rivet_core::{CreativeMove, PerformanceSettings, Result, RoleMode, Session};
use std::sync::Arc;
@@ -181,9 +181,9 @@ pub fn performance(ui: &mut egui::Ui, settings: &mut PerformanceSettings) {
});
if curve.enabled {
let (rect, _) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 58.), egui::Sense::hover());
let painter = ui.painter(); painter.rect_filled(rect, 3., egui::Color32::from_rgb(30,43,63));
let painter = ui.painter(); painter.rect_filled(rect, 3., SURFACE);
let points: Vec<_> = curve.points.iter().map(|p| egui::pos2(rect.left() + p.position as f32 / 1000. * rect.width(), rect.bottom() - p.value as f32 / 127. * rect.height())).collect();
for pair in points.windows(2) { painter.line_segment([pair[0],pair[1]], egui::Stroke::new(1.5_f32, if role == 0 { NOTE } else { egui::Color32::from_rgb(107,201,194) })); }
for pair in points.windows(2) { painter.line_segment([pair[0],pair[1]], egui::Stroke::new(1.5_f32, if role == 0 { NOTE } else { ROLE })); }
egui::ScrollArea::vertical().id_salt("curve-points").max_height(115.).show(ui, |ui| {
let mut remove = None;
for index in 0..curve.points.len() {
+68 -17
View File
@@ -11,12 +11,21 @@ mod creative_editor;
use rivet_core::{
Audition, ChordQuality, Ending, Error, Harmony, Phrase, Result, Scale, Session, Task, BAR,
};
const INK: Color32 = Color32::from_rgb(225, 231, 243);
const MUTED: Color32 = Color32::from_rgb(154, 172, 196);
const NOTE: Color32 = Color32::from_rgb(178, 164, 237);
const LOCK: Color32 = Color32::from_rgb(241, 192, 109);
const SURFACE: Color32 = Color32::from_rgb(30, 43, 63);
const GRID: Color32 = Color32::from_rgb(56, 72, 95);
// Dark anodized hardware, shared with Patina: graphite, one signal colour, steel.
const INK: Color32 = Color32::from_rgb(232, 233, 236);
const MUTED: Color32 = Color32::from_rgb(140, 146, 156);
/// Ordinary notes and neutral data.
const NOTE: Color32 = Color32::from_rgb(184, 192, 204);
/// Signal colour: anchors, pins, active state, errors.
const LOCK: Color32 = Color32::from_rgb(255, 1, 96);
/// Second sound role.
const ROLE: Color32 = Color32::from_rgb(126, 167, 216);
/// Recessed screens: piano rolls and curves.
const SURFACE: Color32 = Color32::from_rgb(10, 11, 12);
const GRID: Color32 = Color32::from_rgb(36, 39, 44);
const BG: Color32 = Color32::from_rgb(14, 15, 17);
const PANEL: Color32 = Color32::from_rgb(25, 27, 31);
const EDGE: Color32 = Color32::from_rgb(44, 47, 53);
const KEYS: [&str; 12] = [
"C", "C♯", "D", "E♭", "E", "F", "F♯", "G", "A♭", "A", "B♭", "B",
];
@@ -54,13 +63,46 @@ impl Default for EditorState {
}
pub fn style(ctx: &egui::Context) {
let mut visuals = egui::Visuals::dark();
visuals.panel_fill = Color32::from_rgb(23, 34, 51);
visuals.window_fill = Color32::from_rgb(30, 43, 63);
visuals.panel_fill = BG;
visuals.window_fill = PANEL;
visuals.extreme_bg_color = SURFACE;
visuals.faint_bg_color = PANEL;
visuals.override_text_color = Some(INK);
visuals.selection.bg_fill = Color32::from_rgb(82, 72, 125);
visuals.widgets.inactive.bg_fill = Color32::from_rgb(48, 64, 87);
visuals.widgets.hovered.bg_fill = Color32::from_rgb(68, 83, 108);
visuals.widgets.active.bg_fill = Color32::from_rgb(91, 78, 135);
visuals.selection.bg_fill = Color32::from_rgb(110, 6, 46);
visuals.selection.stroke = Stroke::new(1.0_f32, LOCK);
visuals.hyperlink_color = LOCK;
visuals.window_stroke = Stroke::new(1.0_f32, EDGE);
for (w, fill, stroke) in [
(&mut visuals.widgets.noninteractive, PANEL, EDGE),
(
&mut visuals.widgets.inactive,
Color32::from_rgb(35, 38, 43),
EDGE,
),
(
&mut visuals.widgets.hovered,
Color32::from_rgb(45, 48, 54),
Color32::from_rgb(78, 82, 90),
),
(
&mut visuals.widgets.active,
Color32::from_rgb(58, 18, 34),
LOCK,
),
(
&mut visuals.widgets.open,
Color32::from_rgb(35, 38, 43),
EDGE,
),
] {
w.bg_fill = fill;
w.weak_bg_fill = fill;
w.bg_stroke = Stroke::new(1.0_f32, stroke);
w.corner_radius = 3.into();
}
visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0_f32, INK);
visuals.widgets.inactive.fg_stroke = Stroke::new(1.0_f32, INK);
visuals.slider_trailing_fill = true;
ctx.set_visuals(visuals);
let mut style = (*ctx.style_of(egui::Theme::Dark)).clone();
style.spacing.item_spacing = Vec2::new(10., 8.);
@@ -349,7 +391,10 @@ pub fn show(ui: &mut egui::Ui, shared: &Arc<Shared>, state: &mut EditorState) {
});
egui::ScrollArea::vertical().show(ui,|ui|{
ui.horizontal(|ui|{
ui.label(RichText::new("Rivet").size(28.).strong());
let (mark, _) = ui.allocate_exact_size(Vec2::new(4., 26.), Sense::hover());
ui.painter().rect_filled(mark, 1., LOCK);
ui.label(RichText::new("RIVET").size(26.).strong());
ui.label(RichText::new("MOTIF STORYBOARD").size(12.).color(MUTED));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center),|ui|{
if ui.add_enabled(!busy,egui::Button::new("Import motif…")).clicked(){
import(shared,state,None,false);
@@ -376,7 +421,7 @@ pub fn show(ui: &mut egui::Ui, shared: &Arc<Shared>, state: &mut EditorState) {
ui.label("Phrase");
ui.selectable_value(&mut session.settings.bars,4,"4 bars");
ui.selectable_value(&mut session.settings.bars,8,"8 bars");
if ui.add_enabled(session.source.is_some(),egui::Button::new("Explore alternatives").fill(Color32::from_rgb(83,70,127))).clicked(){
if ui.add_enabled(session.source.is_some(),egui::Button::new(RichText::new("Explore alternatives").color(Color32::WHITE)).fill(LOCK)).clicked(){
generate=true;
}
});
@@ -504,7 +549,7 @@ pub fn show(ui: &mut egui::Ui, shared: &Arc<Shared>, state: &mut EditorState) {
} else{
"Pin"
},section*2+1,section*2+2)).fill(if pinned{
Color32::from_rgb(99,77,45)
Color32::from_rgb(58,18,34)
} else{
SURFACE
}
@@ -732,7 +777,13 @@ fn roll(
},
);
let painter = ui.painter();
painter.rect_filled(rect, 4., SURFACE);
painter.rect_filled(rect, 3., SURFACE);
painter.rect_stroke(
rect,
3.,
Stroke::new(1.0_f32, EDGE),
egui::StrokeKind::Inside,
);
let plot = Rect::from_min_max(
Pos2::new(rect.left() + 32., rect.top() + 17.),
Pos2::new(rect.right() - 5., rect.bottom() - 5.),
@@ -792,7 +843,7 @@ fn roll(
if n.is_anchor() {
LOCK
} else if style.roles.role(i, n) == 1 {
Color32::from_rgb(107, 201, 194)
ROLE
} else {
NOTE
},