makepad/libs/milkdrop/examples/explorer.rs
Admin 012e6f7df5 tools, libs and the rest
cargo-makepad, makepad-builder (with an Android step on every OS), CI tools
and tiles, and the remaining libraries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

219 lines
7.6 KiB
Rust

//! MilkDrop explorer: browse a preset pack by category, search it, rate and
//! favourite presets, and play them through `MilkdropView` driven by a
//! synthetic beat (no microphone, nothing played aloud).
//!
//! The browsing is the shared [`MilkdropBrowser`] widget, which keeps the
//! pack in the MilkDrop data folder every app shares
//! (`makepad_milkdrop::browser::data_dir`) and offers to download projectM's
//! "cream of the crop" pack while it is missing.
//!
//! Keys: Cmd/Ctrl+→ and ← next and previous in the shown list, Cmd/Ctrl+R a
//! random preset, Cmd/Ctrl+D favourite, Cmd/Ctrl+1…5 rate (0 clears).
pub use makepad_milkdrop::makepad_widgets;
use makepad_milkdrop::{MilkdropBrowserWidgetRefExt, MilkdropViewWidgetRefExt, PresetPick};
use makepad_widgets::*;
app_main!(App);
script_mod! {
use mod.prelude.widgets.*
use mod.widgets.*
startup() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
window.title: "MilkDrop Explorer"
window.inner_size: vec2(1400 900)
body +: {
flow: Overlay
View{
width: Fill
height: Fill
flow: Down
show_bg: true
draw_bg +: {color: theme.color_bg_app}
stage := View{
width: Fill
height: 520
show_bg: true
draw_bg +: {color: #x000000}
milk := MilkdropView{width: Fill height: Fill}
}
View{
width: Fill height: Fit flow: Right spacing: 8 padding: Inset{left: 10 right: 10 top: 6 bottom: 6}
align: Align{y: 0.5}
previous := Button{text: "Previous"}
random := Button{text: "Random"}
next := Button{text: "Next"}
favourite := Button{text: "Favourite"}
now := Label{width: Fill text: ""}
}
presets := MilkdropBrowser{padding: Inset{left: 10 right: 10 bottom: 8}}
}
menus := MenuLayer{}
}
}
}
}
}
// ---- the synthetic beat ----
/// A 124 BPM kick, a hi-hat on the off-beats and a slow bass sweep: enough
/// for bass, mid and treble to move, generated per frame, never played.
struct Beat {
rate: f32,
t: f64,
noise: u32,
buffer: Vec<f32>,
}
impl Beat {
fn new() -> Self {
Self { rate: 44_100.0, t: 0.0, noise: 0x1234_5678, buffer: Vec::new() }
}
/// Samples covering `dt` seconds (at most a tenth of a second).
fn fill(&mut self, dt: f64) -> &[f32] {
let count = ((dt.clamp(0.0, 0.1)) * self.rate as f64) as usize;
self.buffer.clear();
let beat = 60.0 / 124.0;
for _ in 0..count {
let t = self.t;
let phase = t % beat;
let kick = (-phase * 18.0).exp() * (std::f64::consts::TAU * (50.0 + 90.0 * (-phase * 30.0).exp()) * phase).sin();
let off = (t + beat * 0.5) % beat;
self.noise ^= self.noise << 13;
self.noise ^= self.noise >> 17;
self.noise ^= self.noise << 5;
let hat = (-off * 60.0).exp() * ((self.noise as f64 / u32::MAX as f64) * 2.0 - 1.0);
let sweep = 0.25 * (std::f64::consts::TAU * (110.0 + 80.0 * (t * 0.2).sin()) * t).sin();
self.buffer.push((0.8 * kick + 0.3 * hat + sweep) as f32 * 0.6);
self.t += 1.0 / self.rate as f64;
}
&self.buffer
}
}
// ---- the app ----
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
#[rust(Beat::new())]
beat: Beat,
/// Whether the view plays (from the first preset on).
#[rust]
playing: bool,
#[rust]
beat_frame: NextFrame,
#[rust]
last_frame: Option<f64>,
}
impl App {
/// Load `pick` into the view (read and parsed off the UI thread, then
/// faded to) and start the beat.
fn play(&mut self, cx: &mut Cx, pick: Option<PresetPick>) {
let Some(pick) = pick else { return };
self.ui.label(cx, ids!(now)).set_text(cx, &pick.line());
let view = self.ui.milkdrop_view(cx, ids!(milk));
view.load_preset(cx, &pick.path);
if !self.playing {
self.playing = true;
view.set_playing(cx, true);
self.beat_frame = cx.new_next_frame();
}
}
fn handle_key(&mut self, cx: &mut Cx, key: &KeyEvent) {
if !(key.modifiers.logo || key.modifiers.control) {
return;
}
let presets = self.ui.milkdrop_browser(cx, ids!(presets));
match key.key_code {
KeyCode::ArrowRight => {
let pick = presets.step(cx, 1);
self.play(cx, pick);
}
KeyCode::ArrowLeft => {
let pick = presets.step(cx, -1);
self.play(cx, pick);
}
KeyCode::KeyR => {
let pick = presets.random(cx);
self.play(cx, pick);
}
KeyCode::KeyD => {
presets.toggle_favourite(cx);
}
KeyCode::Key0 => presets.rate(cx, 0),
KeyCode::Key1 => presets.rate(cx, 1),
KeyCode::Key2 => presets.rate(cx, 2),
KeyCode::Key3 => presets.rate(cx, 3),
KeyCode::Key4 => presets.rate(cx, 4),
KeyCode::Key5 => presets.rate(cx, 5),
_ => {}
}
}
/// Feed the view the next slice of the synthetic beat, while it plays.
fn feed_beat(&mut self, cx: &mut Cx, time: f64) {
if !self.playing {
self.last_frame = None;
return;
}
let view = self.ui.milkdrop_view(cx, ids!(milk));
let dt = self.last_frame.map(|last| time - last).unwrap_or(1.0 / 60.0);
self.last_frame = Some(time);
let rate = self.beat.rate;
view.feed_audio(self.beat.fill(dt), rate);
self.beat_frame = cx.new_next_frame();
}
}
impl MatchEvent for App {
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
let presets = self.ui.milkdrop_browser(cx, ids!(presets));
if let Some(pick) = presets.played(actions) {
self.play(cx, Some(pick));
}
if self.ui.button(cx, ids!(next)).clicked(actions) {
let pick = presets.step(cx, 1);
self.play(cx, pick);
}
if self.ui.button(cx, ids!(previous)).clicked(actions) {
let pick = presets.step(cx, -1);
self.play(cx, pick);
}
if self.ui.button(cx, ids!(random)).clicked(actions) {
let pick = presets.random(cx);
self.play(cx, pick);
}
if self.ui.button(cx, ids!(favourite)).clicked(actions) {
presets.toggle_favourite(cx);
}
}
}
impl AppMain for App {
fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
crate::makepad_widgets::script_mod(vm);
makepad_milkdrop::script_mod(vm);
self::script_mod(vm)
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
if let Some(frame) = self.beat_frame.is_event(event) {
self.feed_beat(cx, frame.time);
}
if let Event::KeyDown(key) = event {
self.handle_key(cx, key);
}
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}