This forum is for Tips and Tricks. Please do not post any questions in this forum. It is only for information.
10 posts
Page 1 of 1
By request, this will be an overview of how to create a basic script package for Studio One to do some things that can't be accomplished with macros.

If you intend to edit or create these types of packages use a code editor so that it will be easier to read with code highlighting and similar. One good choice is Visual Studio Code which is cross platform and lightweight, but any decent editor will do, Notepad++ or whatever else you prefer.

** This is an undocumented and unofficial "feature" so please treat it as such and don't expect any official support. **

Learning JavaScript

JavaScript (JS) is a relatively easy language to learn on a basic level and there are many learning resources on the Internet. One good resource is https://www.w3schools.com/js/ which has lots of examples of common things like formatting strings, building lists and arrays, etc. Don't be intimidated by it if you've never done it before.

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Sat Jul 29, 2017 7:03 am
Tutorial Script: The Basics

This tutorial script parses all selected clip names and any clips that have 'vocal' in their name get renamed to "Tutorial'. It's a basic demonstration of selective targeting, the kind of logic not possible with macros. It puts a new action in the Event menu named "Do Something" and it will also appear in the action list as Event | Do Something. The code is fairly well commented for a more complete understanding so it looks longer than it would be otherwise.

Requirements: At a minimum you'll need the three file types described below for a working script package. The package being demonstrated in this post below can be downloaded here.

metainfo.xml

This file identifies the package. The example below is identified as tutorial.package but the ID can be anything really. The name should be unique to not potentially conflict with other package names. Example: "Package:ID" value="joe.smith.audio.editing"

Code: Select all
<?xml version="1.0" encoding="UTF-8"?>
<MetaInformation>
   <Attribute id="Package:ID" value="tutorial.package"/>
</MetaInformation>

classfactory.xml

Every action or new function you want to add to Studio One requires it's own class definition in this file.

Code: Select all
<?xml version="1.0" encoding="UTF-8"?>
<ClassFactory>
   
    <!-- A class definition is required for every new action  -->
    <ScriptClass
        classID="{D2CFB1F9-D827-42C4-8832-F6D7B75E930A}"
        category="EditTask"
        subCategory="EventEdit"
        name="Do Something"
        sourceFile="dosomething.js"
        functionName="startSomething"
        metaClassID="{C42842ED-C01B-427F-A53F-B402CAD744DB}">
        <Attribute id="menuPriority" value="0"/>
    </ScriptClass>

    <!-- Explanation of <ScriptClass
    classID & metaClassID: Both require unique ID's. You can
    create those from websites like https://guidgenerator.com/

    category:  EditTask

    subCategory:  This defines how the action will be categorized.
    In this case EventEdit puts it in the Event action category
    TrackEdit would put it in the Track action category, etc.

    name:  The name of the action in Studio One

    sourceFile:  The name of the JavaScript code file

    functionName:  The name of the entry function in the JS code file

    menuPriority: Position of where the item shows in the menu or not.
    -1 would mean not, it would only show in the action list.   -->
 
</ClassFactory>

dosomething.js

The JavaScript code file follows a basic standard format or outline as shown below. This code file is named dosomething.js as that's what the definition in classfactory.xml is looking for above with the line sourceFile="dosomething.js". Remember that JS is case sensitive so if the source file was named doSomething.js it would fail to load.

Code: Select all
function doSomething() {

    this.interfaces = [Host.Interfaces.IEditTask];   
   
    // This function is used to prepare things not
    // necessary for this basic tutorial script
    this.prepareEdit = function (context) {
         return Host.Results.kResultOk;
    }

    // this function performs the edit
    this.performEdit = function (context) {

        // access the functions from the host
        // for the current context and also
        // tell them to execute immediately
        var functions = context.functions;
        functions.executeImmediately = true;

        // create an iterator for object collections
        // like clips, midi notes, etc
        var iterator = context.iterator;

        // loop through all the selected clips
        while (!iterator.done()) {
            var event = iterator.next()
           
            // parse the current clip name as lower case and
            // if it has 'vocal' in the name, rename it.
            if (event.name.toLowerCase().indexOf("vocal") > -1 ) {
                functions.renameEvent(event,"Tutorial");
            }
        }

        // when it's all done, de-select all clips
        // you can send any action to the UI this way
        Host.GUI.Commands.interpretCommand("Edit","Deselect All");
    }   
}

