Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Gifsync

Gifsync is a free browser tool that retimes a GIF so its animation lands on the beat of a song, and saves the result as an MP4 with the audio in it. Nothing is uploaded: the decoding, the retiming and the encoding all happen in the page. https://p3zo.github.io/gifsync/

Most tools that offer to add music to a GIF lay an audio track underneath and leave the animation alone, so the loop slides out of time with the song. This one changes the animation instead. A GIF stores a duration per frame, so nothing has to be added or dropped to retime one. The frames just get held for different lengths. Mark the frames the beat should land on, mark the beats of the song, and the animation is stretched so the two line up. The output is an mp4 of the retimed animation with the audio muxed in, looped to the length of the audio — or of just the part of the song selected on the waveform.

The page itself explains how to use it, and also runs from site/index.html off disk. This file is about how it works.

How it works

  1. ImageDecoder decodes the GIF a frame at a time.
  2. Each pair of consecutive beat frames is stretched to span exactly one beat, however many frames sit between them. The last pair wraps around to the first, so the loop closes on a beat.
  3. The easing spreads that beat across those frames. The per-frame durations are the differences between successive points on the curve.
  4. Playback starts at the first beat frame rather than at frame 0, so that frame lands on the song's downbeat and every later beat frame lands on a beat too.
  5. VideoEncoder and AudioEncoder encode the frames and the audio, and the page muxes them into an mp4 itself. Frame timestamps are written to the microsecond.

The easing changes the character of the movement more than you would expect. Spreading a 500ms beat over eight frames:

Between beatsPer-frame durations (ms)
even62.5 each
ease in and out more3.9, 27.3, 74.2, 144.5, 144.5, 74.2, 27.3, 3.9

even moves at a constant speed, so only the landmarks land on the beat. The eased curves hold the frames midway between beats and flash through the ones on either side of the beat, so the motion snaps on the beat and drifts between them.

Beats in the song.TempoCNN in site/tempocnn/ gives the tempo through TensorFlow.js. Its front end (11025Hz mono, 1024-sample frames every 512, Hann, magnitude spectrum, 40 Slaney mel bands over 20-5000Hz with unit-triangle normalisation) matches essentia's TensorflowInputTempoCNN to a relative error of 1.5e-7. That tempo grid then slides across an envelope of the attacks and stops wherever it catches the most. One envelope frame is 5.8ms, and shifting a track by a known amount shifts the answer with it to within about 10ms.

Beat frames in the GIF. An even spread around the loop starting at frame 0, with each mark nudged onto whichever of its two neighbouring frames changes most. Against hand labels that comes out 0.40 and 0.50 frames off, where marks thrown down at random score about 3.

How many beats the loop lasts is moved to a nearby divisor of the frame count first. A beat covers a whole number of frames, so 20 frames over 8 beats has to alternate 3 and 2, and that 50% swing in speed is plain to see. Ten beats over those 20 frames is even the whole way round. A prime frame count has no divisor worth moving to, so it keeps the count it was given.

The picker searches Wikimedia Commons, which needs no key, answers cross-origin, and states a licence for every file. Author and licence come back as HTML written by whoever uploaded the file, so it is parsed detached from the page and only its text is used.

Development

node test/test_timing.mjs
node test/test_tempo.mjs

pull the retiming and the tempo front end out of the page and check them against test/timing_cases.json and test/tempo_cases.json. Both fixtures were recorded from the Python this tool replaced, so the page stays pinned to what that produced rather than to whatever it does today. The Pages workflow runs both before deploying.

The link preview card and video in site/media/ are rendered by the page itself, and the bundled example GIF is drawn from scratch. tools/make-media.mjs and tools/make-example-gif.mjs each say at the top of the file how to run them.

Limitations

  • Deriving tempo from beat marks assumes they are consecutive beats. Mark every bar instead and the tempo comes out too slow by that factor.
  • A frame shorter than a display refresh will not read, however exactly it is timed. The eased curves reach that first, since their shortest frames sit right at the beat. The page warns when any duration falls below it.
  • Transparency is flattened onto black, in the preview as well as the render. Chrome's VideoEncoder refuses alpha: "keep" for every codec, so keeping an alpha channel would mean a Firefox-only output in a WebM container.

About

Retime a GIF so its animation lands on the beat of a song, in the browser. Nothing uploaded.

Topics

Resources

Stars

7 stars

Watchers

1 watching

Forks

Contributors

Languages