This repository has no description
0

Configure Feed

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

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}