Getting Started With Wwise WAAPI And Max8
This article originally appeared on the Audiokinetic Website.
When setting up more complex RTPC-driven events in Wwise, we often face the dilemma of not being able to easily test drive them.
We can't really control more than one parameter at once using a mouse, and while mapping parameters to MIDI controllers or using MIDI automation works, it might not be ideal.
Testing directly in game can also be cumbersome and slow down the iteration process if there are no shortcuts in place to skip to the things/scenarios we want to test, or if the systems we are designing audio for aren't fully implemented yet, or don't exist at all.
Max by Cycling74 could help with all of that. With NodeJS it offers a way to connect to Wwise directly via WAAPI, allowing to call events and update RTPCs.
It also provides many useful graphical UI objects like curves, sliders, buttons, dials. It has built-in snapshot saving to store presets of different parameter configurations, and it allows simulating parallel parameter changes over time. It is a great environment for rapid prototyping.
A final note before we dive in: there will be a link to the project on my github at the end of the article :).
Setup
To follow this article and to make the most out of this you need:
- a recent installation of Wwise including the Sample Project
- Max 8 (the trial should theoretically work but I haven’t confirmed)
- a basic understanding using Max
- decent knowledge in javascript programming
Note that the screenshots were all made on OSX but the steps are identical on Windows.
First we need to create a Max Project, instead of a simple Max Patcher, because we want everything contained in its own folder. In the Max project folder we create a code sub directory and copy the contents from the hello-wwise-node-wamp example project which can be found here:
<WwiseInstallationPath>/SDK/samples/WwiseAuthoringAPI/js/
Additionally we copy the wwise.js file from
<WwiseInstallationPath>/SDK/include/js/
into the code folder of our Max project, so we can easily reference it
in the index.js
script.
Then we add a node.script object to our patcher and point it to the
index.js
file that comes with the
hello-wwise-node-wamp
example, open the object help
patcher (ALT+Click on the Max object) and copy the node debug tool
into our patch.
Side note: by default Max will open the js file in its functional but a bit limited built-in text editor. If you, like me, prefer to use VSCode or another editor you can change that in the Max preferences.
Finally we send the node.script a script npm install message. This will install all the node modules listed in the packages.json file.
In our Wwise project we now need to confirm that WAAPI is enabled in the Wwise User Preferences.
Next we open the index.js
by using CTRL+Double Clicking
it, update the path of the wwise.js
file to our local
copy, include the max-api module and finally replace the
console.log()
calls with Max.post()
calls to
print to the max console.
1 | var ak = require('./maxmsp_wwise_waapi.js').ak; |
2 | var Max = require('max-api'); |
3 | var autobahn = require('autobahn'); |
4 | |
5 | // create the WAMP connection |
6 | var connection = new autobahn.Connection({ |
7 | url: 'ws://localhost:8080/maxmsp_wwise_waapi', |
8 | realm: 'realm1', |
9 | protocols: ['wamp.2.json'] |
10 | }); |
11 | |
12 | let akSession; |
13 | |
14 | // setup handler for connection opened |
15 | connection.onopen = function (session) { |
16 | |
17 | // call getInfo |
18 | session.call(ak.wwise.core.getInfo, [], {}).then( |
19 | function (res) { |
20 | Max.post(`Hello ${res.kwargs.displayName} ${res.kwargs.version.displayName}!`); |
21 | |
22 | // setup audio listener |
23 | akSession.call(ak.soundengine.registerGameObj, [], { |
24 | "gameObject": 1, "name": "default listener" |
25 | }); |
26 | |
27 | akSession.call(ak.soundengine.setDefaultListeners, [1], |
28 | { "listeners": [1] }); |
29 | }, |
30 | function (error) { |
31 | Max.post(JSON.stringify(error)); |
32 | } |
33 | ).then( |
34 | function() { |
35 | connection.close(); |
36 | } |
37 | ); |
38 | }; |
39 | |
40 | connection.onclose = function (reason, details) { |
41 | if (reason !== 'lost') { |
42 | Max.post("Connection closed. Reason: " + reason); |
43 | } |
44 | process.exit(); |
45 | }; |
46 | |
47 | // open the connection |
48 | connection.open(); |
Now it's time to test the connection; for that we enable capture in Wwise and navigate to the WAAPI section of the log window in the Profiler view, then test the connection sending a script start message to the node object in Max.
If everything went well, the Max console should print a hello message and the Wwise version and the Wwise Log as well.
Note that in the index.js
provided by the SDK sample, the
connection is closed immediately again.
To change that, we modify the onopen()
function to cache
the akSession
and also add an exit handler for the node
process to close the WAAPI connection when the node script is stopped.
1 | let akSession; |
2 | |
3 | // setup handler for connection opened |
4 | connection.onopen = function (session) { |
5 | |
6 | // call getInfo |
7 | session.call(ak.wwise.core.getInfo, [], {}).then( |
8 | function (res) { |
9 | Max.post(`Hello ${res.kwargs.displayName} ${res.kwargs.version.displayName}!`); |
10 | |
11 | // cache session for future use |
12 | akSession = session; |
13 | |
14 | // setup audio listener |
15 | akSession.call(ak.soundengine.registerGameObj, [], { |
16 | "gameObject": 1, "name": "default listener" |
17 | }); |
18 | |
19 | akSession.call(ak.soundengine.setDefaultListeners, [1], |
20 | { "listeners": [1] }); |
21 | }, |
22 | function (error) { |
23 | Max.post(JSON.stringify(error)); |
24 | } |
25 | ); |
26 | }; |
27 | |
28 | process.on("exit", () => connection.close()); |
Next we add MaxHandlers for posting events and updating RTPCs by UUID. I chose UUID deliberately because I had some issues with posting RTPCs by name.
1 | // setup handlers |
2 | Max.addHandler("postEvent", (...event) => { |
3 | let [eventName, UUID, objID] = event; |
4 | |
5 | akSession.call(ak.soundengine.registerGameObj, [], |
6 | { "gameObject": objID, "name": eventName }).then( |
7 | (res) => Max.post(`Register GameObject with id ${objID}`), |
8 | (error) => Max.post(JSON.stringify(error)) |
9 | ); |
10 | |
11 | akSession.call(ak.soundengine.postEvent, [], |
12 | { "event": UUID, "gameObject": objID }).then( |
13 | (res) => Max.post(`postEvent ${eventName}`), |
14 | (error) => Max.post(JSON.stringify(error)) |
15 | ); |
16 | }); |
17 | |
18 | Max.addHandler("setRTCP", (...rtpc) => { |
19 | let [UUID, objID, rtpcValue] = rtpc; |
20 | |
21 | akSession.call(ak.soundengine.setRTPCValue, [], |
22 | { "rtpc": UUID, "value": rtpcValue, "gameObject": objID }).then( |
23 | (res) => {}, |
24 | (error) => Max.post(JSON.stringify(error)) |
25 | ); |
26 | }); |
Next we need the UUIDs of the events and RTPCs we want to call from Max. Thankfully Wwise provides WAQL , a sophisticated query language that makes this very easy. We add two additional max handlers which return all Events/RTPCs and send them to the first outlet of the Max node object which we can connect to a Max dictionary that we can embed permanently in the patcher to access the UUIDs whenever we need them at a later point.
1 | Max.addHandler("getAllEvents", () => { |
2 | akSession.call(ak.wwise.core.object.get, [], { "waql": "$ from type Event" }).then( |
3 | (res) => { |
4 | Max.post("Success: getAllEvents"); |
5 | Max.outlet(res); |
6 | }, |
7 | (error) => Max.post(JSON.stringify(error)) |
8 | ); |
9 | }); |
10 | |
11 | Max.addHandler("getAllRTCP", () => { |
12 | akSession.call(ak.wwise.core.object.get, [], |
13 | { "waql": "$ from type GameParameter" }).then( |
14 | (res) => { |
15 | Max.post("Success: getAllRTCP"); |
16 | Max.outlet(res); |
17 | }, |
18 | (error) => Max.post(JSON.stringify(error)) |
19 | ); |
20 | }); |
Now that we have the UUIDs at hand we can finally post our first events. I'm using the NYC ambiance events for testing. Our postEvent() Handler takes three arguments, an event name, a UUID and an object ID.
This object ID is important later, for both the play/stop events as well as the RTPC updates we plan on sending later it needs to match. Or otherwise the stop/RTPC updates won’t work since Wwise thinks they’re not related.
Here's a final video of the aforementioned example project driving the Time and Rain_Intensity parameter on the NYC ambiances of the Wwise SampleProject.
And here's the link to the sample project on github. Just download it as .zip and open it in Max 8.
https://github.com/michaelhartung/wwise-waapi-starter/
What's next?
Of course this all still requires a lot of manual setup to get Wwise events setup in a Max patch, but Max also supports patcher scripting via javascript, including programmatically creating objects, etc., so all of this can be automated.
I've started experimenting with ways to automatically create event buttons, etc. which, if it goes anywhere, might eventually turn into a small Max package.
Happy patching!