Playing audio
Handles¶
Handles represent an instance to specific sound object. There are many handle types, each of them corresponding to a sound object type.
// Sounds
SoundHandle backgroundSound = amEngine->GetSoundHandle("bg_forest");
// Collections
CollectionHandle gunFires = amEngine->GetCollectionHandle("ak47_gunfires");
// Switch Containers
SwitchContainerHandle footsteps = amEngine->GetSwitchContainerHandle("footsteps");
In the case you don't care about your sound object type and want a generic sound object handle, you can do it by using the GetSoundObjectHandle() method. The returned handle will be one of the previous ones, according to the type of sound object you are querying.
// Get a generic sound handle
SoundObjectHandle handle = amEngine->GetSoundObjectHandle("dialogue_01");
Note
When using the GetSoundObjectHandle() method, Amplitude will scan your assets in this order: Sounds, then Collections, and then Switch Containers.
It's always safe to check if your handle is valid before using it. Once you get your handle, you can check for its validity by comparing it to the AM_INVALID_HANDLE macro, or by directly using the AM_IS_VALID_HANDLE() macro function.
// Get a handle
auto handle = amEngine->GetSoundHandle("footsteps"); // This is instead a switch container, but we are querying it as a sound, which will return an invalid handle
if (handle == AM_INVALID_HANDLE)
amLogError("The returned handle is invalid");
// Same as doing
if (!AM_IS_VALID_HANDLE(handle))
amLogError("The returned handle is invalid");
Channels¶
Each time you play a sound object, you have a Channel that will help you manage the playback of that sound object.
To play an audio, you need either its name, its ID, or a handle to its sound object instance, and then use any overrides of the Play() method in the Engine
// Using the name
Channel sound = amEngine->Play("dialogue_01");
// Using the ID
Channel sound = amEngine->Play(1234);
// Using a handle
SoundHandle handle = amEngine->GetSoundHandle(1234); // or amEngine->GetSoundHandle("dialogue_01")
Channel sound = amEngine->Play(handle);
Tip
Playing a sound object using a handle is always faster than using its name or its ID. Thus, it's recommended to prefer using handles for sound objects you plan to frequently play.
For sound sources configured with Position spatialization enabled, you can play them in the world by giving an initial location:
// Play a sound source at a given position
Channel sound = amEngine->Play("voice_01", AM_V3(10, 20, 30));
The given position can be updated later in realtime using the Channel API.
Note
For sound sources configured with any other spatialization other than Position, you should use an Entity to play them. Read the Managing Game Objects documentation to learn more.
When you obtain a Channel for a sound object, it's recommended to check if the returned Channel is valid before using it.
// Get a channel
Channel sound = amEngine->Play(1234);
// Check if the channel is valid
if (!sound.Valid())
amLogError("The returned channel is not valid");
Info
Amplitude may return an invalid Channel when the maximum number of channels is exceeded, or the requested sound object was not found in any loaded sound bank.
Using a Channel, you can manage the playback state and other sound object properties:
// Check whether the sound object in the channel is currently playing
bool isPlaying = channel.Playing();
// Stop the playback
channel.Stop();
// Pause the playback
channel.Pause();
// Resume the playback
channel.Resume();
// Get the current location of the sound object
AmVector3 location = channel.GetLocation();
// Set the sound object location
channel.SetLocation(newLocation);
// Get the current playback state of the channel
eChannelPlaybackState state = channel.GetPlaybackState(); // eChannelPlaybackState_Playing, _Paused, _Stopped, _FadingIn, _FadingOut, _SwitchingState, or _Pending
API Reference available
Check out the API Reference to see the complete list of methods you can use with a Channel.
Seeking and Scheduling¶
A Channel can report and change its own playback position, and align playback actions to the engine's audio clock for sample-accurate timing:
// Get the current playback position, in milliseconds
AmTime position = channel.GetPlaybackPosition();
// Seek to a new position, in milliseconds
channel.SetPlaybackPosition(5000.0);
Stop(), Pause(), Resume(), and SetPlaybackPosition() each have an overload that takes a clock parameter instead of acting immediately: the action is scheduled to take effect at that exact frame of Engine::GetAudioClock(), rather than on whichever frame the game thread happens to call it from.
// Schedule a seek to land in exactly half a second
AmUInt64 clock = amEngine->GetAudioClock() + amEngine->GetAudioClockRate() / 2;
channel.SetPlaybackPosition(0.0, clock);
// Schedule a stop with a fade, at the same clock frame
channel.Stop(kMinFadeDuration, clock);
A freshly played channel can also be scheduled to start at an exact clock frame, as long as it hasn't started rendering yet:
Channel sound = amEngine->Play("countdown_beep");
sound.ScheduleStart(amEngine->GetAudioClock() + amEngine->GetAudioClockRate()); // Starts in exactly one second.
Playback Events¶
Amplitude allows you to register callbacks for notable playback events within a Channel:
// Add a callback when the playback has started
channel.On(eChannelEvent_Begin, [](ChannelEventInfo info) {
amLogDebug("Playback started");
});
// Add a callback when the playback is paused
channel.On(eChannelEvent_Pause, [](ChannelEventInfo info) {
amLogDebug("Playback paused");
});
// Add a callback when the playback is resumed
channel.On(eChannelEvent_Resume, [](ChannelEventInfo info) {
amLogDebug("Playback resumed");
});
// Add a callback when the playback is stopped
channel.On(eChannelEvent_Stop, [](ChannelEventInfo info) {
amLogDebug("Playback stopped");
});
// Add a callback when the playback has looped
channel.On(eChannelEvent_Loop, [](ChannelEventInfo info) {
amLogDebug("Playback looped");
});
// Add a callback when the playback has ended
channel.On(eChannelEvent_End, [](ChannelEventInfo info) {
amLogDebug("Playback ended");
});
Delivery timing
Events fire when the voice reports them, or on the next frame — never synchronously at the moment the thing they describe actually happens. There is no way to unregister a callback once added, so only register callbacks whose lifetime you control. Any userData you pass must outlive the channel's Stop event, including the Stop that Engine::Deinitialize() raises for channels still playing.
API Reference available
You can also pass arbitrary data to the method and access it in the callback through info.m_userData. Check out the Channel API Reference and the ChannelEventInfo API Reference to learn more.
Multi-Position Channels¶
A Channel can play from more than one world position at once — useful for a sound that should be heard from several places simultaneously, such as a river running through a level, or a bank of identical, independently-timed machines.
// Enable instancing before adding any instance
channel.EnableInstancing(eChannelInstanceMode_Blended);
Two modes are available:
| Mode | Behavior | Example |
|---|---|---|
eChannelInstanceMode_Blended | All instances share one playback cursor; the sound is perceived from every position at once, with weighted-blend attenuation. | A river heard from multiple points along its path. |
eChannelInstanceMode_Separate | Each instance has its own playback cursor, so the same sound plays independently and out of sync at each position. | Multiple torches sharing a fire sound, unsynchronized. |
Add and remove positions with AddInstance() / RemoveInstance():
ChannelInstance a = channel.AddInstance({ 0.0f, 0.0f, 0.0f });
ChannelInstance b = channel.AddInstance({ 50.0f, 0.0f, 0.0f });
// ... later
channel.RemoveInstance(a.GetId());
// Or remove every instance at once
channel.ClearInstances();
Each ChannelInstance has its own location, and can override the orientation, obstruction, occlusion, room, and (in Blended mode) the attenuation weight it would otherwise inherit from the channel's entity:
// Give this instance its own orientation instead of following the entity's
instance.SetOrientation(Orientation({ 0.0f, 0.0f, -1.0f }, { 0.0f, 1.0f, 0.0f }));
// Give it its own obstruction and occlusion, independent of the entity
instance.SetObstruction(0.3f);
instance.SetOcclusion(0.0f);
// Put this instance in a specific room, independent of the entity's room tracking
instance.SetRoom(amEngine->GetRoom(2));
// Revert any of these back to following the entity
instance.ResetOrientation();
instance.ResetObstruction();
instance.ResetOcclusion();
instance.ResetRoom();
// Weight this instance's contribution in Blended mode (default 1.0)
instance.SetWeight(0.5f);
Info
In Blended mode, obstruction, occlusion, and room are averaged across every instance, weighted by how much each one contributes to what the listener actually hears. In Separate mode, each instance is rendered — and so obstructed, occluded, and roomed — on its own.
API Reference available
Check out the Channel and ChannelInstance API references for the complete list of methods.