Skip to content

Instantly share code, notes, and snippets.

@redxdev
Created April 19, 2020 22:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save redxdev/dd0bb1344527f6c3cacfa78697e10c9c to your computer and use it in GitHub Desktop.
Save redxdev/dd0bb1344527f6c3cacfa78697e10c9c to your computer and use it in GitHub Desktop.
Simple FMOD wrapper

This is based on the wrapper I use for my own project, with anything depending on my engine's internals stripped out. I'm not sure if this compiles as-is, but it should be pretty easy to modify and fix up.

It doesn't bind absolutely everything that FMOD provides (no 3d audio functions yet) but it should be easy enough to add those.

You must provide fmod.cs, fmod_dsp.cs, fmod_errors.cs, and fmod_studio.cs yourself (along with the required DLLs for fmod). You can get them by downloading and installing the API from fmod's download page here.

using System;
using Flame.Engine.Assets;
using FMOD.Studio;
namespace Flame.Engine.Audio
{
public sealed class AudioBank : IDisposable, IEquatable<AudioBank>
{
private readonly Bank bank;
public AudioBank(Bank bank)
{
if (!bank.isValid())
{
throw new ArgumentException("Cannot use an invalid audio bank", nameof(bank));
}
this.bank = bank;
}
public void Dispose()
{
this.bank.unload().ThrowIfError();
}
public override bool Equals(object obj)
{
return this.Equals(obj as AudioBank);
}
public bool Equals(AudioBank other)
{
return other != null
&& this.bank.handle == other.bank.handle;
}
public override int GetHashCode()
{
return this.bank.handle.GetHashCode();
}
}
}
using System;
using FMOD.Studio;
namespace Flame.Engine.Audio
{
public sealed class AudioBus : IEquatable<AudioBus>
{
private Bus bus;
public AudioBus(Bus bus)
{
if (!bus.isValid())
{
throw new ArgumentException("Cannot use an invalid bus", nameof(bus));
}
this.bus = bus;
}
public Guid Id
{
get
{
this.bus.getID(out var value).ThrowIfError();
return value;
}
}
public string Path
{
get
{
this.bus.getPath(out var value).ThrowIfError();
return value;
}
}
public uint ExclusiveCpuUsage
{
get
{
this.bus.getCPUUsage(out var value, out _).ThrowIfError();
return value;
}
}
public uint InclusiveCpuUsage
{
get
{
this.bus.getCPUUsage(out _, out var value).ThrowIfError();
return value;
}
}
public bool IsPaused
{
get
{
this.bus.getPaused(out var value).ThrowIfError();
return value;
}
set
{
this.bus.setPaused(value).ThrowIfError();
}
}
public float Volume
{
get
{
this.bus.getVolume(out var value).ThrowIfError();
return value;
}
set
{
this.bus.setVolume(value).ThrowIfError();
}
}
public float FinalVolume
{
get
{
this.bus.getVolume(out _, out var value).ThrowIfError();
return value;
}
}
bool IsMuted
{
get
{
this.bus.getMute(out var value).ThrowIfError();
return value;
}
set
{
this.bus.setMute(value).ThrowIfError();
}
}
public void StopAllEvents(bool allowFadeOut = true)
{
this.bus.stopAllEvents(allowFadeOut ? STOP_MODE.ALLOWFADEOUT : STOP_MODE.IMMEDIATE).ThrowIfError();
}
public override bool Equals(object obj)
{
return this.Equals(obj as AudioBus);
}
public bool Equals(AudioBus other)
{
return other != null
&& this.bus.handle == other.bus.handle;
}
public override int GetHashCode()
{
return this.bus.handle.GetHashCode();
}
}
}
using System;
using FMOD.Studio;
namespace Flame.Engine.Audio
{
public sealed class AudioEvent : IEquatable<AudioEvent>
{
private readonly EventDescription eventDescription;
public AudioEvent(EventDescription eventDescription)
{
if (!eventDescription.isValid())
{
throw new ArgumentException("Cannot use an invalid event description", nameof(eventDescription));
}
this.eventDescription = eventDescription;
}
public Guid Id
{
get
{
this.eventDescription.getID(out var value).ThrowIfError();
return value;
}
}
public string Path
{
get
{
this.eventDescription.getPath(out var value).ThrowIfError();
return value;
}
}
public int Length
{
get
{
this.eventDescription.getLength(out var value).ThrowIfError();
return value;
}
}
public bool Is3D
{
get
{
this.eventDescription.is3D(out var value).ThrowIfError();
return value;
}
}
public bool IsOneShot
{
get
{
this.eventDescription.isOneshot(out var value).ThrowIfError();
return value;
}
}
public bool HasCue
{
get
{
this.eventDescription.hasCue(out var value).ThrowIfError();
return value;
}
}
public float MaximumDistance
{
get
{
this.eventDescription.getMaximumDistance(out var value).ThrowIfError();
return value;
}
}
public float MinimumDistance
{
get
{
this.eventDescription.getMinimumDistance(out var value).ThrowIfError();
return value;
}
}
public float SoundSize
{
get
{
this.eventDescription.getSoundSize(out var value).ThrowIfError();
return value;
}
}
public AudioEventInstance CreateInstance()
{
this.eventDescription.createInstance(out var instance).ThrowIfError();
return new AudioEventInstance(this.eventDescription, instance);
}
/// <summary>
/// Creates an audio instance, starts it, and immediately releases it.
/// </summary>
public void QuickPlayInstance()
{
this.eventDescription.createInstance(out var instance).ThrowIfError();
instance.start().ThrowIfError();
instance.release().ThrowIfError();
}
public override bool Equals(object obj)
{
return this.Equals(obj as AudioEvent);
}
public bool Equals(AudioEvent other)
{
return other != null
&& this.eventDescription.handle == other.eventDescription.handle;
}
public override int GetHashCode()
{
return this.eventDescription.handle.GetHashCode();
}
}
}
using System;
using FMOD;
using FMOD.Studio;
using Serilog;
namespace Flame.Engine.Audio
{
public sealed class AudioEventInstance : IEquatable<AudioEventInstance>, IDisposable
{
private readonly EventDescription eventDescription;
private readonly EventInstance eventInstance;
public AudioEventInstance(EventDescription eventDescription, EventInstance instance)
{
if (!eventDescription.isValid())
{
throw new ArgumentException("Cannot use an invalid event description", nameof(eventDescription));
}
this.eventDescription = eventDescription;
if (!instance.isValid())
{
throw new ArgumentException("Cannot use an invalid event instance", nameof(instance));
}
this.eventInstance = instance;
}
public PLAYBACK_STATE PlaybackState
{
get
{
this.eventInstance.getPlaybackState(out var value).ThrowIfError();
return value;
}
}
public bool IsPlaying => this.PlaybackState != PLAYBACK_STATE.STOPPED;
public bool IsStopped => this.PlaybackState == PLAYBACK_STATE.STOPPED;
public bool IsPaused
{
get
{
this.eventInstance.getPaused(out var value).ThrowIfError();
return value;
}
set
{
this.eventInstance.setPaused(value).ThrowIfError();
}
}
public float Pitch
{
get
{
this.eventInstance.getPitch(out var value).ThrowIfError();
return value;
}
set
{
this.eventInstance.setPitch(value).ThrowIfError();
}
}
public float FinalPitch
{
get
{
this.eventInstance.getPitch(out _, out var value).ThrowIfError();
return value;
}
}
public float ChannelPriority
{
get => this.GetProperty(EVENT_PROPERTY.CHANNELPRIORITY);
set => this.SetProperty(EVENT_PROPERTY.CHANNELPRIORITY, value);
}
public float ScheduleDelay
{
get => this.GetProperty(EVENT_PROPERTY.SCHEDULE_DELAY);
set => this.SetProperty(EVENT_PROPERTY.SCHEDULE_DELAY, value);
}
public float ScheduleLookahead
{
get => this.GetProperty(EVENT_PROPERTY.SCHEDULE_LOOKAHEAD);
set => this.SetProperty(EVENT_PROPERTY.SCHEDULE_LOOKAHEAD, value);
}
public float MinimumDistance
{
get => this.GetProperty(EVENT_PROPERTY.MINIMUM_DISTANCE);
set => this.SetProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, value);
}
public float MaximumDistance
{
get => this.GetProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE);
set => this.SetProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, value);
}
public int TimelinePosition
{
get
{
this.eventInstance.getTimelinePosition(out var value).ThrowIfError();
return value;
}
set
{
this.eventInstance.setTimelinePosition(value).ThrowIfError();
}
}
public float Volume
{
get
{
this.eventInstance.getVolume(out var value).ThrowIfError();
return value;
}
set
{
this.eventInstance.setVolume(value).ThrowIfError();
}
}
public float FinalVolume
{
get
{
this.eventInstance.getVolume(out _, out var value).ThrowIfError();
return value;
}
}
public bool IsVirtual
{
get
{
this.eventInstance.isVirtual(out var value).ThrowIfError();
return value;
}
}
public void Start()
{
this.eventInstance.start().ThrowIfError();
}
public void Stop(bool allowFadeOut = true)
{
this.eventInstance.stop(allowFadeOut ? STOP_MODE.ALLOWFADEOUT : STOP_MODE.IMMEDIATE).ThrowIfError();
}
public void TriggerCue()
{
this.eventInstance.triggerCue().ThrowIfError();
}
public void SetProperty(EVENT_PROPERTY property, float value)
{
this.eventInstance.setProperty(property, value).ThrowIfError();
}
public float GetProperty(EVENT_PROPERTY property)
{
this.eventInstance.getProperty(property, out var value).ThrowIfError();
return value;
}
public LocalAudioParameter GetParameterByName(string name)
{
var result = this.eventDescription.getParameterDescriptionByName(name, out var param);
if (result == RESULT.OK)
{
return new LocalAudioParameter(this.eventInstance, param.id);
}
Log.Warning("Failed to get parameter description for {ParameterName}: {Error}", name, Error.String(result));
return null;
}
public override bool Equals(object obj)
{
return this.Equals(obj as AudioEventInstance);
}
public bool Equals(AudioEventInstance other)
{
return other != null
&& this.eventDescription.handle == other.eventDescription.handle
&& this.eventInstance.handle == other.eventInstance.handle;
}
public override int GetHashCode()
{
return HashCode.Combine(
this.eventDescription.handle,
this.eventInstance.handle);
}
public void Dispose()
{
this.eventInstance.release();
}
}
}
using System;
using System.Runtime.Serialization;
using FMOD;
namespace Flame.Engine.Audio
{
[Serializable]
public class AudioSystemException : Exception
{
public AudioSystemException()
: base()
{
}
public AudioSystemException(string message)
: base(message)
{
}
public AudioSystemException(RESULT result)
: base(Error.String(result))
{
}
public AudioSystemException(string message, RESULT result)
: base($"{message}: {Error.String(result)}")
{
}
public AudioSystemException(string message, Exception innerException)
: base(message, innerException)
{
}
public AudioSystemException(RESULT result, Exception innerException)
: base(Error.String(result), innerException)
{
}
public AudioSystemException(string message, RESULT result, Exception innerException)
: base($"{message}: {Error.String(result)}", innerException)
{
}
protected AudioSystemException(SerializationInfo serializationInfo, StreamingContext streamingContext)
: base(serializationInfo, streamingContext)
{
}
}
}
using System;
using FMOD.Studio;
namespace Flame.Engine.Audio
{
public sealed class AudioVCA : IEquatable<AudioVCA>
{
private VCA vca;
public AudioVCA(VCA vca)
{
if (!vca.isValid())
{
throw new ArgumentException("Cannot use an invalid VCA", nameof(vca));
}
this.vca = vca;
}
public Guid Id
{
get
{
this.vca.getID(out var value).ThrowIfError();
return value;
}
}
public string Path
{
get
{
this.vca.getPath(out var value).ThrowIfError();
return value;
}
}
public float Volume
{
get
{
this.vca.getVolume(out var value).ThrowIfError();
return value;
}
set
{
this.vca.setVolume(value).ThrowIfError();
}
}
public float FinalVolume
{
get
{
this.vca.getVolume(out _, out var value).ThrowIfError();
return value;
}
}
public override bool Equals(object obj)
{
return this.Equals(obj as AudioVCA);
}
public bool Equals(AudioVCA other)
{
return other != null
&& this.vca.handle == other.vca.handle;
}
public override int GetHashCode()
{
return this.vca.handle.GetHashCode();
}
}
}
using System;
using FMOD;
using FMOD.Studio;
using Microsoft.Xna.Framework;
namespace Flame.Engine.Audio
{
public sealed class FMODAudioSystem : IDisposable
{
public const int MaxChannels = 512;
#if DEBUG
public const FMOD.Studio.INITFLAGS StudioInitFlags = FMOD.Studio.INITFLAGS.LIVEUPDATE;
public const FMOD.INITFLAGS CoreInitFlags = FMOD.INITFLAGS.PROFILE_ENABLE;
#else
public const FMOD.Studio.INITFLAGS StudioInitFlags = FMOD.Studio.INITFLAGS.NORMAL;
public const FMOD.INITFLAGS CoreInitFlags = FMOD.INITFLAGS.NORMAL;
#endif
private readonly FMOD.Studio.System audioSystem;
public FMODAudioSystem()
{
FMOD.Studio.System.create(out this.audioSystem).ThrowIfError();
if (!this.audioSystem.isValid())
{
throw new AudioSystemException("Unable to create audio system: invalid audio system");
}
this.audioSystem.initialize(MaxChannels, StudioInitFlags, CoreInitFlags, IntPtr.Zero).ThrowIfError();
}
public AudioBank LoadBankFile(string bankPath, bool blocking)
{
this.audioSystem.loadBankFile(bankPath, blocking ? LOAD_BANK_FLAGS.NORMAL : LOAD_BANK_FLAGS.NONBLOCKING, out var bank).ThrowIfError();
return new AudioBank(bank);
}
public AudioEvent GetEvent(string path)
{
var result = this.audioSystem.getEvent(path, out var eventDesc);
if (result == RESULT.OK)
{
return new AudioEvent(eventDesc);
}
return null;
}
public AudioEvent GetEventById(Guid eventId)
{
var result = this.audioSystem.getEventByID(eventId, out var eventDesc);
if (result == RESULT.OK)
{
return new AudioEvent(eventDesc);
}
return null;
}
public GlobalAudioParameter GetParameterByName(string name)
{
var result = this.audioSystem.getParameterDescriptionByName(name, out var param);
if (result == RESULT.OK)
{
return new GlobalAudioParameter(this.audioSystem, param.id);
}
return null;
}
public AudioBus GetBus(string path)
{
var result = this.audioSystem.getBus(path, out var bus);
if (result == RESULT.OK)
{
return new AudioBus(bus);
}
return null;
}
public AudioBus GetBusById(Guid busId)
{
var result = this.audioSystem.getBusByID(busId, out var bus);
if (result == RESULT.OK)
{
return new AudioBus(bus);
}
return null;
}
public AudioVCA GetVCA(string path)
{
var result = this.audioSystem.getVCA(path, out var vca);
if (result == RESULT.OK)
{
return new AudioVCA(vca);
}
return null;
}
public AudioVCA GetVCAById(Guid vcaId)
{
var result = this.audioSystem.getVCAByID(vcaId, out var vca);
if (result == RESULT.OK)
{
return new AudioVCA(vca);
}
return null;
}
// call this once per update
public void Update()
{
this.audioSystem.update().ThrowIfError();
}
public void Dispose()
{
this.audioSystem.release().ThrowIfError();
}
}
}
using System.Runtime.CompilerServices;
using FMOD;
namespace Flame.Engine.Audio
{
public static class FMODResultExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfError(this RESULT result)
{
if (result != RESULT.OK)
{
throw new AudioSystemException(result);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfError(this RESULT result, string message)
{
if (result != RESULT.OK)
{
throw new AudioSystemException(message, result);
}
}
}
}
using System;
using FMOD.Studio;
namespace Flame.Engine.Audio
{
public sealed class GlobalAudioParameter : IAudioParameter, IEquatable<GlobalAudioParameter>
{
private readonly FMOD.Studio.System audioSystem;
private readonly PARAMETER_ID paramId;
public GlobalAudioParameter(FMOD.Studio.System audioSystem, PARAMETER_ID paramId)
{
if (!audioSystem.isValid())
{
throw new ArgumentException("Cannot use an invalid audio system", nameof(audioSystem));
}
this.audioSystem = audioSystem;
this.paramId = paramId;
}
public float GetValue()
{
this.audioSystem.getParameterByID(this.paramId, out var value).ThrowIfError();
return value;
}
public void SetValue(float value, bool ignoreSeekSpeed = false)
{
this.audioSystem.setParameterByID(this.paramId, value, ignoreSeekSpeed).ThrowIfError();
}
public float GetFinalValue()
{
this.audioSystem.getParameterByID(this.paramId, out _, out var finalValue).ThrowIfError();
return finalValue;
}
public override bool Equals(object obj)
{
return this.Equals(obj as GlobalAudioParameter);
}
public bool Equals(GlobalAudioParameter other)
{
return other != null
&& this.audioSystem.handle == other.audioSystem.handle
&& this.paramId.data1 == other.paramId.data1
&& this.paramId.data2 == other.paramId.data2;
}
public override int GetHashCode()
{
return HashCode.Combine(
this.audioSystem.handle,
this.paramId.data1,
this.paramId.data2);
}
}
}
namespace Flame.Engine.Audio
{
public interface IAudioParameter
{
float GetValue();
void SetValue(float value, bool ignoreSeekSpeed = false);
float GetFinalValue();
}
}
using System;
using FMOD.Studio;
namespace Flame.Engine.Audio
{
public sealed class LocalAudioParameter : IAudioParameter, IEquatable<LocalAudioParameter>
{
private readonly EventInstance eventInstance;
private readonly PARAMETER_ID paramId;
public LocalAudioParameter(EventInstance eventInstance, PARAMETER_ID paramId)
{
if (!eventInstance.isValid())
{
throw new ArgumentException("Cannot use an invalid event instance", nameof(eventInstance));
}
this.eventInstance = eventInstance;
this.paramId = paramId;
}
public float GetValue()
{
this.eventInstance.getParameterByID(this.paramId, out var value).ThrowIfError();
return value;
}
public void SetValue(float value, bool ignoreSeekSpeed = false)
{
this.eventInstance.setParameterByID(this.paramId, value, ignoreSeekSpeed).ThrowIfError();
}
public float GetFinalValue()
{
this.eventInstance.getParameterByID(this.paramId, out _, out var finalValue).ThrowIfError();
return finalValue;
}
public override bool Equals(object obj)
{
return this.Equals(obj as LocalAudioParameter);
}
public bool Equals(LocalAudioParameter other)
{
return other != null
&& this.eventInstance.handle == other.eventInstance.handle
&& this.paramId.data1 == other.paramId.data1
&& this.paramId.data2 == other.paramId.data2;
}
public override int GetHashCode()
{
return HashCode.Combine(
this.eventInstance.handle,
this.paramId.data1,
this.paramId.data2);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment