mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
282 lines
10 KiB
HTML
282 lines
10 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> |