// ----------------------------------------------------
// The action calls this function initially, which
// in turn creates a new instance of the main function
function startSomething() {

    // In this case we could optionally use an action to
    // select all clips (or notes) before calling the main function

    // Host.GUI.Commands.interpretCommand("Edit", "Select All");

    // run the main function
    return new doSomething;
}

Making The Package
To compile a script package for use in Studio One, zip up the files and change the zip file extender to *.package and put it in Studio One's application \scripts folder. Once you get a new action listing in Studio One successfully you can edit the JS code, zip and rename the package again, replace it in the scripts folder again, and test your code changes, without having to restart Studio One.

If you edit the class definitions, like for example changing the action name, you'll have to restart Studio One to see the changes.

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Sat Jul 29, 2017 7:44 am
A few things to look at, some event or clip properties or methods.

isMuted [bool]
    if (event.isMuted) { ... }

transpose [number]
    if (event.transpose != 0 ) { ... }

tune [number] audio clips only, testing for null excludes others, avoids potential errors
    if (event.tune != null) ( ... )

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Sat Jul 29, 2017 1:14 pm
Basic Selection Filtering

The example code below shows one way to make selections by random criteria. In this example, only selecting clips named "Bass". This code sample assumes "Select All" manually to have all timeline clips selected or firing it in the entry function, that all clips would be initially selected or it only applies to the clips that are currently selected, not all clips on the timeline.

The method being used is to go through all of the items and the ones that match the criteria get added to an array. Then it's deselects everything and then selects all of the items in the array, clips named "Bass", or whatever the filter criteria you choose to use that loads the array.

This allows you to do things like select all clips that are transposed, or muted, or by any other criteria you can parse from object properties. Or with midi notes, select all notes with velocity lower or higher than x, or length shorter or longer than y, etc, etc.

Code: Select all
    var functions = context.functions;
    var selectFunctions = context.editor.createSelectFunctions(functions);
    var iterator = context.iterator;

    // create an array to hold the items
    // that meet the criteria we want
    var filter = new Array();
   
    // loop through the items
    while(!iterator.done())
    {
        var event = iterator.next();

        // if the current event meets the criteria
        // then add it to the filter array
        if (event.name == "Bass") {
            filter.push(event);
        }
    }
   
    // deselect everything
    context.editor.selection.unselectAll();

    // select all of the items in the filter array which
    // results in only clips named Bass being selected
    selectFunctions.selectMultiple(filter);

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Sun Jul 30, 2017 10:34 am
Creating A Basic GUI

There will be times when you may want to use a GUI for your script and in Studio One this is handled via an XML skin. The following will explain how to create a basic GUI for a script.

The first thing you'll need to do is tell the package where the skin.xml file is located. This is done in the metainfo.xml file. The edit of that file is shown below, adding the Package:SkinFile attribute.

metainfo.xml

Code: Select all
<?xml version="1.0" encoding="UTF-8"?>
<MetaInformation>

    <!-- package ID, should be something unique -->
    <Attribute id="Package:ID" value="tutorial.package"/>

    <!-- to identify where the skin file is,
         in a /skin folder in the package     -->
    <Attribute id="Package:SkinFile" value="skin/"/>
   
</MetaInformation>

Create A Skin Folder & File

Create a folder named \skin in the same folder where your script files are and also create a file named skin.xml inside of the \skin folder. The skin.xml file itself is pretty easy to understand...

    1. It inherits controls like buttons, labels, etc, from the application. <Style>
    2. It allows laying out the UI controls much like designing a web page.
    3. The method to create and call an OS window for the form is already built in.

skin.xml

The skin.xml file below is a simple form with one button and one text box. The name of our form below is "testForm". A skin file can contain as many different forms as you need for different functions.

