This repository has no description
1use crate::{
2 Canvas, Scene,
3 synchronization::{
4 cue_markers::CueMarkersSynchronizer,
5 midi::MidiSynchronizer,
6 sync::{SyncData, Syncable},
7 },
8 ui::{self, Log, Pretty},
9 video::hooks::{AttachHooks, CommandAction, Hook},
10};
11use anyhow::Result;
12use chrono::DateTime;
13use measure_time::debug_time;
14use std::{
15 collections::HashMap, fmt::Formatter, ops::Range, path::PathBuf,
16 time::Duration,
17};
18
19pub struct Command<C> {
20 pub name: String,
21 pub action: Box<CommandAction<C>>,
22}
23
24impl<C> std::fmt::Debug for Command<C> {
25 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("Command")
27 .field("name", &self.name)
28 .field("action", &"Box<CommandAction>")
29 .finish()
30 }
31}
32
33#[derive(PartialEq, PartialOrd, Eq, Ord)]
34pub struct Timestamp(pub usize);
35
36impl Timestamp {
37 pub fn from_ms_range(range: &Range<usize>) -> Range<Self> {
38 Self::from_ms(range.start)..Self::from_ms(range.end)
39 }
40
41 pub fn ms(&self) -> usize {
42 self.0
43 }
44
45 pub fn seconds(&self) -> f64 {
46 self.0 as f64 / 1000.0
47 }
48
49 pub fn seconds_string(&self) -> String {
50 format!("{:.3}", self.seconds())
51 }
52
53 pub fn from_seconds(seconds: f64) -> Self {
54 Self((seconds * 1000.0) as usize)
55 }
56
57 pub fn from_ms(ms: usize) -> Self {
58 Self(ms)
59 }
60}
61
62impl Pretty for Timestamp {
63 fn pretty(&self) -> String {
64 format!(
65 "{}",
66 DateTime::from_timestamp_millis(self.ms() as i64)
67 .unwrap()
68 .format("%H:%M:%S%.3f")
69 )
70 }
71}
72
73impl Pretty for Range<Timestamp> {
74 fn pretty(&self) -> String {
75 format!("from {} to {}", self.start.pretty(), self.end.pretty())
76 }
77}
78
79impl Default for Timestamp {
80 fn default() -> Self {
81 Self(0)
82 }
83}
84
85impl std::fmt::Display for Timestamp {
86 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
87 write!(f, "{}", self.pretty())
88 }
89}
90
91pub struct VideoProgressBars {
92 pub loading: indicatif::ProgressBar,
93 pub rendering: indicatif::ProgressBar,
94 pub encoding: indicatif::ProgressBar,
95}
96
97pub struct Video<C> {
98 pub fps: usize,
99 pub initial_canvas: Canvas,
100 pub hooks: Vec<Hook<C>>,
101 pub commands: Vec<Box<Command<C>>>,
102 pub frames: Vec<Canvas>,
103 pub frames_output_directory: &'static str,
104 pub syncdata: SyncData,
105 pub audiofile: PathBuf,
106 pub resolution: u32,
107 pub duration_override: Option<Duration>,
108 pub start_rendering_at: Timestamp,
109 pub progress_bars: VideoProgressBars,
110 pub progress: indicatif::MultiProgress,
111}
112
113impl<C: Default> AttachHooks<C> for Video<C> {
114 fn with_hook(self, hook: Hook<C>) -> Self {
115 let mut hooks = self.hooks;
116 hooks.push(hook);
117 Self { hooks, ..self }
118 }
119}
120
121impl<C: Default> Default for Video<C> {
122 fn default() -> Self {
123 Self::new(Canvas::with_layers(vec!["root"]))
124 }
125}
126
127impl<C: Default> Video<C> {
128 pub fn new(canvas: Canvas) -> Self {
129 let progress_bars = VideoProgressBars {
130 loading: ui::setup_progress_bar(0, "Loading"),
131 rendering: ui::setup_progress_bar(0, "Rendering"),
132 encoding: ui::setup_progress_bar(0, "Encoding"),
133 };
134
135 let progress = indicatif::MultiProgress::new();
136 progress.add(progress_bars.loading.clone());
137 progress.add(progress_bars.rendering.clone());
138 progress.add(progress_bars.encoding.clone());
139
140 Self {
141 fps: 30,
142 initial_canvas: canvas,
143 hooks: vec![],
144 commands: vec![],
145 frames: vec![],
146 frames_output_directory: "frames/",
147 resolution: 1920,
148 syncdata: SyncData::default(),
149 audiofile: PathBuf::new(),
150 duration_override: None,
151 start_rendering_at: Timestamp::from_ms(0),
152 progress_bars,
153 progress,
154 }
155 }
156
157 pub fn sync_audio_with(
158 mut self,
159 filepath: impl Into<PathBuf>,
160 ) -> Result<Self> {
161 debug_time!("sync_audio_with");
162
163 let file_path: PathBuf = filepath.into();
164 let pb = Some(&self.progress_bars.loading);
165
166 let syncdata = match file_path.extension().and_then(|s| s.to_str()) {
167 Some("mid" | "midi") => {
168 MidiSynchronizer::new(file_path.clone()).load(pb)
169 }
170 Some("flac" | "wav") => {
171 CueMarkersSynchronizer::new(file_path.clone()).load(pb)
172 }
173 _ => panic!("Unsupported sync data format"),
174 }?;
175
176 let pb = pb.unwrap();
177
178 pb.finish();
179
180 if let Some(bpm) = syncdata.bpm {
181 pb.log(
182 "BPM",
183 &format!("set to {bpm} from {}", (&file_path).pretty()),
184 );
185 }
186
187 pb.log(
188 "Loaded",
189 &format!(
190 "{things} from {path} in {elapsed}",
191 path = (&file_path).pretty(),
192 elapsed = (pb.elapsed().pretty()),
193 things = (HashMap::from([
194 ("markers", syncdata.markers.len()),
195 ("stems", syncdata.stems.len()),
196 (
197 "notes",
198 syncdata
199 .stems
200 .values()
201 .map(|v| v.notes.len())
202 .sum::<usize>()
203 ),
204 ]))
205 .pretty(),
206 ),
207 );
208
209 self.syncdata.merge_with(syncdata);
210
211 Ok(self)
212 }
213
214 pub fn ms_to_frames(&self, ms: usize) -> usize {
215 self.fps * ms / 1000
216 }
217
218 // Duration of the video, taking into account a possible duration override.
219 pub fn duration_ms(&self) -> usize {
220 match self.duration_override {
221 Some(duration) => duration.as_millis() as _,
222 None => self.total_duration_ms(),
223 }
224 }
225
226 pub fn constrained_ms_range(&self) -> Range<usize> {
227 let start_ms = self.start_rendering_at.ms();
228 let end_ms = start_ms + self.duration_ms();
229 start_ms..end_ms.min(self.total_duration_ms())
230 }
231
232 pub fn total_ms_range(&self) -> Range<usize> {
233 0..self.total_duration_ms()
234 }
235
236 /// Duration of the video, without taking into account a possible duration override.
237 pub fn total_duration_ms(&self) -> usize {
238 self.syncdata
239 .stems
240 .values()
241 .map(|stem| stem.duration_ms)
242 .max()
243 .expect("No audio sync data provided. Use .sync_audio_with() to load a MIDI file, or provide a duration override.")
244 }
245
246 /// Adds hooks from the given scene to the video.
247 /// Hooks will be triggered when the current scene matches the scene's name.
248 /// Use Context#switch_scene to change scenes during rendering.
249 /// See also `with_marked_scene` for a more ergonomic way to add scenes.
250 pub fn with_scene(self, mut scene: Scene<C>) -> Self {
251 for hook in self.hooks {
252 scene.hooks.push(hook);
253 }
254 Self {
255 hooks: scene.hooks,
256 ..self
257 }
258 }
259
260 /// Adds the given scene and a hook that switches to it immediately.
261 pub fn with_init_scene(self, scene: Scene<C>) -> Self {
262 let scene_name = scene.name.clone();
263 self.with_scene(scene).with_hook(Hook {
264 when: Box::new(|_, ctx, _, _| ctx.rendered_frames == 0),
265 render_function: Box::new(move |_, ctx| {
266 ctx.switch_scene(&scene_name);
267 Ok(())
268 }),
269 })
270 }
271
272 /// Adds the given scene, and a hook that switches to it when a marker with the same name is reached
273 pub fn with_marked_scene(self, scene: Scene<C>) -> Self {
274 let scene_name = scene.name.clone();
275
276 self.with_scene(scene).with_hook(Hook {
277 when: Box::new(move |_, ctx, _, _| ctx.marker() == scene_name),
278 render_function: Box::new(move |_, ctx| {
279 ctx.switch_scene(ctx.marker());
280 Ok(())
281 }),
282 })
283 }
284
285 pub fn command(
286 self,
287 command_name: &'static str,
288 action: &'static CommandAction<C>,
289 ) -> Self {
290 let mut commands = self.commands;
291 commands.push(Box::new(Command {
292 name: command_name.to_string(),
293 action: Box::new(action),
294 }));
295 Self { commands, ..self }
296 }
297}