Files
archived-vdo.ninja/timecode.html
steveseguin a631bc074c v29.0
2026-01-18 03:27:00 -05:00

282 lines
9.8 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MIDI Timecode Generator</title>
<script src="./thirdparty/webmidi3.js"></script>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: 'Arial', sans-serif;
background-color: #2c3e50;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%234a6b8a' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");
color: #ecf0f1;
}
h1, h2 {
color: #2c3e50;
}
.section {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #333;
}
select, input, button {
width: 100%;
padding: 8px;
margin-bottom: 10px;
}
button {
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
border-radius: 5px;
}
.container {
background-color: rgba(44, 62, 80, 0.8);
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
max-width: 600px;
width: 100%;
}
h1 {
text-align: center;
color: #3498db;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
</style>
</head>
<body>
<h1>MIDI Timecode Generator</h1>
<div class="section">
<h2>MIDI Device Selection</h2>
<label for="midiOutDevice">MIDI Output Device:</label>
<select id="midiOutDevice"></select>
</div>
<div class="section">
<h2>Timecode Settings</h2>
<label for="frameRate">Frame Rate:</label>
<select id="frameRate">
<option value="24">24 fps</option>
<option value="25">25 fps</option>
<option value="29.97">29.97 fps (drop-frame)</option>
<option value="30">30 fps</option>
</select>
<label for="startTime">Start Time (HH:MM:SS:FF):</label>
<input type="text" id="startTime" value="00:00:00:00" pattern="[0-9]{2}:[0-9]{2}:[0-9]{2}:[0-9]{2}">
</div>
<div id="currentTime">00:00:00:00</div>
<div class="section">
<button id="startStop">Start Timecode</button>
</div>
<script>
let midiOutput;
let isRunning = false;
let currentFrame = 0;
let startFrame = 0;
let audioContext;
let oscillator;
let frameRate;
let currentOscillatorId = 0;
let lastFrameTime = 0;
WebMidi.enable({ sysex: true })
.then(() => {
const midiOutSelect = document.getElementById('midiOutDevice');
WebMidi.outputs.forEach((device, index) => {
midiOutSelect.options.add(new Option(device.name, index));
});
midiOutput = WebMidi.outputs[0];
console.log("WebMidi enabled. SysEx status:", WebMidi.sysexEnabled);
})
.catch(err => console.error("WebMidi could not be enabled:", err));
document.getElementById('midiOutDevice').addEventListener('change', (event) => {
midiOutput = WebMidi.outputs[event.target.value];
});
document.getElementById('startStop').addEventListener('click', toggleTimecode);
function toggleTimecode() {
if (isRunning) {
stopTimecode();
} else {
startTimecode();
}
}
function startTimecode() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
audioContext.resume();
}
frameRate = parseFloat(document.getElementById('frameRate').value);
const startTime = document.getElementById('startTime').value;
startFrame = timeToFrames(startTime, frameRate);
currentFrame = startFrame;
isRunning = true;
document.getElementById('startStop').textContent = 'Stop Timecode';
lastFrameTime = audioContext.currentTime;
setupTimecodeOscillator();
}
function stopTimecode() {
isRunning = false;
currentOscillatorId++;
document.getElementById('startStop').textContent = 'Start Timecode';
}
function setupTimecodeOscillator(timeOne = null, thisOscillatorId = null) {
if (!thisOscillatorId) {
thisOscillatorId = ++currentOscillatorId;
} else if (currentOscillatorId !== thisOscillatorId) {
return;
}
if (!timeOne) {
timeOne = audioContext.currentTime;
}
oscillator = audioContext.createOscillator();
const silence = audioContext.createGain();
silence.gain.value = 0;
oscillator.connect(silence);
silence.connect(audioContext.destination);
const frameInterval = 1 / frameRate;
oscillator.onended = () => {
oscillator.disconnect();
silence.disconnect();
if (currentOscillatorId === thisOscillatorId && isRunning) {
let timeTwo = audioContext.currentTime;
if (timeTwo - lastFrameTime >= frameInterval) {
console.log("Current Frame:", currentFrame);
sendMIDITimecode(currentFrame, frameRate);
currentFrame++;
updateDisplayTime(currentFrame, frameRate);
lastFrameTime = timeTwo;
}
setupTimecodeOscillator(timeTwo, thisOscillatorId);
}
};
oscillator.start(timeOne);
oscillator.stop(timeOne + 0.01); // Short duration to allow frequent checks
}
let lastQuarterFrame = -1;
function sendMIDITimecode(frame, frameRate) {
const time = framesToTime(frame, frameRate);
// Send all 8 quarter frames in sequence
for (let i = 0; i < 8; i++) {
const quarterFrame = (lastQuarterFrame + 1) % 8;
const mtcQuarterFrame = getMTCQuarterFrame(quarterFrame, time, frameRate);
if (midiOutput) {
midiOutput.send(mtcQuarterFrame);
}
lastQuarterFrame = quarterFrame;
}
// Send Full Frame message every second
if (frame % Math.round(frameRate) === 0) {
sendFullFrameMessage(time, frameRate);
}
}
function getMTCQuarterFrame(quarterFrame, time, frameRate) {
switch (quarterFrame) {
case 0: return [0xF1, 0x00 | (time.frame & 0x0F)];
case 1: return [0xF1, 0x10 | ((time.frame >> 4) & 0x01)];
case 2: return [0xF1, 0x20 | (time.second & 0x0F)];
case 3: return [0xF1, 0x30 | ((time.second >> 4) & 0x03)];
case 4: return [0xF1, 0x40 | (time.minute & 0x0F)];
case 5: return [0xF1, 0x50 | ((time.minute >> 4) & 0x03)];
case 6: return [0xF1, 0x60 | (time.hour & 0x0F)];
case 7: return [0xF1, 0x70 | ((time.hour >> 4) & 0x01) | (getFrameRateCode(frameRate) << 1)];
}
}
function sendFullFrameMessage(time, frameRate) {
if (midiOutput) {
const fullFrame = [
0xF0, // Start of SysEx
0x7F, // Universal Real Time SysEx ID
0x7F, // Device ID (7F = all devices)
0x01, // Sub-ID #1 (01 = Full Time Code)
0x01, // Sub-ID #2 (01 = Full Time Code)
time.hour & 0x1F | (getFrameRateCode(frameRate) << 5),
time.minute,
time.second,
time.frame,
0xF7 // End of SysEx
];
midiOutput.sendSysex(0x7F, fullFrame.slice(1, -1));
}
}
function framesToTime(frame, frameRate) {
let totalSeconds = Math.floor(frame / frameRate);
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const frames = Math.floor(frame % frameRate);
console.log("Time calculated:", { hour: hours, minute: minutes, second: seconds, frame: frames });
return { hour: hours, minute: minutes, second: seconds, frame: frames };
}
function timeToFrames(timeString, frameRate) {
const [hours, minutes, seconds, frames] = timeString.split(':').map(Number);
return (hours * 3600 + minutes * 60 + seconds) * frameRate + frames;
}
function updateDisplayTime(frame, frameRate) {
const time = framesToTime(frame, frameRate);
document.getElementById('currentTime').textContent =
`${time.hour.toString().padStart(2, '0')}:${time.minute.toString().padStart(2, '0')}:${time.second.toString().padStart(2, '0')}:${time.frame.toString().padStart(2, '0')}`;
}
function getFrameRateCode(frameRate) {
switch (frameRate) {
case 24: return 0;
case 25: return 1;
case 29.97: return 2;
case 30: return 3;
default: return 0;
}
}
</script>
</body>
</html>