Skip to content

Instantly share code, notes, and snippets.

@CHFR-wide
Last active November 6, 2020 20:27
Show Gist options
  • Save CHFR-wide/ab4b4e206f6f399ba23e9227adfffaca to your computer and use it in GitHub Desktop.
Save CHFR-wide/ab4b4e206f6f399ba23e9227adfffaca to your computer and use it in GitHub Desktop.
ScreenFlip - Audio-based YTPMV/otoMAD style screenflipping for After Effects

Readme

A fork of Mmaker's screenflip.jsx. With his help, I've made it a bit more confortable to work with, if you already know how to use his version this new one shouldn't require any tutorial

/*
--ScreenFlip release v7--
YTPMV/otoMAD screen flip script by MMaker @stysmmaker, reworked on by CH FR as practice.
Use however you like. Go crazy and experiment to come up with something unique by using this.
Place script file in <After Effects Install Location>/Support Files/Scripts/ScriptUI Panels/
Access via the Window menu in After Effects
beat.wav can be found at https://mmaker.pw/junk/beat.wav
Changelog can be found at https://gist.github.com/stysmmaker
--USAGE--
Create a 'beat' audio track using beat.wav by replacing samples or just pasting it where you want it to go.
Export the audio track as it's own separate file (make sure the timing matches your original audio!)
Select video and audio beat track to use in the current comp.
Run script.
Done!
--CHANGELOG--
v7 (CHFR here, I've added and fixed a few things in the script with the help of mmaker to fit it more to my likings)
-Added auto renaming, script can now be executed more than once per comp
-Selected layers can now be changed without closing the script
-Flips video layer is now automatically muted
-Re-enabled AudioAmp null shying, disabled automatic pressing of hideShyLayers
-Added checkbox to clean up the beat.wav layers after execution
-Work Area preservation
-Disabled Reverse w/ stretching for being too unreliable (re-enable it line 60 if you want to)
v6
-Refactored code, now requires no keyframes!
-Now powered by expressions^tm.
v5
-Added 'Reverse w/ stretching' option
v4
-Modified calculation of time remap keyframes (properly sets the time now, for both short and long video!)
-Refactored time remap code
-Updated undo group name to be more informative (now shows video and audio layers used)
v3
-Implemented GUI
-Added option to select flip type (default, no flip)
v2
-Fixed short video clips not working (now pauses on last frame when distance between beat is longer than video clip)
-Published to GitHub
v1
-Initial release, supports images/videos and automatically flips based on audio
--TODO--
More features (delay, stretching, better controls)
Interface for quicker access and more options
Better error handling, better beat sample, and other general optimizations
*/
(function () {
function createUI(thisObj) {
var w = 196;
var h = 150;
var pad = 16
var win = (thisObj instanceof Panel) ? thisObj : new Window("palette", "ScreenFlip", undefined);
var radio_type_group = win.add("panel");
radio_type_group.alignChildren = "left";
radio_type_group.add("radiobutton",undefined,"Default");
radio_type_group.add("radiobutton",undefined,"No flip");
//radio_type_group.add("radiobutton",undefined,"Reverse w/ stretching");
var checkbox_type_group = win.add("panel");
var check1 = checkbox_type_group.add("checkbox",undefined,"Clean audio layers");
check1.value = true;
radio_type_group.children[0].value = true;
var run = win.add("button", undefined, "Go!");
function selected_rbutton(rbuttons)
{
for (var i = 0; i < rbuttons.children.length; i++)
if (rbuttons.children[i].value == true)
return i+1;
}
run.onClick = function () {main(selected_rbutton(radio_type_group), check1);}
return win;
}
var ytpmvpanel = createUI(this);
ytpmvpanel.center();
ytpmvpanel.show();
// Helper variables
var proj = app.project; // active project
var comp = proj.activeItem; // active comp
//var layer = comp.selectedLayers; //no, has to be declared when we click on the button
var numLayers = comp.numLayers; // count of comp layers
var flip = 100; // keep track of flip value. multiplied by -1 to flip stuffs.
var minVol = 3; // minimum keyframe/volume value to count as a beat.
var cooldownFrames = 2; // max keyframe distance between flips
var keyInterpolate = { // easier access to keyframe interpolation types
'linear':KeyframeInterpolationType.LINEAR,
'hold':KeyframeInterpolationType.HOLD};
//--------------------------------------------------------------
function generateNullName(comp){
nullName = "audioAmp ";
nullIndex = 0
for (i=1; i <= numLayers; i++){
if (comp.layer(i).name.split("audioAmp ")[0] == ''){
if (parseInt (comp.layer(i).name.split("audioAmp ")[1]) > nullIndex){
nullIndex = parseInt(comp.layer(i).name.split("audioAmp ")[1]);
}
}
}
nullIndex += 1
return nullName+=nullIndex.toString();
}
function main(fliptype, checkboxVal) { // Core of the function, stuff happens here
var layerAudioReset = []; // for storing indexes of currently audio-enabled layers not used
function setDesignatedLayers() // Automatically sets video and audio layer based on user selection (also handles different selection order)
{
var layer = comp.selectedLayers; // selected layers
if (layer.length == 2 && ( (layer[0].hasVideo && (!layer[1].hasVideo && layer[1].hasAudio)) || (layer[1].hasVideo && (!layer[0].hasVideo && layer[0].hasAudio)) ) )
{
audioLayer = (!layer[1].hasVideo && layer[1].hasAudio) ? layer[1] : layer[0];
videoLayer = (!layer[1].hasVideo && layer[1].hasAudio) ? layer[0] : layer[1];
}
else
{
alert("Invalid selection.");
return;
}
}
setDesignatedLayers();
app.beginUndoGroup("ScreenFlip - V: {" + videoLayer.name + '} A: {' + audioLayer.name + '}');
var formerWAS = comp.workAreaStart;
var formerWAD = comp.workAreaDuration;
comp.workAreaStart=0;
comp.workAreaDuration=comp.duration;
function setLayerAudioStateOff() // Keeps track of audio layers NOT used in the script, and mutes then during audiotokeyframe runtime. These layers are later unmuted.
{
for (var i = 1; i-1 < numLayers; i++) // AE stores indexes of layers starting at 1
{
if (comp.layer(i).hasAudio == true)
{
if (comp.layer(i) != audioLayer)
{
if (comp.layer(i).audioEnabled == true)
{
layerAudioReset.push(i); // store index so audio can be re-enabled after running audioToKey
}
comp.layer(i).audioEnabled = false;
}
else
{
comp.layer(i).audioEnabled = true;
}
}
}
}
function setLayerAudioStateOn() // Unmutes layers previously muted by setLayerAudioStateOff()
{
for (var i = 0; i < layerAudioReset.length; i++)
{
comp.layer(layerAudioReset[i]+1).audioEnabled = !comp.layer(layerAudioReset[i]+1).audioEnabled; // re-enable the rest of previously enabled audio tracks
}
}
setDesignatedLayers();
setLayerAudioStateOff();
app.executeCommand(app.findMenuCommandId("Convert Audio to Keyframes"));
audioLayer.audioEnabled = false; // Disable beat audio automatically after running audiotokeyframes
setLayerAudioStateOn();
audioAmp = comp.layer(1) // audio to keyframes automatically becomes highest layer in comp
audioAmp.name = generateNullName(comp);
audioAmp.shy = true;
audioAmpVal = audioAmp.property("Effects").property(3).property(1); // property 3 is "Both Channels", property 1 is "Slider" (the actual audio amp)
flipProp = audioAmp.property("Marker");
var timeOffset = videoLayer.Effects.addProperty("ADBE Slider Control");
timeOffset.name ="Time offset (seconds)";
timeOffset.property(1).expression ='value/10';
if (audioAmpVal.numKeys > 0) //markers creation
{
var k = 0; // for cooldown
videoLayer.Effects.addProperty("Transform");
videoLayer.property("Effects").property("Transform").property(3).setValue(false); // property 3 is "Uniform Scale", gotta do this because using the property name messes stuff up.
for (var i = 1; i < audioAmpVal.numKeys; i++)
{
if (k > 0)
{
k=k-1
}
if (audioAmpVal.keyValue(i) > minVol && k == 0)
{
k = cooldownFrames;
audioAmp.property("Marker").setValueAtTime(audioAmpVal.keyTime(i), new MarkerValue(""));
}
}
}
expressionList = { //list for storing expressions
1: {
"timeRemap": "a = thisComp.layer('"+audioAmp.name+"').marker; \
n = 0; \
if (a.numKeys > 0){ \
n = a.nearestKey(time).index; \
if (a.key(n).time > time) n--; \
} \
if (n > 0) \
d = time - a.key(n).time + effect(\"Time offset (seconds)\")(\"Slider\"); \
else \
d = 0;",
"flip": "a = thisComp.layer('"+audioAmp.name+"').marker; \
n = 0; \
if (a.numKeys > 0){ \
n = a.nearestKey(time).index; \
if (a.key(n).time > time) n--; \
} \
\
if (n % 2 != 0) \
{ \
100 \
} \
else \
{ \
-100 \
}"
},
2: {
"timeRemap": "a = thisComp.layer('"+audioAmp.name+"').marker; \
n = 0; \
if (a.numKeys > 0){ \
n = a.nearestKey(time).index; \
if (a.key(n).time > time) n--; \
} \
if (n > 0) \
d = time - a.key(n).time + effect(\"Time offset (seconds)\")(\"Slider\")\
else \
d = 0;",
"flip": ""
},
3: {
"timeRemap":"a = thisComp.layer('"+audioAmp.name+"').marker; \
n = 0; \
if (a.numKeys > 0){ \
n = a.nearestKey(time).index; \
if (a.key(n).time > time) n--; \
\
} \
\
try \
{ \
d = Math.abs(a.key(n).time - a.key(n+1).time) \
} \
catch(e) \
{ \
d = thisLayer.source.duration \
} \
\
if (n % 2 == 0) \
{ \
b = thisLayer.source.duration; \
c = 0; \
} \
else \
{ \
b = 0; \
c = thisLayer.source.duration; \
} \
\
linear(time-a.key(n).time, 0, d, b, c)",
"flip": ""
}
}
if (videoLayer.canSetTimeRemapEnabled == true) // The video layer can either be an image or video, so this handles for that case.
{
videoLayer.timeRemapEnabled = true;
videoLayer.inPoint = flipProp.keyTime(1); // set video to appear at first beat
videoLayer.outPoint = comp.duration; // set video to end at end of the comp
var videoLayerTimeRemap = videoLayer.property("Time Remap");
videoLayerTimeRemap.expression = expressionList[fliptype]["timeRemap"]
videoLayer.property("Effects").property("Transform").property(5).expression = expressionList[fliptype]["flip"]
}
//comp.hideShyLayers = true; // hide the audio to keyframes layer
//alert(checkboxVal.value);
if (checkboxVal.value == true){
audioLayer.remove();
}
comp.workAreaStart=formerWAS;
comp.workAreaDuration=formerWAD;
videoLayer.audioEnabled = false;
app.endUndoGroup();
}
}) ();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment