This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1use super::context::Context; 2use crate::animation::AnimationUpdateFunction; 3use crate::synchronization::audio::MusicalDurationUnit; 4use crate::{Canvas, Object}; 5use anyhow::Result; 6use chrono::NaiveDateTime; 7use std::{fmt::Formatter, panic}; 8 9pub type BeatNumber = usize; 10pub type FrameNumber = usize; 11pub type Millisecond = usize; 12 13pub type RenderFunction<C> = 14 dyn Fn(&mut Canvas, &mut Context<C>) -> anyhow::Result<()> + Send + Sync; 15 16pub type CommandAction<C> = dyn Fn(String, &mut Canvas, &mut Context<C>) -> anyhow::Result<()> 17 + Send 18 + Sync; 19 20/// Arguments: canvas, context, previous rendered beat, previous rendered frame 21pub type HookCondition<C> = 22 dyn Fn(&Canvas, &Context<C>, BeatNumber, FrameNumber) -> bool + Send + Sync; 23 24/// Arguments: canvas, context, current milliseconds timestamp 25pub type InnerHookRenderFunction = 26 dyn Fn(&mut Canvas, Millisecond) -> anyhow::Result<()> + Send + Sync; 27 28/// Arguments: canvas, context, previous rendered beat 29pub type InnerHookCondition<C> = 30 dyn Fn(&Canvas, &Context<C>, BeatNumber) -> bool + Send + Sync; 31 32pub struct Hook<C> { 33 pub when: Box<HookCondition<C>>, 34 pub render_function: Box<RenderFunction<C>>, 35} 36 37/// Hooks that are triggered within a regular hook 38/// Used to implement animations: they create a inner hook 39/// triggered on each frame for a certain duration 40pub struct InnerHook<C> { 41 pub when: Box<InnerHookCondition<C>>, 42 pub render_function: Box<InnerHookRenderFunction>, 43 /// Whether the hook should be run only once 44 pub once: bool, 45} 46 47impl<C> std::fmt::Debug for Hook<C> { 48 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 49 f.debug_struct("Hook") 50 .field("when", &"Box<HookCondition>") 51 .field("render_function", &"Box<RenderFunction>") 52 .finish() 53 } 54} 55 56pub trait AttachHooks<C>: Sized { 57 fn with_hook(self, hook: Hook<C>) -> Self; 58 59 fn hook( 60 self, 61 when: &'static super::hooks::HookCondition<C>, 62 render_function: &'static super::hooks::RenderFunction<C>, 63 ) -> Self { 64 self.with_hook(Hook { 65 when: Box::new(when), 66 render_function: Box::new(render_function), 67 }) 68 } 69 70 fn init(self, render_function: &'static RenderFunction<C>) -> Self { 71 self.hook( 72 &|_, context: &Context<C>, _, _| context.rendered_frames == 0, 73 render_function, 74 ) 75 } 76 77 fn dump_frame_when(self, when: &'static HookCondition<C>) -> Self { 78 self.hook(when, &|canvas, ctx| { 79 canvas 80 .render_to_svg_file(format!("frame-{}.svg", ctx.rendered_frames)) 81 }) 82 } 83 84 // TODO The &'static requirement might be possibly liftable, see https://users.rust-lang.org/t/how-to-store-functions-in-structs/58089 85 fn on( 86 self, 87 marker_text: &'static str, 88 render_function: &'static RenderFunction<C>, 89 ) -> Self { 90 self.with_hook(Hook { 91 when: Box::new(move |_, context, _, _| { 92 context.marker() == marker_text 93 }), 94 render_function: Box::new(render_function), 95 }) 96 } 97 98 fn assign_scene_to( 99 self, 100 marker_text: &'static str, 101 scene_name: &'static str, 102 ) -> Self { 103 self.with_hook(Hook { 104 when: Box::new(move |_, context, _, _| { 105 context.marker() == marker_text 106 }), 107 render_function: Box::new(move |_, context| { 108 context.switch_scene(scene_name); 109 Ok(()) 110 }), 111 }) 112 } 113 114 fn each_beat(self, render_function: &'static RenderFunction<C>) -> Self { 115 self.with_hook(Hook { 116 when: Box::new( 117 move |_, 118 context, 119 previous_rendered_beat, 120 previous_rendered_frame| { 121 previous_rendered_frame != context.frame() 122 && (context.ms == 0 123 || previous_rendered_beat != context.beat()) 124 }, 125 ), 126 render_function: Box::new(render_function), 127 }) 128 } 129 130 fn every( 131 self, 132 amount: f32, 133 unit: MusicalDurationUnit, 134 render_function: &'static RenderFunction<C>, 135 ) -> Self { 136 let beats = match unit { 137 MusicalDurationUnit::Beats => amount, 138 MusicalDurationUnit::Halves => amount / 2.0, 139 MusicalDurationUnit::Quarters => amount / 4.0, 140 MusicalDurationUnit::Eighths => amount / 8.0, 141 MusicalDurationUnit::Sixteenths => amount / 16.0, 142 MusicalDurationUnit::Thirds => amount / 3.0, 143 }; 144 145 self.with_hook(Hook { 146 when: Box::new(move |_, context, _, _| { 147 context.beat_fractional() % beats < 0.01 148 }), 149 render_function: Box::new(render_function), 150 }) 151 } 152 153 fn each_frame(self, render_function: &'static RenderFunction<C>) -> Self { 154 self.each_n_frame(1, render_function) 155 } 156 157 fn each_n_frame( 158 self, 159 n: usize, 160 render_function: &'static RenderFunction<C>, 161 ) -> Self { 162 self.with_hook(Hook { 163 when: Box::new(move |_, context, _, previous_rendered_frame| { 164 if context.frame() == previous_rendered_frame { 165 return false; 166 } 167 168 context.frame() % n == 0 169 }), 170 render_function: Box::new(render_function), 171 }) 172 } 173 174 /// threshold is a value between 0 and 1: current amplitude / max amplitude of stem 175 fn on_stem( 176 self, 177 stem_name: &'static str, 178 threshold: f32, 179 above_amplitude: &'static RenderFunction<C>, 180 below_amplitude: &'static RenderFunction<C>, 181 ) -> Self { 182 self.with_hook(Hook { 183 when: Box::new(move |_, context, _, _| { 184 context.stem(stem_name).amplitude_relative() > threshold 185 }), 186 render_function: Box::new(above_amplitude), 187 }) 188 .with_hook(Hook { 189 when: Box::new(move |_, context, _, _| { 190 context.stem(stem_name).amplitude_relative() <= threshold 191 }), 192 render_function: Box::new(below_amplitude), 193 }) 194 } 195 196 /// Triggers when a note starts on one of the stems in the comma-separated list of stem names `stems`. 197 fn on_note( 198 self, 199 stems: &'static str, 200 render_function: &'static RenderFunction<C>, 201 ) -> Self { 202 self.with_hook(Hook { 203 when: Box::new(move |_, ctx, _, _| { 204 stems 205 .split(',') 206 .map(|stem_name| ctx.stem(stem_name.trim())) 207 .any(|stem| stem.notes.iter().any(|note| note.is_on())) 208 }), 209 render_function: Box::new(render_function), 210 }) 211 } 212 213 /// Triggers when a note stops on one of the stems in the comma-separated list of stem names `stems`. 214 fn on_note_end( 215 self, 216 stems: &'static str, 217 render_function: &'static RenderFunction<C>, 218 ) -> Self { 219 self.with_hook(Hook { 220 when: Box::new(move |_, ctx, _, _| { 221 stems 222 .split(',') 223 .map(|n| ctx.stem(n.trim())) 224 .any(|stem| stem.notes.iter().any(|note| note.is_off())) 225 }), 226 render_function: Box::new(render_function), 227 }) 228 } 229 230 // Adds an object using object_creation on note start and removes it on note end 231 fn with_note<ObjectCreator>( 232 self, 233 stems: &'static str, 234 cutoff_amplitude: f32, 235 layer_name: &'static str, 236 object_name: &'static str, 237 create_object: &'static ObjectCreator, 238 ) -> Self 239 where 240 ObjectCreator: 241 Fn(&Canvas, &mut Context<C>) -> Result<Object> + Send + Sync, 242 { 243 self.with_hook(Hook { 244 when: Box::new(move |_, ctx, _, _| { 245 stems.split(',').any(|stem_name| { 246 ctx.stem(stem_name).notes.iter().any(|note| note.is_on()) 247 }) 248 }), 249 render_function: Box::new(move |canvas, ctx| { 250 let object = create_object(canvas, ctx)?; 251 canvas.layer(layer_name)?.set(object_name, object); 252 Ok(()) 253 }), 254 }) 255 .with_hook(Hook { 256 when: Box::new(move |_, ctx, _, _| { 257 stems.split(',').any(|stem_name| { 258 ctx.stem(stem_name).amplitude_relative() < cutoff_amplitude 259 || ctx 260 .stem(stem_name) 261 .notes 262 .iter() 263 .any(|note| note.is_off()) 264 }) 265 }), 266 render_function: Box::new(move |canvas, _| { 267 canvas.remove_object(object_name); 268 Ok(()) 269 }), 270 }) 271 } 272 273 fn at_frame( 274 self, 275 frame: usize, 276 render_function: &'static RenderFunction<C>, 277 ) -> Self { 278 self.with_hook(Hook { 279 when: Box::new(move |_, context, _, _| context.frame() == frame), 280 render_function: Box::new(render_function), 281 }) 282 } 283 284 fn when_remaining( 285 self, 286 seconds: usize, 287 render_function: &'static RenderFunction<C>, 288 ) -> Self { 289 self.with_hook(Hook { 290 when: Box::new(move |_, ctx, _, _| { 291 ctx.ms >= ctx.duration_ms().max(seconds * 1000) - seconds * 1000 292 }), 293 render_function: Box::new(render_function), 294 }) 295 } 296 297 fn at_timestamp( 298 self, 299 timestamp: &'static str, 300 render_function: &'static RenderFunction<C>, 301 ) -> Self { 302 let hook = Hook { 303 when: Box::new(move |_, context, _, previous_rendered_frame| { 304 if previous_rendered_frame == context.frame() { 305 return false; 306 } 307 let (precision, criteria_time): (&str, NaiveDateTime) = 308 if let Ok(criteria_time_parsed) = 309 NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S%.3f") 310 { 311 ("milliseconds", criteria_time_parsed) 312 } else if let Ok(criteria_time_parsed) = 313 NaiveDateTime::parse_from_str(timestamp, "%M:%S%.3f") 314 { 315 ("milliseconds", criteria_time_parsed) 316 } else if let Ok(criteria_time_parsed) = 317 NaiveDateTime::parse_from_str(timestamp, "%S%.3f") 318 { 319 ("milliseconds", criteria_time_parsed) 320 } else if let Ok(criteria_time_parsed) = 321 NaiveDateTime::parse_from_str(timestamp, "%S") 322 { 323 ("seconds", criteria_time_parsed) 324 } else if let Ok(criteria_time_parsed) = 325 NaiveDateTime::parse_from_str(timestamp, "%M:%S") 326 { 327 ("seconds", criteria_time_parsed) 328 } else if let Ok(criteria_time_parsed) = 329 NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S") 330 { 331 ("seconds", criteria_time_parsed) 332 } else { 333 panic!("Unhandled timestamp format: {}", timestamp); 334 }; 335 match precision { 336 "milliseconds" => { 337 let current_time: NaiveDateTime = 338 NaiveDateTime::parse_from_str( 339 timestamp, 340 "%H:%M:%S%.3f", 341 ) 342 .unwrap(); 343 current_time == criteria_time 344 } 345 "seconds" => { 346 let current_time: NaiveDateTime = 347 NaiveDateTime::parse_from_str(timestamp, "%H:%M:%S") 348 .unwrap(); 349 current_time == criteria_time 350 } 351 _ => panic!("Unknown precision"), 352 } 353 }), 354 render_function: Box::new(render_function), 355 }; 356 self.with_hook(hook) 357 } 358 359 fn bind_amplitude( 360 self, 361 stem: &'static str, 362 update: &'static AnimationUpdateFunction, 363 ) -> Self { 364 self.with_hook(Hook { 365 when: Box::new(move |_, _, _, _| true), 366 render_function: Box::new(move |canvas, context| { 367 let amplitude = context.stem(stem).amplitude_relative(); 368 update(amplitude, canvas, context.ms)?; 369 Ok(()) 370 }), 371 }) 372 } 373}