Skip to content

Commit f0d94c0

Browse files
committed
Add lyrics to the bottom of video
If you provide `lyrics_file` to your midi.json, it'll load the LRC file and parse the lyrics to be used on-screen. Do keep in mind that it's currently programmed to assume each line is in chronological order, and doesn't support multiple timestamps for repeated lines. I would really like to support coloured text, but LRC files are by far the simplest format to work with, and I absolutely do not fancy parsing XML or other, more complicated formats (for now)!
1 parent 425460f commit f0d94c0

File tree

7 files changed

+99
-10
lines changed

7 files changed

+99
-10
lines changed

lyrics-example.lrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[00:01.00] This is a lyric
2+
[00:02.50]
3+
[00:03.00] And it changes!
4+
[02:05.00]

midi-example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"midi_file": "midi-file.mid",
44
"video_file_out": "./song/output.mp4",
55
"use_gradients": true,
6+
"lyrics_file": "./lyrics-example.lrc",
67
"channels": {
78
"DRUMS": {
89
"order": 0,

src/data/lyrics.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#[derive(Debug)]
2+
pub struct LyricLine {
3+
time_start: f64,
4+
time_end: f64,
5+
text: String,
6+
}
7+
8+
#[derive(Debug)]
9+
pub struct Lyrics {
10+
lines: Vec<LyricLine>,
11+
}
12+
13+
impl Lyrics {
14+
pub fn new(path: &Option<String>) -> Option<Self> {
15+
if path.is_none() {
16+
return None;
17+
}
18+
19+
let file = std::fs::read_to_string(path.as_ref().unwrap());
20+
if file.is_err() {
21+
panic!("Could not load lyrics file");
22+
}
23+
let file = file.unwrap();
24+
let lines_raw = file.lines();
25+
26+
let mut lines: Vec<LyricLine> = vec![];
27+
let re = regex::Regex::new(r"^\[(\d\d):(\d\d.\d\d)\](.*)$").unwrap();
28+
for haystack in lines_raw {
29+
if let Some(capture) = re.captures(haystack) {
30+
let (_, [minute, second, lyric]) = capture.extract();
31+
let time_mins = minute.parse::<f64>().unwrap();
32+
let time_secs = second.parse::<f64>().unwrap();
33+
let time = (time_mins * 60.0) + time_secs;
34+
if lines.len() > 0 {
35+
let len = lines.len();
36+
lines[len - 1].time_end = time;
37+
}
38+
if lyric.is_empty() {
39+
continue;
40+
}
41+
lines.push(LyricLine {
42+
time_start: time,
43+
time_end: 999999.0,
44+
text: lyric.trim().to_uppercase().to_owned(),
45+
});
46+
}
47+
}
48+
49+
Some(Lyrics { lines })
50+
}
51+
52+
pub fn find_line(&self, time: f64) -> Option<&str> {
53+
for line in &self.lines {
54+
if line.time_start <= time && line.time_end >= time {
55+
return Some(&line.text);
56+
}
57+
}
58+
59+
None
60+
}
61+
}

src/data/midi.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use super::{
22
channel::SongError,
33
defaults::{default_output, default_true},
4+
lyrics::Lyrics,
45
video::Encoding,
56
};
67
use crate::{
7-
display::{draw, RGB},
8-
SCREEN_FRAME_RATE, SCREEN_HEIGHT, SCREEN_WIDTH, SCREEN_DURATION_SECS
8+
display::{draw, font::FONT_SEPARATION, RGB},
9+
SCREEN_DURATION_SECS, SCREEN_FRAME_RATE, SCREEN_HEIGHT, SCREEN_WIDTH,
910
};
1011
use image::RgbImage;
1112
use midly::{
@@ -87,6 +88,8 @@ pub struct MidiSongConfig {
8788

8889
#[serde(default = "default_true")]
8990
pub use_gradients: bool,
91+
92+
pub lyrics_file: Option<String>,
9093
}
9194

9295
#[derive(Debug)]
@@ -100,6 +103,7 @@ pub struct MidiSong {
100103
pub config: MidiSongConfig,
101104
pub channels: HashMap<usize, MidiChannel>,
102105
pub channels_vec: Vec<MidiChannel>,
106+
pub lyrics: Option<Lyrics>,
103107
}
104108

105109
impl MidiSong {
@@ -113,6 +117,7 @@ impl MidiSong {
113117
seconds_per_frame: *SCREEN_DURATION_SECS,
114118
channels: HashMap::new(),
115119
channels_vec: Vec::with_capacity(16),
120+
lyrics: Lyrics::new(&config.lyrics_file),
116121
config,
117122
}
118123
}
@@ -308,9 +313,22 @@ impl MidiSong {
308313
}
309314

310315
pub fn draw(&mut self, frame: &mut RgbImage, encoding: &mut Encoding) -> Result<(), SongError> {
311-
let channel_height = *SCREEN_HEIGHT / self.channels_vec.len() as u32;
316+
let mut channel_height = *SCREEN_HEIGHT / self.channels_vec.len() as u32;
312317
let channel_width = *SCREEN_WIDTH;
313318

319+
if let Some(lyrics) = &self.lyrics {
320+
let y = *SCREEN_HEIGHT - 11;
321+
channel_height = y / self.channels_vec.len() as u32;
322+
draw::rect(frame, 0, y, *SCREEN_WIDTH, *SCREEN_HEIGHT, [0, 0, 0]);
323+
324+
if let Some(line) = lyrics.find_line(self.playhead_secs + (*SCREEN_DURATION_SECS / 2.0))
325+
{
326+
let x =
327+
((*SCREEN_WIDTH - (line.len() as u32 * FONT_SEPARATION)) as f64 / 2.0) as u32;
328+
draw::text(frame, x, y + 1, &line);
329+
}
330+
}
331+
314332
let x_min = 0;
315333
let x_min_f = x_min as f64;
316334
let x_max = x_min + channel_width - 1;

src/data/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod channel;
22
pub mod cli;
33
pub mod defaults;
4+
pub mod lyrics;
45
pub mod midi;
56
pub mod song;
67
pub mod video;

src/data/window.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
use std::{fs::File, io::BufReader};
2-
use serde::Deserialize;
3-
use crate::Args;
41
use super::defaults::default_five;
2+
use crate::Args;
3+
use serde::Deserialize;
4+
use std::{fs::File, io::BufReader};
55

66
#[derive(Deserialize, Clone, Debug)]
77
pub struct Window {
88
pub width: u32,
99
pub height: u32,
1010
pub scale: u32,
1111
pub frame_rate: usize,
12-
12+
1313
#[serde(default = "default_five")]
1414
pub duration_secs: f64,
1515
}
@@ -62,4 +62,4 @@ impl Window {
6262

6363
serde_json::from_reader(rdr).unwrap()
6464
}
65-
}
65+
}

src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use clap::Parser;
2-
use data::{channel::SongError, song::Song, window::Window, cli::Args, video::Encoding, midi::MidiSong};
2+
use data::{
3+
channel::SongError, cli::Args, midi::MidiSong, song::Song, video::Encoding, window::Window,
4+
};
35
use image::RgbImage;
46
use indicatif::{ProgressBar, ProgressStyle};
57
use lazy_static::lazy_static;
@@ -65,7 +67,9 @@ fn encode_midi(cmd: &Args) {
6567
let mut encoding = Encoding::new(&midi.config.video_file_out);
6668
let mut frame = RgbImage::new(*SCREEN_WIDTH, *SCREEN_HEIGHT);
6769

68-
let pb = generate_progressbar(((midi.get_song_duration() + (*SCREEN_DURATION_SECS / 2.0)) * 1000.0) as u64);
70+
let pb = generate_progressbar(
71+
((midi.get_song_duration() + (*SCREEN_DURATION_SECS / 2.0)) * 1000.0) as u64,
72+
);
6973

7074
// Step 2: Render waveforms
7175
println!("\nStarting render");

0 commit comments

Comments
 (0)