Add metronome
This commit is contained in:
parent
e97dc5695f
commit
e51a6b803d
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::audio::buffer_pool::BufferPool;
|
use crate::audio::buffer_pool::BufferPool;
|
||||||
use crate::audio::clip::ClipId;
|
use crate::audio::clip::ClipId;
|
||||||
|
use crate::audio::metronome::Metronome;
|
||||||
use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent};
|
use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent};
|
||||||
use crate::audio::node_graph::{nodes::*, AudioGraph};
|
use crate::audio::node_graph::{nodes::*, AudioGraph};
|
||||||
use crate::audio::pool::AudioPool;
|
use crate::audio::pool::AudioPool;
|
||||||
|
|
@ -55,6 +56,9 @@ pub struct Engine {
|
||||||
|
|
||||||
// MIDI input manager for external MIDI devices
|
// MIDI input manager for external MIDI devices
|
||||||
midi_input_manager: Option<MidiInputManager>,
|
midi_input_manager: Option<MidiInputManager>,
|
||||||
|
|
||||||
|
// Metronome for click track
|
||||||
|
metronome: Metronome,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
|
|
@ -96,6 +100,7 @@ impl Engine {
|
||||||
recording_progress_counter: 0,
|
recording_progress_counter: 0,
|
||||||
midi_recording_state: None,
|
midi_recording_state: None,
|
||||||
midi_input_manager: None,
|
midi_input_manager: None,
|
||||||
|
metronome: Metronome::new(sample_rate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,6 +252,15 @@ impl Engine {
|
||||||
// Copy mix to output
|
// Copy mix to output
|
||||||
output.copy_from_slice(&self.mix_buffer);
|
output.copy_from_slice(&self.mix_buffer);
|
||||||
|
|
||||||
|
// Mix in metronome clicks
|
||||||
|
self.metronome.process(
|
||||||
|
output,
|
||||||
|
self.playhead,
|
||||||
|
self.playing,
|
||||||
|
self.sample_rate,
|
||||||
|
self.channels,
|
||||||
|
);
|
||||||
|
|
||||||
// Update playhead (convert total samples to frames)
|
// Update playhead (convert total samples to frames)
|
||||||
self.playhead += (output.len() / self.channels as usize) as u64;
|
self.playhead += (output.len() / self.channels as usize) as u64;
|
||||||
|
|
||||||
|
|
@ -756,6 +770,10 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Command::SetMetronomeEnabled(enabled) => {
|
||||||
|
self.metronome.set_enabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
// Node graph commands
|
// Node graph commands
|
||||||
Command::GraphAddNode(track_id, node_type, x, y) => {
|
Command::GraphAddNode(track_id, node_type, x, y) => {
|
||||||
eprintln!("[DEBUG] GraphAddNode received: track_id={}, node_type={}, x={}, y={}", track_id, node_type, x, y);
|
eprintln!("[DEBUG] GraphAddNode received: track_id={}, node_type={}, x={}, y={}", track_id, node_type, x, y);
|
||||||
|
|
@ -2143,6 +2161,11 @@ impl EngineController {
|
||||||
let _ = self.command_tx.push(Command::SetActiveMidiTrack(track_id));
|
let _ = self.command_tx.push(Command::SetActiveMidiTrack(track_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enable or disable the metronome click track
|
||||||
|
pub fn set_metronome_enabled(&mut self, enabled: bool) {
|
||||||
|
let _ = self.command_tx.push(Command::SetMetronomeEnabled(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
// Node graph operations
|
// Node graph operations
|
||||||
|
|
||||||
/// Add a node to a track's instrument graph
|
/// Add a node to a track's instrument graph
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
/// Metronome for providing click track during playback
|
||||||
|
pub struct Metronome {
|
||||||
|
enabled: bool,
|
||||||
|
bpm: f32,
|
||||||
|
time_signature_numerator: u32,
|
||||||
|
time_signature_denominator: u32,
|
||||||
|
last_beat: i64, // Last beat number that was played (-1 = none)
|
||||||
|
|
||||||
|
// Pre-generated click samples (mono)
|
||||||
|
high_click: Vec<f32>, // Accent click for first beat
|
||||||
|
low_click: Vec<f32>, // Normal click for other beats
|
||||||
|
|
||||||
|
// Click playback state
|
||||||
|
click_position: usize, // Current position in the click sample (0 = not playing)
|
||||||
|
playing_high_click: bool, // Which click we're currently playing
|
||||||
|
|
||||||
|
sample_rate: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metronome {
|
||||||
|
/// Create a new metronome with pre-generated click sounds
|
||||||
|
pub fn new(sample_rate: u32) -> Self {
|
||||||
|
let (high_click, low_click) = Self::generate_clicks(sample_rate);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
bpm: 120.0,
|
||||||
|
time_signature_numerator: 4,
|
||||||
|
time_signature_denominator: 4,
|
||||||
|
last_beat: -1,
|
||||||
|
high_click,
|
||||||
|
low_click,
|
||||||
|
click_position: 0,
|
||||||
|
playing_high_click: false,
|
||||||
|
sample_rate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate woodblock-style click samples
|
||||||
|
fn generate_clicks(sample_rate: u32) -> (Vec<f32>, Vec<f32>) {
|
||||||
|
let click_duration_ms = 10.0; // 10ms click
|
||||||
|
let click_samples = ((sample_rate as f32 * click_duration_ms) / 1000.0) as usize;
|
||||||
|
|
||||||
|
// High click (accent): 1200 Hz + 2400 Hz (higher pitched woodblock)
|
||||||
|
let high_freq1 = 1200.0;
|
||||||
|
let high_freq2 = 2400.0;
|
||||||
|
let mut high_click = Vec::with_capacity(click_samples);
|
||||||
|
|
||||||
|
for i in 0..click_samples {
|
||||||
|
let t = i as f32 / sample_rate as f32;
|
||||||
|
let envelope = 1.0 - (i as f32 / click_samples as f32); // Linear decay
|
||||||
|
let envelope = envelope * envelope; // Square for faster decay
|
||||||
|
|
||||||
|
// Mix two sine waves for woodblock character
|
||||||
|
let sample = 0.3 * (2.0 * std::f32::consts::PI * high_freq1 * t).sin()
|
||||||
|
+ 0.2 * (2.0 * std::f32::consts::PI * high_freq2 * t).sin();
|
||||||
|
|
||||||
|
// Add a bit of noise for attack transient
|
||||||
|
let noise = (i as f32 * 0.1).sin() * 0.1;
|
||||||
|
|
||||||
|
high_click.push((sample + noise) * envelope * 0.5); // Scale down to avoid clipping
|
||||||
|
}
|
||||||
|
|
||||||
|
// Low click: 800 Hz + 1600 Hz (lower pitched woodblock)
|
||||||
|
let low_freq1 = 800.0;
|
||||||
|
let low_freq2 = 1600.0;
|
||||||
|
let mut low_click = Vec::with_capacity(click_samples);
|
||||||
|
|
||||||
|
for i in 0..click_samples {
|
||||||
|
let t = i as f32 / sample_rate as f32;
|
||||||
|
let envelope = 1.0 - (i as f32 / click_samples as f32);
|
||||||
|
let envelope = envelope * envelope;
|
||||||
|
|
||||||
|
let sample = 0.3 * (2.0 * std::f32::consts::PI * low_freq1 * t).sin()
|
||||||
|
+ 0.2 * (2.0 * std::f32::consts::PI * low_freq2 * t).sin();
|
||||||
|
|
||||||
|
let noise = (i as f32 * 0.1).sin() * 0.1;
|
||||||
|
|
||||||
|
low_click.push((sample + noise) * envelope * 0.4); // Slightly quieter than high click
|
||||||
|
}
|
||||||
|
|
||||||
|
(high_click, low_click)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable the metronome
|
||||||
|
pub fn set_enabled(&mut self, enabled: bool) {
|
||||||
|
self.enabled = enabled;
|
||||||
|
if !enabled {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update BPM and time signature
|
||||||
|
pub fn update_timing(&mut self, bpm: f32, time_signature: (u32, u32)) {
|
||||||
|
self.bpm = bpm;
|
||||||
|
self.time_signature_numerator = time_signature.0;
|
||||||
|
self.time_signature_denominator = time_signature.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process audio and mix in metronome clicks
|
||||||
|
pub fn process(
|
||||||
|
&mut self,
|
||||||
|
output: &mut [f32],
|
||||||
|
playhead_samples: u64,
|
||||||
|
playing: bool,
|
||||||
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
|
) {
|
||||||
|
if !self.enabled || !playing {
|
||||||
|
self.click_position = 0; // Reset if not playing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames = output.len() / channels as usize;
|
||||||
|
|
||||||
|
for frame in 0..frames {
|
||||||
|
let current_sample = playhead_samples + frame as u64;
|
||||||
|
|
||||||
|
// Calculate current beat number
|
||||||
|
let current_time_seconds = current_sample as f64 / sample_rate as f64;
|
||||||
|
let beats_per_second = self.bpm as f64 / 60.0;
|
||||||
|
let current_beat = (current_time_seconds * beats_per_second).floor() as i64;
|
||||||
|
|
||||||
|
// Check if we crossed a beat boundary
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue playing click sample if we're currently in one
|
||||||
|
let click = if self.playing_high_click {
|
||||||
|
&self.high_click
|
||||||
|
} else {
|
||||||
|
&self.low_click
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.click_position < click.len() {
|
||||||
|
let click_sample = click[self.click_position];
|
||||||
|
|
||||||
|
// Mix into all channels
|
||||||
|
for ch in 0..channels as usize {
|
||||||
|
let output_idx = frame * channels as usize + ch;
|
||||||
|
output[output_idx] += click_sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.click_position += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod bpm_detector;
|
||||||
pub mod buffer_pool;
|
pub mod buffer_pool;
|
||||||
pub mod clip;
|
pub mod clip;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
|
pub mod metronome;
|
||||||
pub mod midi;
|
pub mod midi;
|
||||||
pub mod node_graph;
|
pub mod node_graph;
|
||||||
pub mod pool;
|
pub mod pool;
|
||||||
|
|
@ -15,6 +16,7 @@ pub use automation::{AutomationLane, AutomationLaneId, AutomationPoint, CurveTyp
|
||||||
pub use buffer_pool::BufferPool;
|
pub use buffer_pool::BufferPool;
|
||||||
pub use clip::{Clip, ClipId};
|
pub use clip::{Clip, ClipId};
|
||||||
pub use engine::{Engine, EngineController};
|
pub use engine::{Engine, EngineController};
|
||||||
|
pub use metronome::Metronome;
|
||||||
pub use midi::{MidiClip, MidiClipId, MidiEvent};
|
pub use midi::{MidiClip, MidiClipId, MidiEvent};
|
||||||
pub use pool::{AudioFile as PoolAudioFile, AudioPool};
|
pub use pool::{AudioFile as PoolAudioFile, AudioPool};
|
||||||
pub use project::Project;
|
pub use project::Project;
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,10 @@ pub enum Command {
|
||||||
/// Set the active MIDI track for external MIDI input routing (track_id or None)
|
/// Set the active MIDI track for external MIDI input routing (track_id or None)
|
||||||
SetActiveMidiTrack(Option<TrackId>),
|
SetActiveMidiTrack(Option<TrackId>),
|
||||||
|
|
||||||
|
// Metronome command
|
||||||
|
/// Enable or disable the metronome click track
|
||||||
|
SetMetronomeEnabled(bool),
|
||||||
|
|
||||||
// Node graph commands
|
// Node graph commands
|
||||||
/// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y)
|
/// Add a node to a track's instrument graph (track_id, node_type, position_x, position_y)
|
||||||
GraphAddNode(TrackId, String, f32, f32),
|
GraphAddNode(TrackId, String, f32, f32),
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,20 @@ pub async fn audio_stop(state: tauri::State<'_, Arc<Mutex<AudioState>>>) -> Resu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_metronome_enabled(
|
||||||
|
state: tauri::State<'_, Arc<Mutex<AudioState>>>,
|
||||||
|
enabled: bool
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut audio_state = state.lock().unwrap();
|
||||||
|
if let Some(controller) = &mut audio_state.controller {
|
||||||
|
controller.set_metronome_enabled(enabled);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Audio not initialized".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn audio_test_beep(state: tauri::State<'_, Arc<Mutex<AudioState>>>) -> Result<(), String> {
|
pub async fn audio_test_beep(state: tauri::State<'_, Arc<Mutex<AudioState>>>) -> Result<(), String> {
|
||||||
let mut audio_state = state.lock().unwrap();
|
let mut audio_state = state.lock().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ pub fn run() {
|
||||||
audio::audio_reset,
|
audio::audio_reset,
|
||||||
audio::audio_play,
|
audio::audio_play,
|
||||||
audio::audio_stop,
|
audio::audio_stop,
|
||||||
|
audio::set_metronome_enabled,
|
||||||
audio::audio_seek,
|
audio::audio_seek,
|
||||||
audio::audio_test_beep,
|
audio::audio_test_beep,
|
||||||
audio::audio_set_track_parameter,
|
audio::audio_set_track_parameter,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Metronome body (trapezoid) -->
|
||||||
|
<path d="M12 4 L8 20 L16 20 Z" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter"/>
|
||||||
|
<!-- Base -->
|
||||||
|
<rect x="6" y="20" width="12" height="2" fill="currentColor"/>
|
||||||
|
<!-- Pendulum arm -->
|
||||||
|
<line x1="12" y1="8" x2="14" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<!-- Pendulum weight -->
|
||||||
|
<circle cx="14" cy="16" r="1.5" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 554 B |
67
src/main.js
67
src/main.js
|
|
@ -1608,6 +1608,10 @@ async function _newFile(width, height, fps, layoutKey) {
|
||||||
// Set default time format to measures for music mode
|
// Set default time format to measures for music mode
|
||||||
if (layoutKey === 'audioDaw' && context.timelineWidget?.timelineState) {
|
if (layoutKey === 'audioDaw' && context.timelineWidget?.timelineState) {
|
||||||
context.timelineWidget.timelineState.timeFormat = 'measures';
|
context.timelineWidget.timelineState.timeFormat = 'measures';
|
||||||
|
// Show metronome button for audio projects
|
||||||
|
if (context.metronomeGroup) {
|
||||||
|
context.metronomeGroup.style.display = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4536,6 +4540,54 @@ function timelineV2() {
|
||||||
|
|
||||||
controls.push(recordGroup);
|
controls.push(recordGroup);
|
||||||
|
|
||||||
|
// Metronome button (only visible in measures mode)
|
||||||
|
const metronomeGroup = document.createElement("div");
|
||||||
|
metronomeGroup.className = "playback-controls-group";
|
||||||
|
|
||||||
|
// Initially hide if not in measures mode
|
||||||
|
if (timelineWidget.timelineState.timeFormat !== 'measures') {
|
||||||
|
metronomeGroup.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const metronomeButton = document.createElement("button");
|
||||||
|
metronomeButton.className = context.metronomeEnabled
|
||||||
|
? "playback-btn playback-btn-metronome active"
|
||||||
|
: "playback-btn playback-btn-metronome";
|
||||||
|
metronomeButton.title = context.metronomeEnabled ? "Disable Metronome" : "Enable Metronome";
|
||||||
|
|
||||||
|
// Load SVG inline for currentColor support
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('./assets/metronome.svg');
|
||||||
|
const svgText = await response.text();
|
||||||
|
metronomeButton.innerHTML = svgText;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load metronome icon:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
metronomeButton.addEventListener("click", async () => {
|
||||||
|
context.metronomeEnabled = !context.metronomeEnabled;
|
||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
try {
|
||||||
|
await invoke('set_metronome_enabled', { enabled: context.metronomeEnabled });
|
||||||
|
// Update button appearance
|
||||||
|
metronomeButton.className = context.metronomeEnabled
|
||||||
|
? "playback-btn playback-btn-metronome active"
|
||||||
|
: "playback-btn playback-btn-metronome";
|
||||||
|
metronomeButton.title = context.metronomeEnabled ? "Disable Metronome" : "Enable Metronome";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set metronome:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
metronomeGroup.appendChild(metronomeButton);
|
||||||
|
|
||||||
|
// Store reference for state updates and visibility toggling
|
||||||
|
context.metronomeButton = metronomeButton;
|
||||||
|
context.metronomeGroup = metronomeGroup;
|
||||||
|
|
||||||
|
controls.push(metronomeGroup);
|
||||||
|
|
||||||
// Time display
|
// Time display
|
||||||
const timeDisplay = document.createElement("div");
|
const timeDisplay = document.createElement("div");
|
||||||
timeDisplay.className = "time-display";
|
timeDisplay.className = "time-display";
|
||||||
|
|
@ -4610,6 +4662,10 @@ function timelineV2() {
|
||||||
timelineWidget.toggleTimeFormat();
|
timelineWidget.toggleTimeFormat();
|
||||||
updateTimeDisplay();
|
updateTimeDisplay();
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
|
// Update metronome button visibility
|
||||||
|
if (context.metronomeGroup) {
|
||||||
|
context.metronomeGroup.style.display = timelineWidget.timelineState.timeFormat === 'measures' ? '' : 'none';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4620,6 +4676,10 @@ function timelineV2() {
|
||||||
timelineWidget.toggleTimeFormat();
|
timelineWidget.toggleTimeFormat();
|
||||||
updateTimeDisplay();
|
updateTimeDisplay();
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
|
// Update metronome button visibility
|
||||||
|
if (context.metronomeGroup) {
|
||||||
|
context.metronomeGroup.style.display = timelineWidget.timelineState.timeFormat === 'measures' ? '' : 'none';
|
||||||
|
}
|
||||||
} else if (action === 'edit-fps') {
|
} else if (action === 'edit-fps') {
|
||||||
// Clicked on FPS - show input to edit framerate
|
// Clicked on FPS - show input to edit framerate
|
||||||
console.log('[FPS Edit] Starting FPS edit');
|
console.log('[FPS Edit] Starting FPS edit');
|
||||||
|
|
@ -10722,6 +10782,13 @@ function switchLayout(layoutKey) {
|
||||||
updateLayers();
|
updateLayers();
|
||||||
updateMenu();
|
updateMenu();
|
||||||
|
|
||||||
|
// Update metronome button visibility based on timeline format
|
||||||
|
// (especially important when switching to audioDaw layout)
|
||||||
|
if (context.metronomeGroup && context.timelineWidget?.timelineState) {
|
||||||
|
const shouldShow = context.timelineWidget.timelineState.timeFormat === 'measures';
|
||||||
|
context.metronomeGroup.style.display = shouldShow ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Layout switched to: ${layoutDef.name}`);
|
console.log(`Layout switched to: ${layoutDef.name}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error switching layout:`, error);
|
console.error(`Error switching layout:`, error);
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ export let context = {
|
||||||
playPauseButton: null, // Reference to play/pause button for updating appearance
|
playPauseButton: null, // Reference to play/pause button for updating appearance
|
||||||
// MIDI activity indicator
|
// MIDI activity indicator
|
||||||
lastMidiInputTime: 0, // Timestamp (Date.now()) of last MIDI input
|
lastMidiInputTime: 0, // Timestamp (Date.now()) of last MIDI input
|
||||||
|
// Metronome state
|
||||||
|
metronomeEnabled: false,
|
||||||
|
metronomeButton: null, // Reference to metronome button for updating appearance
|
||||||
|
metronomeGroup: null, // Reference to metronome button group for showing/hiding
|
||||||
};
|
};
|
||||||
|
|
||||||
// Application configuration
|
// Application configuration
|
||||||
|
|
|
||||||
|
|
@ -1022,6 +1022,24 @@ button {
|
||||||
animation: pulse 1s ease-in-out infinite;
|
animation: pulse 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Metronome Button - Inline SVG with currentColor */
|
||||||
|
.playback-btn-metronome {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playback-btn-metronome svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active metronome state - use highlight color */
|
||||||
|
.playback-btn-metronome.active {
|
||||||
|
background-color: var(--highlight);
|
||||||
|
border-color: var(--highlight);
|
||||||
|
}
|
||||||
|
|
||||||
/* Dark mode playback button adjustments */
|
/* Dark mode playback button adjustments */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.playback-btn {
|
.playback-btn {
|
||||||
|
|
|
||||||
|
|
@ -814,27 +814,11 @@ class TimelineWindowV2 extends Widget {
|
||||||
const fadeTime = 1000 // Fade out over 1 second (increased for visibility)
|
const fadeTime = 1000 // Fade out over 1 second (increased for visibility)
|
||||||
|
|
||||||
if (elapsed < fadeTime) {
|
if (elapsed < fadeTime) {
|
||||||
// const mindicatorSize = 12 // Made larger
|
|
||||||
// const mindicatorX = this.trackHeaderWidth - 35 // Position to the left of buttons
|
|
||||||
// const mindicatorY = y + this.trackHierarchy.trackHeight / 2
|
|
||||||
|
|
||||||
// console.log(`[MIDI mIndicator] Drawing at (${mindicatorX}, ${mindicatorY}) with alpha ${1}`)
|
|
||||||
|
|
||||||
// // Draw pulsing circle with border
|
|
||||||
// ctx.strokeStyle = `rgba(0, 255, 0, ${1})`
|
|
||||||
// ctx.fillStyle = `rgba(0, 255, 0, ${1 * 0.5})`
|
|
||||||
// ctx.lineWidth = 2
|
|
||||||
// ctx.beginPath()
|
|
||||||
// ctx.arc(mindicatorX, mindicatorY, mindicatorSize / 2, 0, Math.PI * 2)
|
|
||||||
// ctx.fill()
|
|
||||||
// ctx.stroke()
|
|
||||||
|
|
||||||
const alpha = Math.max(0.2, 1 - (elapsed / fadeTime)) // Minimum alpha of 0.3 for visibility
|
const alpha = Math.max(0.2, 1 - (elapsed / fadeTime)) // Minimum alpha of 0.3 for visibility
|
||||||
const indicatorSize = 10
|
const indicatorSize = 10
|
||||||
const indicatorX = this.trackHeaderWidth - 35 // Position to the left of buttons
|
const indicatorX = this.trackHeaderWidth - 35 // Position to the left of buttons
|
||||||
const indicatorY = y + this.trackHierarchy.trackHeight / 2
|
const indicatorY = y + this.trackHierarchy.trackHeight / 2
|
||||||
|
|
||||||
console.log(`[MIDI Indicator] Drawing at (${indicatorX}, ${indicatorY}) with alpha ${alpha}`)
|
|
||||||
|
|
||||||
// Draw pulsing circle with border
|
// Draw pulsing circle with border
|
||||||
ctx.strokeStyle = `rgba(0, 255, 0, ${alpha})`
|
ctx.strokeStyle = `rgba(0, 255, 0, ${alpha})`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue