• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
0

Stuck with this 'keyframes to checkbox controls' script I'm writing

Explorer ,
Oct 21, 2017 Oct 21, 2017

Copy link to clipboard

Copied

Hi, I started a thread here about the project I'm working on: https://forums.adobe.com/thread/2398912

As Dan Ebberts suggested, I should create a script out of what I was trying to achieve. My project structure looks like this.

Main Comp (which is the active comp):

midi_layer.png

MIDI Control comp:

midi_control_comp_checkbox_controls.png

Here's the expression I was using on each individual checkbox control for activeLeftHand and activeRightHand layers if it's of any use. Also modified from something Dan Ebberts helped me out with a while back:

midiNote = comp('Main comp').layer( 'midi' ).effect( 'ch_0_pitch' )( 'Slider' );

midiNum = thisProperty.propertyGroup(1).name;

n = 0;

for (i = 1; i <= midiNote.numKeys; i++){

  if (midiNote.key(i).time > time) break;

  if (midiNote.key(i).value == midiNum) n++;

}

if (n%2) 1 else 0

I've got this far but need to complete the section for lines 26-27:

var proj = app.project; // The Application Object is at the top level followed by the Project Object.

var comp = proj.activeItem; // Set the comp variable to the active composition.

var midiLayer = comp.layer("midi"); // Set the 'midi' layer of the active composition to a variable.

var leftHandKeys = midiLayer.effect("ch_0_pitch")("Slider"); // Get channel_0_pitch values - used for left hand notes.

var rightHandKeys = midiLayer.effect("ch_1_pitch")("Slider"); // Get channel_1_pitch values - used for right hand notes.

var midiControlComp = comp.layer("MIDI Control").source; // Set the 'MIDI Control' composition to a variable.

var leftHandController = midiControlComp.layer("activeLeftHand"); // Get the checkbox control layer for the left hand.

var rightHandController = midiControlComp.layer("activeRightHand"); // Get the checkbox control layer for the right hand.

// TODO: At the end use the following to run the function.

// midiDataToControlComp( leftHandKeys, leftHandController );

// midiDataToControlComp( rightHandKeys, rightHandController );

function midiDataToControlComp(channelPitchSlider, checkBoxControlLayerName) {

    // If there are keyframes available, continue.

    if (channelPitchSlider.numKeys > 0) {

        for (k = 1; k <= channelPitchSlider.numKeys; k++) {

            // Break loop if the keyframe's time more than the current time.

            if (channelPitchSlider.key(i).time > app.project.activeItem.time) break;

            // TODO: Get keyframe(s) value and assign it to the corresponding checkbox(es) on the checkBoxControlLayerName layer.

            // IMPORTANT NOTE: There may be MIDI chord data at a given time which means there will be multiple keyframes stacked. Get all values out.

        }

    // If there are no keyframes availble, display an error alert.

    } else {

        alert("There are no keyframes in " + channelPitchSlider + " within the midi layer.");

    } // End if statement.

} // End midiDataToControlComp() function.

Not sure if line 24 is also doing its job too.

If anyone can help me out here that would be brilliant.

Thanks!

TOPICS
Scripting

Views

2.6K

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct answer

Explorer , Oct 30, 2017 Oct 30, 2017

Might have figured out but not in a clean way.

app.beginUndoGroup("MIDI Keyframes to Control Layers"); // Create undo group for ease undoing should something go wrong.

     // Swap 'Left' and 'Right' below if colours come out opposite for both hands.

     midiDataToControlComp( "1", "Left" );

     midiDataToControlComp( "0", "Right" );

app.endUndoGroup();  // End undo group.

