This is the preview documentation for integrating ODIN Voice version 2.x with FMOD. There is no repository yet and screenshots are missing.
Integrating ODIN Voice Chat with the FMOD Audio Solution in Unity

Introduction
Welcome to this guide on integrating the ODIN Voice Chat Plugin with the FMOD Audio Solution in Unity. It targets the ODIN Unity SDK 2.x. If you are still on the 1.x SDK, please use the 1.x version of this guide.
What You'll Learn:
- Why ODIN needs custom capture and playback scripts once Unity's built-in audio is disabled
- How the
FMODMicrophoneReader,FMODPlaybackComponentandFMODOdinPlaybackscripts work and how to use them - Properly set up the
OdinInstanceprefab when FMOD is your audio engine - Deal with limitations and potential pitfalls
Note: This guide assumes that your project has disabled Unity's built-in audio, as FMOD recommends. Every script in this guide works with the audio engine enabled as well, but then the default ODIN components would work too.
Getting Started
To follow this guide, you'll need to have some prerequisites:
- Basic knowledge of Unity
- The FMOD Plugin for Unity, which you can get here
- The ODIN Unity SDK 2.x, available here
To set up FMOD in your project, please follow FMOD's in-depth integration tutorial. You can find the
tutorial here. FMOD recommends disabling the Unity
built-in audio on all platforms to prevent it from conflicting with FMOD. You do this in
Project Settings > Audio > Disable Unity Audio, and the FMOD Setup Wizard offers to do it for you.
To set up the ODIN Unity SDK, please take a look at our manual for the high-level API:
Read the High Level API manualWhy you need custom scripts
Disabling Unity's audio engine changes two things the default ODIN components rely on:
- Unity's
Microphoneclass no longer reports any devices, so theOdinMicrophoneReader
has nothing to capture. AudioClip.Createis not available, so the OdinDecoder cannot build the clip it streams into itsAudioSource. The component detects this, logs an error once and stays silent.
The SDK still handles the network side. Unity reports a samplerate of OdinRoom0 without the audio engine, and
the OdinRoom.DefaultSampleRate (48 kHz)
for its encoders and decoders, so nothing has to change in the room setup as long as FMOD mixes at 48 kHz, which is
its default.
If your FMOD mixer runs at a different rate, set OdinRoom.SampleRateOverride (SDK 20206.1.0 and later) before the
scene with the OdinRoom loads. The room, its decoders and every OdinEncoder then use that rate instead of asking
Unity:
So the FMOD integration consists of three scripts that replace the audio ends of the pipeline:
| Script | Replaces | Job |
|---|---|---|
FMODMicrophoneReader | OdinMicrophoneReader | Records the microphone with FMOD and pushes the samples into an OdinEncoder |
FMODOdinPlayback | The default OnDecoderAdded / OnDecoderRemoved handlers of OdinRoom | Creates and destroys one FMODPlaybackComponent per remote decoder |
FMODPlaybackComponent | OdinDecoder | Pops decoded audio from a MediaDecoder and plays it as an FMOD user stream |
Scene setup
- Add the
OdinInstanceprefab fromPackages/io.fourplayers.odin/Runtime/to your scene, as described in the High Level API manual. - Add an OdinEncoder component to the same GameObject and assign
the
OdinRoomto itsRoomfield. LeaveAudio Providerempty, the FMOD reader pushes audio itself. - Add the
FMODMicrophoneReaderscript and assign theOdinEncoderto itsEncoderfield. - Add the
FMODOdinPlaybackscript and assign theOdinRoomto itsRoomfield. - On the
OdinRoomcomponent, change the event wiring in the Inspector. TheOdinInstanceprefab of the 2.x SDK ships with default listeners on these events that create UnityAudioSourceplayback, and the FMOD scripts have to take their place:OnDecoderAdded: replace theDecoderAddedPeerCreateComponentlistener withFMODOdinPlayback.OnDecoderAdded.OnDecoderRemoved: replace theDecoderRemovedPeerRemoveComponentlistener withFMODOdinPlayback.OnDecoderRemoved.OnPeerLeft: keepPeerLeftRemoveComponentand addFMODOdinPlayback.OnPeerLeftas a second listener.- Keep
OnPeerJoinedas it is. It creates one GameObject with anOdinPeer
component per remote peer, and the FMOD playback objects are parented to them.
- Leave
Auto Create Mediaon theOdinRoomenabled. The room then creates a decoder for a peer as soon as the first audio from that peer arrives and raisesOnDecoderAdded, which is exactly the event the playback manager listens to. - Join the room as usual, for example by setting
OdinRoom.Token
.
The scripts use the FMOD Core API through FMODUnity.RuntimeManager.CoreSystem, so they work with or without an FMOD
Studio project. They only need the FMOD for Unity plugin to be installed and initialised.
FMODMicrophoneReader
The OdinMicrophoneReaderFMODMicrophoneReader script replaces
the
Usage
- Add an
OdinEncoderto theOdinInstanceGameObject and assign theOdinRoomto it. - Add the
FMODMicrophoneReaderscript and assign theOdinEncoderto itsEncoderfield. - Optionally change
Device Idto record from a device other than the system default. The index refers to FMOD's record driver list, seeSystem.getRecordNumDrivers.
The reader configures the encoder to the recording format of the device, so the Samplerate and Stereo fields of
the OdinEncoder do not need to be set by hand.
The script does not handle recording devices that are plugged in or removed at runtime. FMOD reports those changes
through SYSTEM_CALLBACK_TYPE.RECORDLISTCHANGED, which is a good starting point if you need device switching. If you'd
like to see extensions to this script, feel free to join our Discord server and let us know.
Full script
How it works
To read data from the microphone using FMOD, we'll need to perform the following steps:
- Set up and create an
FMOD.Soundobject, into which FMOD can store the microphone input data. - Start the microphone recording and tell the
OdinEncoderwhich format to expect. - Continually read the FMOD microphone data and push it to the ODIN servers.
1. Setup
The setup is performed in Unity's Start() method.
Retrieve Microphone Info
You need to retrieve details about the microphone, such as the sampling rate and the number of channels. We'll use this info to configure the FMOD recording sound in the next step and the ODIN encoder later on.
FMOD recommends recording at the device's native rate. With any other rate FMOD allocates a resampler, which adds latency. ODIN encodes mono or stereo, so the script refuses devices with more than two channels.
Configure Recording Sound Info
After obtaining the input device details, the next action is to set up the CREATESOUNDEXINFO object. This object
carries essential metadata that FMOD needs for audio capture.
We use SOUND_FORMAT.PCMFLOAT because ODIN takes float samples. This avoids the need for audio data conversions
later on. The length is set to capture one second of audio, which is the ring buffer the recording loops over.
Create Recording Sound
To hold the captured audio, FMOD requires us to create an FMOD Sound object.
MODE.OPENUSER tells FMOD that we provide the format ourselves and MODE.LOOP_NORMAL lets the recording wrap
around once the sound is full.
2. Recording
At this point, we're ready to start capturing audio. To do so, call the recordStart method from FMOD's core system
and remember the length of the sound in PCM frames, which we need to wrap our read position.
Configure the encoder
This is the part that is new in 2.x. The OdinEncoder creates its native
MediaEncoder when the room fires OnRoomJoined, using its Samplerate and Stereo fields. The reader sets both
fields to the recording format before that happens.
If the room was already joined when the reader started, the encoder already exists with the default 48 kHz mono
format. Disabling and re-enabling the OdinEncoder unlinks that encoder and creates a new one with the updated
fields.
3. Continually push microphone data
In the Update() method, we manage the ongoing capture of audio data from the FMOD microphone and its transmission
to the ODIN servers.
Find out how much was recorded
getRecordPosition returns the write position of the recording in PCM frames. The difference to our read position
is the amount of new audio, taking the wrap-around of the ring buffer into account.
If the game stalled, for example during a scene load, the ring buffer contains audio that is far too old to send. The script skips everything older than 200 ms instead of pushing a burst of stale voice.
Push fixed-size chunks
In 2.x the encoder pushes the whole array it receives, there is no separate length parameter anymore. The script therefore reads chunks of a fixed size (20 ms by default) into a reusable buffer and pushes them one by one until less than one chunk is left.
OdinMicrophoneReaderOdinEncoder.PushAudio has the same signature as the OnAudioData event of
the isSilent flag is
always false.
Read Microphone Data
Microphone data is read from the FMOD sound object with Sound.lock and copied into the chunk buffer with
Marshal.Copy.
It's crucial to be aware of the unit differences between FMOD, ODIN and Marshal.Copy. FMOD expects the lock offset
and length in bytes, where one frame is sizeof(float) times the channel count. ODIN and Marshal.Copy work in
samples. When the requested range crosses the end of the ring buffer, FMOD returns the remainder in the second
pointer, so both parts have to be copied.
FMODOdinPlayback
The OdinRoomFMODOdinPlayback script replaces the default decoder handling of
the OnDecoderAdded by
adding an OdinDecoder with an AudioSource to the peer's GameObject.
This script creates an FMODPlaybackComponent instead.
Usage
- Add the
FMODOdinPlaybackscript to theOdinInstanceGameObject and assign theOdinRoom. - Rewire the
OnDecoderAdded,OnDecoderRemovedandOnPeerLeftevents of theOdinRoomas described in Scene setup. - Optionally assign a prefab with an
FMODPlaybackComponenttoPlayback Prefab, for example to configure 3D playback or to attach your own components.
Full script
How it works
Both OnDecoderAdded and OnDecoderRemoved are Unity events of the OdinRoom. The room raises them on the main
thread, so the handlers can create and destroy GameObjects directly.
The event arguments carry the peer id and the media id of the decoder. With Auto Create Media enabled, the room
creates one default decoder per peer with the media id OdinRoom.AutoDecoderMediaId, so there is exactly one
playback object per remote peer. The handler looks the native decoder up in the wrapper room and hands it to the
playback component.
The playback object is parented to the GameObject that OnPeerJoined created for the peer. When the peer leaves,
the default PeerLeftRemoveComponent listener destroys that GameObject, and Unity destroys the playback object with
it. The OnPeerLeft handler of this script covers the case where you removed the default listener or keep your
peer objects elsewhere.
The room owns the MediaDecoder. It disposes the decoder when the peer leaves or the room closes, so the playback
component never disposes it. Once the decoder is gone, MediaDecoder.Pop returns false and the playback component
hands FMOD silence until it is destroyed.
FMODPlaybackComponent
The FMODPlaybackComponent script replaces the OdinDecoder. It creates
an FMOD user stream whose read callback pops decoded audio from the ODIN MediaDecoder, and plays that stream on
the master channel group.
Usage
The script is created at runtime by FMODOdinPlayback. If you want to customise it, build a prefab with the
component attached and assign it to Playback Prefab:
- Enable
Spatialto play the voice as a 3D sound. The component then updates the FMOD channel with the position of its GameObject every frame, so put the prefab or the peer object where the player's avatar is. FMOD needs aStudioListenerin the scene for 3D playback. - Lower
Read Millisecondsto reduce playback latency, at the cost of more callbacks.
Full script
How it works
To play ODIN audio through FMOD we need to perform the following steps:
- Create an FMOD user stream that matches the decoder format.
- Provide audio in the stream's read callback by popping it from the decoder.
- Release everything when the playback is stopped or destroyed.
1. Create the stream
The MediaDecoder reports its Samplerate and Stereo flag, so the FMOD sound is created with exactly that format
and no conversion is needed. With the audio engine disabled, the room creates its decoders at 48 kHz mono.
decodebuffersize is the number of PCM frames FMOD requests per read callback. This value sets the playback
latency, because FMOD buffers that much audio ahead of the mixer. The 1.x sample left this at FMOD's default, which
added roughly 500 ms of latency. With a 20 ms decode buffer the added latency stays in the range of a normal FMOD
stream.
The sound is created with MODE.OPENUSER | MODE.CREATESTREAM | MODE.LOOP_NORMAL. OPENUSER makes FMOD take the
format from the CREATESOUNDEXINFO, CREATESTREAM makes FMOD pull audio through the read callback instead of loading
it once, and LOOP_NORMAL keeps the stream running past the one second length. Then it is simply played on the
master channel group with playSound.
2. Fill the stream
FMOD invokes the read callback from its own stream thread, not from the Unity main thread. This has two consequences for the code:
- The callback has to be a static method marked with
AOT.MonoPInvokeCallback, otherwise IL2CPP builds crash when FMOD calls into managed code. The component instance is found through the raw sound handle FMOD passes in, which is whyPlayregisters the sound in a static dictionary. - Unity API calls are off limits inside the callback. The callback only touches the decoder and the FMOD buffer.
Fill pops exactly the requested number of samples from the decoder and copies them into FMOD's buffer. Popping
from a second thread is safe: the room pushes incoming datagrams into the decoder from a network thread while
the OdinDecoder pops on the main thread in the default setup, so the
native decoder is built for that.
Pop fills the whole array and returns a silence flag, which you could use to drive a talking indicator. When the
decoder has no audio, for example because the peer is muted, the popped buffer contains silence, so FMOD always gets
a full buffer. When Pop fails, the decoder was disposed and the callback writes zeros instead.
3. Clean up
Stop removes the sound from the lookup dictionary first, so a callback that is already running cannot find the
component anymore, then stops the channel and releases the sound. OnDestroy calls Stop, so destroying the
GameObject is all FMODOdinPlayback has to do.
Limitations and pitfalls
- Two Unity warnings on startup with SDK versions before 20206.1.0.
Audio system is disabled, so AudioSettings.outputSampleRate cannot be queriedappears once fromOdinRoom.Awakeand once fromOdinEncoder.Awake. Older SDKs read that property to detect the disabled engine and fall back to 48 kHz, and Unity logs the warning on every read. Since 20206.1.0 the SDK detects the state without the warning. - Rewire the room events. If the
OdinRoomstill callsDecoderAddedPeerCreateComponent, every remote peer gets anOdinDecoderthat logscannot create a playback clip while the Unity audio engine is disabledand stays silent. That message means the default handler is still wired. - The encoder format is fixed at creation. The
OdinEncodercreates itsMediaEncoderonOnRoomJoined. The reader sets the format inStart, which runs before the room joins as long as both components are in the scene from the beginning. For readers added at runtime the script recreates the encoder by toggling the component. - Device changes are not handled. Neither script reacts to a recording or playback device being plugged in or
removed. FMOD reports record device changes through
SYSTEM_CALLBACK_TYPE.RECORDLISTCHANGED. - One reader per encoder. To send the same microphone into several rooms, add one
OdinEncoderper room and callPushAudioon each of them, or push throughOdinRoom.ProxyAudiowhen all rooms use the same format. - Positional culling is separate from 3D playback.
Spatialon the playback component only affects how FMOD renders the voice. To let the ODIN server cull peers by distance, set positions on the encoder withMediaEncoder.SetPositionas described in the manual. - Effects still work. Components like
OdinVadComponentorOdinVolumeBoostComponentoperate on the native pipeline of the encoder or decoder, not on Unity audio. Add them to theOdinEncoderwithAddEffect, or set theirMediato theMediaDecoderof a playback component.