diff --git a/src/render/filter.rs b/src/render/filter.rs
index c51aa23ebcb33eae285b923aef52116962523e19..b5a7e5cd6ea93f3e0bfcfc791704f24a44574a3d 100644
--- a/src/render/filter.rs
+++ b/src/render/filter.rs
@@ -220,10 +220,10 @@ impl Filter {
 				writeln!(
 					complex,
 					"{}asplit=3{}{}{};",
-					channel('v', input),
-					vin[0],
-					vin[1],
-					vin[2]
+					channel('a', input),
+					ain[0],
+					ain[1],
+					ain[2]
 				);
 
 				// next we cut those audio/videos into before, ff, after
@@ -233,16 +233,24 @@ impl Filter {
 				writeln!(complex, "{}atrim=duration={start}{};", ain[0], acut[0]);
 				writeln!(
 					complex,
-					"{}trim=start={start}:duration={duration}{};",
+					"{}trim=start={start}:duration={duration},setpts=PTS-STARTPTS{};",
 					vin[1], vcut[1]
 				);
 				writeln!(
 					complex,
-					"{}atrim=start={start}:duration={duration}{};",
+					"{}atrim=start={start}:duration={duration},asetpts=PTS-STARTPTS{};",
 					ain[1], acut[1]
 				);
-				writeln!(complex, "{}trim=start={end}{};", vin[2], vcut[2]);
-				writeln!(complex, "{}atrim=start={end}{};", ain[2], acut[2]);
+				writeln!(
+					complex,
+					"{}trim=start={end},setpts=PTS-STARTPTS{};",
+					vin[2], vcut[2]
+				);
+				writeln!(
+					complex,
+					"{}atrim=start={end},asetpts=PTS-STARTPTS{};",
+					ain[2], acut[2]
+				);
 
 				// now we speed up the ff part
 				let vff = next_tmp(filter_idx);
diff --git a/src/render/mod.rs b/src/render/mod.rs
index 521aae72c25567e4d9d74429689343423312399d..4fb17cd3f227dfc8ae961efe03c3fd81c507dac3 100644
--- a/src/render/mod.rs
+++ b/src/render/mod.rs
@@ -28,11 +28,11 @@ const OUTRO_LEN: Time = Time {
 	seconds: 5,
 	micros: 0
 };
-const TRANSITION: &str = "fadeblack";
 const TRANSITION_LEN: Time = Time {
 	seconds: 0,
 	micros: 200_000
 };
+const FF_MULTIPLIER: usize = 3;
 
 fn cmd() -> Command {
 	let mut cmd = Command::new("busybox");
@@ -208,10 +208,7 @@ impl<'a> Renderer<'a> {
 				.into_bytes()
 		)?;
 		let intro_mp4 = self.target.join("intro.mp4");
-		svg2mp4(intro_svg, intro_mp4, Time {
-			seconds: 3,
-			micros: 0
-		})?;
+		svg2mp4(intro_svg, intro_mp4, INTRO_LEN)?;
 
 		// render outro to svg then mp4
 		let outro_svg = self.target.join("outro.svg");
@@ -220,10 +217,7 @@ impl<'a> Renderer<'a> {
 			outro(source_res).to_string_pretty().into_bytes()
 		)?;
 		let outro_mp4 = self.target.join("outro.mp4");
-		svg2mp4(outro_svg, outro_mp4, Time {
-			seconds: 5,
-			micros: 0
-		})?;
+		svg2mp4(outro_svg, outro_mp4, OUTRO_LEN)?;
 
 		// copy logo then render to png
 		let logo_svg = self.target.join("logo.svg");
@@ -284,7 +278,20 @@ impl<'a> Renderer<'a> {
 		});
 		part2 = rectrim.into();
 
-		// TODO ff
+		// fast-forward the requested parts in reverse order
+		project.source.fast.sort();
+		for (i, (ff_st, ff_end)) in project.source.fast.iter().rev().enumerate() {
+			let recff = format!("recff{i}");
+			ffmpeg.add_filter(Filter::FastForward {
+				input: part2,
+				ffinput: ff.clone().into(),
+				start: *ff_st - start,
+				duration: *ff_end - *ff_st,
+				multiplier: FF_MULTIPLIER,
+				output: recff.clone().into()
+			});
+			part2 = recff.into();
+		}
 
 		// TODO fade
 
diff --git a/src/time.rs b/src/time.rs
index e799cc89c57b6e63eed02132f6a37c8c6c52e6c9..c77443e173669a77f860163398949e1a5395e197 100644
--- a/src/time.rs
+++ b/src/time.rs
@@ -77,7 +77,7 @@ pub fn format_date_long(d: Date) -> String {
 	format!("{:02}. {month} {:04}", d.day, d.year)
 }
 
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
 pub struct Time {
 	pub seconds: u32,
 	pub micros: u32