Last active
February 17, 2022 11:02
-
-
Save DevWouter/4c77532fc10c220709f4ad1920e46a6b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//////////////////////////////////////////////////////////////// | |
// Below is an example of how Audio drivers work in video games | |
// and how you interact with them. It's a bit simplified but if | |
// you have no prior knowledge it should give you some general | |
// idea how it works and what the limitations are. | |
// | |
// A more complex example would go more in to details about: | |
// - How a DspNode works (here we only use it) | |
// - Events in music files (for example: "Loop this part 4 times | |
// and then start back at the beginning") | |
// - MultiPlexing (for example: surround->stereo while keeping | |
// the illusion of surround) | |
//////////////////////////////////////////////////////////////// | |
// Create the audioSystem and retrieve our first audio driver | |
// listed by the OS. | |
var audioSystem = new AudioSystem(); | |
var driver = audioSystem.GetDrivers().First(); | |
//////////////////////////////////////////////////////////////// | |
// Initializing the driver and normally we can provide it some parameters. | |
// What it does: | |
// - It creates the buffers that the hardware reads to play sound | |
// - They are multiple ones | |
// - These buffers are small (as in 100ms of data) | |
// - It does multiplexing depending on whether the hardware wants mono/stereo/surround | |
// - From this point forward you application is already playing sound. | |
// - But the buffers are empty so you won't hear anything | |
// - It will swap in the next buffer once the hardware is done with the previous buffer | |
// - Fun trick: Try pausing any application using a process manager while it has | |
// dedicated access to the hardware, you will often hear it loop the last swapped | |
// in buffer. | |
driver.Init(); | |
//////////////////////////////////////////////////////////////// | |
// Tell the driver how many channels we expect to *hear* at the same time | |
// With `3` we can only play 3 channels at the time. Normally in games we | |
// use a higher number | |
const int maxChannels = 3; | |
driver.SetMaxChannels(maxChannels); | |
//////////////////////////////////////////////////////////////// | |
// Setting the mix matrix of channels. This is naive version | |
// but it gives you a good idea of the challenge when mixing | |
// multiple voices/channels. | |
// The max volume the hardware is always 100% and if we go above | |
// that we get clipping (a distortion in the output). Below is a | |
// really safe example where we assume that all tracks might | |
// require the max volume. normally we take a bit more risk. | |
float[][] mixMatrix = new float[maxChannels][maxChannels]{ | |
{ 1.0f, 0.0f, 0.0f}, | |
{ 0.5f, 0.5f, 0.0f}, | |
{ 0.4f, 0.3f, 0.3f}, | |
}; | |
// The below example is a bit more risky since the mix config | |
// when all channels play goes to 120%. However clipping occurs | |
// when all track require require the max volume and with more | |
// channels that is less likely to happen. | |
// The first row also uses only 70% to prevent the user from | |
// having to constantly changing the volume when a lot of things | |
// on the screen happen. | |
float[][] altMixMatrix = new float[maxChannels][maxChannels]{ | |
{ 0.7f, 0.0f, 0.0f}, // Max volume of speaker = 70%, mostly to prevent volume flucation | |
{ 0.5f, 0.4f, 0.0f}, // Max volume of speaker = 90%, the first channel becomes a bit softer | |
{ 0.4f, 0.4f, 0.4f}, // Max volume of speaker = 120%, we risk clipping | |
}; | |
driver.SetMixMatrix(mixMatrix); | |
//////////////////////////////////////////////////////////////// | |
// Create the DSP (Digital signal processor) for the music | |
// It's the default one but we add a processor that can perform music fade. | |
// Each DspNode receives an array of bits and is allowed to modify it. | |
var dsp_msc = driver.CreateDsp(); | |
var dsp_msc_fade = dsp.CreateProcessorNode<FadeProcessor>(); | |
var dsp_msc_in = dsp.GetInputNode(); | |
var dsp_msc_out = dsp.GetOutputNode(); | |
dsp_msc.BreakConnection(dsp_msc_in, dsp_msc_out); | |
dsp_msc.CreateConnection(dsp_msc_in, dsp_msc_fade); | |
dsp_msc.CreateConnection(dsp_msc_fade, dsp_out); | |
// Diagram of the DSP for music | |
// | |
// (IN)---(FADER)---(OUT) | |
// | |
//////////////////////////////////////////////////////////////// | |
// Creating the "sound" audio banks | |
// sounds means we try avoid unloading it since we need it all the time) | |
var snd_explosion = driver.CreateSoundFromFile("sfx/explosion.wav"); | |
var snd_bulletfire = driver.CreateSoundFromFile("sfx/bullet_fire.wav"); | |
var snd_footsteps = driver.CreateSoundFromFile("sfx/footsteps.wav"); | |
//////////////////////////////////////////////////////////////// | |
// Creating the "music" audio banks | |
// Meaning we don't fully load the file and stream what we need from disk. | |
var msc_relax = driver.CreateStreamFromFile("music/relax-music.mp3"); | |
var msc_fight = driver.CreateStreamFromFile("music/fight-music.mp3"); | |
//////////////////////////////////////////////////////////////// | |
// Create the group that plays music | |
// and by setting them to the max priority we always include them in the mix | |
var msc_group = driver.CreateChannelGroup(); | |
msc_group.SetPriority(256); | |
msc_group.SetDsp(dsp_msc); | |
//////////////////////////////////////////////////////////////// | |
// Start playing some music | |
var chl_msc_ctrl = msc_group.Add(msc_relax); | |
chl_msc_ctrl.SetDspAttribute(dsp_fade, "fade-in", 3_000); // Fade in 3 seconds | |
chl_msc_ctrl.SetLooping(true); | |
chl_msc_ctrl.SetPosition(0); // Start at the beginning | |
chl_msc_ctrl.Play(); | |
//////////////////////////////////////////////////////////////// | |
// Example of how we switch music. | |
Events.OnEnteringCombat += () => { | |
// Send stop signal to channel. | |
// The channel will be removed and released once stopped. | |
var old_ctrl = chl_msc_ctrl; | |
var fadeOutDuration = 5_000; | |
old_ctrl.SetDsp(dsp_fade, "fade-out", fadeOutDuration); | |
old_ctrl.Stop(new {Delay=fadeOutDuration}); | |
// Start thew new music | |
chl_msc_ctrl = msc_group.Add(msc_fight); | |
chl_msc_ctrl.SetDspAttribute(dsp_fade, "fade-in", 3_000); // Fade in 3 seconds | |
chl_msc_ctrl.SetLooping(true); | |
chl_msc_ctrl.SetPosition(0); // Start at the beginning | |
chl_msc_ctrl.Play(); | |
}; | |
//////////////////////////////////////////////////////////////// | |
// Creating the DSP for sound effects | |
var dsp_sfx = driver.CreateDsp(); | |
var dsp_sfx_in = dsp_sfx.GetInputNode(); | |
var dsp_sfx_out = dsp_sfx.GetOutputNode(); | |
var dsp_sfx_3dPosition = dsp_sfx.CreateProcessorNode<Pos3dProcessor>(); | |
var dsp_sfx_echo = dsp_sfx.CreateProcessorNode<EchoProcessor>(); | |
var dsp_sfx_mix = dsp_sfx.CreateMixNode(); | |
// The player should hear the music instantly with 3d positioning, but also an echo | |
dsp_sfx.BreakConnection(dsp_sfx_in, dsp_out); | |
dsp_sfx.CreateConnection(dsp_sfx_in, dsp_sfx_3dPosition); | |
dsp_sfx.CreateConnection(dsp_sfx_3dPosition, dsp_sfx_mix); | |
dsp_sfx.CreateConnection(dsp_sfx_3dPosition, dsp_sfx_echo); | |
dsp_sfx.CreateConnection(dsp_sfx_echo, dsp_sfx_out); | |
dsp_sfx.CreateConnection(dsp_sfx_mix, dsp_sfx_out); | |
// We also setup the default parameters | |
dsp_sfx_mix.SetAttribute("MixMatrix", new []{0.8f, 0.2f}); | |
dsp_sfx_echo.SetAttribute("Delay", 200); // 200ms | |
// Diagram of the DSP for SFX | |
// /---(ECHO)---\ | |
// (IN)---(3D)---| |---(MIX)---(OUT) | |
// \------------/ | |
//////////////////////////////////////////////////////////////// | |
// Create the sfx group | |
// It has a lower priority since we rather miss a sound effect than music. | |
var sfx_group = driver.CreateChannelGroup(); | |
sfx_group.SetPriority(128); | |
sfx_group.SetDsp(dsp_sfx); | |
Events.PlayerMove += ()=>{ | |
dsp_sfx_3dPosition.SetAttribute("OutPos", Player.Pos); | |
var chl = sfx_group.Add(snd_footsteps); | |
chl.SetDspAttribute(dsp_sfx_3dPosition, "InPos", Player.Pos); | |
// Override the other effects since the player is nearby | |
chl.SetDspAttribute(dsp_sfx_echo, "Delay", 50); | |
chl.SetDspAttribute("MixMatrix", new []{0.9f, 0.1}); | |
// Since we are not looping, the channel can use the DSP to determine when the track | |
// has been stopped and when it can be removed. | |
chl.Play(); | |
} | |
Events.EnemyMoves += (enemy)=>{ | |
var chl = sfx_group.Add(snd_footsteps); | |
chl.SetDspAttribute(dsp_sfx_3dPosition, "InPos", enemy.Pos); | |
chl.SetVolume(0.5f); // Enemy is farther away so make foot steps more silent. | |
chl.Play(); | |
} | |
Events.PlayerFiresGun += () => { | |
var chl = sfx_group.Add(snd_bulletfire); | |
chl.SetDspAttribute(dsp_sfx_3dPosition, "InPos", Player.Pos); | |
chl.Play(); | |
}; | |
Events.ExplodingBarrel += (barrel)=>{ | |
// The barrel explodes in many pieces. We now try to play 5 channels/voices | |
// but keep in mind that we only allow 3 at the same time | |
// And since we always have music, that leaves us with only two if nobody moves or shoots. | |
for(var i = 0; i < 5; ++i){ | |
var pos = barrel.Pos + Vector3f.RandomOffset(min: 0.2f, max: 1.0f); | |
var chl = sfx_group.Add(snd_explosion); | |
chl.SetDspAttribute(dsp_sfx_3dPosition, "InPos", pos); | |
chl.Play(); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment