ADXTutorials

Robot API and external tools

We conclude our introduction to the CRI Robot API with a script that will not only create new assets in Atom Craft but also communicate with another creative tool, demonstrating how you can easily implement brand new audio workflows!

Indeed, we will render procedural audio patches designed in GameSynth directly in Atom Craft as Materials, and automatically create all the objects needed to play them back.

Connecting to GameSynth

GameSynth is a procedural sound design tool. Its specialized synthesizers (for whooshes, impacts, weather…) and its modular patching environment make it possible to generate any sound effect needed for game production.

The GameSynth Tool API allows for the remote control of GameSynth via the transmission control protocol (TCP). To create a TCP connection in Python, we simply need to import the “socket” module.

  • The script starts by setting the address family and the socket type.
  • The “connect” function creates the connection to the GameSynth tool (which must be started beforehand). The IP address is currently fixed to the local machine (127.0.0.1) and the port number is 28542 by default.
  • We make sure to intercept any exception triggered by a connection failure with try / except.

# --Description: Use the GameSynth Tool API to render patch in AtomCraft project
import sys
import os
import cri.atomcraft.debug as acdebug
import cri.atomcraft.project as acproject
import socket

#Connect to the GameSynth Tool API
try:
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #Set address family and socket type
  s.connect(("localhost", 28542)) #Connect to GSAPI
  acdebug.log("*** Connected to the GameSynth Tool API ***")

except ConnectionRefusedError:
  acdebug.warning("Could not connect to GameSynth, please start GameSynth first.")
  sys.exit()

Once the connection established, we can send command messages to GameSynth.  All the GameSynth commands can be found on the API’s website.

In this case, the first thing we want to do is to retrieve the name of the patch that is currently open in GameSynth. This name will be used to create some of the necessary objects in Atom Craft (Materials, Cue, Tracks…).

  • The command “get_patchname” is sent using the “send” function.
  • It will return the name of the patch, that we catch with the “recv” function.
  • The information returned includes a delimiting character (by default the carriage return) that should be removed from the messages with the “strip” function.

#Get the name of the current GameSynth patch
s.send("get_patchname".encode('utf-8')) #Send the command to GameSynth
gs_patch_name = s.recv(28542).decode('utf-8') #Receive the result
gs_patch_name = gs_patch_name.rstrip("\r") #Remove the delimiter

We are now able to connect to the GameSynth Tool API from Atom Craft, send a command, and retrieve some information. The code above will work whenever you want to connect to other tools via TCP, simply change the address / port as necessary.

Determining the rendering folder

Before rendering our patch in Atom Craft we need to retrieve the path of the project and the work units to determine the path of the Materials Folder.

  • The “find_objects” function from the Robot API is called to get the current project.
  • Then, with the “get_value” function, the “ProjectFilePath” is retrieved.
  • Finally, the “path.dirname” function from the OS gives us the absolute path of the project.

# Find the current project and its absolute path
current_project = acproject.find_objects(None, "Project")["data"]
project_path = acproject.get_value(current_project[0], "ProjectFilePath")["data"]
project_path_root = os.path.dirname(project_path)

For the path of the WorkUnits, we need to find what objects are selected in the WorkUnit tree.

  • First, a list of objects that must be selected to access WorkUnit is built (valid objects are WorkUnits, CueSheets, Cues and Tracks).
  • Then a loop tests which object is currently selected.
  • The code varies a bit whether the selected object is a WorkUnit or not, but in both cases the “get_object_path” function is used.
  • If the selected object is not in the list, the script is aborted.

# Find the WorkUnits and its path from selection and check if the valid object is selected
object_types = ["WorkUnit", "CueSheet", "Cue", "Track"]

for object_type in object_types:
  selected_objects = acproject.get_selected_objects(object_type)["data"]

  if object_type == "WorkUnit" and selected_objects:    
    selected_workunits = acproject.get_selected_objects("WorkUnit")["data"]
    workunit_path = acproject.get_object_path(selected_workunits[0])["data"]
    break

  elif object_type in ["CueSheet", "Cue", "Track"] and selected_objects:
    selected_workunit = acproject.get_parent(selected_objects[0], "WorkUnit")["data"]
    selected_workunits = []
    selected_workunits.append(selected_workunit)
    workunit_path = acproject.get_object_path(selected_workunit)["data"]
    break

else:
  acdebug.warning(f"Select one of those objects: {object_types}.")
  s.close()
  acdebug.log("*** Disconnected from the GameSynth Tool API ***")
  sys.exit()

Once both the project and the WorkUnit paths are known, the Materials folder path can be retrieved, and the GameSynth folder in which our files will be rendered can be created.

  • The Materials folder path is built via a simple concatenation.
  • If it doesn’t already exist, the “os.makedirs” function is called to create it under the Materials folder.

#Get the absolute Material path
materials_path = project_path_root + workunit_path + "/Materials"

#Create the GameSynth folder to render the patch if it doesn't already exist
gs_path = materials_path + "/GameSynth"
if not os.path.exists(gs_path):
  os.makedirs(gs_path)
  acdebug.log("GameSynth folder successfully created!")
else:
  acdebug.log("The GameSynth folder already exists.")

The path of a rendered wave file will be:


gs_wav_path = gs_path + "/" + gs_patch_name + "_" + str(file_index) + ".wav"

Rendering the GameSynth patch

GameSynth only requires a single command with four arguments to render a patch:


render_patch path bits channels duration
render_patch gs_wav_path 16 1 0

In our final script, we will use User Variables (see our previous blog) to select the bit depth, number of channels, and duration arguments.

The duration will only be used if the GameSynth patch is infinite, which is determined by calling the “is_infinite” command of the GameSynth API.
Then,  if the patch is not infinite or a duration has been set, the rendering command can be sent to GameSynth.


#Check if the patch is infinite
s.send("is_infinite".encode('utf-8'))
is_infinite_result = s.recv(28542).decode('utf-8')
is_infinite_result = int(str(is_infinite_result).replace("\r", ""))

if ((is_infinite_result == 0) or (is_infinite_result == 1 and DURATION > 0)):
  gsapi_render_patch = f"render_patch \"{gs_wav_path}\" {BITDEPTH} {CHANNELS} {DURATION}"
  s.send(gsapi_render_patch.encode('utf-8'))
    
else:
  acdebug.warning("The patch is infinite, please set a duration higher than 0.")
  s.close()
  acdebug.log("*** Disconnected from the GameSynth Tool API ***")
  sys.exit()

Creating Objects in AtomCraft

We can now start creating the required objects in Atom Craft. First the rendered files must be registered as Materials.

  • The Material Root folder is determined by calling the “get_material_rootfolder” function.
  • Then, the “register_unregistered_materials” function can be used on the root folder to automatically register the new wave files.
  • From the root folder, “get_child_object” is used to create a list containing the names of all the Materials we just registered (it will be used later to create our Waveform Regions).
  • Finally, the “sync_local_files” function is used on the GameSynth folder to update the Materials in case they already existed.

#Build the lists of Materials and Material folders
material_folder = acproject.get_child_object(material_root_folder, "MaterialSubFolder", "GameSynth")["data"]

base_name = gs_patch_name + "_" + str(file_index) #Get the name of the rendered patch
material = acproject.get_child_object(material_folder, "Material", base_name + ".wav")["data"]
material_list.append(material)

materials_folders_list.append(material_folder)
#Sync with local files to refresh the Materials
acproject.sync_local_files(materials_folders_list)

Once the Materials registered, higher level objects can be created as needed, depending on what was initially selected in the WorkUnit tree. For instance, if the user selected a WorkUnit before running the script, a whole object hierarchy will need to be created: CueSheet Folder, CueSheet, Cue, Track and Waveform Region. But if the selected object was a Cue, only the Track and Waveform Region will have to be created.

  • First, the type of the selected object is retrieved by using the “get_type_name” function.
  • From there, at each level of the hierarchy, we check which objects aren’t selected.
  • With the “get_child_object” function we also check beforehand that the object we want to create doesn’t already exist, to avoid a conflict.
  • If the object doesn’t exist, the “create_object” function is called.

Here is the code for the two first levels of the hierarchy (WorkUnit and CueSheet):


#Create AtomCraft objects based on the selected object
selected_object_type = acproject.get_type_name(selected_objects[0]) ["data"]

#If a WorkUnit is selected
if selected_object_type not in ["CueSheet", "Cue", "Track"]:
  # ----- Creating Cue Sheet/Cue -----
  # Get the Cue Sheet Root folder
  cuesheet_rootfolder = acproject.get_cuesheet_rootfolder(selected_workunits[0])["data"]

  # Create a Cue Sheet Root folder named "GameSynth" if it does not exist yet
  cuesheet_folder = acproject.get_child_object(cuesheet_rootfolder, "CueSheetFolder", "GameSynth")["data"]
  if not cuesheet_folder:
    cuesheet_folder = acproject.create_object(cuesheet_rootfolder, "CueSheetFolder", "GameSynth")["data"]

  # Create a Cue Sheet in the Cue Sheet folder if it does not exist yet
  cuesheet = acproject.get_child_object(cuesheet_folder, "CueSheet", "GameSynth")["data"]
  if not cuesheet:
    cuesheet = acproject.create_object(cuesheet_folder, "CueSheet", "GameSynth")["data"]

#If a CueSheet is selected
if selected_object_type not in ["Cue", "Track"]:
  # ----- Creating Cue -----
  if selected_object_type == "CueSheet":
    cuesheet = selected_objects[0]

  # Create a Cue unless it already exists
  cue = acproject.get_child_object(cuesheet, "Cue", gs_patch_name)["data"]
  if not cue:
    cue = acproject.create_object(cuesheet, "Cue", gs_patch_name)["data"]

The code continues in a similar fashion for the Cue and Track levels.

Note that in the final script, we also added a “Variations” User Variable to render several sound variations at once, which is why the code rendering the sounds differs a bit. Indeed, when rendering variations, a Random No Repeat Cue is created instead of a Polyphonic Cue. And when a Track is selected, a Random No Repeat SubSequence is added instead of a WaveForm Region.

Download the script below to check everything by yourself, and expand it as you need!