function midiDataToControlComp( channelNumber, checkBoxControlLayerHand ) { // Usage example: midiDataToControlComp( "[0/1]", "[

...

Votes

Translate

Translate
Community Expert ,
Oct 21, 2017 Oct 21, 2017

Copy link to clipboard

Copied

There are a number of ways to approach it, but one way that should only require the pitch channel would be to set up an array to count keyframes for each key (so you can use the odd/even test to see if your checkbox should be on or off). Initialize the array to all zeros, and then as you loop through the pitch keyframes, for each keyframe, increment the appropriate array value for the key, and create a checkbox keyframe at the same time, setting the value based on whether the note count is currently odd or even for that key.

I'm not sure what your line 24 is about, but I think you'd want to process all the pitch keyframes.

Dan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

I don't really know how to start the looping process especially when it involves arrays and creating objects.

I can see how it worked within the expression but when it comes to writing this all in the script, I feel lost.

Would you be able to help me out here? I'd really appreciate your time.

So far I've tried this but don't know if I'm doing this right.

var keyframesArray, totalKeys, prop, curKeyIndex, curKeyValue, inin, outin;

keyframesArray = new Array, // Create an empty array to store the keyframe data.

totalKeys = channelPitchSlider.numKeys, // Total number of keyframes in channel pitch.

if ( totalKeys > 0 ) {  // If there are keyframes available, continue.

     for ( k = 1; k <= channelPitchSlider.numKeys; k++ ) { // Start looping through the keyframes

          curKeyIndex = k;

          curKeyTime = channelPitchSlider.keyTime(i);

          curKeyValue = channelPitchSlider.keyValue(i);

          inin = channelPitchSlider.keyInInterpolationType(curKeyIndex);

          outin = channelPitchSlider.keyOutInterpolation(curKeyIndex);

          keyframesArray[keyframesArray.length] = {

               'curKeyTime':curKeyTime,

               'curKeyIndex':curKeyIndex,

               'curKeyValue':curKeyValue

          };

     } // End looping through the keyframes

}

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Advocate ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

Not too sure whether i fully understood, but the follwing might work :

var initCheckboxValue = 0;

var checkboxControls = {};

var counts = {};

var midiNum;

for (midiNum=1; midiNum<=88; midiNum++){

    checkboxControls[midiNum] = // reference to the appropriate checkbox control

    checkboxControls[midiNum].property(1).setValueAtTime(0, initCheckboxValue);

    counts[midiNum] = 0;

    };

for (i = 1; i <= midiNoteSlider.numKeys; i++){

    midiNum = midiNoteSlider.keyValue(i);

    ++counts[midinum];

    checkboxControls[midiNum].property(1).setValueAtTime(midiNoteSlider.keyTime(i), counts[midiNum]%2);

    };

Xavier

Edit: Before the line ++counts[midiNum]; there should be a check that midiNum is indeed an integer in the range 1-88, else this will create an error

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

This is pretty rough (and not tested at all), and I'm making some assumptions based on screen caps, but it might be helpful:

function getComp(theName) {

    for (var i = 1; i <= app.project.numItems; i++) {

        if ((app.project.item(i).name == theName) && (app.project.item(i) instanceof CompItem)) {

            return app.project.item(i);

        }

    }

    return null;

}

var midiControlComp = getComp("MIDI Control");

var leftHandLayer = midiControlComp.layer("activeLeftHand");

var totalNotes = 88;

var startNote = 21;

var notes = [];

for (var i = 0; i < totalNotes; i ++) notes = 0; // initialize array

var curKeyTime, curKeyValue, curNoteIdx, curCB;

for (var i = 1; i <= channelPitchSlider.numKeys; i++){

    curKeyTime = channelPitchSlider.keyTime(i); 

    curKeyValue = channelPitchSlider.keyValue(i);

    curNoteIdx = curKeyValue - startNote;

    if (curNoteIdx >= 0 && curNoteIdx < totalNotes){

        notes[curNoteIdx] += 1;

        curCB = leftHandLayer.property("Effects").property(curKeyValue.toString()).property("Checkbox");

        curCB.setValueAtTime(curKeyTime,(notes[curNoteIdx]%2);

    }

}

Dan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

Amazing as always, Dan! Your code worked brilliantly for setting up the keyframes as I needed them.

However, it is setting the start of every checkbox to 'On' / value of 1 (at 0 time), the rest of the keyframing works perfectly.

I've added line 5 to your code which sets a starting keyframe to 0:

if (curNoteIdx >= 0 && curNoteIdx < totalNotes){

notes[curNoteIdx] += 1;

curCB = controlLayer.property("Effects").property(curKeyValue.toString()).property("Checkbox");

curCB.setValueAtTime(curKeyTime,(notes[curNoteIdx]%2));

curCB.setValueAtTime(0,(0));

}

I'm guessing it is creating multiple keyframes on top of each other with a value of 0 if the midi key is being pressed since it is inside the for loop.

Also, one more thing. How can I mass delete all existing keyframes on the control layers before running the function that adds the keyframes?

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

>How can I mass delete all existing keyframes on the control layers?

Something like this should work:

curCB = controlLayer.property("Effects").property(curKeyValue.toString()).property("Checkbox");

while (curCB.numKeys > 0){

    curCB.removeKey(curCB.numKeys);

}

Dan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

Doesn't seem to work, the next time I run the same script, the checkboxes aren't set to On anymore but I can see the keyframes set.

Do I place the while loop after line 25 of your code (i.e. the curCB variable)?

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

If you put it there, you need to make sure it only runs when i == 1. It might be cleaner though, to put it in a separate loop before you get to the loop that sets the keyframes.

Dan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

Thanks for your help Dan, much appreciated!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

How easy would it be to set a keyframe with 'Off' state, and set it back by 2 frames, if the same midi key is being played twice or more in a row?

For example in Synthesia it does something like this (if you look at 7 seconds in):

Dancing Line - The Sailor's Tale // Custom Arrangement (Synthesia Tutorial + MIDI + PDF) - YouTube

Want to be able to show that the piano key has been lifted and pressed again.

Currently, the keyframe structure doesn't show lifts if the key is being pressed in quick succession.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

You could do that, but it seems like it might be better to process the duration channel. I'm not familiar with the script you're using to capture the channel data, but I'm wondering if the duration channel has the info you need for the timing of the checkbox "off" keyframes.

Dan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 22, 2017 Oct 22, 2017

Copy link to clipboard

Copied

There is data available within the duration channel. I'll try tackling it myself but I've just sent you a PM with the project file link if it's easier for you to modify the script you've written. Hope you don't mind!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 23, 2017 Oct 23, 2017

Copy link to clipboard

Copied

Here are the keyframes for channel 1's pitch, velocity and duration if it's easier to understand.

You can see that the small fraction of frame differences are the lifts and presses.

ch_1_pitch (values are from 21-108😞

Adobe After Effects 8.0 Keyframe Data

  Units Per Second 29.97

  Source Width 100

  Source Height 100

  Source Pixel Aspect Ratio 1

  Comp Pixel Aspect Ratio 1

Effects Slider Control #4 Slider #2

  Frame

  0.015 38

  0.03 50

  8.99125 38

  9.00625 50

  35.9638 45

  35.9788 52

  53.9463 45

  53.9613 52

  53.9763 38

  53.9913 50

  62.9375 38

  62.9525 50

  89.91 48

  89.925 52

  89.94 57

  107.893 48

  107.907 52

  107.923 57

  107.938 46

  107.953 50

  107.968 53

  116.883 46

  116.898 50

  116.913 53

  143.856 48

  143.871 52

  143.886 55

  …

  etc

  …

End of Keyframe Data

ch_1_vel (values are from 0-127😞

Adobe After Effects 8.0 Keyframe Data

  Units Per Second 29.97

  Source Width 100

  Source Height 100

  Source Pixel Aspect Ratio 1

  Comp Pixel Aspect Ratio 1

Effects Slider Control #5 Slider #2

  Frame

  0 0

  0.015 89

  0.03 89

  8.99125 0

  9.00625 0

  35.9638 87

  35.9788 87

  53.9463 0

  53.9613 0

  53.9763 95

  53.9913 95

  62.9375 0

  62.9525 0

  89.91 87

  89.925 87

  89.94 87

  107.893 0

  107.907 0

  107.923 0

  107.938 92

  107.953 92

  107.968 92

  116.883 0

  116.898 0

  116.913 0

  143.856 87

  143.871 87

  143.886 87

  ...

  etc

  ...

End of Keyframe Data

ch_0_dur (values are based on how long a note is held):

Adobe After Effects 8.0 Keyframe Data

  Units Per Second 29.97

  Source Width 100

  Source Height 100

  Source Pixel Aspect Ratio 1

  Comp Pixel Aspect Ratio 1

Effects Slider Control #6 Slider #2

  Frame

  0.015 0.3

  0.03 0.3

  35.9638 0.6

  35.9788 0.6

  53.9763 0.3

  53.9913 0.3

  89.91 0.6

  89.925 0.6

  89.94 0.6

  107.938 0.3

  107.953 0.3

  107.968 0.3

  143.856 0.6

  143.871 0.6

  143.886 0.6

  …

  etc

  …

End of Keyframe Data

Going to give this a crack but feel free to assist if you guys can help out!

Thanks.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 23, 2017 Oct 23, 2017

Copy link to clipboard

Copied

Tidied up the script a fair bit and added a bunch of comments to it.

I've also passed through the ch_X_vel and ch_X_dur sliders so trying to work out setting the keyframes for the hold / release of the same key.

Here's the script for future reference.

app.beginUndoGroup("MIDI Keyframes to Control Layers");

  // Swap 'Left' and 'Right' below if colours come out opposite for both hands.

  midiDataToControlComp( 1, "Left" );

  midiDataToControlComp( 0, "Right" );

app.endUndoGroup();

function midiDataToControlComp( channelNumber, checkBoxControlLayerHand ) {

  var proj = app.project, // The Application Object is at the top level followed by the Project Object.

  comp = proj.activeItem, // Set the comp variable to the active composition.

  midiLayer = comp.layer( "midi" ), // Set the 'midi' layer of the active composition to a variable.

  midiPitch = midiLayer.effect( "ch_"+channelNumber+"_pitch" )( "Slider" ), // Get channel pitch.

  midiVelocity = midiLayer.effect( "ch_"+channelNumber+"_vel" )( "Slider" ), // Get channel velocity.

  midiDuration = midiLayer.effect( "ch_"+channelNumber+"_dur" )( "Slider" ), // Get channel duration.

  midiControlComp = comp.layer( "MIDI Control" ).source, // Set the 'MIDI Control' composition to a variable.

  handController = midiControlComp.layer( "active"+checkBoxControlLayerHand+"Hand" ), // Get the checkbox control layer for the hand.

  totalNotes = 88, // Total number of keys on the piano.

  startNote = 21, // Starting MIDI note.

  notes = [];

  for (var i = 0; i < totalNotes; i ++) notes = 0; // Initialize the notes array.

  var curKeyTime, curKeyValue, curNoteIdx, curCB; // Set up couple of variable to use for 'Main Comp' > 'midi' keyframes and 'Midi Control' > 'active[Left/Right]Hand' checkboxes.

  for (var i = 1; i <= midiPitch.numKeys; i++){ // Loop through the keyframes.

  curKeyTime = midiPitch.keyTime(i); // Get current keyframe's time.

  curKeyValue = midiPitch.keyValue(i); // Get current keyframe's value.

  curNoteIdx = curKeyValue - startNote; // Get current keyframe's note index.

  if (curNoteIdx >= 0 && curNoteIdx < totalNotes){

  notes[curNoteIdx] += 1;

  curCB = handController.property("Effects").property(curKeyValue.toString()).property("Checkbox"); // Get current key value's corresponding checkbox in 'Midi Control' > 'active[Left/Right]Hand'.

  curCB.setValueAtTime(curKeyTime,(notes[curNoteIdx]%2)); // Set a keyframe at current time to 0 or 1 (checking for odd/even here).

  curCB.setValueAtTime(0,(0)); // Set a keyframe at 0 time for the checkbox to 0 so the key starts at an 'off' state.

  }

  } // End looping through the keyframes.

} // End midiDataToControlComp().

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 29, 2017 Oct 29, 2017

Copy link to clipboard

Copied

Still struggling with utilising all the data that's been provided in the midi layer.

Currently, the custom script is doing a great job, but I'm just not getting the key releases when the same key is being pressed again (such as the same chord being played 2 times in a row).

Here's the screenshot from the project:

channel_1_project_ss.png

I've copied keyframe data for the highlighted sliders above, pasted it into a spreadsheet and aligned each data row with the corresponding frame to visualise the data better.

channel_1_kayframe_spreadsheet.png

I can see that is has an 'on' state and 'off' state for each midi note, looking at the pitch and velocity values.

For example:

- Cell B5 is midi number 38. Since the velocity is not 0 (in this case it's 89), this is the on / pressed state.

- Cell B7 is midi number 38 again. This time it is showing velocity of 0. It would mean the note is in the off / released state.

I don't understand how the duration channel is working with the value given, but the values only appear where there is an 'on' state for a midi number.

I want to somehow make use of all this data and make the keyframes a bit more accurate.

Anyone able to help here?

Thanks.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Oct 30, 2017 Oct 30, 2017

Copy link to clipboard

Copied

Might have figured out but not in a clean way.

app.beginUndoGroup("MIDI Keyframes to Control Layers"); // Create undo group for ease undoing should something go wrong.

     // Swap 'Left' and 'Right' below if colours come out opposite for both hands.

     midiDataToControlComp( "1", "Left" );

     midiDataToControlComp( "0", "Right" );

app.endUndoGroup();  // End undo group.

function midiDataToControlComp( channelNumber, checkBoxControlLayerHand ) { // Usage example: midiDataToControlComp( "[0/1]", "[Right / Left]" );

     var proj = app.project, // The Application Object is at the top level followed by the Project Object.

     comp = proj.activeItem, // Set the comp variable to the active composition.

     midiLayer = comp.layer( "midi" ), // Set the 'midi' layer of the active composition to a variable.

     midiPitch = midiLayer.effect( "ch_"+channelNumber+"_pitch" )( "Slider" ), // Get channel pitch.

     midiVelocity = midiLayer.effect( "ch_"+channelNumber+"_vel" )( "Slider" ), // Get channel velocity. Not making use of yet.

     midiDuration = midiLayer.effect( "ch_"+channelNumber+"_dur" )( "Slider" ), // Get channel duration. Not making use of yet.

     midiControlComp = comp.layer( "MIDI Control" ).source, // Set the 'MIDI Control' composition to a variable.

     handController = midiControlComp.layer( "active"+checkBoxControlLayerHand+"Hand" ), // Get the checkbox control layer for the hand.

     totalNotes = 88, // Total number of keys on the piano.

     startNote = 21, // Starting MIDI note.

     notes = []; // Create the notes array.

     for (var i = 0; i < totalNotes; i ++) notes = 0; // Initialize the notes array.

     var curKeyTime, curKeyValue, curNoteIdx, curCB, checkCB, iFirstKey; // Set up couple of variable to use for 'Main Comp' > 'midi' keyframes and 'Midi Control' > 'active[Left/Right]Hand' checkboxes.

     for (var i = 1; i <= midiPitch.numKeys; i++){ // Loop through the keyframes.

          curKeyTime = midiPitch.keyTime(i); // Get current keyframe's time.

          curKeyValue = midiPitch.keyValue(i); // Get current keyframe's value.

          curNoteIdx = curKeyValue - startNote; // Get current keyframe's note index.

          if (curNoteIdx >= 0 && curNoteIdx < totalNotes){ //

               notes[curNoteIdx] += 1; //

               iFirstKey = 0; // Set up variable for later use on checking if there are keyframes.

               checkCB = ( notes[curNoteIdx]%2 ) ? "On" : "Off"; // If current note index notes is odd, say it is 'on' - if it's even, say it is 'off'.

               curCB = handController.property("Effects").property(curKeyValue.toString()).property("Checkbox"); // Get current key value's corresponding checkbox in 'Midi Control' > 'active[Left/Right]Hand'.

               if ( checkCB == "On" ) { // If checkbox is 'On'...

                    curCB.setValueAtTime(curKeyTime,(1)); // Set a keyframe at current time to 1.

                    curCB.setValueAtTime((curKeyTime-0.05),(0)); // Set a keyframe at current time minus 0.05 to 0. Useful for when the same midi key is being pressed twice.

               } else { //... Otherwise if checkbox is 'Off'

                    curCB.setValueAtTime(curKeyTime,(0)); // Set a keyframe at current time to 0.

               } // End if statement for checkCB.

               while ( iFirstKey < 1 ) { // If there is a first keyframe and while iFirstKey is less than 1...

                    curCB.setValueAtTime(0,(0)); // ...set a keyframe at 0 time for the checkbox to 0 so the key starts at an 'off' state.

                    iFirstKey++; // Increment iFirstKey by 1.

               } // End while loop for iFirstKey.

          } // End if statement for curNoteIdx.

     } // End looping through the keyframes.

} // End midiDataToControlComp().

Seems to be doing a good job with the effect I'm looking to achieve.

Thanks for everyone's help in the replies, especially Dan Ebberts for getting me almost there!

Learnt a fair bit from this as it was my first ever script

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Explorer ,
Nov 04, 2017 Nov 04, 2017

Copy link to clipboard

Copied

LATEST

Here's where I got to with it, pleased with the outcome! Moving on to creating particles

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines