This repository has no description
0

Configure Feed

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

🚧 Add Scenes and cue marker synchronisation (#65)

+853 -438
+149 -39
Cargo.lock
··· 1218 1218 ] 1219 1219 1220 1220 [[package]] 1221 + name = "extended" 1222 + version = "0.1.0" 1223 + source = "registry+https://github.com/rust-lang/crates.io-index" 1224 + checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" 1225 + 1226 + [[package]] 1221 1227 name = "fake-simd" 1222 1228 version = "0.1.2" 1223 1229 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2635 2641 "js-sys", 2636 2642 "log", 2637 2643 "wasm-bindgen", 2638 - "windows-core 0.62.2", 2644 + "windows-core", 2639 2645 ] 2640 2646 2641 2647 [[package]] ··· 4595 4601 "slug", 4596 4602 "strum", 4597 4603 "strum_macros", 4604 + "symphonia", 4605 + "symphonia-bundle-flac", 4598 4606 "tiny-skia", 4599 4607 "tokio", 4600 4608 "toml 0.9.8", ··· 4835 4843 ] 4836 4844 4837 4845 [[package]] 4846 + name = "symphonia" 4847 + version = "0.5.5" 4848 + source = "registry+https://github.com/rust-lang/crates.io-index" 4849 + checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" 4850 + dependencies = [ 4851 + "lazy_static", 4852 + "symphonia-bundle-flac", 4853 + "symphonia-codec-adpcm", 4854 + "symphonia-codec-pcm", 4855 + "symphonia-codec-vorbis", 4856 + "symphonia-core", 4857 + "symphonia-format-mkv", 4858 + "symphonia-format-ogg", 4859 + "symphonia-format-riff", 4860 + "symphonia-metadata", 4861 + ] 4862 + 4863 + [[package]] 4864 + name = "symphonia-bundle-flac" 4865 + version = "0.5.5" 4866 + source = "registry+https://github.com/rust-lang/crates.io-index" 4867 + checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" 4868 + dependencies = [ 4869 + "log", 4870 + "symphonia-core", 4871 + "symphonia-metadata", 4872 + "symphonia-utils-xiph", 4873 + ] 4874 + 4875 + [[package]] 4876 + name = "symphonia-codec-adpcm" 4877 + version = "0.5.5" 4878 + source = "registry+https://github.com/rust-lang/crates.io-index" 4879 + checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" 4880 + dependencies = [ 4881 + "log", 4882 + "symphonia-core", 4883 + ] 4884 + 4885 + [[package]] 4886 + name = "symphonia-codec-pcm" 4887 + version = "0.5.5" 4888 + source = "registry+https://github.com/rust-lang/crates.io-index" 4889 + checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" 4890 + dependencies = [ 4891 + "log", 4892 + "symphonia-core", 4893 + ] 4894 + 4895 + [[package]] 4896 + name = "symphonia-codec-vorbis" 4897 + version = "0.5.5" 4898 + source = "registry+https://github.com/rust-lang/crates.io-index" 4899 + checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" 4900 + dependencies = [ 4901 + "log", 4902 + "symphonia-core", 4903 + "symphonia-utils-xiph", 4904 + ] 4905 + 4906 + [[package]] 4907 + name = "symphonia-core" 4908 + version = "0.5.5" 4909 + source = "registry+https://github.com/rust-lang/crates.io-index" 4910 + checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" 4911 + dependencies = [ 4912 + "arrayvec", 4913 + "bitflags 1.3.2", 4914 + "bytemuck", 4915 + "lazy_static", 4916 + "log", 4917 + ] 4918 + 4919 + [[package]] 4920 + name = "symphonia-format-mkv" 4921 + version = "0.5.5" 4922 + source = "registry+https://github.com/rust-lang/crates.io-index" 4923 + checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" 4924 + dependencies = [ 4925 + "lazy_static", 4926 + "log", 4927 + "symphonia-core", 4928 + "symphonia-metadata", 4929 + "symphonia-utils-xiph", 4930 + ] 4931 + 4932 + [[package]] 4933 + name = "symphonia-format-ogg" 4934 + version = "0.5.5" 4935 + source = "registry+https://github.com/rust-lang/crates.io-index" 4936 + checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" 4937 + dependencies = [ 4938 + "log", 4939 + "symphonia-core", 4940 + "symphonia-metadata", 4941 + "symphonia-utils-xiph", 4942 + ] 4943 + 4944 + [[package]] 4945 + name = "symphonia-format-riff" 4946 + version = "0.5.5" 4947 + source = "registry+https://github.com/rust-lang/crates.io-index" 4948 + checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" 4949 + dependencies = [ 4950 + "extended", 4951 + "log", 4952 + "symphonia-core", 4953 + "symphonia-metadata", 4954 + ] 4955 + 4956 + [[package]] 4957 + name = "symphonia-metadata" 4958 + version = "0.5.5" 4959 + source = "registry+https://github.com/rust-lang/crates.io-index" 4960 + checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" 4961 + dependencies = [ 4962 + "encoding_rs", 4963 + "lazy_static", 4964 + "log", 4965 + "symphonia-core", 4966 + ] 4967 + 4968 + [[package]] 4969 + name = "symphonia-utils-xiph" 4970 + version = "0.5.5" 4971 + source = "registry+https://github.com/rust-lang/crates.io-index" 4972 + checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" 4973 + dependencies = [ 4974 + "symphonia-core", 4975 + "symphonia-metadata", 4976 + ] 4977 + 4978 + [[package]] 4838 4979 name = "syn" 4839 4980 version = "1.0.109" 4840 4981 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5467 5608 [[package]] 5468 5609 name = "vgv" 5469 5610 version = "0.1.0" 5470 - source = "git+https://github.com/gwennlbh/vgvf#fbde8ac4efde5f3c95548f82d6e09424800aea8a" 5611 + source = "git+https://github.com/gwennlbh/vgvf#a9e996c8be627564c8466f945c3f4e104895b39c" 5471 5612 dependencies = [ 5472 5613 "anyhow", 5473 5614 "base64", ··· 5753 5894 checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 5754 5895 dependencies = [ 5755 5896 "windows-collections", 5756 - "windows-core 0.61.2", 5897 + "windows-core", 5757 5898 "windows-future", 5758 5899 "windows-link 0.1.3", 5759 5900 "windows-numerics", ··· 5765 5906 source = "registry+https://github.com/rust-lang/crates.io-index" 5766 5907 checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 5767 5908 dependencies = [ 5768 - "windows-core 0.61.2", 5909 + "windows-core", 5769 5910 ] 5770 5911 5771 5912 [[package]] ··· 5777 5918 "windows-implement", 5778 5919 "windows-interface", 5779 5920 "windows-link 0.1.3", 5780 - "windows-result 0.3.4", 5781 - "windows-strings 0.4.2", 5782 - ] 5783 - 5784 - [[package]] 5785 - name = "windows-core" 5786 - version = "0.62.2" 5787 - source = "registry+https://github.com/rust-lang/crates.io-index" 5788 - checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 5789 - dependencies = [ 5790 - "windows-implement", 5791 - "windows-interface", 5792 - "windows-link 0.2.1", 5793 - "windows-result 0.4.1", 5794 - "windows-strings 0.5.1", 5921 + "windows-result", 5922 + "windows-strings", 5795 5923 ] 5796 5924 5797 5925 [[package]] ··· 5800 5928 source = "registry+https://github.com/rust-lang/crates.io-index" 5801 5929 checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 5802 5930 dependencies = [ 5803 - "windows-core 0.61.2", 5931 + "windows-core", 5804 5932 "windows-link 0.1.3", 5805 5933 "windows-threading", 5806 5934 ] ··· 5845 5973 source = "registry+https://github.com/rust-lang/crates.io-index" 5846 5974 checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 5847 5975 dependencies = [ 5848 - "windows-core 0.61.2", 5976 + "windows-core", 5849 5977 "windows-link 0.1.3", 5850 5978 ] 5851 5979 ··· 5859 5987 ] 5860 5988 5861 5989 [[package]] 5862 - name = "windows-result" 5863 - version = "0.4.1" 5864 - source = "registry+https://github.com/rust-lang/crates.io-index" 5865 - checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 5866 - dependencies = [ 5867 - "windows-link 0.2.1", 5868 - ] 5869 - 5870 - [[package]] 5871 5990 name = "windows-strings" 5872 5991 version = "0.4.2" 5873 5992 source = "registry+https://github.com/rust-lang/crates.io-index" 5874 5993 checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 5875 5994 dependencies = [ 5876 5995 "windows-link 0.1.3", 5877 - ] 5878 - 5879 - [[package]] 5880 - name = "windows-strings" 5881 - version = "0.5.1" 5882 - source = "registry+https://github.com/rust-lang/crates.io-index" 5883 - checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 5884 - dependencies = [ 5885 - "windows-link 0.2.1", 5886 5996 ] 5887 5997 5888 5998 [[package]]
+2
Cargo.toml
··· 108 108 axum = { version = "0.8.6", optional = true, features = ["json"] } 109 109 quick-xml = "0.38.3" 110 110 vgv = { git = "https://github.com/gwennlbh/vgvf", version = "0.1.0", optional = true} 111 + symphonia-bundle-flac = { version = "0.5.5" } 112 + symphonia = { version = "0.5.5" } 111 113 112 114 113 115 [dev-dependencies]
+9 -178
examples/schedule-hell/src/main.rs
··· 1 1 use std::path::PathBuf; 2 2 3 + mod scenes; 3 4 use anyhow::Result; 4 - use itertools::Itertools; 5 5 use rand::{SeedableRng, rngs::SmallRng}; 6 - use shapemaker::{graphics::fill::FillOperations, *}; 6 + use shapemaker::*; 7 7 8 - struct State { 8 + pub struct State { 9 9 bass_pattern_at: Region, 10 10 kick_color: Color, 11 11 rng: SmallRng, ··· 65 65 video.audiofile = PathBuf::from("schedule-hell.flac"); 66 66 video = video 67 67 .sync_audio_with("schedule-hell.midi") 68 - .init(&|canvas, _| { 69 - canvas.set_background(Color::Black); 70 - 71 - let mut kicks = Layer::new("anchor kick"); 72 - 73 - let circle_at = |x: usize, y: usize| Object::SmallCircle(Point(x, y)); 74 - 75 - let (end_x, end_y) = { 76 - let Point(x, y) = canvas.world_region.end; 77 - (x - 2, y - 2) 78 - }; 79 - kicks.set("top left", circle_at(1, 1)); 80 - kicks.set("top right", circle_at(end_x, 1)); 81 - kicks.set("bottom left", circle_at(1, end_y)); 82 - kicks.set("bottom right", circle_at(end_x, end_y)); 83 - canvas.add_or_replace_layer(kicks); 84 - 85 - let mut ch = Layer::new("ch"); 86 - ch.set("0", Object::Dot(Point(0, 0))); 87 - canvas.add_or_replace_layer(ch); 88 - 89 - Ok(()) 90 - }) 91 - .on_note("anchor kick", &|canvas, ctx| { 92 - canvas 93 - .layer("anchor kick") 94 - .paint_all_objects(Fill::Translucent(ctx.extra.kick_color, 1.0)); 95 - 96 - ctx.animate_layer("anchor kick", 200, &|t, layer, _| { 97 - layer.objects.values_mut().for_each( 98 - |ColoredObject { fill, .. }| { 99 - *fill = fill.opacify(1.0 - t); 100 - }, 101 - ); 102 - Ok(()) 103 - }); 104 - 105 - Ok(()) 106 - }) 107 - .on_note("bass", &|canvas, ctx| { 108 - let pitch = ctx 109 - .notes_of_stem("bass") 110 - .find(|note| note.is_on()) 111 - .map(|note| note.pitch); 112 - 113 - let area = (2, 2); 114 - let bounds = canvas.world_region.resized(-2, -2); 115 - ctx.extra.bass_pattern_at = match pitch { 116 - Some(32 | 33 | 34) => bounds.starting_from_topleft(area), 117 - Some(39) => bounds.starting_from_topright(area), 118 - Some(35) => bounds.starting_from_bottomleft(area), 119 - Some(42 | 41) => bounds.starting_from_bottomright(area), 120 - _ => bounds.starting_from_bottomleft(area), 121 - } 122 - .unwrap(); 123 - 124 - let mut bass = canvas.random_layer_within( 125 - &mut ctx.extra.rng, 126 - "bass", 127 - &ctx.extra.bass_pattern_at, 128 - ); 129 - 130 - bass.paint_all_objects(Fill::Solid(Color::White)); 131 - canvas.add_or_replace_layer(bass); 132 - 133 - Ok(()) 134 - }) 135 - .on_note("powerful clap hit, clap, perclap", &|canvas, ctx| { 136 - let mut claps = canvas.random_layer_within( 137 - &mut ctx.extra.rng, 138 - "claps", 139 - &ctx.extra.bass_pattern_at.translated(2, 0), 140 - ); 141 - claps.paint_all_objects(Fill::Solid(Color::Red)); 142 - canvas.add_or_replace_layer(claps); 143 - Ok(()) 144 - }) 145 - .on_note( 146 - "rimshot, glitchy percs, hitting percs, glitchy percs", 147 - &|canvas, ctx| { 148 - let mut foley = canvas.random_layer_within( 149 - &mut ctx.extra.rng, 150 - "percs", 151 - &ctx.extra.bass_pattern_at.translated(2, 0), 152 - ); 153 - foley.paint_all_objects(Fill::Translucent(Color::Red, 0.5)); 154 - canvas.add_or_replace_layer(foley); 155 - Ok(()) 156 - }, 157 - ) 158 - .on_note("qanda", &|canvas, ctx| { 159 - let canvas_line_width = canvas.object_sizes.default_line_width; 160 - let mut qanda = canvas.random_curves_within( 161 - &mut ctx.extra.rng, 162 - "qanda", 163 - &ctx.extra.bass_pattern_at.translated(-1, -1).enlarged(1, 1), 164 - 3..=5, 165 - ); 166 - qanda.paint_all_objects(Fill::Solid(Color::Orange)); 167 - qanda.object_sizes.default_line_width = 168 - canvas_line_width * 4.0 * ctx.stem("qanda").velocity_relative(); 169 - 170 - canvas.add_or_replace_layer(qanda); 171 - Ok(()) 172 - }) 173 - .on_note("brokenup", &|canvas, ctx| { 174 - let canvas_line_width = canvas.object_sizes.default_line_width; 175 - let mut brokenup = canvas.random_curves_within( 176 - &mut ctx.extra.rng, 177 - "brokenup", 178 - &ctx.extra.bass_pattern_at.translated(0, -2), 179 - 3..=5, 180 - ); 181 - brokenup.paint_all_objects(Fill::Solid(Color::Yellow)); 182 - brokenup.object_sizes.default_line_width = canvas_line_width 183 - * 4.0 184 - * ctx.stem("brokenup").velocity_relative(); 185 - 186 - canvas.add_or_replace_layer(brokenup); 187 - Ok(()) 188 - }) 189 - .on_note("goup", &|canvas, ctx| { 190 - let canvas_line_width = canvas.object_sizes.default_line_width; 191 - let mut goup = canvas.random_curves_within( 192 - &mut ctx.extra.rng, 193 - "goup", 194 - &ctx.extra.bass_pattern_at.translated(0, 2), 195 - 3..=5, 196 - ); 197 - goup.paint_all_objects(Fill::Solid(Color::Green)); 198 - goup.object_sizes.default_line_width = 199 - canvas_line_width * 4.0 * ctx.stem("goup").velocity_relative(); 200 - 201 - canvas.add_or_replace_layer(goup); 202 - Ok(()) 203 - }) 204 - .on_note("ch", &|canvas, ctx| { 205 - let world = canvas.world_region.clone(); 206 - 207 - // keep only the last 2 dots 208 - let dots_to_keep = canvas 209 - .layer("ch") 210 - .objects 211 - .iter() 212 - .sorted_by_key(|(name, _)| name.parse::<usize>().unwrap()) 213 - .rev() 214 - .take(2) 215 - .map(|(name, _)| name.clone()) 216 - .collect::<Vec<_>>(); 217 - 218 - let layer = canvas.layer("ch"); 219 - layer.object_sizes.empty_shape_stroke_width = 2.0; 220 - layer.objects.retain(|name, _| dots_to_keep.contains(name)); 221 - 222 - let object_name = format!("{}", ctx.ms); 223 - layer.set( 224 - &object_name, 225 - Object::Dot( 226 - world.resized(-1, -1).random_point(&mut ctx.extra.rng), 227 - ) 228 - .colored(Color::Cyan), 229 - ); 230 - 231 - canvas.put_layer_on_top("ch"); 232 - Ok(()) 233 - }) 68 + .with_init_scene(scenes::intro()) 69 + .with_marked_scene(scenes::first_break()) 234 70 .when_remaining(10, &|canvas, _| { 235 71 let world = canvas.world_region; 236 72 canvas.root().set( ··· 243 79 .colored(Color::White), 244 80 ); 245 81 Ok(()) 246 - }) 247 - .command("remove", &|argumentsline, canvas, _| { 248 - let args = argumentsline.splitn(3, ' ').collect::<Vec<_>>(); 249 - canvas.remove_object(args[0]); 250 - Ok(()) 251 82 }); 252 83 253 - if args.contains("--vgv") { 84 + if args.contains("--serve") { 85 + video.serve("localhost:8000").await; 86 + } else if args.contains("--vgv") { 254 87 video.encode_to_vgv( 255 88 args.free_from_str() 256 89 .unwrap_or(String::from("schedule-hell.vgv")), 257 - ); 258 - } else if args.contains("--serve") { 259 - video.serve("localhost:8000").await; 90 + )?; 260 91 } else { 261 92 video.encode( 262 93 args.free_from_str()
+9
examples/schedule-hell/src/scenes/first_break.rs
··· 1 + use crate::State; 2 + use shapemaker::*; 3 + 4 + pub fn first_break() -> Scene<State> { 5 + Scene::<State>::new("first break").init(&|canvas, _| { 6 + canvas.set_background(Color::Black); 7 + Ok(()) 8 + }) 9 + }
+173
examples/schedule-hell/src/scenes/intro.rs
··· 1 + use crate::State; 2 + use itertools::Itertools; 3 + use shapemaker::*; 4 + 5 + pub fn intro() -> Scene<State> { 6 + Scene::<State>::new("intro") 7 + .init(&|canvas, _| { 8 + canvas.set_background(Color::Black); 9 + 10 + let mut kicks = Layer::new("anchor kick"); 11 + 12 + let circle_at = |x: usize, y: usize| Object::SmallCircle(Point(x, y)); 13 + 14 + let (end_x, end_y) = { 15 + let Point(x, y) = canvas.world_region.end; 16 + (x - 2, y - 2) 17 + }; 18 + kicks.set("top left", circle_at(1, 1)); 19 + kicks.set("top right", circle_at(end_x, 1)); 20 + kicks.set("bottom left", circle_at(1, end_y)); 21 + kicks.set("bottom right", circle_at(end_x, end_y)); 22 + canvas.add_or_replace_layer(kicks); 23 + 24 + let mut ch = Layer::new("ch"); 25 + ch.set("0", Object::Dot(Point(0, 0))); 26 + canvas.add_or_replace_layer(ch); 27 + 28 + Ok(()) 29 + }) 30 + .on_note("anchor kick", &|canvas, ctx| { 31 + canvas 32 + .layer("anchor kick") 33 + .paint_all_objects(Fill::Translucent(ctx.extra.kick_color, 1.0)); 34 + 35 + ctx.animate_layer("anchor kick", 200, &|t, layer, _| { 36 + layer.objects.values_mut().for_each( 37 + |ColoredObject { fill, .. }| { 38 + *fill = fill.opacify(1.0 - t); 39 + }, 40 + ); 41 + Ok(()) 42 + }); 43 + 44 + Ok(()) 45 + }) 46 + .on_note("bass", &|canvas, ctx| { 47 + let pitch = ctx 48 + .notes_of_stem("bass") 49 + .find(|note| note.is_on()) 50 + .map(|note| note.pitch); 51 + 52 + let area = (2, 2); 53 + let bounds = canvas.world_region.resized(-2, -2); 54 + ctx.extra.bass_pattern_at = match pitch { 55 + Some(32 | 33 | 34) => bounds.starting_from_topleft(area), 56 + Some(39) => bounds.starting_from_topright(area), 57 + Some(35) => bounds.starting_from_bottomleft(area), 58 + Some(42 | 41) => bounds.starting_from_bottomright(area), 59 + _ => bounds.starting_from_bottomleft(area), 60 + } 61 + .unwrap(); 62 + 63 + let mut bass = canvas.random_layer_within( 64 + &mut ctx.extra.rng, 65 + "bass", 66 + &ctx.extra.bass_pattern_at, 67 + ); 68 + 69 + bass.paint_all_objects(Fill::Solid(Color::White)); 70 + canvas.add_or_replace_layer(bass); 71 + 72 + Ok(()) 73 + }) 74 + .on_note("powerful clap hit, clap, perclap", &|canvas, ctx| { 75 + let mut claps = canvas.random_layer_within( 76 + &mut ctx.extra.rng, 77 + "claps", 78 + &ctx.extra.bass_pattern_at.translated(2, 0), 79 + ); 80 + claps.paint_all_objects(Fill::Solid(Color::Red)); 81 + canvas.add_or_replace_layer(claps); 82 + Ok(()) 83 + }) 84 + .on_note( 85 + "rimshot, glitchy percs, hitting percs, glitchy percs", 86 + &|canvas, ctx| { 87 + let mut foley = canvas.random_layer_within( 88 + &mut ctx.extra.rng, 89 + "percs", 90 + &ctx.extra.bass_pattern_at.translated(2, 0), 91 + ); 92 + foley.paint_all_objects(Fill::Translucent(Color::Red, 0.5)); 93 + canvas.add_or_replace_layer(foley); 94 + Ok(()) 95 + }, 96 + ) 97 + .on_note("qanda", &|canvas, ctx| { 98 + let canvas_line_width = canvas.object_sizes.default_line_width; 99 + let mut qanda = canvas.random_curves_within( 100 + &mut ctx.extra.rng, 101 + "qanda", 102 + &ctx.extra.bass_pattern_at.translated(-1, -1).enlarged(1, 1), 103 + 3..=5, 104 + ); 105 + qanda.paint_all_objects(Fill::Solid(Color::Orange)); 106 + qanda.object_sizes.default_line_width = 107 + canvas_line_width * 4.0 * ctx.stem("qanda").velocity_relative(); 108 + 109 + canvas.add_or_replace_layer(qanda); 110 + Ok(()) 111 + }) 112 + .on_note("brokenup", &|canvas, ctx| { 113 + let canvas_line_width = canvas.object_sizes.default_line_width; 114 + let mut brokenup = canvas.random_curves_within( 115 + &mut ctx.extra.rng, 116 + "brokenup", 117 + &ctx.extra.bass_pattern_at.translated(0, -2), 118 + 3..=5, 119 + ); 120 + brokenup.paint_all_objects(Fill::Solid(Color::Yellow)); 121 + brokenup.object_sizes.default_line_width = canvas_line_width 122 + * 4.0 123 + * ctx.stem("brokenup").velocity_relative(); 124 + 125 + canvas.add_or_replace_layer(brokenup); 126 + Ok(()) 127 + }) 128 + .on_note("goup", &|canvas, ctx| { 129 + let canvas_line_width = canvas.object_sizes.default_line_width; 130 + let mut goup = canvas.random_curves_within( 131 + &mut ctx.extra.rng, 132 + "goup", 133 + &ctx.extra.bass_pattern_at.translated(0, 2), 134 + 3..=5, 135 + ); 136 + goup.paint_all_objects(Fill::Solid(Color::Green)); 137 + goup.object_sizes.default_line_width = 138 + canvas_line_width * 4.0 * ctx.stem("goup").velocity_relative(); 139 + 140 + canvas.add_or_replace_layer(goup); 141 + Ok(()) 142 + }) 143 + .on_note("ch", &|canvas, ctx| { 144 + let world = canvas.world_region.clone(); 145 + 146 + // keep only the last 2 dots 147 + let dots_to_keep = canvas 148 + .layer("ch") 149 + .objects 150 + .iter() 151 + .sorted_by_key(|(name, _)| name.parse::<usize>().unwrap()) 152 + .rev() 153 + .take(2) 154 + .map(|(name, _)| name.clone()) 155 + .collect::<Vec<_>>(); 156 + 157 + let layer = canvas.layer("ch"); 158 + layer.object_sizes.empty_shape_stroke_width = 2.0; 159 + layer.objects.retain(|name, _| dots_to_keep.contains(name)); 160 + 161 + let object_name = format!("{}", ctx.ms); 162 + layer.set( 163 + &object_name, 164 + Object::Dot( 165 + world.resized(-1, -1).random_point(&mut ctx.extra.rng), 166 + ) 167 + .colored(Color::Cyan), 168 + ); 169 + 170 + canvas.put_layer_on_top("ch"); 171 + Ok(()) 172 + }) 173 + }
+4
examples/schedule-hell/src/scenes/mod.rs
··· 1 + pub mod intro; 2 + pub use intro::intro; 3 + pub mod first_break; 4 + pub use first_break::first_break;
+2
src/graphics/canvas.rs
··· 22 22 pub object_sizes: ObjectSizes, 23 23 pub font_options: FontOptions, 24 24 pub colormap: ColorMapping, 25 + pub name: String, 25 26 26 27 /// The layers are in order of top to bottom: the first layer will be rendered on top of the second, etc. 27 28 pub layers: Vec<Layer>, ··· 42 43 43 44 pub fn default_settings() -> Self { 44 45 Self { 46 + name: String::new(), 45 47 grid_size: (3, 3), 46 48 cell_size: 50, 47 49 objects_count_range: 3..7,
+1 -1
src/graphics/mod.rs
··· 8 8 9 9 pub use canvas::Canvas; 10 10 pub use color::{Color, ColorMapping}; 11 - pub use fill::Fill; 11 + pub use fill::{Fill, FillOperations}; 12 12 pub use filter::{Filter, FilterType}; 13 13 pub use layer::Layer; 14 14 pub use objects::{ColoredObject, LineSegment, Object, ObjectSizes};
+3 -3
src/lib.rs
··· 33 33 34 34 pub use geometry::{Angle, Axis, Containable, Point, Region}; 35 35 pub use graphics::{ 36 - Canvas, Color, Color::*, ColorMapping, ColoredObject, Fill, Filter, 37 - FilterType, Layer, LineSegment, Object, Object::*, ObjectSizes, 36 + Canvas, Color, Color::*, ColorMapping, ColoredObject, Fill, FillOperations, 37 + Filter, FilterType, Layer, LineSegment, Object, Object::*, ObjectSizes, 38 38 Transformation, 39 39 }; 40 40 pub use rendering::{ 41 41 fonts, CSSRenderable, SVGAttributesRenderable, SVGRenderable, 42 42 }; 43 - pub use video::{animation, context, Animation, Video}; 43 + pub use video::{animation, context, Animation, AttachHooks, Scene, Video}; 44 44 45 45 trait Toggleable { 46 46 fn toggle(&mut self);
+52
src/synchronization/cue_markers.rs
··· 1 + use crate::synchronization::sync::Syncable; 2 + use std::{collections::HashMap, fs::File, path::PathBuf}; 3 + use symphonia::core::{formats::FormatReader, io::MediaSourceStream}; 4 + use symphonia::default::formats::{FlacReader, WavReader}; 5 + 6 + use super::sync::TimestampMS; 7 + 8 + pub struct CueMarkersSynchronizer { 9 + pub path: PathBuf, 10 + } 11 + 12 + impl Syncable for CueMarkersSynchronizer { 13 + fn new(path: impl Into<PathBuf>) -> Self { 14 + Self { path: path.into() } 15 + } 16 + 17 + fn load( 18 + &self, 19 + progress: Option<&indicatif::ProgressBar>, 20 + ) -> super::sync::SyncData { 21 + let mut markers: HashMap<TimestampMS, String> = HashMap::new(); 22 + 23 + let file = File::open(&self.path) 24 + .expect(&format!("Failed to open {:?} for CUE analysis", self.path)); 25 + let stream = MediaSourceStream::new(Box::new(file), Default::default()); 26 + let reader: Box<dyn FormatReader> = 27 + match self.path.extension().and_then(|s| s.to_str()) { 28 + Some("wav") => Box::new( 29 + WavReader::try_new(stream, &Default::default()) 30 + .expect("Failed to create WAV reader for CUE analysis"), 31 + ), 32 + Some("flac") => Box::new( 33 + FlacReader::try_new(stream, &Default::default()) 34 + .expect("Failed to create FLAC reader for CUE analysis"), 35 + ), 36 + _ => panic!("Unsupported audio format for CUE analysis"), 37 + }; 38 + 39 + for cue in reader.cues() { 40 + panic!("Found cue {cue:?}"); 41 + if let Some(pb) = progress { 42 + pb.set_message(format!("{cue:?}")); 43 + } 44 + } 45 + 46 + super::sync::SyncData { 47 + stems: HashMap::new(), 48 + markers, 49 + bpm: 120, 50 + } 51 + } 52 + }
+31 -8
src/synchronization/midi.rs
··· 1 1 use super::audio::{self, Stem}; 2 2 use super::sync::{SyncData, Syncable}; 3 + use crate::synchronization::sync::TimestampMS; 3 4 use crate::ui::{Log, MaybeProgressBar}; 4 5 use indicatif::ProgressBar; 5 6 use itertools::Itertools; 6 7 use measure_time::debug_time; 7 8 use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind}; 9 + use std::io::Read; 8 10 use std::{collections::HashMap, fmt::Debug, path::PathBuf}; 9 11 10 12 pub struct MidiSynchronizer { ··· 22 24 } 23 25 24 26 impl Syncable for MidiSynchronizer { 25 - fn new(path: &str) -> Self { 27 + fn new(path: impl Into<PathBuf>) -> Self { 26 28 Self { 27 - midi_path: PathBuf::from(path), 29 + midi_path: path.into(), 28 30 } 29 31 } 30 32 31 33 fn load(&self, progressbar: Option<&ProgressBar>) -> SyncData { 32 - let (now, notes_per_instrument) = 33 - load_notes(&self.midi_path, progressbar); 34 + let (now, notes_per_instrument, markers) = 35 + load_midi_file(&self.midi_path, progressbar); 36 + 37 + println!("Found markers {markers:?}"); 34 38 35 39 SyncData { 40 + markers, 36 41 bpm: tempo_to_bpm(now.tempo), 37 42 stems: HashMap::from_iter(notes_per_instrument.iter().map( 38 43 |(name, notes)| { ··· 97 102 ) 98 103 }, 99 104 )), 100 - markers: HashMap::new(), 101 105 } 102 106 } 103 107 } ··· 151 155 } 152 156 } 153 157 154 - fn load_notes( 158 + fn load_midi_file( 155 159 source: &PathBuf, 156 160 progressbar: Option<&ProgressBar>, 157 - ) -> (Now, HashMap<String, Vec<Note>>) { 161 + ) -> ( 162 + Now, 163 + HashMap<String, Vec<Note>>, 164 + HashMap<TimestampMS, String>, 165 + ) { 158 166 debug_time!("load_midi_notes"); 167 + 168 + let mut markers = HashMap::<TimestampMS, String>::new(); 169 + 159 170 // Read midi file using midly 160 171 if let Some(pb) = progressbar { 161 172 pb.set_length(1); ··· 266 277 let mut stem_notes = StemNotes::new(); 267 278 for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { 268 279 for (track_name, event) in tracks { 280 + if let TrackEventKind::Meta(MetaMessage::Marker(mut marker)) = 281 + event.kind 282 + { 283 + let mut text = String::new(); 284 + 285 + marker 286 + .read_to_string(&mut text) 287 + .expect("Marker is not valid UTF8"); 288 + 289 + markers.insert(absolute_tick_to_ms[tick], text); 290 + } 291 + 269 292 if let TrackEventKind::Midi { 270 293 channel: _, 271 294 message, ··· 312 335 } 313 336 } 314 337 315 - (now, result) 338 + (now, result, markers) 316 339 } 317 340 318 341 fn midi_tick_to_ms(tick: u32, tempo: usize, ppq: usize) -> usize {
+1
src/synchronization/mod.rs
··· 1 1 pub mod audio; 2 2 pub mod midi; 3 3 pub mod sync; 4 + pub mod cue_markers;
+18 -2
src/synchronization/sync.rs
··· 1 1 use super::audio::Stem; 2 2 use serde::{Deserialize, Serialize}; 3 - use std::collections::HashMap; 3 + use std::{collections::HashMap, iter::zip, path::PathBuf}; 4 4 5 5 pub type TimestampMS = usize; 6 6 7 7 pub trait Syncable { 8 - fn new(path: &str) -> Self; 8 + fn new(path: impl Into<PathBuf>) -> Self; 9 9 fn load(&self, progress: Option<&indicatif::ProgressBar>) -> SyncData; 10 10 } 11 11 ··· 15 15 pub markers: HashMap<TimestampMS, String>, 16 16 pub bpm: usize, 17 17 } 18 + 19 + impl SyncData { 20 + pub fn union(self, other: SyncData) -> Self { 21 + let mut combined = Self::default(); 22 + 23 + combined.bpm = other.bpm; 24 + 25 + combined.stems.extend(self.stems); 26 + combined.stems.extend(other.stems); 27 + 28 + combined.markers.extend(self.markers); 29 + combined.markers.extend(other.markers); 30 + 31 + combined 32 + } 33 + }
+23 -1
src/ui.rs
··· 4 4 use std::sync::{Arc, Mutex}; 5 5 use std::thread::{self, JoinHandle}; 6 6 use std::time; 7 + use crate::context::Context; 8 + use crate::video::engine::EngineProgression; 7 9 8 10 pub const PROGRESS_BARS_STYLE: &str = 9 - "{prefix:>12.bold.cyan} {percent:03}% [{bar:25}] {pos}/{len}: {msg} ({elapsed} ago, {eta} left)"; 11 + "{prefix:>12.bold.cyan} {percent:03}% [{bar:25}] {msg} ({elapsed} ago)"; 10 12 11 13 pub struct Spinner { 12 14 pub spinner: ProgressBar, ··· 86 88 if let Some(pb) = self { 87 89 pb.println(format_log_msg(verb, message)); 88 90 } 91 + } 92 + } 93 + 94 + pub trait EngineProgressBar { 95 + fn step_with_engine(&self, progression: EngineProgression); 96 + } 97 + 98 + impl EngineProgressBar for ProgressBar { 99 + fn step_with_engine(&self, progression: EngineProgression) { 100 + let EngineProgression { 101 + ms, 102 + scene_name, 103 + timestamp, 104 + } = progression; 105 + 106 + self.set_position(ms as _); 107 + self.set_message(match scene_name { 108 + Some(scene) => format!("{}: {}", timestamp, scene), 109 + None => format!("{}", timestamp), 110 + }); 89 111 } 90 112 } 91 113
+14
src/video/context.rs
··· 5 5 use crate::synchronization::sync::SyncData; 6 6 use itertools::Itertools; 7 7 use nanoid::nanoid; 8 + use std::fmt::Display; 8 9 use std::fs::{self}; 9 10 use std::path::PathBuf; 11 + use std::time::Duration; 10 12 11 13 pub struct Context<'a, AdditionalContext = ()> { 12 14 pub frame: usize, ··· 20 22 pub later_hooks: Vec<LaterHook<AdditionalContext>>, 21 23 pub extra: AdditionalContext, 22 24 pub duration_override: Option<usize>, 25 + pub current_scene: Option<String>, 26 + pub scene_frame: Option<usize>, 27 + pub scene_started_at_ms: Option<usize>, 23 28 } 24 29 25 30 impl<C> Context<'_, C> { ··· 50 55 duration: stems[name].duration_ms, 51 56 notes: stems[name].notes.get(&self.ms).cloned().unwrap_or(vec![]), 52 57 } 58 + } 59 + 60 + pub fn since_start(&self) -> Duration { 61 + Duration::from_millis(self.ms as _) 53 62 } 54 63 55 64 pub fn notes_of_stem(&self, name: &str) -> impl Iterator<Item = Note> + '_ { ··· 184 193 }; 185 194 186 195 self.start_animation(duration, animation); 196 + } 197 + 198 + pub fn switch_scene(&mut self, scene_name: impl Display) { 199 + self.current_scene = Some(scene_name.to_string()); 200 + self.scene_started_at_ms = Some(self.ms); 187 201 } 188 202 }
+48 -51
src/video/encoding.rs
··· 1 - use super::{hooks::milliseconds_to_timestamp, Video}; 1 + use super::{hooks::format_duration, Video}; 2 2 use crate::rendering::svg; 3 + use crate::ui::EngineProgressBar; 4 + use crate::video::engine::EngineOutput; 3 5 use crate::Canvas; 4 6 use anyhow::Result; 7 + use chrono::{DateTime, NaiveDateTime}; 5 8 use itertools::Itertools; 6 9 use measure_time::debug_time; 7 10 use std::fs::File; ··· 39 42 let resolution = self.resolution; 40 43 let (width, height) = initial_canvas.resolution_to_size_even(resolution); 41 44 42 - let (tx, rx) = 43 - std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(1_000); 45 + let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(10_000); 44 46 45 47 let pb = self.progress_bar.clone(); 46 48 47 - let mut vgv_encoder = vgv::Encoder::new( 48 - vgv::InitializationParameters { 49 - w: width as _, 50 - h: height as _, 51 - d: (1000.0 / self.fps as f64) as _, 52 - bg: initial_canvas 53 - .background 54 - .unwrap_or_default() 55 - .render(&initial_canvas.colormap), 56 - }, 57 - format!( 49 + let mut vgv_encoder = vgv::Encoder::new(vgv::Frame::Initialization { 50 + w: width as _, 51 + h: height as _, 52 + d: (1000.0 / self.fps as f64) as _, 53 + bg: initial_canvas 54 + .background 55 + .unwrap_or_default() 56 + .render(&initial_canvas.colormap), 57 + svg: format!( 58 58 r#"width={w} height={h} viewBox="-{pad} -{pad} {w} {h}""#, 59 59 w = initial_canvas.width(), 60 60 h = initial_canvas.height(), 61 61 pad = initial_canvas.canvas_outer_padding 62 62 ), 63 - ); 63 + }); 64 + 65 + vgv_encoder.full_diff_ratio = 500; 64 66 65 67 let vgv_thread = thread::spawn(move || { 66 - for (time, svg) in rx.iter() { 67 - if svg.is_empty() { 68 - break; 68 + for output in rx.iter() { 69 + match output { 70 + EngineOutput::Finished => break, 71 + EngineOutput::Frame(progression, ref svg) => { 72 + pb.step_with_engine(progression); 73 + vgv_encoder.encode_svg(match svg { 74 + svg::Node::Text(text) => text.to_string(), 75 + svg::Node::SVG(svg) => svg.to_string(), 76 + svg::Node::Element(element) => element 77 + .children 78 + .iter() 79 + .map(|child| child.to_string()) 80 + .join(""), 81 + }); 82 + } 69 83 } 70 - 71 - vgv_encoder.encode_svg(match svg { 72 - svg::Node::Text(text) => text, 73 - svg::Node::SVG(svg) => svg, 74 - svg::Node::Element(element) => element 75 - .children 76 - .iter() 77 - .map(|child| child.to_string()) 78 - .join(""), 79 - }); 80 - 81 - pb.set_position(time.as_millis() as _); 82 - pb.set_message(milliseconds_to_timestamp(time.as_millis() as _)); 83 84 } 84 85 85 86 vgv_encoder.dump(&mut file); ··· 158 159 let initial_canvas = self.initial_canvas.clone(); 159 160 let resolution = self.resolution; 160 161 161 - let (tx, rx) = 162 - std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(1_000); 162 + let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(1_000); 163 163 164 164 let pb = self.progress_bar.clone(); 165 165 166 166 let encoder_thread = thread::spawn(move || { 167 - for (time, svg) in rx.iter() { 168 - if svg.is_empty() { 169 - break; 167 + for output in rx.iter() { 168 + match output { 169 + EngineOutput::Finished => break, 170 + EngineOutput::Frame(progression, svg) => { 171 + pb.step_with_engine(progression); 172 + encode_frame( 173 + &mut encoder, 174 + resolution, 175 + &initial_canvas, 176 + svg, 177 + ) 178 + .unwrap(); 179 + } 170 180 } 171 - 172 - encode_frame( 173 - &mut encoder, 174 - resolution, 175 - time, 176 - &initial_canvas, 177 - &svg.to_string(), 178 - ) 179 - .unwrap(); 180 - 181 - pb.set_position(time.as_millis() as _); 182 - pb.set_message(milliseconds_to_timestamp(time.as_millis() as _)); 183 181 } 184 182 185 183 encoder.stdin.take().unwrap().flush().unwrap(); ··· 201 199 fn encode_frame( 202 200 encoder: &mut std::process::Child, 203 201 resolution: u32, 204 - _timestamp: Duration, 205 202 canvas: &Canvas, 206 - svg: &String, 203 + svg: svg::Node, 207 204 ) -> anyhow::Result<()> { 208 205 debug_time!("encode_frame"); 209 206 // Make sure that width and height are divisible by 2, as the encoder requires it 210 207 let (width, height) = canvas.resolution_to_size_even(resolution); 211 208 212 - let pixmap = canvas.svg_to_pixmap(width, height, svg)?; 209 + let pixmap = canvas.svg_to_pixmap(width, height, &svg.to_string())?; 213 210 // Send frame 214 211 encoder.stdin.as_mut().unwrap().write_all(&pixmap.data())?; 215 212
+45 -16
src/video/engine.rs
··· 1 - use super::{context::Context, hooks::milliseconds_to_timestamp, Video}; 1 + use super::{context::Context, hooks::format_duration, Video}; 2 2 use crate::rendering::svg; 3 - use crate::{Canvas, SVGRenderable}; 3 + use crate::{Canvas, Object, Point, SVGRenderable}; 4 4 use anyhow::Result; 5 5 use measure_time::debug_time; 6 6 use std::sync::mpsc::SyncSender; 7 + use std::sync::Arc; 7 8 use std::time::Duration; 8 9 10 + /// What data is sent to the output by the rendering engine for each rendered frame 11 + pub enum EngineOutput { 12 + Finished, 13 + Frame(EngineProgression, svg::Node), 14 + } 15 + 16 + pub struct EngineProgression { 17 + pub ms: usize, 18 + pub timestamp: String, 19 + pub scene_name: Option<String>, 20 + } 21 + 22 + impl<'a, C: Default> Context<'a, C> { 23 + pub fn engine_progression(&self) -> EngineProgression { 24 + EngineProgression { 25 + ms: self.ms, 26 + timestamp: self.timestamp.clone(), 27 + scene_name: self.current_scene.clone(), 28 + } 29 + } 30 + } 31 + 9 32 impl<AdditionalContext: Default> Video<AdditionalContext> { 10 33 pub fn render( 11 34 &self, 12 - output: SyncSender<(Duration, svg::Node)>, 35 + output: SyncSender<EngineOutput>, 13 36 controller: impl Fn(&Context<AdditionalContext>) -> EngineControl, 14 37 ) -> Result<usize> { 15 38 debug_time!("render"); ··· 17 40 let mut rendered_frames_count: usize = 0; 18 41 let mut context = Context { 19 42 frame: 0, 43 + scene_frame: None, 44 + current_scene: None, 20 45 beat: 0, 21 46 beat_fractional: 0.0, 22 47 timestamp: "00:00:00.000".to_string(), ··· 27 52 later_hooks: vec![], 28 53 audiofile: self.audiofile.clone(), 29 54 duration_override: self.duration_override, 55 + scene_started_at_ms: None, 30 56 }; 31 57 32 58 let mut canvas = self.initial_canvas.clone(); ··· 40 66 41 67 for _ in render_ms_range { 42 68 context.ms += 1_usize; 43 - context.timestamp = milliseconds_to_timestamp(context.ms).to_string(); 69 + context.timestamp = format_duration(context.ms).to_string(); 44 70 context.beat_fractional = 45 71 (context.bpm * context.ms) as f32 / (1000.0 * 60.0); 46 72 context.beat = context.beat_fractional as usize; 47 73 context.frame = self.fps * context.ms / 1000; 74 + context.scene_frame = context 75 + .scene_started_at_ms 76 + .map(|start_ms| self.fps * (context.ms - start_ms) / 1000); 48 77 49 78 let control = controller(&context); 50 79 ··· 124 153 } 125 154 126 155 if !skip_rendering && context.frame != previous_rendered_frame { 127 - output.send(( 128 - Duration::from_millis(context.ms as _), 156 + output.send(EngineOutput::Frame( 157 + context.engine_progression(), 129 158 canvas.render_to_svg( 130 159 canvas.colormap.clone(), 131 160 canvas.cell_size, ··· 149 178 } 150 179 } 151 180 152 - output 153 - .send((Duration::from_millis(context.ms as _), svg::node("svg")))?; 181 + output.send(EngineOutput::Finished)?; 154 182 155 183 println!("Rendered {rendered_frames_count} frames"); 156 184 Ok(rendered_frames_count) ··· 159 187 pub fn render_single_frame( 160 188 &self, 161 189 frame_no: usize, 162 - ) -> Result<(Duration, svg::Node)> { 190 + ) -> Result<(String, svg::Node)> { 163 191 debug_time!("render_single_frame"); 164 - let (tx, rx) = std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(2); 192 + let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(2); 165 193 166 194 self.render(tx, |ctx| { 167 195 if ctx.frame == frame_no { ··· 174 202 })?; 175 203 176 204 println!("Waiting for rendered frame..."); 177 - for (timecode, svg) in rx.iter() { 178 - if svg.is_empty() { 179 - continue; 205 + for output in rx.iter() { 206 + match output { 207 + EngineOutput::Finished => break, 208 + EngineOutput::Frame(progression, svg) => { 209 + return Ok((progression.timestamp, svg)) 210 + } 180 211 } 181 - 182 - return Ok((timecode, svg)); 183 212 } 184 213 185 214 return Err(anyhow::format_err!( ··· 189 218 190 219 pub fn render_all_frames( 191 220 &self, 192 - output: SyncSender<(Duration, svg::Node)>, 221 + output: SyncSender<EngineOutput>, 193 222 ) -> Result<usize> { 194 223 self.render(output, |_| EngineControl::Render) 195 224 }
+35 -137
src/video/hooks.rs
··· 1 1 use super::animation::LayerAnimationUpdateFunction; 2 2 use super::context::Context; 3 3 use crate::synchronization::audio::MusicalDurationUnit; 4 - use crate::synchronization::midi::MidiSynchronizer; 5 - use crate::synchronization::sync::{SyncData, Syncable}; 6 - use crate::ui::{self, setup_progress_bar, Log as _}; 7 4 use crate::{Canvas, ColoredObject}; 8 5 use anyhow::Result; 9 6 use chrono::{DateTime, NaiveDateTime}; 10 - use indicatif::ProgressBar; 11 - use measure_time::debug_time; 12 - use std::{fmt::Formatter, panic, path::PathBuf}; 7 + use std::{fmt::Formatter, panic}; 13 8 14 9 pub type BeatNumber = usize; 15 10 pub type FrameNumber = usize; ··· 33 28 /// Arguments: canvas, context, previous rendered beat 34 29 pub type LaterHookCondition<C> = 35 30 dyn Fn(&Canvas, &Context<C>, BeatNumber) -> bool + Send + Sync; 36 - 37 - pub struct Video<C> { 38 - pub fps: usize, 39 - pub initial_canvas: Canvas, 40 - pub hooks: Vec<Hook<C>>, 41 - pub commands: Vec<Box<Command<C>>>, 42 - pub frames: Vec<Canvas>, 43 - pub frames_output_directory: &'static str, 44 - pub syncdata: SyncData, 45 - pub audiofile: PathBuf, 46 - pub resolution: u32, 47 - pub duration_override: Option<usize>, 48 - pub start_rendering_at: usize, 49 - pub progress_bar: indicatif::ProgressBar, 50 - } 51 31 52 32 pub struct Hook<C> { 53 33 pub when: Box<HookCondition<C>>, ··· 70 50 } 71 51 } 72 52 73 - pub struct Command<C> { 74 - pub name: String, 75 - pub action: Box<CommandAction<C>>, 76 - } 77 - 78 - impl<C> std::fmt::Debug for Command<C> { 79 - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 80 - f.debug_struct("Command") 81 - .field("name", &self.name) 82 - .field("action", &"Box<CommandAction>") 83 - .finish() 84 - } 85 - } 86 - 87 - impl<AdditionalContext: Default> Default for Video<AdditionalContext> { 88 - fn default() -> Self { 89 - Self::new(Canvas::with_layers(vec!["root"])) 90 - } 91 - } 92 - 93 - impl<AdditionalContext: Default> Video<AdditionalContext> { 94 - pub fn new(canvas: Canvas) -> Self { 95 - Self { 96 - fps: 30, 97 - initial_canvas: canvas, 98 - hooks: vec![], 99 - commands: vec![], 100 - frames: vec![], 101 - frames_output_directory: "frames/", 102 - resolution: 1920, 103 - syncdata: SyncData::default(), 104 - audiofile: PathBuf::new(), 105 - duration_override: None, 106 - start_rendering_at: 0, 107 - progress_bar: setup_progress_bar(0, ""), 108 - } 109 - } 110 - 111 - pub fn sync_audio_with(self, sync_data_path: &str) -> Self { 112 - debug_time!("sync_audio_with"); 113 - if sync_data_path.ends_with(".mid") || sync_data_path.ends_with(".midi") { 114 - let loader = MidiSynchronizer::new(sync_data_path); 115 - let syncdata = loader.load(Some(&self.progress_bar)); 116 - self.progress_bar.finish(); 117 - self.progress_bar.log( 118 - "Loaded", 119 - &format!( 120 - "{} notes from {sync_data_path}", 121 - syncdata 122 - .stems 123 - .values() 124 - .map(|v| v.notes.len()) 125 - .sum::<usize>(), 126 - ), 127 - ); 128 - return Self { syncdata, ..self }; 129 - } 130 - 131 - panic!("Unsupported sync data format"); 132 - } 133 - 134 - pub fn with_hook(self, hook: Hook<AdditionalContext>) -> Self { 135 - let mut hooks = self.hooks; 136 - hooks.push(hook); 137 - Self { hooks, ..self } 138 - } 53 + pub trait AttachHooks<AdditionalContext>: Sized { 54 + fn with_hook(self, hook: Hook<AdditionalContext>) -> Self; 139 55 140 - pub fn init( 56 + fn init( 141 57 self, 142 58 render_function: &'static RenderFunction<AdditionalContext>, 143 59 ) -> Self { ··· 148 64 } 149 65 150 66 // TODO The &'static requirement might be possibly liftable, see https://users.rust-lang.org/t/how-to-store-functions-in-structs/58089 151 - pub fn on( 67 + fn on( 152 68 self, 153 69 marker_text: &'static str, 154 70 render_function: &'static RenderFunction<AdditionalContext>, ··· 161 77 }) 162 78 } 163 79 164 - pub fn each_beat( 80 + fn each_beat( 165 81 self, 166 82 render_function: &'static RenderFunction<AdditionalContext>, 167 83 ) -> Self { ··· 180 96 }) 181 97 } 182 98 183 - pub fn every( 99 + fn every( 184 100 self, 185 101 amount: f32, 186 102 unit: MusicalDurationUnit, ··· 203 119 }) 204 120 } 205 121 206 - pub fn each_frame( 122 + fn each_frame( 207 123 self, 208 124 render_function: &'static RenderFunction<AdditionalContext>, 209 125 ) -> Self { 210 126 self.each_n_frame(1, render_function) 211 127 } 212 128 213 - pub fn each_n_frame( 129 + fn each_n_frame( 214 130 self, 215 131 n: usize, 216 132 render_function: &'static RenderFunction<AdditionalContext>, ··· 224 140 } 225 141 226 142 /// threshold is a value between 0 and 1: current amplitude / max amplitude of stem 227 - pub fn on_stem( 143 + fn on_stem( 228 144 self, 229 145 stem_name: &'static str, 230 146 threshold: f32, ··· 246 162 } 247 163 248 164 /// Triggers when a note starts on one of the stems in the comma-separated list of stem names `stems`. 249 - pub fn on_note( 165 + fn on_note( 250 166 self, 251 167 stems: &'static str, 252 168 render_function: &'static RenderFunction<AdditionalContext>, ··· 263 179 } 264 180 265 181 /// Triggers when a note stops on one of the stems in the comma-separated list of stem names `stems`. 266 - pub fn on_note_end( 182 + fn on_note_end( 267 183 self, 268 184 stems: &'static str, 269 185 render_function: &'static RenderFunction<AdditionalContext>, ··· 280 196 } 281 197 282 198 // Adds an object using object_creation on note start and removes it on note end 283 - pub fn with_note<ObjectCreator>( 199 + fn with_note<ObjectCreator>( 284 200 self, 285 201 stems: &'static str, 286 202 cutoff_amplitude: f32, ··· 323 239 }) 324 240 } 325 241 326 - pub fn at_frame( 242 + fn at_frame( 327 243 self, 328 244 frame: usize, 329 245 render_function: &'static RenderFunction<AdditionalContext>, ··· 334 250 }) 335 251 } 336 252 337 - pub fn when_remaining( 253 + fn when_remaining( 338 254 self, 339 255 seconds: usize, 340 256 render_function: &'static RenderFunction<AdditionalContext>, ··· 347 263 }) 348 264 } 349 265 350 - pub fn at_timestamp( 266 + fn at_timestamp( 351 267 self, 352 268 timestamp: &'static str, 353 269 render_function: &'static RenderFunction<AdditionalContext>, ··· 409 325 self.with_hook(hook) 410 326 } 411 327 412 - pub fn command( 413 - self, 414 - command_name: &'static str, 415 - action: &'static CommandAction<AdditionalContext>, 416 - ) -> Self { 417 - let mut commands = self.commands; 418 - commands.push(Box::new(Command { 419 - name: command_name.to_string(), 420 - action: Box::new(action), 421 - })); 422 - Self { commands, ..self } 423 - } 424 - 425 - pub fn bind_amplitude( 328 + fn bind_amplitude( 426 329 self, 427 330 layer: &'static str, 428 331 stem: &'static str, ··· 437 340 }), 438 341 }) 439 342 } 440 - 441 - pub fn total_frames(&self) -> usize { 442 - self.fps * (self.duration_ms() + self.start_rendering_at) / 1000 443 - } 444 - 445 - pub fn duration_ms(&self) -> usize { 446 - if let Some(duration_override) = self.duration_override { 447 - return duration_override; 448 - } 449 - 450 - self.syncdata 451 - .stems 452 - .values() 453 - .map(|stem| stem.duration_ms) 454 - .max() 455 - .expect("No audio sync data provided. Use .sync_audio_with() to load a MIDI file, or provide a duration override.") 456 - } 457 - 458 - pub fn setup_progress_bar(&self) -> ProgressBar { 459 - ui::setup_progress_bar(self.total_frames() as u64, "Rendering") 460 - } 461 343 } 462 344 463 - pub fn milliseconds_to_timestamp(ms: usize) -> String { 345 + pub fn format_duration(duration: impl IntoTimestamp) -> String { 464 346 format!( 465 347 "{}", 466 - DateTime::from_timestamp_millis(ms as i64) 348 + DateTime::from_timestamp_millis(duration.as_millis() as i64) 467 349 .unwrap() 468 350 .format("%H:%M:%S%.3f") 469 351 ) 470 352 } 353 + 354 + trait IntoTimestamp { 355 + fn as_millis(&self) -> usize; 356 + } 357 + 358 + impl IntoTimestamp for usize { 359 + fn as_millis(&self) -> usize { 360 + *self 361 + } 362 + } 363 + 364 + // impl IntoTimestamp for std::time::Duration { 365 + // fn as_millis(&self) -> usize { 366 + // self.as_millis() as usize 367 + // } 368 + // }
+5 -1
src/video/mod.rs
··· 2 2 pub mod context; 3 3 pub mod engine; 4 4 pub mod hooks; 5 + pub mod scene; 6 + pub mod video; 5 7 6 8 #[cfg(feature = "video")] 7 9 pub mod encoding; ··· 10 12 pub mod server; 11 13 12 14 pub use animation::Animation; 13 - pub use hooks::Video; 15 + pub use video::Video; 16 + pub use hooks::AttachHooks; 17 + pub use scene::Scene;
+45
src/video/scene.rs
··· 1 + use crate::video::hooks::{AttachHooks, Hook}; 2 + 3 + pub struct Scene<C: Default> { 4 + pub name: String, 5 + pub hooks: Vec<Hook<C>>, 6 + } 7 + 8 + impl<C: Default> Scene<C> { 9 + pub fn new(name: &str) -> Self { 10 + Self { 11 + name: name.to_string(), 12 + hooks: Vec::new(), 13 + } 14 + } 15 + } 16 + 17 + impl<C: Default + 'static> AttachHooks<C> for Scene<C> { 18 + fn with_hook(self, hook: Hook<C>) -> Self { 19 + let mut hooks = self.hooks; 20 + let scene_name = self.name.clone(); 21 + 22 + hooks.push(Hook { 23 + when: Box::new(move |canvas, ctx, prev_beat, prev_frame| { 24 + if ctx.current_scene.as_ref() == Some(&scene_name) { 25 + (hook.when)(canvas, ctx, prev_beat, prev_frame) 26 + } else { 27 + false 28 + } 29 + }), 30 + ..hook 31 + }); 32 + 33 + Self { hooks, ..self } 34 + } 35 + 36 + fn init( 37 + self, 38 + render_function: &'static super::hooks::RenderFunction<C>, 39 + ) -> Self { 40 + self.with_hook(Hook { 41 + render_function: Box::new(render_function), 42 + when: Box::new(move |_, ctx, _, _| ctx.scene_frame == Some(0)), 43 + }) 44 + } 45 + }
+1 -1
src/video/server.rs
··· 28 28 match video.render_single_frame(number) { 29 29 Ok((timecode, svg)) => svg.to_string().replace( 30 30 "</svg>", 31 - &format!(r#"<meta name="shapemaker:timecode" content="{timecode:?}" /></svg>"#) 31 + &format!(r#"<meta name="shapemaker:timecode" content="{timecode}" /></svg>"#) 32 32 ), 33 33 Err(err) => format!("{err:?}"), 34 34 }
+183
src/video/video.rs
··· 1 + use crate::{ 2 + synchronization::{ 3 + cue_markers::CueMarkersSynchronizer, 4 + midi::MidiSynchronizer, 5 + sync::{SyncData, Syncable}, 6 + }, 7 + ui::{self, Log}, 8 + video::hooks::{AttachHooks, CommandAction, Hook}, 9 + Canvas, Scene, 10 + }; 11 + use indicatif::ProgressBar; 12 + use measure_time::debug_time; 13 + use std::{fmt::Formatter, path::PathBuf}; 14 + 15 + pub struct Command<C> { 16 + pub name: String, 17 + pub action: Box<CommandAction<C>>, 18 + } 19 + 20 + impl<C> std::fmt::Debug for Command<C> { 21 + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 22 + f.debug_struct("Command") 23 + .field("name", &self.name) 24 + .field("action", &"Box<CommandAction>") 25 + .finish() 26 + } 27 + } 28 + 29 + pub struct Video<C> { 30 + pub fps: usize, 31 + pub initial_canvas: Canvas, 32 + pub hooks: Vec<Hook<C>>, 33 + pub commands: Vec<Box<Command<C>>>, 34 + pub frames: Vec<Canvas>, 35 + pub frames_output_directory: &'static str, 36 + pub syncdata: SyncData, 37 + pub audiofile: PathBuf, 38 + pub resolution: u32, 39 + pub duration_override: Option<usize>, 40 + pub start_rendering_at: usize, 41 + pub progress_bar: indicatif::ProgressBar, 42 + } 43 + 44 + impl<C: Default> AttachHooks<C> for Video<C> { 45 + fn with_hook(self, hook: Hook<C>) -> Self { 46 + let mut hooks = self.hooks; 47 + hooks.push(hook); 48 + Self { hooks, ..self } 49 + } 50 + } 51 + 52 + impl<C: Default> Default for Video<C> { 53 + fn default() -> Self { 54 + Self::new(Canvas::with_layers(vec!["root"])) 55 + } 56 + } 57 + 58 + impl<C: Default> Video<C> { 59 + pub fn new(canvas: Canvas) -> Self { 60 + Self { 61 + fps: 30, 62 + initial_canvas: canvas, 63 + hooks: vec![], 64 + commands: vec![], 65 + frames: vec![], 66 + frames_output_directory: "frames/", 67 + resolution: 1920, 68 + syncdata: SyncData::default(), 69 + audiofile: PathBuf::new(), 70 + duration_override: None, 71 + start_rendering_at: 0, 72 + progress_bar: ui::setup_progress_bar(0, ""), 73 + } 74 + } 75 + 76 + pub fn sync_audio_with(self, sync_data_path: impl Into<PathBuf>) -> Self { 77 + debug_time!("sync_audio_with"); 78 + 79 + let file_path: PathBuf = sync_data_path.into(); 80 + let pb = Some(&self.progress_bar); 81 + 82 + let syncdata = match file_path.extension().and_then(|s| s.to_str()) { 83 + Some("mid" | "midi") => { 84 + MidiSynchronizer::new(file_path.clone()).load(pb) 85 + } 86 + Some("flac" | "wav") => { 87 + CueMarkersSynchronizer::new(file_path.clone()).load(pb) 88 + } 89 + _ => panic!("Unsupported sync data format"), 90 + }; 91 + 92 + self.progress_bar.finish(); 93 + self.progress_bar.log( 94 + "Loaded", 95 + &format!( 96 + "{} notes from {file_path:?}", 97 + syncdata 98 + .stems 99 + .values() 100 + .map(|v| v.notes.len()) 101 + .sum::<usize>(), 102 + ), 103 + ); 104 + 105 + return Self { 106 + syncdata: self.syncdata.union(syncdata), 107 + ..self 108 + }; 109 + } 110 + 111 + pub fn total_frames(&self) -> usize { 112 + self.fps * (self.duration_ms() + self.start_rendering_at) / 1000 113 + } 114 + 115 + pub fn duration_ms(&self) -> usize { 116 + if let Some(duration_override) = self.duration_override { 117 + return duration_override; 118 + } 119 + 120 + self.syncdata 121 + .stems 122 + .values() 123 + .map(|stem| stem.duration_ms) 124 + .max() 125 + .expect("No audio sync data provided. Use .sync_audio_with() to load a MIDI file, or provide a duration override.") 126 + } 127 + 128 + pub fn setup_progress_bar(&self) -> ProgressBar { 129 + ui::setup_progress_bar(self.total_frames() as u64, "Rendering") 130 + } 131 + 132 + /// Adds hooks from the given scene to the video. 133 + /// Hooks will be triggered when the current scene matches the scene's name. 134 + /// Use Context#switch_scene to change scenes during rendering. 135 + /// See also `with_marked_scene` for a more ergonomic way to add scenes. 136 + pub fn with_scene(self, mut scene: Scene<C>) -> Self { 137 + for hook in self.hooks { 138 + scene.hooks.push(hook); 139 + } 140 + Self { 141 + hooks: scene.hooks, 142 + ..self 143 + } 144 + } 145 + 146 + /// Adds the given scene and a hook that switches to it immediately. 147 + pub fn with_init_scene(self, scene: Scene<C>) -> Self { 148 + let scene_name = scene.name.clone(); 149 + self.with_scene(scene).with_hook(Hook { 150 + when: Box::new(|_, ctx, _, _| ctx.frame == 0), 151 + render_function: Box::new(move |_, ctx| { 152 + ctx.switch_scene(&scene_name); 153 + Ok(()) 154 + }), 155 + }) 156 + } 157 + 158 + /// Adds the given scene, and a hook that switches to it when a marker with the same name is reached 159 + pub fn with_marked_scene(self, scene: Scene<C>) -> Self { 160 + let scene_name = scene.name.clone(); 161 + 162 + self.with_scene(scene).with_hook(Hook { 163 + when: Box::new(move |_, ctx, _, _| ctx.marker() == scene_name), 164 + render_function: Box::new(move |_, ctx| { 165 + ctx.switch_scene(ctx.marker()); 166 + Ok(()) 167 + }), 168 + }) 169 + } 170 + 171 + pub fn command( 172 + self, 173 + command_name: &'static str, 174 + action: &'static CommandAction<C>, 175 + ) -> Self { 176 + let mut commands = self.commands; 177 + commands.push(Box::new(Command { 178 + name: command_name.to_string(), 179 + action: Box::new(action), 180 + })); 181 + Self { commands, ..self } 182 + } 183 + }