Code: Select all
<?xml version="1.0" encoding="UTF-8"?>
<Skin>
    <Styles>

        <!-- Import shared styles provided by the application -->
        <Style name="MyButton" inherit="Standard.AddIn.Button"/>
        <Style name="MyEditBox" inherit="Standard.AddIn.EditBox"/>

    </Styles>

    <Forms>

        <!-- =========  Test form for tutorial ============ -->
        <Form name="testForm" title="Test Form" width="150" height="175" windowstyle="dialogstyle restorepos" >

            <!--    create a view to put the controls in for group positioning
                    size="left,top,width,height" -->
            <View size="10,10,150,175">

            <!--  add a button and a text box -->
            <Vertical spacing="2" margin="0">   
                <EditBox name='target' width='100' height='30' style="MyEditBox"/>
                <Button name='button1' title='Go!' width='100' height='30' style='MyButton'/>
            </Vertical>

            </View>

        </Form>
        <!--   ===========    end of Test Form ============ -->

    </Forms>
</Skin>
You can also create custom styles by inheriting the default controls or current styles and changing some things, like the text color or size of text on buttons, etc as shown in the sample style below...

Code: Select all
        <!-- custom button style with yellow text, bold font, etc -->
        <Style name="YellowButton" inherit="MyButton" >
            <Font name="textfont" themeid="PresonusUI" size="11" style="bold"/>
            <Color name="textcolor" color="#FFFF00"/>
        </Style>

One not so great thing about this is that forms called this way are application modal, which means that they're modal dialogs, that you can't keep them open and at the same time manually operate the main application UI. Not a major deal if you only want to present the user with some choices for performing an edit.

Opening The UI From The JS Code File

To open a GUI from a script you call it directly like shown in the code sample below. The package being targeted for the skin,"tutorial.package", should typically be using a constant but it's literal there to make the code line more understandable for learning.

So the one code line will open the UI / form in it's performEdit() function, Host.GUI.runDialog(). Notice that the other code from the first example is no longer there, it will be somewhere else now.

Code: Select all
// this function typically performs the edit.  
// but in this case only opens the GUI
    this.performEdit = function (context) {
       
        // define the context more globally
        this.context = context;
       
        // open a GUI dialog, get the theme from tutorial.package using the testForm form
        Host.GUI.runDialog(Host.GUI.Themes.getTheme("tutorial.package"), "testForm", this);

        return Host.Results.kResultOk;
       
    }   

So now when you fire the action it opens a user interface with a textbox and a button on it. They don't do anything at all yet though. We'll get to that in the next post, editing the JS code file to respond to the button being clicked and to use whatever text you type into the text box.

Image

You can alao use graphic backgrounds and/or your own graphic buttons in a skin, like what you see on some External Device UI's which are also xml skins created the same way.

When of if you do use a GUI, the common convention is to use "..." after the action name to indicate that it calls a dialog, so we should rename our action to "Do Something...".

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Sun Jul 30, 2017 12:36 pm
Linking GUI Controls To JS Code

In order for UI controls to do anything they have to be defined in the JS code file, so we first need to add a parameter observer to the interface list as shown below... IParamObserver.

Code: Select all
    this.interfaces = [
        Host.Interfaces.IParamObserver,
        Host.Interfaces.IEditTask,
    ];   
Defining Parameters

Next, we need to define the UI objects in Javascript so that we can react to things like buttons being clicked or read and write values to text boxes in the code. This is done by adding a parameter list and adding each UI control to it. We do this in the this.prepareEdit function.

Code: Select all
    // This function is used to prepare things 
    this.prepareEdit = function (context) {

        // create a parameter list to link the UI controls to the JS code
        this.paramList = Host.Classes.createInstance("CCL:ParamList");

        // our UI button is named "button1", so we create a parameter
        // for it so we can act on it being clicked
        this.button1 = this.paramList.addInteger(0,1,"button1");

        // our UI text box is named "target", so we create a parameter
        // in code for it so we can read and write to it.
        this.editTarget = this.paramList.addString("target");

        return Host.Results.kResultOk;
    }
Reacting To UI Controls

Now that the JS code knows what those things on the UI are, we can use them. To do that, we create a new function in our main function named this.paramChanged. What that does is run code based on what UI parameter has changed, like clicking the UI button in our sample case. So in this case the code that renames the clips only runs when the button is clicked.

The code that did the parsing and renaming was moved from this.performEdit to this.paramChanged. Also, instead of using the static text "Bass" like we did earlier for matching, it uses whatever you type into the text box. That value is represented as this.editTarget.string, the current value of the text box. Using the same method, instead of replacing everything that matches the filter with "Tutorial" which is hard coded in this example, you could add another edit box to put in any random text to use as the replacement text.

Code: Select all
    this.paramChanged = function (param) {

        // now our rename code only fires if
        // button1 is clicked

        if (param.name == "button1") {

            var iterator = this.context.iterator;
            var functions = this.context.functions;
            functions.executeImmediately = true;

            while (!iterator.done()) {
                var event = iterator.next();

                if (event.name.toLowerCase().indexOf(this.editTarget.string) > -1 ) {
                    functions.renameEvent(event,"Tutorial");
                }
            }
        }
    }   

dosomething.js

Here's the edited JS code file after all of the changes shown above.

Code: Select all
function doSomething() {

    this.interfaces = [
        Host.Interfaces.IParamObserver,
        Host.Interfaces.IEditTask,
    ];   

    // =================================================================================
    // this functions reacts to buttons clicks, other UI controls, etc
    this.paramChanged = function (param) {

        // now our rename code only fires if
        // button1 is clicked

        if (param.name == "button1") {

            var iterator = this.context.iterator;
            var functions = this.context.functions;
            functions.executeImmediately = true;

            while (!iterator.done()) {
                var event = iterator.next();

                // test the matching as lower case to make it case insensitive
                // assumes all text box entries are lower case
                if (event.name.toLowerCase().indexOf(this.editTarget.string) > -1 ) {
                    functions.renameEvent(event,"Tutorial");
                }
            }
        }
    } 

    // =================================================================================
    // This function is used to prepare things like UI controls
    this.prepareEdit = function (context) {

        // create a parameter list to link the UI controls to the JS code
        this.paramList = Host.Classes.createInstance("CCL:ParamList");

        // our UI button is named "button1", so we create a parameter
        // for it so we can act on it being clicked
        this.button1 = this.paramList.addInteger(0,1,"button1");

        // our UI text box is named "target", so we create a parameter
        // in code for it so we can read and write to it.
        this.editTarget = this.paramList.addString("target");

        return Host.Results.kResultOk;
    }

    // =================================================================================
    //this function sets the context and launches the GUI 
    this.performEdit = function (context) {
       
        // define the context more globally so that it can be
        // used in this.paramChanged where our code is
        this.context = context;
       
        // open the GUI form
        Host.GUI.runDialog(Host.GUI.Themes.getTheme("tutorial.package"), "testForm", this);

        return Host.Results.kResultOk; 
    }       
}

// =================================================================================
// Script entry.  The action initially calls this function which
// in turn creates a new instance of the main function
function startSomething() {

    // In this case we could optionally also fire an action to
    // select all clips or notes before instancing the main function

    // Host.GUI.Commands.interpretCommand("Edit", "Select All");

    // instance the main function
    return new doSomething;
}

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Sun Jul 30, 2017 1:06 pm
Example: Editing Midi Notes

