this repo has no description
3
fork

Configure Feed

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

✨ Improve progress UI, fix midi audiosync importer, other stuff?

authored by

Gwenn Le Bihan and committed by
Ewen Le Bihan
7ab06921 006a9eb4

+364 -166
+36
Cargo.lock
··· 283 283 ] 284 284 285 285 [[package]] 286 + name = "heck" 287 + version = "0.4.1" 288 + source = "registry+https://github.com/rust-lang/crates.io-index" 289 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 290 + 291 + [[package]] 286 292 name = "hound" 287 293 version = "3.5.1" 288 294 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 617 623 checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 618 624 619 625 [[package]] 626 + name = "rustversion" 627 + version = "1.0.15" 628 + source = "registry+https://github.com/rust-lang/crates.io-index" 629 + checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" 630 + 631 + [[package]] 620 632 name = "ryu" 621 633 version = "1.0.17" 622 634 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 699 711 "serde_cbor", 700 712 "serde_json", 701 713 "slug", 714 + "strum", 715 + "strum_macros", 702 716 "svg", 703 717 "tiny_http", 704 718 "wasm-bindgen", ··· 720 734 version = "0.10.0" 721 735 source = "registry+https://github.com/rust-lang/crates.io-index" 722 736 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 737 + 738 + [[package]] 739 + name = "strum" 740 + version = "0.26.2" 741 + source = "registry+https://github.com/rust-lang/crates.io-index" 742 + checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" 743 + dependencies = [ 744 + "strum_macros", 745 + ] 746 + 747 + [[package]] 748 + name = "strum_macros" 749 + version = "0.26.2" 750 + source = "registry+https://github.com/rust-lang/crates.io-index" 751 + checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" 752 + dependencies = [ 753 + "heck", 754 + "proc-macro2", 755 + "quote", 756 + "rustversion", 757 + "syn", 758 + ] 723 759 724 760 [[package]] 725 761 name = "svg"
+2
Cargo.toml
··· 49 49 backtrace = "0.3.71" 50 50 slug = "0.1.5" 51 51 roxmltree = "0.19.0" 52 + strum = { version = "0.26.2", features = ["strum_macros"] } 53 + strum_macros = "0.26.2" 52 54 53 55 54 56 [dev-dependencies]
+19 -22
src/canvas.rs
··· 1 1 use core::panic; 2 - use std::{cmp, collections::HashMap, io::Write as _, ops::Range}; 2 + use std::{collections::HashMap, io::Write as _, ops::Range}; 3 3 4 4 use anyhow::Result; 5 5 use itertools::Itertools as _; ··· 63 63 } 64 64 65 65 pub fn layer(&mut self, name: &str) -> &mut Layer { 66 + if !self.layer_exists(name) { 67 + panic!("Layer {} does not exist", name); 68 + } 69 + 66 70 self.layer_safe(name).unwrap() 67 71 } 68 72 ··· 72 76 } 73 77 74 78 self.layers.push(Layer::new(name)); 75 - self.layer(name) 79 + self.layers.last_mut().unwrap() 76 80 } 77 81 78 82 pub fn layer_or_empty(&mut self, name: &str) -> &mut Layer { ··· 96 100 /// puts this layer on top, and the others below, without changing their order 97 101 pub fn put_layer_on_top(&mut self, name: &str) { 98 102 self.ensure_layer_exists(name); 99 - self.layers.sort_by(|a, _| { 100 - if a.name == name { 101 - cmp::Ordering::Less 102 - } else { 103 - cmp::Ordering::Greater 104 - } 105 - }) 103 + let target_index = self.layers.iter().position(|l| l.name == name).unwrap(); 104 + self.layers.swap(0, target_index) 106 105 } 107 106 108 107 /// puts this layer on bottom, and the others above, without changing their order 109 108 pub fn put_layer_on_bottom(&mut self, name: &str) { 110 109 self.ensure_layer_exists(name); 111 - self.layers.sort_by(|a, _| { 112 - if a.name == name { 113 - cmp::Ordering::Greater 114 - } else { 115 - cmp::Ordering::Less 116 - } 117 - }) 110 + let target_index = self.layers.iter().position(|l| l.name == name).unwrap(); 111 + let last_index = self.layers.len() - 1; 112 + self.layers.swap(last_index, target_index) 118 113 } 119 114 120 115 /// re-order layers. The first layer in the list will be on top, the last at the bottom ··· 223 218 ); 224 219 } 225 220 Layer { 226 - object_sizes: self.object_sizes.clone(), 221 + object_sizes: self.object_sizes, 227 222 name: name.to_string(), 228 223 objects, 229 224 _render_cache: None, ··· 258 253 ); 259 254 } 260 255 Layer { 261 - object_sizes: self.object_sizes.clone(), 256 + object_sizes: self.object_sizes, 262 257 name: layer_name.to_owned(), 263 258 objects, 264 259 _render_cache: None, ··· 433 428 ((resolution as f32 / aspect_ratio) as usize, resolution) 434 429 }; 435 430 436 - let mut spawned = std::process::Command::new("magick") 437 - .args(["-background", "none"]) 438 - .args(["-size", &format!("{}x{}", width, height)]) 431 + let mut spawned = std::process::Command::new("resvg") 432 + .args(["--background", "transparent"]) 433 + .args(["--width", &format!("{width}")]) 434 + .args(["--height", &format!("{height}")]) 435 + .args(["--resources-dir", "."]) 439 436 .arg("-") 440 437 .arg(at) 441 438 .stdin(std::process::Stdio::piped()) ··· 462 459 } 463 460 464 461 pub fn aspect_ratio(&self) -> f32 { 465 - return self.width() as f32 / self.height() as f32; 462 + self.width() as f32 / self.height() as f32 466 463 } 467 464 468 465 pub fn remove_all_objects_in(&mut self, region: &Region) {
+10 -4
src/color.rs
··· 7 7 8 8 use rand::Rng; 9 9 use serde::Deserialize; 10 + use strum::IntoEnumIterator; 11 + use strum_macros::EnumIter; 10 12 use wasm_bindgen::prelude::*; 11 13 12 14 #[wasm_bindgen] 13 - #[derive(Debug, Clone, Copy, PartialEq)] 15 + #[derive(Debug, Clone, Copy, PartialEq, EnumIter)] 14 16 pub enum Color { 15 17 Black, 16 18 White, ··· 53 55 *candidates[rand::thread_rng().gen_range(0..candidates.len())] 54 56 } 55 57 58 + pub fn all_colors() -> Vec<Color> { 59 + Color::iter().collect() 60 + } 61 + 56 62 impl Default for Color { 57 63 fn default() -> Self { 58 64 Self::Black ··· 160 166 pub fn from_css(content: &str) -> ColorMapping { 161 167 let mut mapping = ColorMapping::default(); 162 168 for line in content.lines() { 163 - mapping.from_css_line(&line); 169 + mapping.from_css_line(line); 164 170 } 165 171 mapping 166 172 } ··· 266 272 } 267 273 268 274 fn from_css_line(&mut self, line: &str) { 269 - if let Some((name, value)) = line.trim().split_once(":") { 270 - let value = value.trim().trim_end_matches(";").to_owned(); 275 + if let Some((name, value)) = line.trim().split_once(':') { 276 + let value = value.trim().trim_end_matches(';').to_owned(); 271 277 match name.trim() { 272 278 "black" => self.black = value, 273 279 "white" => self.white = value,
+2 -2
src/fill.rs
··· 58 58 match self { 59 59 Fill::Solid(color) => Fill::Translucent(*color, opacity), 60 60 Fill::Translucent(color, _) => Fill::Translucent(*color, opacity), 61 - _ => self.clone(), 61 + _ => *self, 62 62 } 63 63 } 64 64 ··· 143 143 .set("viewBox", format!("0,0,{},{}", size, size)) 144 144 .set( 145 145 "patternTransform", 146 - format!("rotate({})", (angle.clone() - Angle(45.0)).degrees()), 146 + format!("rotate({})", (*angle - Angle(45.0)).degrees()), 147 147 ) 148 148 // https://stackoverflow.com/a/55104220/9943464 149 149 .add(
+1 -1
src/filter.rs
··· 41 41 format!( 42 42 "filter-{}-{}", 43 43 self.name(), 44 - self.parameter.to_string().replace(".", "_") 44 + self.parameter.to_string().replace('.', "_") 45 45 ) 46 46 } 47 47 }
+16 -8
src/layer.rs
··· 11 11 pub _render_cache: Option<svg::node::element::Group>, 12 12 } 13 13 14 + static DISABLE_CACHE: bool = true; 15 + 14 16 impl Layer { 15 17 pub fn new(name: &str) -> Self { 16 18 Layer { ··· 35 37 } 36 38 37 39 pub fn object(&mut self, name: &str) -> &mut ColoredObject { 38 - self.objects.get_mut(name).unwrap() 40 + self.safe_object(name).unwrap() 41 + } 42 + 43 + pub fn safe_object(&mut self, name: &str) -> Option<&mut ColoredObject> { 44 + self.objects.get_mut(name) 39 45 } 40 46 41 47 // Flush the render cache. 42 - pub fn flush(&mut self) -> () { 48 + pub fn flush(&mut self) { 43 49 self._render_cache = None; 44 50 } 45 51 46 - pub fn replace(&mut self, with: Layer) -> () { 47 - self.objects = with.objects.clone(); 52 + pub fn replace(&mut self, with: Layer) { 53 + self.objects.clone_from(&with.objects); 48 54 self.flush(); 49 55 } 50 56 ··· 55 61 56 62 pub fn paint_all_objects(&mut self, fill: Fill) { 57 63 for (_id, obj) in &mut self.objects { 58 - obj.fill = Some(fill.clone()); 64 + obj.fill = Some(fill); 59 65 } 60 66 self.flush(); 61 67 } ··· 119 125 cell_size: usize, 120 126 object_sizes: ObjectSizes, 121 127 ) -> svg::node::element::Group { 122 - if let Some(cached_svg) = &self._render_cache { 123 - return cached_svg.clone(); 128 + if !DISABLE_CACHE { 129 + if let Some(cached_svg) = &self._render_cache { 130 + return cached_svg.clone(); 131 + } 124 132 } 125 133 126 134 let mut layer_group = svg::node::element::Group::new() ··· 128 136 .set("data-layer", self.name.clone()); 129 137 130 138 for (id, obj) in &self.objects { 131 - layer_group = layer_group.add(obj.render(cell_size, object_sizes, &colormap, &id)); 139 + layer_group = layer_group.add(obj.render(cell_size, object_sizes, &colormap, id)); 132 140 } 133 141 134 142 self._render_cache = Some(layer_group.clone());
+11 -7
src/lib.rs
··· 26 26 pub use color::*; 27 27 pub use fill::*; 28 28 pub use filter::*; 29 + use itertools::Itertools; 29 30 pub use layer::*; 30 31 pub use midi::MidiSynchronizer; 31 32 pub use objects::*; ··· 59 60 pub fn stem(&self, name: &str) -> StemAtInstant { 60 61 let stems = &self.syncdata.stems; 61 62 if !stems.contains_key(name) { 62 - panic!("No stem named {:?} found.", name); 63 + panic!( 64 + "No stem named {:?} found. Available stems:\n{}\n", 65 + name, 66 + stems 67 + .keys() 68 + .sorted() 69 + .fold(String::new(), |acc, k| format!("{acc}\n\t{k}")) 70 + ); 63 71 } 64 72 StemAtInstant { 65 73 amplitude: *stems[name].amplitude_db.get(self.ms).unwrap_or(&0.0), ··· 76 84 } 77 85 } 78 86 79 - pub fn dump_stems(&self, to: PathBuf) -> Result<()> { 80 - std::fs::create_dir_all(&to)?; 81 - for (name, stem) in self.syncdata.stems.iter() { 82 - fs::write(to.join(name), format!("{:?}", stem))?; 83 - } 84 - Ok(()) 87 + pub fn dump_syncdata(&self, to: PathBuf) -> Result<()> { 88 + Ok(serde_cbor::to_writer(fs::File::create(to)?, self.syncdata)?) 85 89 } 86 90 87 91 pub fn marker(&self) -> String {
+52 -31
src/midi.rs
··· 3 3 use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind}; 4 4 use std::{collections::HashMap, fmt::Debug, path::PathBuf}; 5 5 6 - use crate::{audio, sync::SyncData, Stem, Syncable}; 6 + use crate::{audio, sync::SyncData, ui::Log as _, ui::MaybeProgressBar as _, Stem, Syncable}; 7 7 8 8 pub struct MidiSynchronizer { 9 9 pub midi_path: PathBuf, ··· 34 34 stems: HashMap::from_iter(notes_per_instrument.iter().map(|(name, notes)| { 35 35 let mut notes_per_ms = HashMap::<usize, Vec<audio::Note>>::new(); 36 36 37 + if let Some(pb) = progressbar { 38 + pb.set_length(notes.len() as u64); 39 + pb.set_position(0); 40 + } 41 + progressbar.set_message(format!("Adding loaded notes for {name}")); 42 + 37 43 for note in notes.iter() { 38 44 notes_per_ms 39 45 .entry(note.ms as usize) ··· 43 49 tick: note.tick, 44 50 velocity: note.vel, 45 51 }); 52 + progressbar.inc(1); 53 + } 46 54 47 - // if is_kick_channel(name) { 48 - // // kicks might not have a note off event, so we added one manually after 100ms 49 - // notes_per_ms 50 - // .entry((note.ms + 100) as usize) 51 - // .or_default() 52 - // .push(audio::Note { 53 - // pitch: note.key, 54 - // tick: note.tick, 55 - // velocity: 0, 56 - // }); 57 - // } 55 + let duration_ms = *notes_per_ms.keys().max().unwrap_or(&0); 56 + 57 + if let Some(pb) = progressbar { 58 + pb.set_length(duration_ms as u64 - 1); 59 + pb.set_position(0); 58 60 } 61 + progressbar.set_message(format!("Infering amplitudes for {name}")); 59 62 60 - let duration_ms = notes_per_ms.keys().max().unwrap_or(&0).clone(); 61 63 let mut amplitudes = Vec::<f32>::new(); 62 64 let mut last_amplitude = 0.0; 63 65 for i in 0..duration_ms { ··· 69 71 .average(); 70 72 } 71 73 amplitudes.push(last_amplitude); 74 + progressbar.inc(1); 72 75 } 73 76 74 77 ( ··· 76 79 Stem { 77 80 amplitude_max: notes.iter().map(|n| n.vel).max().unwrap_or(0) as f32, 78 81 amplitude_db: amplitudes, 79 - duration_ms: duration_ms, 82 + duration_ms, 80 83 notes: notes_per_ms, 81 84 name: name.clone(), 82 85 }, ··· 96 99 } 97 100 98 101 struct Now { 99 - ms: f32, 100 - tempo: f32, 102 + ms: usize, 103 + tempo: usize, 101 104 ticks_per_beat: u16, 102 105 } 103 106 ··· 111 114 } 112 115 } 113 116 114 - fn tempo_to_bpm(µs_per_beat: f32) -> usize { 115 - (60_000_000.0 / µs_per_beat) as usize 117 + fn tempo_to_bpm(µs_per_beat: usize) -> usize { 118 + (60_000_000.0 / µs_per_beat as f32).round() as usize 116 119 } 117 120 118 121 // fn to_ms(delta: u32, bpm: f32) -> f32 { ··· 152 155 let midifile = midly::Smf::parse(&raw).unwrap(); 153 156 154 157 let mut timeline = Timeline::new(); 158 + progressbar.set_message(format!("MIDI file has {} tracks", midifile.tracks.len())); 159 + 155 160 let mut now = Now { 156 - ms: 0.0, 157 - tempo: 500_000.0, 161 + ms: 0, 162 + tempo: 0, 158 163 ticks_per_beat: match midifile.header.timing { 159 164 midly::Timing::Metrical(ticks_per_beat) => ticks_per_beat.as_int(), 160 165 midly::Timing::Timecode(fps, subframe) => (1.0 / fps.as_f32() / subframe as f32) as u16, 161 166 }, 162 167 }; 163 168 164 - 165 - // Get track names 169 + // Get track names and (initial) BPM 166 170 let mut track_no = 0; 167 171 let mut track_names = HashMap::<usize, String>::new(); 168 172 for track in midifile.tracks.iter() { ··· 172 176 match event.kind { 173 177 TrackEventKind::Meta(MetaMessage::TrackName(name_bytes)) => { 174 178 track_name = String::from_utf8(name_bytes.to_vec()).unwrap_or_default(); 179 + } 180 + TrackEventKind::Meta(MetaMessage::Tempo(tempo)) => { 181 + if now.tempo == 0 { 182 + now.tempo = tempo.as_int() as usize; 183 + } 175 184 } 176 185 _ => {} 177 186 } ··· 185 194 }, 186 195 ); 187 196 } 197 + 198 + progressbar.log( 199 + "Detected", 200 + &format!( 201 + "MIDI file {} with {} stems and initial tempo of {} BPM", 202 + source.to_str().unwrap(), 203 + track_names.len(), 204 + tempo_to_bpm(now.tempo) 205 + ), 206 + ); 188 207 189 208 // Convert ticks to absolute 190 209 let mut track_no = 0; ··· 201 220 } 202 221 203 222 // Convert ticks to ms 204 - let mut absolute_tick_to_ms = HashMap::<u32, f32>::new(); 223 + let mut absolute_tick_to_ms = HashMap::<u32, usize>::new(); 205 224 let mut last_tick = 0; 206 225 for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { 207 226 for (_, event) in tracks { 208 227 match event.kind { 209 228 TrackEventKind::Meta(MetaMessage::Tempo(tempo)) => { 210 - now.tempo = tempo.as_int() as f32; 229 + now.tempo = tempo.as_int() as usize; 211 230 } 212 231 _ => {} 213 232 } 214 233 } 215 234 let delta = tick - last_tick; 216 235 last_tick = *tick; 217 - let delta_µs = now.tempo * delta as f32 / now.ticks_per_beat as f32; 218 - now.ms += delta_µs / 1000.0; 236 + now.ms += midi_tick_to_ms(delta, now.tempo, now.ticks_per_beat as usize); 219 237 absolute_tick_to_ms.insert(*tick, now.ms); 220 238 } 221 239 222 - if let Some(ref pb) = progressbar { 223 - pb.set_length(midifile.tracks.iter().map(|t| t.len()).sum::<usize>() as u64); 240 + if let Some(pb) = progressbar { 241 + pb.set_length(midifile.tracks.iter().map(|t| t.len() as u64).sum::<u64>()); 224 242 pb.set_prefix("Loading"); 225 243 pb.set_message("parsing MIDI events"); 226 244 pb.set_position(0); ··· 257 275 }, 258 276 _ => {} 259 277 } 260 - if let Some(ref pb) = progressbar { 261 - pb.inc(1); 262 - } 278 + progressbar.inc(1) 263 279 } 264 280 } 265 281 ··· 276 292 277 293 (now, result) 278 294 } 295 + 296 + fn midi_tick_to_ms(tick: u32, tempo: usize, ppq: usize) -> usize { 297 + let with_floats = (tempo as f32 / 1e3) / ppq as f32 * tick as f32; 298 + with_floats.round() as usize 299 + }
+63 -31
src/objects.rs
··· 1 + use std::collections::HashMap; 2 + 1 3 use crate::{ColorMapping, Fill, Filter, Point, Region, Transformation}; 2 4 use itertools::Itertools; 3 5 use wasm_bindgen::prelude::*; ··· 24 26 Rectangle(Point, Point), 25 27 Image(Region, String), 26 28 RawSVG(Box<dyn svg::Node>), 29 + // Tiling(Region, Box<Object>), 27 30 } 28 31 29 32 impl Object { ··· 72 75 ) -> svg::node::element::Group { 73 76 let mut group = self.object.render(cell_size, object_sizes, id); 74 77 75 - match self 78 + for (key, value) in self 76 79 .transformations 77 - .render_attribute(colormap, !self.object.fillable()) 80 + .render_attributes(colormap, !self.object.fillable()) 78 81 { 79 - (key, _) if key.is_empty() => (), 80 - (key, value) => group = group.set(key, value), 82 + group = group.set(key, value); 81 83 } 82 84 85 + let start = self.object.region().start.coords(cell_size); 86 + let (w, h) = ( 87 + self.object.region().width() * cell_size, 88 + self.object.region().height() * cell_size, 89 + ); 90 + 91 + group = group.set( 92 + "transform-origin", 93 + format!( 94 + "{} {}", 95 + start.0 + (w as f32 / 2.0), 96 + start.1 + (h as f32 / 2.0) 97 + ), 98 + ); 99 + 83 100 let mut css = String::new(); 84 101 if !matches!(self.object, Object::RawSVG(..)) { 85 102 css = self.fill.render_css(colormap, !self.object.fillable()); ··· 91 108 .filters 92 109 .iter() 93 110 .map(|f| f.render_fill_css(colormap)) 94 - .into_iter() 95 111 .join(" ") 96 112 .as_ref(); 97 113 ··· 168 184 } 169 185 } 170 186 171 - pub trait RenderAttribute { 187 + pub trait RenderAttributes { 172 188 const MULTIPLE_VALUES_JOIN_BY: &'static str = ", "; 173 189 174 - fn render_fill_attribute(&self, colormap: &ColorMapping) -> (String, String); 175 - fn render_stroke_attribute(&self, colormap: &ColorMapping) -> (String, String); 176 - fn render_attribute( 190 + fn render_fill_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String>; 191 + fn render_stroke_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String>; 192 + fn render_attributes( 177 193 &self, 178 194 colormap: &ColorMapping, 179 195 fill_as_stroke_color: bool, 180 - ) -> (String, String) { 196 + ) -> HashMap<String, String> { 181 197 if fill_as_stroke_color { 182 198 self.render_stroke_attribute(colormap) 183 199 } else { ··· 185 201 } 186 202 } 187 203 } 188 - impl<T: RenderAttribute> RenderAttribute for Vec<T> { 189 - fn render_fill_attribute(&self, colormap: &ColorMapping) -> (String, String) { 190 - ( 191 - self.first() 192 - .map(|v| v.render_fill_attribute(colormap).0) 193 - .unwrap_or_default() 194 - .clone(), 195 - self.iter() 196 - .map(|v| v.render_fill_attribute(colormap).1.clone()) 197 - .join(", "), 198 - ) 204 + impl<T: RenderAttributes> RenderAttributes for Vec<T> { 205 + fn render_fill_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String> { 206 + let mut attrs = HashMap::<String, String>::new(); 207 + for attrmap in self.iter().map(|v| v.render_fill_attribute(colormap)) { 208 + for (key, value) in attrmap { 209 + if attrs.contains_key(&key) { 210 + attrs.insert( 211 + key.clone(), 212 + format!("{}{}{}", attrs[&key], T::MULTIPLE_VALUES_JOIN_BY, value), 213 + ); 214 + } else { 215 + attrs.insert(key, value); 216 + } 217 + } 218 + } 219 + attrs 199 220 } 200 221 201 - fn render_stroke_attribute(&self, colormap: &ColorMapping) -> (String, String) { 202 - ( 203 - self.first() 204 - .map(|v| v.render_stroke_attribute(colormap).0) 205 - .unwrap_or_default() 206 - .clone(), 207 - self.iter() 208 - .map(|v| v.render_stroke_attribute(colormap).1.clone()) 209 - .join(", "), 210 - ) 222 + fn render_stroke_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String> { 223 + let mut attrs = HashMap::<String, String>::new(); 224 + for attrmap in self.iter().map(|v| v.render_stroke_attribute(colormap)) { 225 + for (key, value) in attrmap { 226 + if attrs.contains_key(&key) { 227 + attrs.insert( 228 + key.clone(), 229 + format!("{}{}{}", attrs[&key], T::MULTIPLE_VALUES_JOIN_BY, value), 230 + ); 231 + } else { 232 + attrs.insert(key, value); 233 + } 234 + } 235 + } 236 + attrs 211 237 } 212 238 } 213 239 ··· 293 319 LineSegment::InwardCurve(anchor) 294 320 | LineSegment::OutwardCurve(anchor) 295 321 | LineSegment::Straight(anchor) => { 322 + // println!( 323 + // "extending region {} with {}", 324 + // region, 325 + // Region::from((start, anchor)) 326 + // ); 296 327 region = *region.max(&(start, anchor).into()) 297 328 } 298 329 } 299 330 } 331 + // println!("region for {:?} -> {}", self, region); 300 332 region 301 333 } 302 334 Object::Line(start, end, _)
+2 -2
src/point.rs
··· 17 17 18 18 pub fn region(&self) -> Region { 19 19 Region { 20 - start: self.clone(), 21 - end: self.clone(), 20 + start: *self, 21 + end: *self, 22 22 } 23 23 } 24 24
+3 -3
src/preview.rs
··· 96 96 let mut first_frame_ms = 0; 97 97 let mut num_frames = 1; 98 98 99 - let (_, querystring) = url.split_once("?").unwrap_or(("", "")); 99 + let (_, querystring) = url.split_once('?').unwrap_or(("", "")); 100 100 for (key, value) in querystring 101 - .split("&") 102 - .map(|pair| pair.split_once("=").unwrap_or(("", ""))) 101 + .split('&') 102 + .map(|pair| pair.split_once('=').unwrap_or(("", ""))) 103 103 { 104 104 match key { 105 105 "from" => first_frame_ms = value.parse().unwrap_or(0),
+11 -4
src/region.rs
··· 73 73 impl From<&Region> for RegionIterator { 74 74 fn from(region: &Region) -> Self { 75 75 Self { 76 - region: region.clone(), 77 - current: region.start.clone(), 76 + region: *region, 77 + current: region.start, 78 78 } 79 79 } 80 80 } ··· 82 82 impl From<(&Point, &Point)> for Region { 83 83 fn from(value: (&Point, &Point)) -> Self { 84 84 Self { 85 - start: value.0.clone(), 86 - end: value.1.clone(), 85 + start: *value.0, 86 + end: *value.1, 87 87 } 88 88 } 89 89 } ··· 153 153 154 154 pub fn topright(&self) -> Point { 155 155 Point(self.end.0, self.start.1) 156 + } 157 + 158 + pub fn center(&self) -> Point { 159 + Point( 160 + (self.start.0 + self.end.0) / 2, 161 + (self.start.1 + self.end.1) / 2, 162 + ) 156 163 } 157 164 158 165 pub fn max<'a>(&'a self, other: &'a Region) -> &'a Region {
+3 -1
src/sync.rs
··· 1 1 use std::collections::HashMap; 2 2 3 + use serde::{Deserialize, Serialize}; 4 + 3 5 use crate::Stem; 4 6 5 7 pub type TimestampMS = usize; ··· 9 11 fn load(&self, progress: Option<&indicatif::ProgressBar>) -> SyncData; 10 12 } 11 13 12 - #[derive(Debug, Default)] 14 + #[derive(Debug, Default, Serialize, Deserialize)] 13 15 pub struct SyncData { 14 16 pub stems: HashMap<String, Stem>, 15 17 pub markers: HashMap<TimestampMS, String>,
+13 -7
src/transform.rs
··· 1 + use std::{ 2 + collections::{HashMap}, 3 + }; 4 + 1 5 use slug::slugify; 2 6 use wasm_bindgen::prelude::*; 3 7 4 - use crate::RenderAttribute; 8 + use crate::RenderAttributes; 5 9 6 10 #[wasm_bindgen] 7 11 #[derive(Debug, Clone, Copy, PartialEq)] ··· 70 74 } 71 75 } 72 76 73 - impl RenderAttribute for Transformation { 77 + impl RenderAttributes for Transformation { 74 78 const MULTIPLE_VALUES_JOIN_BY: &'static str = " "; 75 79 76 - fn render_fill_attribute(&self, _colormap: &crate::ColorMapping) -> (String, String) { 77 - ( 78 - "transform".to_owned(), 80 + fn render_fill_attribute(&self, _colormap: &crate::ColorMapping) -> HashMap<String, String> { 81 + let mut attrs = HashMap::new(); 82 + attrs.insert( 83 + "transform".to_string(), 79 84 match self { 80 85 Transformation::Scale(x, y) => format!("scale({} {})", x, y), 81 86 Transformation::Rotate(angle) => format!("rotate({})", angle), ··· 84 89 format!("matrix({}, {}, {}, {}, {}, {})", a, b, c, d, e, f) 85 90 } 86 91 }, 87 - ) 92 + ); 93 + attrs 88 94 } 89 95 90 - fn render_stroke_attribute(&self, colormap: &crate::ColorMapping) -> (String, String) { 96 + fn render_stroke_attribute(&self, colormap: &crate::ColorMapping) -> HashMap<String, String> { 91 97 self.render_fill_attribute(colormap) 92 98 } 93 99 }
+47 -2
src/ui.rs
··· 1 1 use console::Style; 2 2 use indicatif::{ProgressBar, ProgressStyle}; 3 + use std::borrow::Cow; 3 4 use std::sync::{Arc, Mutex}; 4 5 use std::thread::{self, JoinHandle}; 5 6 use std::time; ··· 14 15 } 15 16 16 17 impl Spinner { 17 - pub fn start(message: &str) -> Self { 18 + pub fn start(verb: &'static str, message: &str) -> Self { 18 19 let spinner = ProgressBar::new(0).with_style( 19 - ProgressStyle::with_template(&("{spinner:.cyan} ".to_owned() + message)).unwrap(), 20 + ProgressStyle::with_template(&format_log_msg_cyan( 21 + &verb, 22 + &(message.to_owned() + " {spinner:.cyan}"), 23 + )) 24 + .unwrap(), 20 25 ); 21 26 spinner.tick(); 22 27 ··· 39 44 } 40 45 41 46 pub fn end(self, message: &str) { 47 + self.spinner.finish_and_clear(); 42 48 *self.finished.lock().unwrap() = true; 43 49 self.thread.join().unwrap(); 44 50 println!("{}", message); ··· 64 70 format!("{} {}", style.apply_to(format!("{verb:>12}")), message) 65 71 } 66 72 73 + pub fn format_log_msg_cyan(verb: &'static str, message: &str) -> String { 74 + let style = Style::new().bold().cyan(); 75 + format!("{} {}", style.apply_to(format!("{verb:>12}")), message) 76 + } 77 + 67 78 impl Log for ProgressBar { 68 79 fn log(&self, verb: &'static str, message: &str) { 69 80 self.println(format_log_msg(verb, message)); 70 81 } 71 82 } 83 + 84 + impl Log for Option<&ProgressBar> { 85 + fn log(&self, verb: &'static str, message: &str) { 86 + if let Some(pb) = self { 87 + pb.println(format_log_msg(verb, message)); 88 + } 89 + } 90 + } 91 + 92 + pub trait MaybeProgressBar<'a> { 93 + fn set_message(&'a self, message: impl Into<Cow<'static, str>>); 94 + fn inc(&'a self, n: u64); 95 + fn println(&'a self, message: impl AsRef<str>); 96 + } 97 + 98 + impl<'a> MaybeProgressBar<'a> for Option<&'a ProgressBar> { 99 + fn set_message(&'a self, message: impl Into<Cow<'static, str>>) { 100 + if let Some(pb) = self { 101 + pb.set_message(message); 102 + } 103 + } 104 + 105 + fn inc(&'a self, n: u64) { 106 + if let Some(pb) = self { 107 + pb.inc(n); 108 + } 109 + } 110 + 111 + fn println(&'a self, message: impl AsRef<str>) { 112 + if let Some(pb) = self { 113 + pb.println(message); 114 + } 115 + } 116 + }
+56 -24
src/video.rs
··· 17 17 use crate::{ 18 18 preview, 19 19 sync::SyncData, 20 - ui::{self, setup_progress_bar, Log as _}, 20 + ui::{self, format_log_msg, setup_progress_bar, Log as _}, 21 21 Canvas, ColoredObject, Context, LayerAnimationUpdateFunction, MidiSynchronizer, 22 22 MusicalDurationUnit, Syncable, 23 23 }; ··· 117 117 let loader = MidiSynchronizer::new(sync_data_path); 118 118 let syncdata = loader.load(Some(&self.progress_bar)); 119 119 self.progress_bar.finish(); 120 + self.progress_bar.log( 121 + "Loaded", 122 + &format!( 123 + "{} notes from {sync_data_path}", 124 + syncdata 125 + .stems 126 + .values() 127 + .map(|v| v.notes.len()) 128 + .sum::<usize>(), 129 + ), 130 + ); 120 131 return Self { syncdata, ..self }; 121 132 } 122 133 ··· 141 152 .args([ 142 153 "-ss", 143 154 &format!("{}", self.start_rendering_at as f32 / 1000.0), 144 - ]) 145 - .args(["-i", self.audiofile.to_str().unwrap()]) 155 + ]); 156 + 157 + if !self.audiofile.to_str().unwrap().is_empty() { 158 + if !self.audiofile.exists() { 159 + return Err(anyhow::format_err!( 160 + "Audio file {} does not exist", 161 + self.audiofile.to_str().unwrap() 162 + )); 163 + } 164 + command.args(["-i", self.audiofile.to_str().unwrap()]); 165 + // so that vscode can read the video file with sound lmao 166 + command.args(["-acodec", "mp3"]); 167 + } 168 + 169 + command 146 170 .args(["-t", &format!("{}", self.duration_ms() as f32 / 1000.0)]) 147 171 .args(["-c:v", "libx264"]) 148 172 .args(["-pix_fmt", "yuv420p"]) 149 173 .arg("-y") 150 174 .arg(render_to); 151 175 152 - println!("Running command: {:?}", command); 153 - 154 176 match command.output() { 155 - Err(e) => Err(anyhow::format_err!("Failed to execute ffmpeg: {}", e).into()), 177 + Err(e) => Err(anyhow::format_err!("Failed to execute ffmpeg: {}", e)), 156 178 Ok(r) => { 157 179 println!("{}", std::str::from_utf8(&r.stdout).unwrap()); 158 180 println!("{}", std::str::from_utf8(&r.stderr).unwrap()); ··· 338 360 }), 339 361 render_function: Box::new(move |canvas, ctx| { 340 362 let object = create_object(canvas, ctx)?; 341 - canvas.layer(&layer_name).set_object(object_name, object); 363 + canvas.layer(layer_name).set_object(object_name, object); 342 364 Ok(()) 343 365 }), 344 366 }) ··· 569 591 context.timestamp = milliseconds_to_timestamp(context.ms).to_string(); 570 592 context.beat_fractional = (context.bpm * context.ms) as f32 / (1000.0 * 60.0); 571 593 context.beat = context.beat_fractional as usize; 572 - context.frame = ((self.fps * context.ms) as f64 / 1000.0) as usize; 594 + context.frame = self.fps * context.ms / 1000; 573 595 574 596 progress_bar.set_message(context.timestamp.clone()); 575 597 ··· 596 618 } 597 619 } 598 620 599 - for hook in &self.hooks { 600 - if (hook.when)( 601 - &canvas, 602 - &context, 603 - previous_rendered_beat, 604 - previous_rendered_frame, 605 - ) { 606 - (hook.render_function)(&mut canvas, &mut context)?; 607 - } 608 - } 621 + // Render later hooks first, so that for example animations that aren't finished yet get overwritten by next frame's hook, if the next frames touches the same object 622 + // This is way better to cancel early animations such as fading out an object that appears on every note of a stem, if the next note is too close for the fade-out to finish. 609 623 610 624 let mut later_hooks_to_delete: Vec<usize> = vec![]; 611 625 ··· 626 640 } 627 641 } 628 642 643 + for hook in &self.hooks { 644 + if (hook.when)( 645 + &canvas, 646 + &context, 647 + previous_rendered_beat, 648 + previous_rendered_frame, 649 + ) { 650 + (hook.render_function)(&mut canvas, &mut context)?; 651 + } 652 + } 653 + 629 654 if context.frame != previous_rendered_frame { 630 655 let rendered = canvas.render(render_background)?; 631 656 ··· 653 678 let mut frame_writer_threads = vec![]; 654 679 let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; 655 680 681 + create_dir_all(self.frames_output_directory)?; 656 682 remove_dir_all(self.frames_output_directory)?; 657 683 create_dir(self.frames_output_directory)?; 658 684 create_dir_all(Path::new(&output_file).parent().unwrap())?; ··· 671 697 } 672 698 673 699 self.progress_bar.log( 674 - "Finished", 675 - &format!("rendering {} frames to SVG", frames_to_write.len()), 700 + "Rendered", 701 + &format!("{} frames to SVG", frames_to_write.len()), 676 702 ); 677 703 678 704 frames_to_write.retain(|(_, _, ms)| *ms >= self.start_rendering_at); ··· 686 712 for (frame, no, _) in &frames_to_write { 687 713 std::fs::write( 688 714 format!("{}/{}.svg", self.frames_output_directory, no), 689 - &frame, 715 + frame, 690 716 )?; 691 717 } 692 718 ··· 723 749 handle.join().unwrap(); 724 750 } 725 751 726 - self.progress_bar.log("Rendered", "SVG frames to PNG"); 752 + self.progress_bar.log( 753 + "Converted", 754 + &format!("{} SVG frames to PNG", self.progress_bar.position()), 755 + ); 727 756 self.progress_bar.finish_and_clear(); 728 757 729 - let spinner = ui::Spinner::start("Building video…"); 758 + let spinner = ui::Spinner::start("Building", "video"); 730 759 let result = self.build_video(&output_file); 731 - spinner.end(&format!("Built video to {}", output_file)); 760 + spinner.end(&format_log_msg( 761 + "Built", 762 + &format!("video to {}", output_file), 763 + )); 732 764 733 765 result 734 766 }
+17 -17
src/web.rs
··· 59 59 pub fn map_to_midi_controller() {} 60 60 61 61 #[wasm_bindgen] 62 - pub fn render_canvas_into(selector: String) -> () { 62 + pub fn render_canvas_into(selector: String) { 63 63 let svgstring = canvas().render(false).unwrap_throw(); 64 64 append_new_div_inside(svgstring, selector) 65 65 } 66 66 67 67 #[wasm_bindgen] 68 - pub fn render_canvas_at(selector: String) -> () { 68 + pub fn render_canvas_at(selector: String) { 69 69 let svgstring = canvas().render(false).unwrap_throw(); 70 70 replace_content_with(svgstring, selector) 71 71 } ··· 130 130 } 131 131 132 132 #[wasm_bindgen] 133 - pub fn render_canvas(render_background: Option<bool>) -> () { 133 + pub fn render_canvas(render_background: Option<bool>) { 134 134 canvas() 135 135 .render(render_background.unwrap_or(false)) 136 136 .unwrap_throw(); 137 137 } 138 138 139 139 #[wasm_bindgen] 140 - pub fn set_palette(palette: ColorMapping) -> () { 140 + pub fn set_palette(palette: ColorMapping) { 141 141 canvas().colormap = palette; 142 142 } 143 143 ··· 182 182 .expect_throw("could not get the element, but is was found (shouldn't happen)") 183 183 } 184 184 185 - fn append_new_div_inside(content: String, selector: String) -> () { 185 + fn append_new_div_inside(content: String, selector: String) { 186 186 let output = document().create_element("div").unwrap(); 187 187 output.set_class_name("frame"); 188 188 output.set_inner_html(&content); 189 189 query_selector(selector).append_child(&output).unwrap(); 190 190 } 191 191 192 - fn replace_content_with(content: String, selector: String) -> () { 192 + fn replace_content_with(content: String, selector: String) { 193 193 query_selector(selector).set_inner_html(&content); 194 194 } 195 195 ··· 206 206 canvas().render(false).unwrap_throw() 207 207 } 208 208 209 - pub fn render_into(&self, selector: String) -> () { 209 + pub fn render_into(&self, selector: String) { 210 210 append_new_div_inside(self.render(), selector) 211 211 } 212 212 213 - pub fn render_at(self, selector: String) -> () { 213 + pub fn render_at(self, selector: String) { 214 214 replace_content_with(self.render(), selector) 215 215 } 216 216 217 - pub fn paint_all(&self, color: Color, opacity: Option<f32>, filter: Filter) -> () { 217 + pub fn paint_all(&self, color: Color, opacity: Option<f32>, filter: Filter) { 218 218 canvas() 219 219 .layer(&self.name) 220 220 .paint_all_objects(Fill::Translucent(color, opacity.unwrap_or(1.0))); ··· 236 236 end: Point, 237 237 thickness: f32, 238 238 color: Color, 239 - ) -> () { 239 + ) { 240 240 canvas().layer(name).add_object( 241 241 name, 242 242 ( ··· 253 253 end: Point, 254 254 thickness: f32, 255 255 color: Color, 256 - ) -> () { 256 + ) { 257 257 canvas().layer(name).add_object( 258 258 name, 259 259 Object::CurveOutward(start, end, thickness).color(Fill::Solid(color)), ··· 266 266 end: Point, 267 267 thickness: f32, 268 268 color: Color, 269 - ) -> () { 269 + ) { 270 270 canvas().layer(name).add_object( 271 271 name, 272 272 Object::CurveInward(start, end, thickness).color(Fill::Solid(color)), 273 273 ) 274 274 } 275 - pub fn new_small_circle(&self, name: &str, center: Point, color: Color) -> () { 275 + pub fn new_small_circle(&self, name: &str, center: Point, color: Color) { 276 276 canvas() 277 277 .layer(name) 278 278 .add_object(name, Object::SmallCircle(center).color(Fill::Solid(color))) 279 279 } 280 - pub fn new_dot(&self, name: &str, center: Point, color: Color) -> () { 280 + pub fn new_dot(&self, name: &str, center: Point, color: Color) { 281 281 canvas() 282 282 .layer(name) 283 283 .add_object(name, Object::Dot(center).color(Fill::Solid(color))) 284 284 } 285 - pub fn new_big_circle(&self, name: &str, center: Point, color: Color) -> () { 285 + pub fn new_big_circle(&self, name: &str, center: Point, color: Color) { 286 286 canvas() 287 287 .layer(name) 288 288 .add_object(name, Object::BigCircle(center).color(Fill::Solid(color))) ··· 294 294 text: String, 295 295 font_size: f32, 296 296 color: Color, 297 - ) -> () { 297 + ) { 298 298 canvas().layer(name).add_object( 299 299 name, 300 300 Object::Text(anchor, text, font_size).color(Fill::Solid(color)), ··· 306 306 topleft: Point, 307 307 bottomright: Point, 308 308 color: Color, 309 - ) -> () { 309 + ) { 310 310 canvas().layer(name).add_object( 311 311 name, 312 312 Object::Rectangle(topleft, bottomright).color(Fill::Solid(color)),