Places the spoken lines of a walkthrough narration in time, keeps their start times and writes them as subtitles.
How the precision is kept
Cue start times are rounded to a tenth of a second when they are read and when they are stored. A line without its own time is placed after the previous line by a reading time of 2.5 words per second, never under 2 seconds. Subtitle times are written in whole milliseconds.
Calculations under this policy
3 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
How long a spoken line takes, for placing the next one.
max(2, round(words ÷ 2.5, 1 decimal))
Words are counted as integers, divided by 2.5 and rounded with Math.round(x × 10) ÷ 10. The minimum is 2 seconds.
narration.js
lines 26 to 29
BimCraft 0.8.1
export function readSeconds(text) {
const words = String(text).trim().split(/\s+/).filter(Boolean).length;
return Math.max(2, Math.round((words / 2.5) * 10) / 10);
}
Cue times from a script
64-bit
The start time of each cue from a typed script, and the squeeze when the speech is longer than the walk.
t = round(min × 60 + s, 1 decimal); squeezed t = round(t × duration ÷ end, 1 decimal)
A typed time is minutes × 60 + seconds in 64-bit and then rounded to 0.1 s. When untimed lines run past the duration, every time is multiplied by duration ÷ end and rounded to 0.1 s again, so the cues carry two roundings.
narration.js
lines 47 to 69
BimCraft 0.8.1
export function parseScript(text, duration = 0) {
const lines = String(text ?? '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
const cues = [], problems = [];
let at = 0;
for (const line of lines) {
const m = TIME_RE.exec(line);
let t, body;
if (m && /^\d/.test(line)) { t = (m[1] ? Number(m[1]) * 60 : 0) + Number(m[2]); body = clean(m[3]); }
else { t = at; body = clean(line); }
if (!body) continue;
if (cues.length >= MAX_CUES) { problems.push({ code: 'narr.toomany', severity: 'warning', text: 'Only the first ' + MAX_CUES + ' cues are kept.' }); break; }
if (cues.length && t < cues[cues.length - 1].t) { t = cues[cues.length - 1].t; problems.push({ code: 'narr.order', severity: 'note', text: 'A cue started before the one above it; it follows it.' }); }
cues.push({ t: Math.round(t * 10) / 10, text: body });
at = t + readSeconds(body);
}
// plain lines that run past the walk are squeezed into it (only the lines without their own time were placed by reading time)
if (duration > 0 && cues.length && at > duration && !lines.some((l) => TIME_RE.test(l) && /^\d/.test(l))) {
const k = duration / at;
for (const c of cues) c.t = Math.round(c.t * k * 10) / 10;
problems.push({ code: 'narr.squeezed', severity: 'note', text: 'The narration was longer than the walk (' + Math.round(at) + ' s of speech in ' + Math.round(duration) + ' s); the cues were placed closer together.' });
}
return { cues, problems };
}
Subtitle timestamp
64-bit
A cue time as an SRT or WebVTT timestamp.
ms = round(s × 1000); h, m, s, ms by integer division
Seconds are multiplied by 1000 and rounded to whole milliseconds first; hours, minutes and seconds are then taken from that integer, so no fraction is lost between fields.