Date: 2026-03-20 Version Analyzed: 1.0.0 (commit 265b882)
File: ltc_generator.py:128-247
This is the most critical issue. The _generate_ltc_word() method has an incorrect bit layout that does not match the SMPTE 12M standard. The code only defines 4 user bits groups (16 bits), but the standard requires 8 user bits groups (32 bits). This shifts all fields after bit 11 to the wrong positions, and the sync word ends up at bits 48-63 instead of the correct position at bits 64-79. Bits 64-79 are dead zeros.
Code layout vs SMPTE standard:
BITS CODE HAS SMPTE REQUIRES
0- 3 Frame units Frame units (OK)
4- 7 User bits 1 User bits 1 (OK)
8- 9 Frame tens Frame tens (OK)
10 Drop frame flag Drop frame flag (OK)
11 Color frame flag Color frame flag (OK)
12-15 Seconds units User bits 2 (WRONG - missing user group)
16-19 User bits 2 Seconds units (WRONG)
20-23 Seconds tens + BGF User bits 3 (WRONG)
24-27 Minutes units Seconds tens + BGF0 (WRONG)
... (all subsequent fields shifted by 16 bits)
48-63 Sync word Hours units + UB7 (WRONG)
64-79 DEAD ZEROS Sync word 0x3FFD (WRONG)
Impact: The generated LTC audio signal is completely invalid. No hardware or software LTC decoder will be able to read the timecode because:
- All data fields after bit 11 are at wrong positions
- The sync word (used by decoders to find frame boundaries) is at the wrong location
- 16 trailing zero-bits corrupt frame timing
Files: app.py:9, app.py:146
eel.init('web') is called at module import time (line 9) and again inside main() (line 146). This double initialization can cause unexpected behavior, errors, or resource conflicts depending on the Eel version.
# Line 9 (module level)
eel.init('web')
# Line 146 (inside main())
def main():
eel.init('web') # DUPLICATE
eel.start('index.html', size=(1000, 700))File: web/script.js:323-338
The validateInputs() method exists but is never invoked. The generateLTC() method (line 165) submits form data directly without any client-side validation. Invalid inputs (negative frames, hours > 23, etc.) are only caught by the Python backend, resulting in poor user experience with generic error messages.
Files: web/script.js:142-144, app.py:100-102
The filename preview in JavaScript treats the raw duration input value as minutes:
// script.js line 142-144
const durationMins = Math.floor(duration); // raw value = minutes
const durationSecs = Math.round((duration - durationMins) * 60);But generate_filename() in Python expects duration in seconds:
# app.py line 100-102
duration_mins = int(duration // 60) # expects seconds, divides by 60
duration_secs = int(duration % 60)The Python function is never actually called from JS (see Issue #5), so this doesn't cause a runtime bug currently, but it reveals an inconsistency that would break if the backend function were ever used.
File: app.py:92-124
The generate_filename() function is @eel.exposed but never called from the frontend. The JavaScript generates the filename locally in updateFilenamePreview() (line 130). This creates two independent filename generation paths that could diverge.
File: ltc_generator.py:280-297
The _apply_drop_frame() method only skips 2 frames (frames + 2) for both 29.97 DF and 59.94 DF. Per SMPTE standards, 59.94 DF should skip 4 frames (0, 1, 2, 3) at the start of each non-tenth minute.
# Current code (line 293) - WRONG for 59.94
if seconds == 0 and frames < 2 and minutes % 10 != 0:
return frames + 2 # Only correct for 29.97 DFExpected: For 59.94 DF, check frames < 4 and return frames + 4.
File: ltc_generator.py:239-240
The polarity correction bit (bit 63) is always left as 0. Per SMPTE 12M, this bit should be set so that the total number of 1 bits in the LTC word (excluding sync) is even. This ensures DC balance of the bi-phase encoded signal.
# Line 239-240 - bit is skipped
# Polarity correction bit (typically 0)
bit_pos += 1 # Never computedFile: ltc_generator.py:94-95
bit_duration_samples is calculated using integer division, causing sample loss per frame. The lost samples accumulate, creating timing drift.
self.frame_duration_samples = int(self.config.sample_rate / config.frame_rate.get_fps())
self.bit_duration_samples = self.frame_duration_samples // 80Worst cases (at 44100 Hz):
| Frame Rate | Lost Samples/Frame | Drift/Second |
|---|---|---|
| 23.976 fps | 79 | ~1894 samples |
| 24 fps | 77 | ~1848 samples |
| 30 fps | 30 | ~900 samples |
| 59.94 fps | 15 | ~899 samples |
At 44100 Hz with 23.976 fps, this is ~4.3% timing error per second. At 48000 Hz the drift is much smaller (typically 1 sample/frame), but still accumulates over long durations.
File: web/script.js:262-281
The downloadFile() method uses file:// protocol to trigger downloads:
link.href = `file://${filePath}`;Most browsers block file:// links from web pages for security reasons. In the Eel desktop context the file is saved directly to disk, so the "download" link is actually non-functional — the file is already on disk, but the user gets no feedback about where. The download link silently fails.
File: web/script.js:198-209
When preroll is enabled, the start time is moved back by 10 seconds, but the frame number (startFrames) is not adjusted. If the user sets a start time with non-zero frames, the preroll start time will have the same frame value, which may not be correct.
if (preroll) {
actualDuration += 10;
let totalSeconds = hours * 3600 + minutes * 60 + seconds - 10;
// ...
startFrames = frames; // BUG: frames should also be adjusted
}File: ltc_generator.py:342-345
The 24-bit export iterates over every sample in pure Python to convert 32-bit to 24-bit:
audio_24bit = bytearray()
for sample in audio_int:
audio_24bit.extend(struct.pack('<i', sample)[:3])Benchmarks show 24-bit export is ~5x slower than 16-bit for the same duration. For long durations (e.g., 2 hours at 192 kHz), this could take a very long time with no progress feedback.
File: ltc_generator.py:320-346
If export_wav() fails mid-write (disk full, permission error, etc.), a corrupted partial WAV file is left on disk. There is no try/finally block to clean up the file on failure.
File: web/script.js:283-304
The progress bar animation is purely cosmetic — it advances on fixed 800ms intervals regardless of actual generation progress. If generation takes longer than expected, the progress bar sits at 95% indefinitely. If generation is very fast, the bar doesn't reflect completion timing.
File: web/script.js:120
The max frames calculation extracts FPS using a regex on the display name:
const fps = parseFloat(frameRate.display.match(/[\d.]+/)[0]);This works for current display names like "29.97 fps DF" but would break if the display format changes (e.g., "DF 29.97 fps" or "29.97fps").
File: app.py:126-140
Both functions just return the string "USE_JS_DIALOG" but no JavaScript code handles this return value. The file browse functionality does not work.
@eel.expose
def browse_output_path():
return "USE_JS_DIALOG" # Never handled by frontendFile: web/index.html:8-9
Font Awesome and Google Fonts are loaded from CDNs. If the app is used offline (common for professional broadcast environments), the icons and fonts will not load, degrading the UI appearance.
<link href="https://fonts.googleapis.com/css2?family=Inter..." rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">File: web/script.js:192-199
When preroll is enabled, 10 seconds are added to the duration. If the user enters 120 minutes (the max), the actual duration becomes 120 minutes + 10 seconds = 7210 seconds. The backend rejects durations over 7200 seconds, causing an unexpected error.
File: web/script.js:255
return defaultPath.replace(/[^\/\\]*\.wav$/, filename);This regex replaces the filename portion of the path. However, if the default path doesn't end with .wav (e.g., the Desktop path doesn't exist), the replace will fail silently and the path will be malformed.
File: ltc_generator.py:299-318
The _increment_timecode() method does not skip dropped frames when advancing the timecode counter. Drop frame compensation is only applied when encoding the LTC word in _generate_ltc_word(). This means the internal timecode counter passes through invalid frame numbers (e.g., 00:01:00:00 and 00:01:00:01 for 29.97 DF), which are then corrected only at encoding time. This could cause issues with frame counting accuracy over long durations.
The project has no test suite. Given the complexity of LTC encoding, drop frame logic, and bi-phase mark encoding, the lack of tests makes it difficult to verify correctness or catch regressions.
| Severity | Count | Key Areas |
|---|---|---|
| Critical | 8 | Wrong LTC bit layout (SMPTE non-compliant), double init, no validation, drop frame bugs, sample drift |
| Moderate | 5 | Download broken, slow export, fake progress |
| Minor | 7 | Stubs, offline support, edge cases |
| Total | 20 |
Yes, but Issue #1 (wrong LTC bit layout) is the root cause of the app producing unusable output. Even if the app runs and generates a WAV file, no LTC decoder can read it because the bit structure doesn't match the SMPTE 12M standard. Fixing this single issue would make the output valid LTC. Fixing all 20 issues would make the app fully production-ready.