ODIN is a versatile cross-platform Software Development Kit (SDK) engineered to seamlessly integrate real-time voice chat into multiplayer games, applications, and websites. Regardless of whether you're employing a native application or your preferred web browser, ODIN simplifies the process of maintaining connections with your significant contacts. Through its intuitive interface and robust functionality, ODIN enhances interactive experiences, fostering real-time engagement and collaboration across various digital platforms.
You can choose between a managed cloud and a self-hosted solution. Let 4Players GmbH deal with the setup, administration and bandwidth costs or run our server software on your own infrastructure allowing you complete control and customization of your deployment environment.
The current release of ODIN is shipped with native pre-compiled binaries for the following platforms:
Platform | x86_64 | aarch64 | x86 | armv7a |
---|---|---|---|---|
Windows | ✅ | ✅ | ✅ | |
Linux | ✅ | ✅ | ✅ | |
macOS | ✅ | ✅ | ||
Android | ✅ | ✅ | ✅ | ✅ |
iOS | ✅ | ✅ |
Support for gaming consoles is available upon request and covers Microsoft Xbox, Sony PlayStation 4/5 and Nintendo Switch.
If your project requires support for any additional platform, please contact us.
To check out the SDK for development, clone the git repo into a working directory of your choice.
This repository uses LFS (large file storage) to manage pre-compiled binaries. Note that a standard clone of the repository might only retrieve the metadata about these files managed with LFS. In order to retrieve the actual data with LFS, please follow these steps:
-
Clone the repository:
git clone https://github.com/4Players/odin-sdk.git
-
Cache the actual LFS data on your local machine:
git lfs fetch
-
Replaces the metadata in the binary files with their actual contents:
git lfs checkout
The following code snippet illustrates how to join a designated room on a specified server using a room token acquired externally:
#include <stdio.h>
#include "odin.h"
int main(int argc, const char *argv[])
{
odin_initialize(ODIN_VERSION);
OdinRoomEvents events = {
.on_datagram = NULL,
.on_rpc = NULL,
.user_data = NULL
};
OdinRoom *room;
odin_room_create(pool, "<SERVER_URL>", "<TOKEN>", &events, NULL, &room);
getchar();
odin_room_free(room);
odin_shutdown();
return 0;
}
To enter a room, an authentication token is requisite. ODIN employs the creation of signed JSON Web Tokens (JWT) to ensure a secure authentication pathway. These tokens encapsulate the details of the room(s) you wish to join alongside a customizable identifier for the user, which can be leveraged to reference an existing record within your specific service.
OdinTokenGenerator *generator;
odin_token_generator_create("<ACCESS_KEY>", &generator);
char token[512];
odin_token_generator_sign(generator, payload, token, sizeof(token));
odin_token_generator_free(generator);
The minimal payload of an ODIN token looks like this:
As ODIN is fully user agnostic, 4Players GmbH does not store any of this information on its servers.
Tokens are signed employing an Ed25519 key pair derived from your distinctive access key. Think of an access key as a singular, unique authentication credential, crucial for generating room tokens to access the ODIN server network. It essentially combines the roles of a username and password into a singular, unobtrusive string of characters, necessitating a comparable degree of protection. For bolstered security, it is strongly recommended to refrain from incorporating an access key in your client-side code. We've created a very basic Node.js server here, to showcase how to issue ODIN tokens to your client apps without exposing your access key.
ODIN uses connection pooling under the hood to manage all network communication and event routing between your application and the ODIN server infrastructure. Multiple rooms automatically share the same underlying pool, but you no longer need to manage it manually - everything is handled transparently.
To get started, define your callbacks for incoming data and control messages:
void on_datagram(OdinRoom* room, const OdinDatagramProperties* props, const uint8_t* bytes, uint32_t bytes_length, void* user_data);
void on_rpc(OdinRoom* room, const char* json, void* user_data);
Then set up the room events and create a room:
OdinRoomEvents events = {
.on_datagram = &on_datagram, // handles incoming voice data
.on_rpc = &on_rpc, // handles signaling and server events
.user_data = &state // user-defined context passed to callbacks
};
OdinRoom* room;
odin_room_create("<GATEWAY_URL>", "<TOKEN>", &events, nullptr, &room);
The authentication
parameter can be provided in two forms: either as a raw JWT string or as a serialized JSON object containing additional fields such as the room ID, channel masks and peer user data. Using the JSON object form can be convenient if you want to include optional metadata or audio channel configuration alongside the token.
Once created, the room automatically connects and joins in the background. Any additional rooms you create will reuse the same underlying connection pool without extra configuration, so you can focus entirely on your application logic.
Voice data is delivered through the on_datagram
callback. Each incoming packet includes metadata such as the source peer ID and channel mask, which you can use to route the data to the correct ODIN decoder. A typical implementation looks like this:
void on_datagram(OdinRoom* room, const OdinDatagramProperties* properties, const uint8_t* bytes, uint32_t bytes_length, void* user_data) {
const auto state = reinterpret_cast<State*>(user_data);
assert(state->room.get() == room);
if (auto it = state->decoders.find(properties->peer_id); it != state->decoders.end()) {
odin_decoder_push(it->second.ptr.get(), bytes, bytes_length);
}
}
Control and event messages are delivered through the on_rpc
callback. These messages are sent as plain UTF-8 encoded JSON strings. This makes it straightforward to inspect and parse events using your favorite JSON library, such as nlohmann::json, without any additional decoding steps.
#include <nlohmann/json.hpp>
using namespace nlohmann;
void on_rpc(OdinRoom* room, const char* json_text, void* user_data) {
const auto state = reinterpret_cast<State*>(user_data);
assert(state->room.get() == room);
try {
json rpc = json::parse(json_text);
std::cout << "event: " << rpc.dump() << std::endl;
} catch (const std::exception& err) {
std::cerr << "error: " << err.what() << std::endl;
}
}
Events follow a simple object format where the event name is the top-level key, for example:
{ "PeerJoined": { "peer_id": 1234, "user_id": "Player A" } }
This structure makes it easy to pattern-match on event names and handle them accordingly.
ODIN separates audio transmission into two core components: encoders for outgoing streams and decoders for incoming ones. These components handle real-time audio conversion and transport using a shared audio pipeline interface. Internally, each encoder/decoder wraps an Opus codec for compression/decompression as well as an ingress or egress resampler for automatic sample rate conversion. This design provides high performance, low latency and flexible hooks for integrating features like Voice Activity Detection (VAD), audio enhancements (APM) or custom effects.
Unlike earlier versions, there are no separate media objects anymore. Instead, applications are expected to create encoders and decoders as needed - for example, one encoder for the local peer and one decoder per remote peer. It’s also possible to transmit voice on multiple audio channels within the same room simultaneously. In theses cases, the on_datagram
callback provides the corresponding channel mask so you can decide how to route or process incoming audio.
An encoder transforms raw PCM audio captured from a local source (e.g., microphone) into compressed ODIN datagrams for transmission.
OdinEncoder* encoder;
odin_encoder_create(peer_id, sample_rate, stereo, &encoder);
const OdinPipeline* pipeline = odin_encoder_get_pipeline(encoder);
Push captured audio samples into the encoder. Samples must be interleaved 32-bit floats in the range [-1.0, 1.0]
:
odin_encoder_push(encoder, samples, sample_count);
Poll for encoded datagrams and send them to the room:
for (;;) {
uint8_t datagram[2048];
uint32_t datagram_length = sizeof(datagram);
switch (odin_encoder_pop(encoder, datagram, &datagram_length)) {
case ODIN_ERROR_SUCCESS:
CHECK(odin_room_send_datagram(room, datagram, datagram_length));
break;
case ODIN_ERROR_NO_DATA:
return; // no more packets ready
default:
std::cerr << "failed to encode audio datagram" << std::endl;
}
}
Note: You can optionally assign 3D positions per channel mask on the encoder to enable positional audio.
A decoder processes datagrams received from remote peers. Typically, you’ll create one decoder per peer, which you update in your on_datagram
callback.
OdinDecoder* decoder;
odin_decoder_create(sample_rate, stereo, &decoder);
const OdinPipeline* pipeline = odin_decoder_get_pipeline(decoder);
Push received datagrams into the decoder:
odin_decoder_push(decoder, bytes, length);
Retrieve decoded audio for playback:
float samples[FRAME_SIZE];
bool is_silent;
odin_decoder_pop(decoder, samples, FRAME_SIZE, &is_silent);
Note: The on_datagram
callback provides the channel mask used for each packet, allowing you to handle multi-channel or spatialized setups efficiently.
Each encoder and decoder in ODIN exposes an internal audio pipeline - a flexible and extensible processing chain that allows real-time manipulation of audio streams. Effects are applied in sequence to all audio flowing through the encoder or decoder. The pipeline automatically manages effect ordering internally, ensuring a consistent and logical execution flow.
The VAD module helps determine when a user is actively speaking. It provides two independent mechanisms:
- Voice Activity Detection:
When enabled, ODIN will analyze the audio input signal using smart voice detection algorithm to determine the presence of speech. You can define both the probability required to start and stop transmitting. - Voice Volume Gate:
When enabled, the volume gate will measure the volume of the input audio signal, thus deciding when a user is speaking loud enough to transmit voice data. You can define both the root mean square power (dBFS) for when the gate should engage and disengage.
uint32_t vad_id;
odin_pipeline_insert_vad_effect(pipeline, index, &vad_id);
OdinVadConfig config = {
.voice_activity = {
.enabled = true,
.attack_threshold = 0.9f,
.release_threshold = 0.8f
},
.volume_gate = {
.enabled = true,
.attack_threshold = -30,
.release_threshold = -40
}
};
odin_pipeline_set_vad_config(pipeline, vad_id, &config);
The APM module uses a variety of smart enhancement algorithms for enhancing audio clarity in noisy environments. It provides the following features:
- Acoustic Echo Cancellation (AEC):
When enabled the echo canceller will try to subtract echoes, reverberation, and unwanted added sounds from the audio input signal. Note, that you need to process the reverse audio stream, also known as the loopback data to be used in the ODIN echo canceller. - Noise Suppression:
When enbabled, the noise suppressor will remove distracting background noise from the input audio signal. You can control the aggressiveness of the suppression. Increasing the level will reduce the noise level at the expense of a higher speech distortion. - High-Pass Filter (HPF):
When enabled, the high-pass filter will remove low-frequency content from the input audio signal, thus making it sound cleaner and more focused. - Transient Suppression:
When enabled, the transient suppressor will try to detect and attenuate keyboard clicks. - Automatic Gain Control (AGC):
When enabled, the gain controller will bring the input audio signal to an appropriate range when it's either too loud or too quiet.
uint32_t apm_id;
odin_pipeline_insert_apm_effect(pipeline, index, playback_sample_rate, stereo, &apm_id);
OdinApmConfig config = {
.echo_canceller = true,
.high_pass_filter = true,
.transient_suppressor = true,
.noise_suppression_level = ODIN_NOISE_SUPPRESSION_LEVEL_MODERATE,
.gain_controller_version = ODIN_GAIN_CONTROLLER_VERSION_V2
};
odin_pipeline_set_apm_config(pipeline, apm_id, &config);
You can also push loopback (reverse) audio into the pipeline to support echo cancellation:
odin_pipeline_update_apm_playback(pipeline, apm_id, reverse_samples, sample_count, delay_ms);
ODIN allows you to define and insert your own audio processing functions. These run inline on the sample stream:
void my_effect_callback(float *samples, uint32_t sample_count, bool *is_silent, const void *user_data) {
// modify samples or observe silence state
}
uint32_t effect_id;
odin_pipeline_insert_custom_effect(pipeline, index, my_effect_callback, user_data, &effect_id);
You can remove or reorder effects dynamically as needed. All modifications are thread-safe and take effect immediately.
Each peer in an ODIN room can carry a custom, user-defined user data payload, represented as a JSON object. This data is automatically synchronized across all connected peers and provides a flexible way to share peer-specific metadata such as usernames, avatars, roles, team assignments, mute flags or gameplay state.
You can set the initial user data in the authentication JSON when creating or joining a room:
nlohmann::json authentication = {
{"token", "<JWT>"},
{"user_data", {
{"name", "Player A"},
{"team", "red"}
}}
};
odin_room_create("<GATEWAY_HOST>", authentication.dump().data(), &events, nullptr, &room);
Once connected, you can update the local peer’s user data dynamically at any time by sending a client command through RPC:
nlohmann::json command = {
{"ChangeSelf", {
{"user_data", {
{"name", "Player A"},
{"team", "red"},
{"status", "ready"}
}}
}}
};
odin_room_send_rpc(room, command.dump().data());
All peers in the room will receive the updated user data automatically via the PeerChanged
event. This makes it simple to keep in-game state or UI elements synchronized without additional signaling layers.
ODIN provides built-in support for proximity-based voice communication through a combination of channel masks and per-channel positions. Each room supports up to sixty-four audio channels, represented by a 64-bit channel mask. Peers can transmit on one or more channels at the same time and each receiver decides exactly which peers and channels to hear by adjusting its masks. Every channel can also carry its own 3D position, which the server uses to perform automatic distance culling within a unit sphere radius of 1.0
. By scaling your world coordinates into this unit sphere before sending, you can implement scalable, spatially-aware voice systems without manually filtering streams on the client.
ODIN provides a powerful channel mask system to determine which peers and channels you receive audio from. Each peer maintains a global/server mask, identified by PeerId::SERVER
(which corresponds to 0
in the public API) and a set of per-peer masks. When deciding whether to forward audio from a given peer and channel, the server takes the bitwise AND of the global mask and the specific peer’s mask. By default, both are set to FULL
, meaning you will receive all audio from all peers.
Clearing the global mask switches the behavior into a more restrictive whitelist mode: since 0 & mask = 0
, no audio is received unless you explicitly add per-peer masks with non-zero bits. This gives you precise control over who is audible at any given time without starting or stopping individual media streams.
You can specify initial masks in the authentication JSON when joining a room. The following example clears the global mask and enables channels 1, 3 and 5 for peer 1234
:
nlohmann::json authentication = {
{"token", "<JWT>"},
{"channel_masks", {
{0, 0x00}, // global/server mask
{1234, 0x15} // 0b00010101 → channels 1, 3, 5
}}
};
odin_room_create("<GATEWAY_HOST>", authentication.dump().data(), &events, nullptr, &room);
Masks can also be updated dynamically at runtime by sending a JSON command. The SetChannelMasks
command lets you modify the global and per-peer masks at any time. The reset
field controls whether the new masks replace or extend the existing configuration. When reset
is set to true
, the server clears all previously stored masks for that peer before applying the new set. When reset
is false
(or omitted), the new entries are additive, which means that they are merged into the existing configuration, allowing you to incrementally enable or adjust individual peers and channels without affecting others.
nlohmann::json command = {
{"SetChannelMasks", {
{"masks", {
{0, 0x00},
{1234, 0x06} // 0b00000110 → channels 2, 3
}},
{"reset", true} // replace all existing masks
}}
};
odin_room_send_rpc(room, command.dump().data());
This makes it easy to switch between fully redefining the audio routing configuration and making small, targeted changes on the fly.
Each encoder can publish per-channel 3D positions, which the server uses to apply distance culling and spatialization for receivers. You can update your position at any time by calling odin_encoder_set_position() with the appropriate channel mask:
OdinPosition pos = {playerXScaled, playerYScaled, playerZScaled};
odin_encoder_set_position(encoder, 0x01 | 0x04, &pos);
By default, new ODIN encoders publish on channel 1 at the origin. This is represented internally by a single position entry for channel 1 (0x01
). If you want to publish on multiple channels, call odin_encoder_set_position()
with the corresponding bitmask. If you clear all positions, the encoder will stop publishing on those channels entirely.
Use odin_encoder_clear_position()
to dynamically remove channel positions when a channel becomes inactive. Once cleared, the server will stop using positional data for those channels and no audio will be published on them until a new position is set.
odin_encoder_clear_position(encoder, 0x01);
The position is automatically sent with every outgoing audio datagram while the encoder is transmitting. When the encoder is silent, it will send background position updates periodically if you configured an update interval when creating the encoder with odin_encoder_create_ex()
. If no interval was set, positions are only transmitted while speaking. For example, to configure an encoder that sends background position updates twice per second when not talking:
uint64_t update_position_interval_ms = 500;
OdinEncoder* encoder;
odin_encoder_create_ex(peer_id, sample_rate, stereo, application_voip, bitrate_kbps, expected_packet_loss_perc, position_update_interval_ms, &encoder);
Background updates allow the server to continue culling and updating spatial relationships even when the user is silent, which is particularly useful in open-world or large multiplayer environments where players may move frequently without constantly transmitting audio.
ODIN allows you to send arbitrary, application-defined messages between peers in the same room. Messages are delivered reliably and in order over the signaling channel, making them ideal for gameplay events, chat messages, state synchronization or any other non-audio communication. The payload format is entirely up to you - it can be raw bytes, JSON, Protocol Buffers or anything else you choose.
To send a message, construct a JSON command and pass it to odin_room_send_rpc()
. If you provide a list of peer_ids
, the message is sent to those peers only. If you omit the list, the message is broadcast to all connected peers.
nlohmann::json command = {
{"SendMessage", {
{"message", nlohmann::json::binary_t(message_data.begin(), message_data.end())},
{"peer_ids", target_peer_ids} // optional
}}
};
odin_room_send_rpc(room, command.dump().data());
Messages are received through the on_rpc
callback, where they arrive as standard JSON objects containing the binary payload. Unlike voice data, messages are not affected by proximity or channel masks. Instead, they are always delivered to the intended recipients, regardless of their position or audio configuration.
ODIN supports end-to-end encryption (E2EE) through the use of a pluggable OdinCipher
module. This enables you to secure all datagrams, messages and peer user data with a shared room password - without relying on the server as a trust anchor.
OdinCipher *cipher = odin_crypto_create(ODIN_CRYPTO_VERSION);
odin_crypto_set_password(cipher, (const uint8_t *)"secret", strlen("secret"));
OdinRoom *room;
odin_room_create("<GATEWAY_HOST>", "<JWT>", &events, cipher, &room);
The encryption system uses a master key derived from the password, then derives peer-specific session keys with random salts. These salts are exchanged in-room so that all participants can reconstruct each other's peer keys. The master key never leaves the client and there are no long-term keys stored or distributed. Keys are rotated automatically after every 1 million packets or 2 GiB of traffic. The system is designed to minimize passive and active compromise by external actors. If the password is kept secure, so is your data.
In addition to the latest binaries and C header files, this repository also contains a simple test client in the test
sub-directory. Please note, that the configure process will try to download, verify and extract dependencies (e.g. miniaudio), which are specified in the CMakeLists.txt
file. miniaudio is used to provide basic audio capture and playback functionality in the test client.
-
Create a build directory:
mkdir -p build && cd build
-
Generate build scripts for your preferred build system:
- For make ...
cmake ../
- ... or for ninja ...
cmake -GNinja ../
- For make ...
-
Build the test client:
- Using make ...
make
- ... or ninja ...
ninja
- Using make ...
On Windows, calling cmake
from the build directory will create a Visual Studio solution, which can be built using the following command:
msbuild odin_client.sln
The test client accepts several arguments to control its functions, but the following three options are particularly crucial for its intended purpose:
odin_client -r <room_id> -k <access_key> -s <server_url>
The -r
argument (or --room-id
) is used to specify the name of the room to join. If no room name is provided, the client will automatically join a room called default.
The -k
argument (or --access-key
) is used to specify an access key for generating tokens. If no access key is provided, the test client will auto-generate a key and display it in the console. An access key is a unique authentication key used to generate room tokens for accessing the 4Players ODIN server network. It is important to use the same access key for all clients that wish to join the same ODIN room. For more information about access keys, please refer to our documentation.
The -s
argument (or --server-url
) allows you to specify an alternate ODIN server address. This address can be either the URL to an ODIN gateway or an ODIN server. You may need to specify an alternate server if you are hosting your own fleet of ODIN servers. If you do not specify an ODIN server URL, the test client will use the default gateway, which is located at https://gateway.odin.4players.io.
Note: You can use the --help
argument to get a full list of options provided by the console client.
Contact us through the listed methods below to receive answers to your questions and learn more about ODIN.
Join our official Discord server to chat with us directly and become a part of the 4Players ODIN community.
Don’t use Discord or X? Send us an email and we’ll get back to you as soon as possible.