Some things that may seem complex on the surface are easier than they appear to be. For example, some users asked for a function to reverse a series of selected midi notes. One way to do that (in code there's often more than one way to do something) would be ...

1. Reading the series of notes into an array and their pitches into a second array.
2. Reversing the second array with the JS .reverse() function.
3. Reading back from that reversed array and sequentially assigning the reversed values back to the original note series.

The result is the note pitches being flipped 180 degrees. You could do the same with the note lengths and velocities at the same time. Once you can access and modify the properties of things everything else is just basic logic. Below it's being triggered with a bound key.

Image

Image

Code: Select all
    this.performEdit = function (context) {
       
        iterator = context.iterator;
        var functions = context.functions
        functions.executeImmediately = true;

        // create an array for the selected notes
        var notes = new Array();
        var reversedNotes = new Array();

        // read the selected notes into two arrays
        while (!iterator.done()) {
            var note = iterator.next()
            notes.push(note) // note
            reversedNotes.push(note.pitch) // pitch
        }

        // reverse the second pitch array
        reversedNotes.reverse();

        // assign the reversed pitches back to the notes
        for (i = 0; i < notes.length; i++){
        functions.modifyPitch(notes[i], reversedNotes[i]);
        }

        return Host.Results.kResultOk; 
    }   

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Tue Aug 08, 2017 6:50 am
Example: Conforming Colors

The example JS code below allows defining RGB colors and conforming track colors by the track names, if a track name contains certain text. It can be expanded to load colors from a file. The basic sample below only targets two names, Bass and Guitar. The idea is to fill in the code with the custom colors you want to use for specific track types and bind it to a key, and whenever you hit that key everything in a song always conforms to your default custom color set.

Image

Code: Select all
function colorTracks() {

   // define custom RGB colors
    var colors = { bass: "255,10,30", guitar: "10,200,38" };

    this.interfaces = [
        Host.Interfaces.IEditTask
    ];   

    // ---------------------------------------------------------------
    this.prepareEdit = function (context) {
        return Host.Results.kResultOk;
    }

    // ---------------------------------------------------------------
    this.performEdit = function (context) {

        var iterator = context.iterator;
        var functions = context.functions;

        var tracks = new Array();

        if (!iterator || !functions)
            return Host.Results.kResultFailed;

        //  push all of the track names to an array
        //  this only works for tracks with media clips on them
        while (!iterator.done()) {
            var event = iterator.next()
            var track = event.getTrack()
            if (tracks.indexOf(track) == -1) { tracks.push(track) }
        }

       // deselect all clips
       Host.GUI.Commands.interpretCommand("Edit", "Deselect All");

        //  Iterate the tracks and set each one to a target color
        for (i = 0; i < tracks.length; i++) {
       
            // compare lower case for matching and coloring
            // duplicate and edit to add other targets
            if ( tracks[i].name.toLowerCase().indexOf ("bass") > -1) {
                var c = colors.bass.split(",");       
                    var value = getColorCode( Number(c[0]), Number(c[1]), Number(c[2]) );
                        functions.colorizeEvent( tracks[i],value ); continue;
            }

            if ( tracks[i].name.toLowerCase().indexOf ("guitar") > -1) {
                var c = colors.guitar.split(",");       
                    var value = getColorCode( Number(c[0]), Number(c[1]), Number(c[2]) );
                        functions.colorizeEvent( tracks[i],value ); continue;
            }
        }

        return Host.Results.kResultOk; 
    }   
}

// ---------------------------------------------------------------
// convert RGB value to integer
function getColorCode  (red, green, blue) {
    var blue = blue * 256 * 256;
    var green = green * 256;
    return red + green + blue;
}

// ---------------------------------------------------------------
// entry function
function doColors() {
    // select all clips in arrange
    Host.GUI.Commands.interpretCommand("Edit", "Select All")

    // instance the main function
    return new colorTracks;
}

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Tue Aug 08, 2017 6:50 am
Basic Outline of Studio One Objects & Functions

(work in progress)

https://docs.google.com/spreadsheets/d/ ... q_/pubhtml

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3
User avatar
by Lawrence on Tue Aug 08, 2017 6:50 am
The above should be enough general information to get anyone started with basic scripting in Studio One. Of course, as the goals become more complex, so can the code, and (see Narech's great Studio One X tool) things can potentially get quite a bit more complex code-wise depending on what you want to accomplish.

Having said that, if you just want to make an action to do something you want to do that can't be done with a macro, or perhaps do something you want that nobody else even wants, writing your own script is the best path.

As scripting in general goes (even without any official documentation) doing it in Studio One is easier than doing it in most other audio workstations that allow it. The combination of using JavaScipt and XML as the foundation raises the "accessibility level" quite a bit from say... Python, or Lua ... especially as relates to GUI's.

Have fun, and if you create anything useful please share.

[MOD EDIT - This topic is locked for obvious reasons but wanted to say "Thanks" to Lawrence, GOOD STUFF here.]

Windows 7 64 SP1 - Intel Core i7 860 2.8GHz 8GB - ATI Radeon HD 5770 1 GB (8.850.0.0) - iPad 2 IOS 8.3

10 posts
Page 1 of 1

Who is online

Users browsing this forum: No registered users and 6 guests