We don’t really have a good solution for this yet, but we are aware of this limitation and are still determining what a proper solution would be.
I think Transceivers are probably still the best way to go in the meantime, despite their low channel count.
The 16 channel limitation is inconvenient, but you can design around it- since you can transmit on the same channel with multiple transceivers, you can mix multiple transmitting receivers together with only a single channel, allowing you to have multiple sounds in a room and a single opening, or multiple openings using a single channel. With this approach, you only need 1 channel per audible room, and you can re-use these channels once other rooms become inaudible.
For example, here the listener can hear the audio from Room 1 and Room 2 through the transceivers receiving on channels one and two respectively:
Now if the listener moves into range of the portals in Room 3, it moves out of the range of Room 1’s portals, allowing us to re-use Channel 1 for Room 3’s audio:
With this approach, you would only ever need 16 channels if the user was within audible range of 16 distinct rooms.
As for the additional design time overhead of configuring all these transmitting transceivers, this can be mitigated by instead adding and removing transceiver DSPs to events at runtime. What you would need is something that:
- Checks whether there is line of sight to the listener.
- Checks whether there is line of sight to events on the other side of the portal.
- If there is line of sight to the listener and event, add transceivers to those events and play a receiving transceiver event at the position of the portal.
- Attenuate each transmitting transceiver based on its distance to the portal for added realism.
- Remove transceivers if line of sight to either the events or listener becomes broken.
Here is a basic script demonstrating how to to achieve this:
/**
This script demonstrates how to add transceivers to an event at runtime
to simulate openings within occluding geometry.
1. Create a looping event inside FMOD Studio containing a receiving
transceiver and position this event in the opening of your geometry.
2. Assign two GameObjects to the InnerSide and OuterSide fields.
These will represent the inner and outer side of your geometry's opening,
and will be used to determine if the listener or event emitters are in the
field of view of the opening.
3. Assign any event emitters in your room to the Source list.
*/
using System.Collections.Generic;
using UnityEngine;
public class DynamicTransceiverExample : MonoBehaviour
{
public GameObject InnerSide; // An object sitting inside the room
public GameObject OuterSide; // An object sitting outside the room
public int OcclusionLayer = 8; // The layer you are using for occlusion
public List<FMODUnity.StudioEventEmitter> Sources = new List<FMODUnity.StudioEventEmitter>();
private Dictionary<FMOD.GUID, FMOD.DSP> transceivers = new Dictionary<FMOD.GUID, FMOD.DSP>();
// Checks whether a Vector 3 is in the filed of view of the inner or outer side of the occluding geometry
private bool PositionInFOV(Vector3 position)
{
Color color = Color.red;
int occlusionMask = 1 << OcclusionLayer;
Vector3 sourcePosition = new Vector3(position.x, position.y, position.z);
// Test one side
float distanceToObject = Vector3.Distance(sourcePosition, InnerSide.transform.position);
bool inFOV = !Physics.Raycast(sourcePosition, InnerSide.transform.position - sourcePosition, out RaycastHit hit, distanceToObject, occlusionMask);
// Test other side
distanceToObject = Vector3.Distance(sourcePosition, OuterSide.transform.position);
inFOV |= !Physics.Raycast(sourcePosition, OuterSide.transform.position - sourcePosition, out hit, distanceToObject, occlusionMask);
if (inFOV)
{
color = Color.green;
}
Debug.DrawRay(sourcePosition, transform.position - sourcePosition, color);
return inFOV;
}
// Add a transceiver to an Event Emitter's event
private void AddTransceiver(FMODUnity.StudioEventEmitter source)
{
FMOD.RESULT result = source.EventInstance.getChannelGroup(out FMOD.ChannelGroup group);
if (result != FMOD.RESULT.OK) return;
result = FMODUnity.RuntimeManager.CoreSystem.createDSPByType(FMOD.DSP_TYPE.TRANSCEIVER, out FMOD.DSP transceiver);
result = transceiver.setParameterBool((int)FMOD.DSP_TRANSCEIVER.TRANSMIT, true);
result = group.addDSP(FMOD.CHANNELCONTROL_DSP_INDEX.TAIL, transceiver);
transceivers[source.EventReference.Guid] = transceiver;
Debug.Log("Transceiver added");
}
// Remove a transceiver from an Event Emitter's event
private void RemoveTransceiver(FMODUnity.StudioEventEmitter source)
{
source.EventInstance.getChannelGroup(out FMOD.ChannelGroup group);
FMOD.RESULT result = group.removeDSP(transceivers[source.EventReference.Guid]);
result = transceivers[source.EventReference.Guid].release();
transceivers.Remove(source.EventReference.Guid);
Debug.Log("Transceiver removed");
}
// Cleanup dynamically created transceivers
private void OnDestroy()
{
Sources.ForEach((source) => { RemoveTransceiver(source); });
}
void Update()
{
FMOD.RESULT result = FMODUnity.RuntimeManager.StudioSystem.getListenerAttributes(0, out FMOD.ATTRIBUTES_3D attributes);
bool canSeeListener = PositionInFOV(new Vector3(attributes.position.x, attributes.position.y, attributes.position.z));
Sources.ForEach((source) =>
{
bool canSeeEmitter = PositionInFOV(source.transform.position);
float distanceToObject = Vector3.Distance(source.transform.position, transform.position);
result = source.EventInstance.getMinMaxDistance(out float minDistance, out float maxDistance);
if (canSeeListener && canSeeEmitter && distanceToObject <= maxDistance)
{
Debug.Log("Listener and Emitter in fov of the portal");
if (!transceivers.ContainsKey(source.EventReference.Guid))
{
AddTransceiver(source);
}
if (transceivers.ContainsKey(source.EventReference.Guid))
{
// Attenuate transceiver proportionally to distance from event emitter
result = transceivers[source.EventReference.Guid].setParameterFloat((int)FMOD.DSP_TRANSCEIVER.GAIN, 20 * Mathf.Log10(1.0f - (distanceToObject / maxDistance)));
}
}
else
{
Debug.Log("Listener and Emitter not in fov of the portal");
if (transceivers.ContainsKey(source.EventReference.Guid))
{
RemoveTransceiver(source);
}
}
});
}
}
Hopefully that makes sense, please let me know if you have any questions.