Add metronome
This commit is contained in:
parent
84a1a98452
commit
c938ea44b0
|
|
@ -90,8 +90,9 @@ impl Metronome {
|
|||
self.last_beat = -1; // Reset beat tracking when disabled
|
||||
self.click_position = 0; // Stop any playing click
|
||||
} else {
|
||||
// When enabling, don't trigger a click until the next beat
|
||||
self.click_position = usize::MAX; // Set to max to prevent immediate click
|
||||
// Reset beat tracking so the next beat boundary (including beat 0) fires a click
|
||||
self.last_beat = -1;
|
||||
self.click_position = self.high_click.len(); // Idle (past end, nothing playing)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,20 +131,11 @@ impl Metronome {
|
|||
if current_beat != self.last_beat && current_beat >= 0 {
|
||||
self.last_beat = current_beat;
|
||||
|
||||
// Only trigger a click if we're not in the "just enabled" state
|
||||
if self.click_position != usize::MAX {
|
||||
// Determine which click to play
|
||||
// Beat 1 of each measure gets the accent (high click)
|
||||
let beat_in_measure = (current_beat as u32 % self.time_signature_numerator) as usize;
|
||||
let is_first_beat = beat_in_measure == 0;
|
||||
|
||||
// Start playing the appropriate click
|
||||
self.playing_high_click = is_first_beat;
|
||||
self.click_position = 0; // Start from beginning of click
|
||||
} else {
|
||||
// We just got enabled - reset position but don't play yet
|
||||
self.click_position = self.high_click.len(); // Set past end so no click plays
|
||||
}
|
||||
// Determine which click to play.
|
||||
// Beat 0 of each measure gets the accent (high click).
|
||||
let beat_in_measure = (current_beat as u32 % self.time_signature_numerator) as usize;
|
||||
self.playing_high_click = beat_in_measure == 0;
|
||||
self.click_position = 0; // Start from beginning of click
|
||||
}
|
||||
|
||||
// Continue playing click sample if we're currently in one
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ enum SplitPreviewMode {
|
|||
}
|
||||
|
||||
/// Rasterize an embedded SVG and upload it as an egui texture
|
||||
fn rasterize_svg(svg_data: &[u8], name: &str, render_size: u32, ctx: &egui::Context) -> Option<egui::TextureHandle> {
|
||||
pub(crate) fn rasterize_svg(svg_data: &[u8], name: &str, render_size: u32, ctx: &egui::Context) -> Option<egui::TextureHandle> {
|
||||
let tree = resvg::usvg::Tree::from_data(svg_data, &resvg::usvg::Options::default()).ok()?;
|
||||
let pixmap_size = tree.size().to_int_size();
|
||||
let scale_x = render_size as f32 / pixmap_size.width() as f32;
|
||||
|
|
@ -855,6 +855,7 @@ struct EditorApp {
|
|||
#[allow(dead_code)]
|
||||
armed_layers: HashSet<Uuid>,
|
||||
is_recording: bool, // Whether recording is currently active
|
||||
metronome_enabled: bool, // Whether metronome clicks during recording
|
||||
recording_clips: HashMap<Uuid, u32>, // layer_id -> backend clip_id during recording
|
||||
recording_start_time: f64, // Playback time when recording started
|
||||
recording_layer_ids: Vec<Uuid>, // Layers being recorded to (for creating clips)
|
||||
|
|
@ -1126,6 +1127,7 @@ impl EditorApp {
|
|||
recording_arm_mode: RecordingArmMode::default(), // Auto mode by default
|
||||
armed_layers: HashSet::new(), // No layers explicitly armed
|
||||
is_recording: false, // Not recording initially
|
||||
metronome_enabled: false, // Metronome off by default
|
||||
recording_clips: HashMap::new(), // No active recording clips
|
||||
recording_start_time: 0.0, // Will be set when recording starts
|
||||
recording_layer_ids: Vec::new(), // Will be populated when recording starts
|
||||
|
|
@ -5771,6 +5773,7 @@ impl eframe::App for EditorApp {
|
|||
playback_time: &mut self.playback_time,
|
||||
is_playing: &mut self.is_playing,
|
||||
is_recording: &mut self.is_recording,
|
||||
metronome_enabled: &mut self.metronome_enabled,
|
||||
recording_clips: &mut self.recording_clips,
|
||||
recording_start_time: &mut self.recording_start_time,
|
||||
recording_layer_ids: &mut self.recording_layer_ids,
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ pub struct SharedPaneState<'a> {
|
|||
pub is_playing: &'a mut bool, // Whether playback is currently active
|
||||
/// Recording state
|
||||
pub is_recording: &'a mut bool, // Whether recording is currently active
|
||||
pub metronome_enabled: &'a mut bool, // Whether metronome clicks during recording
|
||||
pub recording_clips: &'a mut std::collections::HashMap<uuid::Uuid, u32>, // layer_id -> clip_id
|
||||
pub recording_start_time: &'a mut f64, // Playback time when recording started
|
||||
pub recording_layer_ids: &'a mut Vec<uuid::Uuid>, // Layers being recorded to
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@ pub struct TimelinePane {
|
|||
automation_cache_generation: u64,
|
||||
/// Last seen graph_topology_generation; used to detect node additions/removals
|
||||
automation_topology_generation: u64,
|
||||
/// Cached metronome icon texture (loaded on first use)
|
||||
metronome_icon: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
/// Check if a clip type can be dropped on a layer type
|
||||
|
|
@ -672,6 +674,7 @@ impl TimelinePane {
|
|||
pending_automation_actions: Vec::new(),
|
||||
automation_cache_generation: u64::MAX,
|
||||
automation_topology_generation: u64::MAX,
|
||||
metronome_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -983,10 +986,14 @@ impl TimelinePane {
|
|||
return;
|
||||
}
|
||||
|
||||
// Auto-start playback if needed
|
||||
if !*shared.is_playing {
|
||||
if let Some(controller_arc) = shared.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
// Auto-start playback if needed, and enable metronome if requested.
|
||||
// Metronome must be enabled BEFORE play() so beat 0 is not missed.
|
||||
if let Some(controller_arc) = shared.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
if *shared.metronome_enabled {
|
||||
controller.set_metronome_enabled(true);
|
||||
}
|
||||
if !*shared.is_playing {
|
||||
controller.play();
|
||||
*shared.is_playing = true;
|
||||
println!("▶ Auto-started playback for recording");
|
||||
|
|
@ -1042,6 +1049,8 @@ impl TimelinePane {
|
|||
controller.stop_recording();
|
||||
eprintln!("[STOP] Audio stop command sent at +{:.1}ms", stop_wall.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
// Always disable metronome on recording stop
|
||||
controller.set_metronome_enabled(false);
|
||||
}
|
||||
|
||||
// Note: Don't clear recording_layer_ids here!
|
||||
|
|
@ -4738,6 +4747,64 @@ impl PaneRenderer for TimelinePane {
|
|||
if *shared.is_recording {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
// Metronome toggle — only visible in Measures mode
|
||||
if self.time_display_format == TimeDisplayFormat::Measures {
|
||||
ui.add_space(4.0);
|
||||
|
||||
let metro_tint = if *shared.metronome_enabled {
|
||||
egui::Color32::from_rgb(100, 180, 255)
|
||||
} else {
|
||||
ui.visuals().text_color()
|
||||
};
|
||||
|
||||
// Lazy-load the metronome SVG icon
|
||||
if self.metronome_icon.is_none() {
|
||||
const METRONOME_SVG: &[u8] = include_bytes!("../../../../src/assets/metronome.svg");
|
||||
self.metronome_icon = crate::rasterize_svg(METRONOME_SVG, "metronome_icon", 64, ui.ctx());
|
||||
}
|
||||
|
||||
let metro_response = if let Some(icon) = &self.metronome_icon {
|
||||
let (rect, response) = ui.allocate_exact_size(button_size, egui::Sense::click());
|
||||
let bg = if *shared.metronome_enabled {
|
||||
egui::Color32::from_rgba_unmultiplied(100, 180, 255, 60)
|
||||
} else if response.hovered() {
|
||||
ui.visuals().widgets.hovered.bg_fill
|
||||
} else {
|
||||
egui::Color32::TRANSPARENT
|
||||
};
|
||||
ui.painter().rect_filled(rect, 4.0, bg);
|
||||
ui.painter().image(
|
||||
icon.id(),
|
||||
rect.shrink(4.0),
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
metro_tint,
|
||||
);
|
||||
response
|
||||
} else {
|
||||
// Fallback if SVG failed to load
|
||||
ui.add_sized(button_size, egui::Button::new(
|
||||
egui::RichText::new("♩").color(metro_tint).size(16.0)
|
||||
))
|
||||
};
|
||||
|
||||
let metro_response = metro_response.on_hover_text(if *shared.metronome_enabled {
|
||||
"Disable metronome"
|
||||
} else {
|
||||
"Enable metronome"
|
||||
});
|
||||
|
||||
if metro_response.clicked() {
|
||||
*shared.metronome_enabled = !*shared.metronome_enabled;
|
||||
// Sync live state if already recording
|
||||
if *shared.is_recording {
|
||||
if let Some(controller_arc) = shared.audio_controller {
|
||||
let mut controller = controller_arc.lock().unwrap();
|
||||
controller.set_metronome_enabled(*shared.metronome_enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue