Ambisonic Rendering in the Story Bubble


Figure 1: Sound Sphere in the Center for Vision, Speech and Signal Processing (CVSSP) at the University of Surrey. The person is shown standing in the intended orientation, facing the front of the sphere.

Introduction

I recently needed some audio content that I could quickly render for playback in the sound sphere shewn in Figure 1. There are a reasonable amount of free ambisonic recordings available online, e.g. So I spent some time putting together tools to render ambisonics in the sphere, and here I wanted to share what I learned in case anybody else at CVSSP wants to try this out in the future.

Speaker Arrangement

The sphere has 24 speakers (plus a subwoofer which I will henceforth ignore), which at the time of writing are arranged as shown in Figure 2 and Figure 3.


Figure 2: Speaker layout. The center of the diagram represents the top of the sphere. The top of the diagram represents the front of the sphere. There is one speaker at the top, and then three rings of speakers. The ring that is at head-level is at 0° elevation, with a ring above that at +30°, and a ring below at -30°. The front of the sphere is at 0° azimuth, with positive azimuth on the left. The channel numbers are labelled. The sphere has a radius of 168 cm, measured from the front of the speaker grilles. Speaker 12 sits a little higher in elevation than the other members of that ring, but I have ignored this fact.


Figure 3: Probably mostly unhelpful 3d graph of speaker positions.

Decoder

I used the Ambisonic Decoder Toolbox (ADT) to create pseudoinverse (pinv) and ALLRAD decoders for ambisonic orders 1 to 3, for the given speaker layout. The script I used to do that is here:
Matlab Script for Creating Ambisonic Decoders
For posterity, the full output including all decoders and graphs is here,
Decoders And Graphs
although generally you will not need all of this, only the .config files, which are here:
Config Files
If for some reason you want to re-run this script yourself, for example if the speaker arrangement changed, you would download ADT, put my script Into ADT's folder called 'adt/examples'. Then in Matlab, you would cd into adt's folder and do something like cd('/Path/To/adt') adt_initialize run_surrey_big_sphere
The results will be written to adt/decoders.

Renderer

I used the ambix plugins to render b-format out to the 24-channel sound sphere. The way you would do this on OSX using Reaper would be to download the correct set of plugins for the ambisonic order you want (I used the first order plugins). Unzip that folder as either 'Macintosh HD/Library/Audio/Plug-Ins/VST/ambix or ~/Library/Audio/Plug-Ins/VST/ambix. Then download the Config Files from above, and place them in ~/Library/ambix/surrey_presets/.

Then, in your DAW of choice, you should be able to use the ambix_decoder plugin to select the appropriate config file, as shown in Figure 4.


Figure 4: Reaper using the ambix renderer with ADT config files to render first order b-format to the 24-channel sound sphere.

If you actually want to generate a 24-channel audio file with Reaper, it might at first seem like it does not want to do this, but it can be done with the render settings shewn in Figure 5.


Figure 4: Repaer render settings for rendering a 24-channel audio file. You must select the b-format audio region. The important settings are circled in red. Reaper will ignore the selection that says Channels: 4.

Observations

I think there was still a slight format mismatch between the audio file, the config file, and the plugin, because sounds were not placed exactly as I expected. According to the ADT FAQ, "The toolbox defaults to Furse-Malham (aka FuMa) order and normalization for 3rd-order or less, and AmbiX (ACN/SN3D) for 4th and above", although in Matlab Script I over-rode this default with ambix order and normalization. According to the ambix plugin page, "The processors use the ambiX convention (full 3D, ACN channel ordering, SN3D normalization)". On Youtube (where I got some b-format source material), they use ACN channel ordering and SN3D normalization. I did not have a chance to exactly iron this out, but there is a format-converter plugin in the ambix toolbox that would presumably fix the mismatch.

S3A and visr

There should be another way to render ambisonics in the sphere using tools developed as part of the S3A project instead of the tools discussed above. I have not tried it, but I received the following instructions from one of the lead developers:
As Dylan has pointed out, the S3A renderer contains an AllRAD-based Ambisonics decoder.

To use it, one needs to send object metadata describing HOAObject audio objects to the renderer.

The "channels" property lets you define the inputs (and their order) through which the Ambisonics component signals are expected. For example "channels": "0,1,2,3" or, equivalently “channels”: “0:3” renders a first-order Ambisonics signal from WXYZ sent to the first 4 input channels.

The following is a trivial command-line script to send HOA objects via UDP to the realtime renderer.
#!/usr/bin/env python import argparse import sys import socket parser = argparse.ArgumentParser() # The positional (mandatory) arguments: parser.add_argument( "objectid", help="The object id, an integer >= 0." ) parser.add_argument( "order", help="The object id, an integer >= 0." ) parser.add_argument( "-v", "--verbosity", action="count", default=0, help="Print the network message after sending." ) parser.add_argument( "-r", "--receiver", default="localhost:4242", help="The destination network address, format: host:port, default: localhost:4242." ) parser.add_argument( "-l", "--level", default=1.0, help="Sound level of the source, linear scale 0..1.0, default: 1.0." ) parser.add_argument( "-p", "--priority", default="0", help="Rendering priority, integer >= 0, default: 0." ); parser.add_argument( "-c", "--channelid", default="-1", help="Channel id, denotes the audio channels for the source signal. string, either comma-separated 1,2,3 or ranges. Default: -1 meaning that the object id is used as channel id."); parser.add_argument( "-g", "--groupid", default="0", help="Group id, integer >= 0." ); args = parser.parse_args() destStrs = args.receiver.split(":") destHost = destStrs[0] destPort = int( destStrs[1] ) objectId = int(args.objectid) if len(args.channelid) <= 0: channelId = str(objectId) else: channelId = args.channelid order = int(args.order) groupId = int(args.groupid) priority = int(args.priority) level = float( args.level ) msg = "{ \"objects\":[{\"id\": %d, \"order\": %d, \"channels\": %s, \"type\": \"hoa\", \"group\": %d, \"priority\": %d, \"level\": %f}] }" % ( objectId, order, channelId, groupId, priority, level ) udpSocket = socket.socket(socket.AF_INET, # Internet socket.SOCK_DGRAM) # UDP udpSocket.sendto( msg, (destHost, destPort) ) if args.verbosity >= 1: print( "Send message %s to %s:%d." % ( msg, destHost, destPort ) )

Comments

Popular posts from this blog

WaveRNN

How I calibrated my contact microphone