Blog homepage

On working with WAAPI and Python in a team, with examples

Wwise Tips & Tools

In this article, I’d like to describe a somewhat opinionated approach to working with WAAPI that I’ve been using for quite some time already. Consisting of Python, command add-ons and a small helper library, the approach helps me write new WAAPI scripts a bit faster than with vanilla waapi-client, and share them with teammates with little to no friction in most cases. I’ll also touch on the basics of how WAAPI works and hopefully demonstrate how actual useful WAAPI scripts look and feel like, — thus this article might also be useful for beginners who just started learning WAAPI, or as a “getting started” kind of a guide for people outside the audio team who happen to be helping their colleagues with workflow tools and automation.

I wrote a few code examples just for this article, so be wary that this code hasn’t been extensively tested. You’ll need to set up your PC environment in order to run them, please refer to instructions in the Appendix.

Basic concepts

I think it may be worth it to go through basic information about Wwise and WAAPI first.

Wwise projects are organized into hierarchies of objects, objects can be of several types (Event, RandomSequenceContainer, etc.), object types describe which properties objects have (BusVolume, IsStreamingEnabled, etc.). Relative position in the hierarchy defines property value inheritance, i.e. child objects inherit values from their parents. All of this defines sound playback rules and configurations for the audio engine at runtime.

WAAPI is a client-server API to manipulate such hierarchies: renaming or removing objects, change their properties, etc. — not everything is accessible though, e.g. RTPCs and blends at the time of writing, but still quite a lot is possible. As such, it’s important to understand how Wwise projects are structured, know about different types of objects, their properties, etc. For this, I have the following reference pages bookmarked for quick access:

However, WAAPI also provides information on objects that isn’t listed among object properties. For example, properties sound:convertedWemFilePath, sound:originalWavFilePath, or even maxDurationSource which provides a value calculated as difference of properties TrimEnd and TrimBegin of object AudioSource, — these aren’t listed as object properties in Wwise Object Reference but WAAPI can still query them (maybe it has special access to the project data).

Also, for advanced users, there are JSON and XML schemas for WAAPI and Wwise Authoring data like Work Units under %WWISEROOT%\Authoring\Data\Schemas, which might become handy in some cases.

Considerations

WAAPI scripts don’t necessarily have to be long and complex in order to be useful, especially when automating laborious tasks. Apart from scripts themselves, ideally, the following things need to be considered:

  • how to execute scripts;
  • how to distribute them among teammates.

One way we used to address this is by using a custom Total Commander build. It is stored under SVN and includes loads of portable utilities for a wide range of technical audio needs, which were made accessible through menu / submenu bars in the top of the app. It also includes an embedded Python distribution with packages installed and maintained by me, including project-specific and WAAPI scripts. This way teammates don’t need to configure their computers in order to run our tools, and I get an easy way to update the whole environment and sync it up with everyone else. By the way, Total Commander is Ace!

Another way I practice sometimes is just putting all Python scripts under a Scripts directory right in a Wwise project. This will scope script provision to specific projects, of course.

Apart from buttons in Total Commander, I also like to invoke WAAPI scripts through command add-ons, which define commands in the Authoring Tool that execute arbitrary external tools with project-specific arguments, such as a GUID of selected object, a path to a Wwise project, etc. And these commands can be integrated in menus of the Authoring Tool, which probably is the most unobstructed way to use custom Wwise-related tools.

Workflow overview

To keep things simple for the purposes of this article, WAAPI scripts will be executed exclusively through command add-ons, and the Python files themselves will be placed under the Scripts directory. Thus, this setup expects a user to configure their PC environment, see the Appendix for instructions.

As an additional requirement, I’d like all errors to be displayed in a GUI — from my experience this makes it easier to deal with problems that sound designers find, rather than to ask them to dig into a console window.

Python project organization

The screenshot below shows the Scripts folder structure:

root5

Some important points:

  • The root directly contains only scripts that are invoked by Wwise from command add-ons, so they should not be imported by other Python files. Two exceptions from this rule are:
    • __init__.py — a file that marks the Scripts folder as module, which allows us to use relative imports from our scripts when current working directory is set to the Wwise project folder;
    • _template.py — a template file for making new scripts; it contains boilerplate and intended to be duplicated, renamed and modified to suit concrete needs.
  • Submodules contain code of size beyond trivial or functions that are reused across scripts.
  • The scripts are executed by passing their paths as arguments to the Python interpreter. I would have preferred to run scripts by module name (flag -m), but at the time of writing I couldn’t make command addons to work when setting current working directory to ${WwiseProjectRoot}, thus not using relative imports for now.1
  • By convention, scripts that encounter an error or detect that their data is in invalid state should raise RuntimeError exceptions. Such exceptions are caught in the outmost frame and their messages are displayed with dialog windows.

The Python template code is listed below. It may look long, but remember, it handles errors and displays them in a GUI, as well as provides common imports. I just copy and paste this template when writing a new script.

# this prevents this file from being imported
if __name__ != '__main__':
    print(f'error: {__file__} should not be imported, aborting script')
    exit(1)

# tkinter is used to display alert dialogs and other simple UI

import tkinter
from tkinter.messagebox import showinfo, showerror
from waapi import WaapiClient, CannotConnectToWaapiException
from waapi_helpers import *
from helpers import *

# feel free delete imports unused by your script and/or add new ones

# initializing Tk widgets and hiding an icon from appearing in the task bar
tk = tkinter.Tk()
tk.withdraw()

# the purpose of this try-except block is to catch runtime errors
# and display it to a user in a GUI window instead of console
try:
    with WaapiClient() as client:
        # our script begins here
        pass

except CannotConnectToWaapiException:
    # print a 'user-friendly' message on failing to connect to a running Wwise Application
    showerror('Error', 'Could not establish the WAAPI connection. Is the Wwise Authoring Tool running?')
except RuntimeError as e:
    # 'expected' errors, i.e. raised straight from the scripts
    showerror('Error', f'{e}')
except Exception as e:
    # unexpected errors always print stacktrace
    import traceback

    showerror('Error', f'{e}\n\n{traceback.format_exc()}')
finally:
    # we need to call this in order to stop Tk window event loop,
    # otherwise the script will stall at this point
    tk.destroy()

Configuring command add-ons

There isn’t much to say about command add-on configuration, as this process is pretty well documented in the official documentation. I’ll be using a single JSON file stored at Wwise Project/Add-ons/Commands/ak_blog_addons.json to hold all examples, you can check this file yourself by downloading the accompanying archive (see Appendix).

Note, after modifying JSON files you’ll need to reload command add-ons in the Wwise Authoring tool. There’s no hotkey to perform this action, but you can search for and execute it by typing a closing angle bracket followed by “command” in the search field2:

root4

Debugging WAAPI scripts

There’s a WAAPI tab in the Logs window, but not everything is logged by default, and you can consider enabling additional logging in the Logs Settings. Additionally, redirectOutputs setting in a command add-ons JSON will force Wwise to redirect console output of Python scripts into the General tab, which is disabled by default.

root22

About waapi_helpers3

I wrote a small library waapi_helpers that I use in examples throughout this article. It consists of a few small stateless helpers that accept WaapiClient as argument, so they can be mixed up with vanilla waapi-client code. All functions follow convention regarding getting properties, such that if a property doesn’t exist, the value should be None, plain and simple. I won’t be going into details here, as examples ahead will do a better job demonstrating what it looks like.

Examples

Most of the examples presented here, and in fact most of my own WAAPI scripts, follow more or less the same structure: walk through a Wwise project and collect information, do something with or transform the information, apply changes to the Wwise project.

E1: copy GUID of selected objects to clipboard

A surprisingly useful command that doesn’t require WAAPI and which does totally exist in Wwise already. It’s also so simple that I’ll list its corresponding command add-on JSON in full together with the Python source.

copy_guid.py (really nothing special, most of the template was stripped out):

if __name__ != '__main__':
    print(f'error: {__file__} should not be imported, aborting script')
    exit(1)

# this is a third-party lib

import pyperclip  
# a simple function that returns arguments in argv as list
# note the use of a relative import because there's a 'helpers' submodule
from helpers import get_selected_guids_list

guids = get_selected_guids_list()
pyperclip.copy(' '.join(guids))

JSON:

{
    "version": 2,
    "commands": [
        {
            "id": "waapi_article.copy_guid",
            "displayName": "Copy GUID",
            "program": "python",
            "startMode": "MultipleSelectionSingleProcessSpaceSeparated",
            "args": "\"${WwiseProjectRoot}/Scripts/copy_guid.py\" ${id}",
            "redirectOutputs": false,
            "contextMenu": {
                "basePath": "WAAPI"
            }
        }
    ]
}

  • id — a unique identifier for this new command.
  • displayName — a human-readable name of this command to be displayed in menus.
  • program — a program to run when a user executes this command; note, the script path is escaped with double quotes — this is important helps Wwise to treat this path as one argument and not split it.
  • startMode — Wwise will call the program once and will pass space-separated arguments, object GUIDs in our case.
  • args — arguments passed to the Python:
    • ${WwiseProjectRoot}/Scripts/copy_guid.py is a script path surrounded by escaped quotes in order to be treated as as single argument when the path contains whitespace;
    • ${id} is a special argument that will be substituted by Wwise with GUIDs of selected objects.
  • redirectOutputs — helps to debug scripts by redirecting their stdout to the Logs window in Wwise; disabled by default.
  • contextMenu — configures this command to be shown in context menus for all objects in Wwise project hierarchies, in our case there’ll be a subgroup called WAAPI.

After refreshing command add-ons, an item will appear in a context menu:

root6

Clicking it will copy the following text into the system clipboard:

{2E9E3B71-C905-4BB0-9B30-06CFF26E0C5E} {3AD5C9DF-C0B5-4A78-B87A-2EE37D64BFCB} {8F7A715D-5704-4F09-9563-4172E250419B}

E2: show names of all events

Totally unpractical, but it shows how to use walk_wproj function to traverse hierarchies. Boilerplate is omitted.

show_event_names.py:

events = []
with WaapiClient() as client:
    for guid, name in walk_wproj(client,
                                 start_guids_or_paths='\\Events',
                                 properties=['id', 'name'],
                                 types=['Event']):
        events.append(name)
showinfo('Hi tutorial!', '\n'.join(events))

Here, walk_project function walks through each object down the hierarchy starting with path \Events, and yields id and name properties of each Event object it encounters. This functions started my library, as I wanted a simple Pythonic interator-based interface to walk Wwise hierarchies, similar to what XML libraries provide. I also wanted to avoid creating and unpacking JSON objects, as their schemas differ between different commands, which is hard to keep in the working memory at all times.

Names are collected into an array and printed together after the iteration is finished. Very useless, for educational purposes only. The result will look something like this:

root9

E3: reset volume faders

Sometimes, maybe during mixing, I want to set all faders in certain part of a hierarchy to zero. The following script will do this for all children of a selected object, except those, marked with an @ignore tag in their notes.

reset_faders.py:

with WaapiClient() as client:
    num_reset_faders = 0
    selected_guid = get_selected_guid()

    for obj_id, obj_type, obj_notes in walk_wproj(client, selected_guid,

                                                  properties=['id', 'type', 'notes']):
        if '@ignore' in obj_notes:
            continue

        # note, we want to change different properties based on whether
        # an object belongs to the Actor-Mixer or the Master Mixer hierarchy
        prop_name = 'Volume'
        if obj_type == 'Bus' or obj_type == 'AuxBus':
            prop_name = 'BusVolume'

        cur_volume = get_property_value(client, obj_id, prop_name)
        if cur_volume is not None:
            # by convention, if a property doesn't exist,
            # `get_property_value` will return None, which
            # allows to skip calling `set_property_value` when
            # there's no volume property on the object
            set_property_value(client, obj_id, prop_name, 0)
            num_reset_faders += 1

    showinfo('Info', f'{num_reset_faders} faders were reset')

Results:

root10

root11

root12

E4: remove invalid events

Let’s say we just deleted a bunch of legacy objects from the Actor-Mixer Hierarchy. This may have left a lot of event actions with invalid references, some events may have even became useless because of this, as all of their actions now reference non-existing objects. With such, we can assume the events can be safely deleted. We can do this with WAAPI by walking through all events and checking whether or not all of their actions refer to non-existing objects. If so, we mark the event for deletion.

delete_invalid_events.py:

# a set of action types identifiers that reference objects,
# see Action object reference for details
action_types_to_check = {1, 2, 7, 9, 34, 37, 41}
events_to_delete = []

with WaapiClient() as client:

    num_obj_visited = 0
    for event_guid, in walk_wproj(client, '\\Events', properties=['id'], types=['Event']):
        print(f'Visited: {num_obj_visited}, To delete: {len(events_to_delete)}', end='\r')
        num_valid_actions = 0
        for action_id, action_type, target in walk_wproj(client, event_guid,
                                                         properties=['id', 'ActionType', 'Target'],
                                                         types=['Action']):
            if action_type in action_types_to_check:
                if does_object_exist(client, target['id']):
                    num_valid_actions += 1
            else:
                num_valid_actions += 1

        if num_valid_actions == 0:
            events_to_delete.append(event_guid)

    num_events_to_delete = len(events_to_delete)
    if num_events_to_delete > 0 \
            and askyesno('Confirm', f'{num_events_to_delete} events are going to be deleted. Proceed?'):
        begin_undo_group(client)
        for event_guid in events_to_delete:
            delete_object(client, event_guid)
        end_undo_group(client, 'Delete Invalid Events')  # capitalized as per Wwise convention

showinfo('Success', f'{len(events_to_delete)} were deleted')

This one is a bit more complex.

First, note that we saved a set of action types that can potentially reference objects (e.g. Play, Stop, Set RTPC, etc.) — every one of them that references a non-existent object is considered to be invalid. The set was populated by hand based on the Action object documentation (see ActionType property description).

A second new thing for us is that we’re asking a user for permission to perform a destructive action, — they will be shown a dialog asking to confirm whether or not certain number of events should be deleted.

And finally, the command uses the undo group feature of WAAPI. It allows us to revert changes done by multiple WAAPI calls in a single undo action, and the action itself will have a special name in the Edit menu: “Delete Invalid Events”.

Relevant screenshots:

root15

root13

root14

E5: set up streaming for long SFX

Bulk-configuration of SFX to use streaming might be non-trivial, because sometimes designers trim sound sources, so Wave file lengths might differ substantially from the SFX duration at runtime. At the time of writing, it’s impossible to get the trimmed SFX duration neither through Wwise Queries nor through WAQL.

Fortunately, WAAPI has the trimmedDuration property. But let’s go one step further and make streaming parameters that our tool will set to be configurable from the Wwise Authoring tool. In this example, I’ll put the configuration in the \Actor-Mixer Hierarchy\Default Work Unit notes section and use Python’s configparser syntax, which is super simple and already built into Python. Let’s say our config will look like this:

[Enable_Streaming_For_SFX]
If_Longer_Than = 10
Non_Cachable = no
Zero_Latency = no
Prefetch_Length_Ms = 400

For a larger project you may want to have more granular control over streaming settings for different kinds of sounds, or even platforms. The good part is that you can add configuration sections and retrieve the one you need for each particular task in the script.

set_streaming_for_long_sfx.py:

with WaapiClient() as client:
    dwu_notes = get_property_value(
        client, '\\Actor-Mixer Hierarchy\\Default Work Unit', 'notes')
    if dwu_notes is None:
        raise RuntimeError('Could not fetch notes from Default Work Unit')

    config = configparser.ConfigParser()
    config.read_string(dwu_notes)
    if 'Enable_Streaming_For_SFX' not in config:
        raise RuntimeError('Could not find [Enable_Streaming_For_SFX] config section')

    stream_config = config['Enable_Streaming_For_SFX']

    objects_to_modify = []
    for guid, name, max_dur_src in walk_wproj(client, '\\Actor-Mixer Hierarchy',
                                              ['id', 'name', 'maxDurationSource'], 'Sound'):
        if max_dur_src is None:
            continue
        # trimmedDuration is in seconds, not milliseconds
        is_long_sound = max_dur_src['trimmedDuration'] > stream_config.getfloat('If_Longer_Than')
        if is_long_sound:
            objects_to_modify.append(guid)
            print(name)
            break

    if len(objects_to_modify) > 0 and \
            askyesno('Confirm',
                     f'The tool is about to modify properties of {len(objects_to_modify)} objects. Proceed?'):
        begin_undo_group(client)
        for guid in objects_to_modify:
            set_property_value(client, guid, 'IsStreamingEnabled', True)
            set_property_value(client, guid, 'IsNonCachable', True)  # stream_config.getboolean('Non_Cachable'))
            set_property_value(client, guid, 'IsZeroLantency', stream_config.getboolean('Zero_Latency'))
            set_property_value(client, guid, 'PreFetchLength', stream_config.getint('Prefetch_Length_Ms'))
        end_undo_group(client, 'Bulk Set SFX Streaming')
        showinfo('Success', f'{len(objects_to_modify)} objects were updated')
    else:
        showinfo('Success', f'No changes have been made')

And here’s how the configuration looks like:

root16

E6: refactor group of containers into a switch

Sometimes we need to refactor several containers in the A-M hierarchy into one switch container. We can try to make a tool that automates this, with a user story being something like this: select a few objects, click a button, a new switch container appears that has the selected objects assigned to switches. For this example, I’ll use a Surface_Type switch from the Wwise Adventure Game project.

refactor_into_switch_surface_type.py:

with WaapiClient() as client:
    obj_names = [get_name_of_guid(client, guid)
                 for guid in selected_guids]
    if None in obj_names:
        raise RuntimeError('Could not get names of all selected objects')
   
    switches = get_switches_for_group_surface_type(client)
    if len(switches) == 0:
        raise RuntimeError("Could not find switches for group 'Surface_Type'")
    parent_obj = get_parent_guid(client, selected_guids[0])
    if parent_obj is None:
        raise RuntimeError(f'{selected_guids[0]} has no parent')
   
    begin_undo_group(client)
   
    switch_obj = create_objects(client, parent_obj, 'RENAME_ME', 'SwitchContainer')[0]
    if switch_obj is not None:
        set_reference(client, switch_obj, 'SwitchGroupOrStateGroup',
                      f'SwitchGroup:{SURFACE_TYPE_SWITCH_GROUP_NAME}')
    else:
        # roll back changes if the script failed in the middle of operation
        end_undo_group(client, 'Refactor Into Surface_Type Switch')
        perform_undo(client)
        raise RuntimeError('Could not create switch container under ' +
                           f'{get_name_of_guid(client, parent_obj)}. '
                           'All changes have been reverted.')
   
    # reparent selected objects
    for guid in selected_guids:
        res = move_object(client, guid, switch_obj)
        if res is None:
            end_undo_group(client, 'Refactor Into Surface_Type Switch')
            perform_undo(client)
            raise RuntimeError(
                f'Could not move object {guid} to parent {switch_obj}. '
                'All changes have been reverted.')
   
    obj_assignments = infer_obj_assignments(selected_guids, switches)
   
    for obj_guid, sw_guid in obj_assignments:
        client.call('ak.wwise.core.switchContainer.addAssignment',
                    {'child': obj_guid, 'stateOrSwitch': sw_guid})
   
    end_undo_group(client, 'Refactor Into Surface_Type Switch')

This code is a bit more complex than previous examples, I even had to refactor some parts into two functions in order to keep the listing shorter. The first function, get_switches_for_group_surface_type, is a helper to get GUIDs and names of all Surface_Type switches. The second one, infer_obj_assignments, tries to match selected objects to switches by comparing their names, pairwise, and selecting the most similar switch name (partial_ratio function of the thefuzz library).

Other things to note:

  • The code performs different data validation along the execution and raises RuntimeError exceptions if the state is invalid. Such exceptions are displayed to the user as error windows.
  • On some error paths, just before raising an exception, the script rolls back all changes made so far through WAAPI by performing an undo operation.
  • Also you can see there’s a direct call to waapi-client as there’s no switch container assignment function in the helper library.
  • This script is far from perfect, probably has weird edge cases, and is not optimized at all, — this was just a fun thing to do in the evening while writing the article. You’ve been warned.

The screenshots below demonstrate how it works.

root17


root18

E7: delete unused Wave files from the Originals folder

Over the course of time, Wwise projects might collect Wave files in the Originals folder that aren’t referenced anywhere and are just wasting disk space. A user story is simple: press a button, script should ask for confirmation, then notify whether everything was deleted or something’s left, e.g. if a file is open in Audition or being locked otherwise. A tip: run Integrity Report after such operations.

remove_unused_wavs.py:

with WaapiClient() as client:
    default_wu_path, = get_object(client, '\\Actor-Mixer Hierarchy\\Default Work Unit', 'filePath')
    # this function parses .wproj file to determine where is 'Originals' directory
    origs_dir = find_originals_dir(default_wu_path)

    wavs_in_origs = set()

    wavs_in_wproj = set()

    # we don't want to touch the 'Plugins' directory
    for subdir in 'SFX', 'Voices':
        for wav_path in glob(os.path.join(origs_dir, subdir, '**', '*.wav'),
                             recursive=True):
            wavs_in_origs.add(normalize_path(wav_path))

    # note, a single walk_wproj can traverse a hierarchy
    # several times from different places
    for guid, wav_path in walk_wproj(
            client,
            start_guids_or_paths=['\\Actor-Mixer Hierarchy', '\\Interactive Music Hierarchy'],
            properties=['id', 'originalWavFilePath'],
            types=['AudioFileSource']
    ):
        wavs_in_wproj.add(normalize_path(wav_path))

    wavs_to_remove = wavs_in_origs.difference(wavs_in_wproj)
    files_left = len(wavs_to_remove)

    if files_left > 0 and askyesno(
            'Confirm', f'You are about to delete {files_left} files. Proceed?'):
        for wav_path in wavs_to_remove:
            try:
                os.remove(wav_path)
                files_left -= 1
            except PermissionError:
                pass

        if files_left == 0:
            showinfo('Success',
                     f'{len(wavs_to_remove)} files were deleted')
        else:
            showwarning('Warning',
                        f'{files_left} files could not have been deleted. '
                        f'Are they open in some apps?')

Even though this code seems simpler than in the previous example, here we encountered some limitations of the WAAPI and had to parse the Wwise project file in order to get the path to the Originals folder. Initially, I thought this information is stored in the Project object, but apparently not, and I wasn't able to find another way to query it.

Another non-obvious thing is that I asked walk_wproj to retrieve all objects of type AudioFileSource — but this type is not listed on the Wwise Objects Reference page! The closest type is AudioSource, which could be a parent type but I cannot confirm this. I guess I tried this because I worked with parsing and generating Work Unit XML at some point and I remember that Wave files are enclosed within <AudioFileSource/> tags, so this choice felt intuitive, believe it or not. Also, this type is listed in the XML schema file which can be found at %WWISEROOT%\Authoring\Data\Schemas.

Obligatory screenshots:

root20

root21

E8: Wave file import automation

In many cases, in-game sounds are represented by non-trivial hierarchies, possibly with several layers, or components, of sounds that can routed to different buses, controlled by different RTPCs, etc. Some of such systems can be modeled as hierarchies with slots, i.e. places, where audio assets are attached in order to create a particular sound of this system. In practice, this can look like duplicating an existing hierarchy and replacing assets at appropriate “slots”.

Of course, we can do this with WAAPI, and for simplicity, I’ll implement only one user story from this workflow.

  • A user configures a template hierarchy and annotates its parts, specifying the template name, slots, their names, etc. In the image below, the template Gun has slots Shot, Tail_Indoor and Tail_Outdoor.

    root23
  • The user right clicks on the template, presses a button, and is prompted to select a directory with name-formatted Wave files. After selection, the tool scans the directory and finds files that match naming for this particular template.

    root27
    root26
    root24

  • After the user confirms import, the template is copied over, renamed, and slots are populated with the Wave files.

    root25

On the screenshots, the tool implemented two sound objects, M4 and M1911, in one go. Moreover, it recorded the template object GUID into the notes of each gun’s virtual folder, so that other scripts can scan through them if needed.

Please, check import_wavs.py sources for a working example. This code is a little bit more complex than others to be fully listed here.

Conclusions

In this article, I presented a workflow I use to work with WAAPI and Python, including ways to organize code, as well as to share scripts with the audio team. To support ideas, a few tool examples were given that implement the workflow. Please, feel free to reach me out or leave a comment under this blog if you have any questions, suggestions, etc.

Some of the (many) things that can be improved:

  • Consider using allow_exception=True when instantiating WaapiClient — this way you can catch WAAPI-specific exceptions and give better error messages to the user, e.g. when some dialog window is open in Wwise.
  • In our Python project convention, all command add-on scripts are placed directly under Scripts folder. This can be used to auto-generate add-on JSON files.
  • Configurations stored in the notes section can be improved by separating them in a way similar to YAML front matter, which is widely used by website building frameworks such as Jekyll. This will allow them to co-exist together with regular human-made notes.
  • In Example 4, in addition to deleting invalid events, it could make sense to delete invalid actions as well, as they’re currently left untouched in the project, possibly causing errors in the Integrity Report.
  • The switch refactoring command can use some algorithm that tries to give the best name to a switch, e.g. by getting a common substring of all selected objects, or… an AI to summarize text!

Acknowledgments

This article is based on a short talk I gave at DevGAMM Fall 20214. Ideas presented were worked out while working in a technical audio team at NetEase Games together with the following people (in alphabetical order): Dmitry Patrakov, Ruslan Nesteruk, Victor Ermakov. Special thanks to Damian Kastbauer for providing feedback on my DevGAMM slides and for the Wave file importer idea; to Bernard Rodrigue for peer review and the suggestion to include script debugging part; to Masha Litvinava for proofreading and assisting with the publication process; to Tyoma Makeev for the “switch container” example idea, to Denis Zlobin for encouragement to actually publish these materials on the Audiokinetic blog.

Appendix: running examples

All examples presented here are attached as an archive to this article, and you can download that here. Just unpack its contents into any Wwise project and you’re ready to go, but note, the examples were made while working with the Wwise Adventure Game project.

Other things you’ll need:

  • Git
  • Python 3, any recent version should suffice, but during installation, be sure to tick boxes that add the Python executable to PATH, as well as install pip and tcl/Tk components
  • Wwise 2021, install Wwise Adventure Game if you want to follow exactly
  • In Wwise project settings, ensure WAAPI is enabled

With Python in the system, you’ll need to install some Python packages:

pip install -U pyperclip waapi-client
pip install -U git+https://github.com/ech2/waapi_helpers.git

Also, Example 6 uses fuzzy string matching algorithm, you’ll also need to install the following packages if you’d like to run it:

pip install -U thefuzz python-Levenshtein


  1. This was a confirmed bug related to how redirectOutput option interacts with custom cwd

  2. I wish I would’ve learned this trick way earlier, as I used to restart the Wwise Authoring tool after each JSON update. 

  3. I had some reservations regarding using a wrapper I wrote on top of waapi-client to avoid this article to be perceived as a plug. But to be honest, I almost never use vanilla WAAPI in Python, and have already began to forget JSON schema of common WAAPI functions. I by no means encourage readers to use my tool, and recommend reading it a pseudo code instead. 

  4. The talk was given in Russian, but the slides are in English. The talk should appear on YouTube, eventually. The slides and code examples can be downloaded at the URL: ech2/DevGAMM_2021_Fall 

Eugene Cherny

Eugene Cherny

An audio programmer, currently working in the game industry. Have a little bit of experience in art and academia. Love game audio.

https://eugn.ch/

Comments

Leave a Reply

Your email address will not be published.

More articles

A Step-by-Step WAAPI Example

In my previous article, we reviewed a few of the various possibilities where the Wwise Authoring...

9.5.2017 - By Bernard Rodrigue

Hitman 2: Enhancing Reverb on Modern CPUs

Popularization of CPUs with six and eight cores means that untapped processing power becomes...

28.8.2019 - By Stepan Boev

Generating Rain With Pure Synthesis

A few years ago, I wondered whether it was possible to synthesize whatever I wanted. I started by...

30.7.2020 - By Aleksandr Khilko

Wwise 2021.1 What's New | Beta Edition

We will be launching Wwise 2021.1 early next year and are excited for you to get your hands on the...

17.12.2020 - By Audiokinetic

Making Interactive Music in Real Life with Wwise

I decided to dive deeper into interactive audio some time ago. I wanted to create a project related...

25.2.2021 - By Ressa Schwarzwald

Wwise 2022.1 What's New | Public Preview Edition

The 2022.1 Public Preview is live and now available to install from the Wwise Launcher. The build...

19.5.2022 - By Audiokinetic

More articles

A Step-by-Step WAAPI Example

In my previous article, we reviewed a few of the various possibilities where the Wwise Authoring...

Hitman 2: Enhancing Reverb on Modern CPUs

Popularization of CPUs with six and eight cores means that untapped processing power becomes...

Generating Rain With Pure Synthesis

A few years ago, I wondered whether it was possible to synthesize whatever I wanted. I started by...