This repository has no description
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}