Subtitle pre-empting and crossfades. I'm happy with this output for now.

This commit is contained in:
Luke Hubmayer-Werner 2024-12-20 03:36:31 +10:30
parent ece3fab18e
commit 4b1af657f7
2 changed files with 39 additions and 11 deletions

View File

@ -248,6 +248,7 @@ function load_song(data) {
// lyrics_input_updated() // lyrics_input_updated()
lyrics_input_element.dispatchEvent(new Event('change', {})); lyrics_input_element.dispatchEvent(new Event('change', {}));
// generate_subtitles();
} }
document.getElementById('load_song').addEventListener('change', event => { document.getElementById('load_song').addEventListener('change', event => {
console.log('Attempting to load json file'); console.log('Attempting to load json file');
@ -265,24 +266,34 @@ btn_download_subtitles.addEventListener('click', () => {
import generate_subtitles_from_data from './subtitle_generator.js' import generate_subtitles_from_data from './subtitle_generator.js'
function generate_subtitles() { function generate_subtitles() {
console.log('Attempting to generate subtitles'); console.log('Attempting to generate subtitles');
// Strip out empties/labels from line numbers, only looking at lines and rests
let arr_line_numbers = arrangement_line_numbers.filter(l => {
if (l < 0) return true; // Rest
if ((typeof tokenized_lyric_lines?.[l] ?? '') == "string") return false; // Label line
return true
});
// Grab manual line timestamps // Grab manual line timestamps
const manual_line_timecodes = line_timer_element.value.split('\n').filter(s=>s.trim()).map(s=>parseFloat(s.split('\t')[0])); const manual_line_timecodes = line_timer_element.value.split('\n').filter(s=>s.trim()).map(s=>parseFloat(s.split('\t')[0]));
const placeholder_t0 = 72.0; const placeholder_t0 = 72.0;
const seconds_per_line = 10.0; const seconds_per_line = 10.0;
const placeholder_line_timecodes = Array(arrangement_line_numbers.length+1).fill().map((_,i) => Math.floor(i*seconds_per_line + placeholder_t0)); const placeholder_line_timecodes = Array(arr_line_numbers.length+1).fill().map((_,i) => Math.floor(i*seconds_per_line + placeholder_t0));
// Grab syllable beat counts // Grab syllable beat counts
const line_syllable_timings = lyrics_timing_input_element.value.split('\n').map(l => l.split(' ').filter(c=>c).map(c=>parseInt(c))); const line_syllable_timings = lyrics_timing_input_element.value.split('\n').map(l => l.split(' ').filter(c=>c).map(c=>parseInt(c)));
const line_syllable_total_beats = line_syllable_timings.map(l=>l.reduce((acc,x)=>acc+x)); const line_syllable_total_beats = line_syllable_timings.map(l=>l.reduce((acc,x)=>acc+x));
let lines = []; let lines = [];
// let t0 = manual_line_timecodes?.[0] ?? placeholder_line_timecodes[0]; // let t0 = manual_line_timecodes?.[0] ?? placeholder_line_timecodes[0];
const early_fade_beats = 2.4;
const early_fadein_beats = 3.0;
const tl_lines = lyrics_tl_input_element.value.split('\n'); const tl_lines = lyrics_tl_input_element.value.split('\n');
let out_idx = 0; let out_idx = 0;
arrangement_line_numbers.forEach(src_line_idx => { const N = arr_line_numbers.length;
const t0 = manual_line_timecodes?.[out_idx] ?? placeholder_line_timecodes[out_idx]; for (let i = 0; i < N; i++) {
const t1 = manual_line_timecodes?.[out_idx+1] ?? placeholder_line_timecodes[out_idx+1]; const src_line_idx = arr_line_numbers[i];
let t0 = manual_line_timecodes?.[out_idx] ?? placeholder_line_timecodes[out_idx];
let t1 = manual_line_timecodes?.[out_idx+1] ?? placeholder_line_timecodes[out_idx+1];
const duration = t1-t0; const duration = t1-t0;
const total_beats = (src_line_idx<0) ? (-src_line_idx) : (line_syllable_total_beats?.[src_line_idx] ?? 1); const total_beats = (src_line_idx<0) ? (-src_line_idx) : (line_syllable_total_beats?.[src_line_idx] ?? 1);
const bpm = 60.0*total_beats/duration; const bpm = 60.0*total_beats/duration;
@ -290,14 +301,28 @@ function generate_subtitles() {
console.log(src_line_idx, out_idx, t0, t1, total_beats, bpm, spb); console.log(src_line_idx, out_idx, t0, t1, total_beats, bpm, spb);
const tokenized_line = tokenized_lyric_lines[src_line_idx]; const tokenized_line = tokenized_lyric_lines[src_line_idx];
if ((typeof tokenized_line) == "string") return; // Label line if ((typeof tokenized_line) == "string") continue; // Label line
if (src_line_idx < 0) { // Rest beats - still increment the output line if (src_line_idx < 0) { // Rest beats - still increment the output line
out_idx++; out_idx++;
return; continue;
} }
// Peek next line to see if we should cut out early or extend into a rest/instrumental
const next_line_num = arr_line_numbers?.[i+1] ?? -16; // Default to nice long rest at end of song
if (next_line_num < 0) {
t1 += (-next_line_num)*spb/4; // Extend 1/4 into the rest
} else {
t1 -= spb*early_fade_beats; // Back off early to make room
}
// Peek previous line to see how early we can fade in
const prev_line_num = arr_line_numbers?.[i-1] ?? -16; // Default to nice long rest at start of song
const early_fade_in_b = (prev_line_num < 0)
? ((-prev_line_num)/8) // Extend 1/8 into the rest
: (early_fadein_beats) // Jump in early
t0 -= early_fade_in_b * spb;
let arr_syllable_cs = []; let arr_syllable_cs = [];
let acc = 0; let acc = early_fade_in_b;
line_syllable_timings?.[src_line_idx]?.forEach(b => { line_syllable_timings?.[src_line_idx]?.forEach(b => {
acc += b; acc += b;
arr_syllable_cs.push(Math.floor(100*acc*spb)); arr_syllable_cs.push(Math.floor(100*acc*spb));
@ -310,7 +335,7 @@ function generate_subtitles() {
translated_line: tl_lines?.[src_line_idx] ?? '', translated_line: tl_lines?.[src_line_idx] ?? '',
}, tokenized_line)); }, tokenized_line));
out_idx++; out_idx++;
}); }
const subtitles = generate_subtitles_from_data({ const subtitles = generate_subtitles_from_data({
song_title: document.getElementById('song_title').value, song_title: document.getElementById('song_title').value,
@ -319,7 +344,8 @@ function generate_subtitles() {
song_lyricist_en: document.getElementById('song_lyricist_en').value, song_lyricist_en: document.getElementById('song_lyricist_en').value,
song_composer: document.getElementById('song_composer').value, song_composer: document.getElementById('song_composer').value,
song_composer_en: document.getElementById('song_composer_en').value, song_composer_en: document.getElementById('song_composer_en').value,
title_fade_duration_ms: 2500, title_fade_duration_ms: 4000,
lyric_fade_duration_ms: 275,
lines: lines, lines: lines,
}); });
subtitle_editor_textarea.value = subtitles; subtitle_editor_textarea.value = subtitles;

View File

@ -87,7 +87,9 @@ Dialogue: 0,${t0s},${t1s},Title,,,,,,${title_fade}${data.song_title_en}\\NMusic:
const res_xh = res_x/2; const res_xh = res_x/2;
// Add the lines // Add the lines
let layer = 0; // Flip this every line so we can crossfade them instead of push them away
data.lines?.forEach(line => { data.lines?.forEach(line => {
layer = 1-layer;
const t0 = line.t0; const t0 = line.t0;
const t1 = line.t1; const t1 = line.t1;
const t0s = timecode(t0); const t0s = timecode(t0);
@ -98,8 +100,8 @@ Dialogue: 0,${t0s},${t1s},Title,,,,,,${title_fade}${data.song_title_en}\\NMusic:
const fallback_syl_duration_cs = duration_cs/num_syllables; const fallback_syl_duration_cs = duration_cs/num_syllables;
const centiseconds = line.arr_syllables_cs ?? Array(num_syllables+1).fill().map((_,i) => Math.floor(i*fallback_syl_duration_cs)); const centiseconds = line.arr_syllables_cs ?? Array(num_syllables+1).fill().map((_,i) => Math.floor(i*fallback_syl_duration_cs));
const sub_preamble = `Dialogue: 0,${t0s},${t1s}`; const sub_preamble = `Dialogue: ${layer},${t0s},${t1s}`;
const furi_preamble = `Dialogue: 1,${t0s},${t1s}`; // Different layer to avoid kanji collision detection const furi_preamble = `Dialogue: ${layer+2},${t0s},${t1s}`; // Different layer to avoid kanji collision detection
// Translation line is easy and static // Translation line is easy and static
s += `${sub_preamble},Translation,,,,,,${lyric_fade}${line.translated_line}\n`; s += `${sub_preamble},Translation,,,,,,${lyric_fade}${line.translated_line}\n`;