bbx_draw/visualizers/
midi_activity.rs1use bbx_midi::message::MidiMessageStatus;
4use nannou::{Draw, color::Rgb, geom::Rect};
5
6use crate::{Visualizer, bridge::MidiBridgeConsumer, color::lerp_color, config::MidiActivityConfig};
7
8pub struct MidiActivityVisualizer {
12 consumer: MidiBridgeConsumer,
13 config: MidiActivityConfig,
14 note_activity: [f32; 128],
15 note_velocities: [u8; 128],
16 decay_per_frame: f32,
17}
18
19impl MidiActivityVisualizer {
20 pub fn new(consumer: MidiBridgeConsumer) -> Self {
22 let config = MidiActivityConfig::default();
23 let decay_per_frame = 1.0 / (config.decay_time_ms / 16.67);
24 Self {
25 consumer,
26 config,
27 note_activity: [0.0; 128],
28 note_velocities: [0; 128],
29 decay_per_frame,
30 }
31 }
32
33 pub fn with_config(consumer: MidiBridgeConsumer, config: MidiActivityConfig) -> Self {
35 let decay_per_frame = 1.0 / (config.decay_time_ms / 16.67);
36 Self {
37 consumer,
38 note_activity: [0.0; 128],
39 note_velocities: [0; 128],
40 decay_per_frame,
41 config,
42 }
43 }
44
45 pub fn config(&self) -> &MidiActivityConfig {
47 &self.config
48 }
49
50 pub fn set_config(&mut self, config: MidiActivityConfig) {
52 self.decay_per_frame = 1.0 / (config.decay_time_ms / 16.67);
53 self.config = config;
54 }
55
56 fn note_color(&self, note: u8) -> Rgb {
57 let activity = self.note_activity[note as usize];
58 if activity <= 0.0 {
59 return nannou::color::rgb(
60 self.config.note_off_color.red,
61 self.config.note_off_color.green,
62 self.config.note_off_color.blue,
63 );
64 }
65
66 let base_color = if self.config.velocity_brightness {
67 let velocity_factor = self.note_velocities[note as usize] as f32 / 127.0;
68 let r = self.config.note_on_color.red * velocity_factor;
69 let g = self.config.note_on_color.green * velocity_factor;
70 let b = self.config.note_on_color.blue * velocity_factor;
71 nannou::color::Srgb::new((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
72 } else {
73 nannou::color::Srgb::new(
74 (self.config.note_on_color.red * 255.0) as u8,
75 (self.config.note_on_color.green * 255.0) as u8,
76 (self.config.note_on_color.blue * 255.0) as u8,
77 )
78 };
79
80 let off_color = nannou::color::Srgb::new(
81 (self.config.note_off_color.red * 255.0) as u8,
82 (self.config.note_off_color.green * 255.0) as u8,
83 (self.config.note_off_color.blue * 255.0) as u8,
84 );
85
86 lerp_color(off_color, base_color, activity)
87 }
88}
89
90impl Visualizer for MidiActivityVisualizer {
91 fn update(&mut self) {
92 let messages = self.consumer.drain();
93
94 for msg in messages {
95 let status = msg.get_status();
96 if let Some(note) = msg.get_note_number() {
97 match status {
98 MidiMessageStatus::NoteOn => {
99 if let Some(velocity) = msg.get_velocity() {
100 if velocity > 0 {
101 self.note_activity[note as usize] = 1.0;
102 self.note_velocities[note as usize] = velocity;
103 } else {
104 self.note_activity[note as usize] = 0.9;
105 }
106 }
107 }
108 MidiMessageStatus::NoteOff => {
109 self.note_activity[note as usize] = 0.9;
110 }
111 _ => {}
112 }
113 }
114 }
115
116 for activity in &mut self.note_activity {
117 if *activity > 0.0 && *activity < 1.0 {
118 *activity -= self.decay_per_frame;
119 *activity = activity.max(0.0);
120 }
121 }
122 }
123
124 fn draw(&self, draw: &Draw, bounds: Rect) {
125 let (min_note, max_note) = self.config.display_range;
126 let num_notes = (max_note - min_note + 1) as usize;
127
128 if num_notes == 0 {
129 return;
130 }
131
132 let key_width = bounds.w() / num_notes as f32;
133 let key_height = bounds.h();
134
135 for i in 0..num_notes {
136 let note = min_note + i as u8;
137 let x = bounds.left() + (i as f32 + 0.5) * key_width;
138 let y = bounds.y();
139
140 let color = self.note_color(note);
141
142 let is_black_key = matches!(note % 12, 1 | 3 | 6 | 8 | 10);
143 let actual_height = if is_black_key { key_height * 0.65 } else { key_height };
144
145 draw.rect().x_y(x, y).w_h(key_width * 0.9, actual_height).color(color);
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::bridge::midi_bridge;
154
155 #[test]
156 fn test_midi_visualizer_creation() {
157 let (_producer, consumer) = midi_bridge(64);
158 let visualizer = MidiActivityVisualizer::new(consumer);
159 assert_eq!(visualizer.note_activity.len(), 128);
160 }
161
162 #[test]
163 fn test_display_range() {
164 let (_producer, consumer) = midi_bridge(64);
165 let config = MidiActivityConfig {
166 display_range: (60, 72),
167 ..Default::default()
168 };
169 let visualizer = MidiActivityVisualizer::with_config(consumer, config);
170 assert_eq!(visualizer.config.display_range, (60, 72));
171 }
172}