Shinobu is my custom audio engine for Godot based on the great miniaudio library.
This audio engine doesn't behave like Godot's audio engine, so I thought it would be nice to write some documentation to explain the basic gist of it.
Unlike godot's audio bus system, shinobu uses miniaudio's audio node system, this is much more powerful but might be harder to use.
Since you won't be using godot's audio system, it is important to disable it, go to Project Settings -> Audio and set the driver to "Dummy" (without quotes).
Unlike godot's audio subsystem, shinobu must be initialized manually.
If you want, you can set the desired audio buffer size by setting Shinobu.desired_buffer_size_msec
.
Note that this is only a suggestion, and the real buffer size might be different.
If you want to get the real buffer size afterwards you can call Shinobu.get_actual_buffer_size()
after initialization.
To do this, call
Shinobu.initialize(), if it doesn't return
OKyou can check what went wrong by calling
Shinobu.get_initialization_error()`.
You likely want to create your own ShinobuGroup
s, these are analogous to the standard Godot audio groups.
To do this, call Shinobu.create_group("group_name", parent_group)
(parent_group can be null).
Afterwards, you can connect your group to either the endpoint (the audio output) or to an effect by calling connect_to_endpoint
or connect_to_effect
.
Groups can have their volume independently changed, you can also change the master volume by modifying Shinobu.master_volume
.
Before being playable, sounds must be registered with the audio system,
for this you should set your audio file's import mode to Keep File
in the Godot import settings for the file.
At runtime, you should load the file as an array of bytes using the File
class instead of using load()
.
Afterwards, you can call Shinobu.register_sound_from_memory(name_hint, data)
,
with name_hint
being part of the internal name to be used for the sound (used mostly for debugging C++ code) and data
being the aforementioned byte array.
This will give you a ShinobuSoundSource
, this is the object you will use to instantiate your sound, make sure to keep it around.
To playback a sound, call your ShinobuSoundSource
's instantiate(group, use_source_channel_count)
method.
Obviously, the group
should be the audio group that will be used to playback this sound, use_source_channel_count
disables
the built-in channel remapping code (for example, for playing 7.1 files on only 2 speakers), it might be desirable to use this together with the ShinobuChannelRemapEffect
class for remapping non-standard channel configurations.
You will then have a ShinobuSoundPlayer
object, this is a normal Godot Node, the reason for this is so that the audio playback can respond to things such as pausing the game and can have its lifetime bound to a node so it gets automatically freed as needed. So you should probably add it to the scene tree (or not, I'm not your father).
Calling ShinobuSoundPlayer.start
will start playback.
You can schedule sounds to be played in the future, this is useful if you, for example, want to sync two sounds to make them play at the same time or you want to make a sound play at a certain time of a backing music track.
For this, you will need the global mix time in milliseconds, you can get this by calling Shinobu.get_dsp_time()
.
If you want to say, schedule a song to be played 1 second in the future, you can do sound.schedule_start_time(dsp_time + 1000)
.
Afterwards, you still need to call sound.start
.
Note that you can also schedule looping sounds to stop at a certain time by using the schedule_stop_time
method.
Effects are a powerful tool to modify sounds or extracting information from them at runtime, effects can receive sounds, groups or other effects as their input.
For example, let's create a pitch shift effect and apply it to our music group.
var music_group := Shinobu.create_group("Music", null)
var pitch_shift := Shinobu.instantiate_pitch_shift()
music_group.connect_to_effect(pitch_shift)
pitch_shift.connect_to_endpoint()
The spectrum analyzer effect works in the exact same way as godot's which means that its output is in linear frequency range, our ears don't work like that, so you might want to process the output, don't ask me how this works because genuinely I forgot.
(The code below is for a decaying example, which means the samples are only updated if the new sample has a bigger magnitude than the current one, the samples are then decayed towards 0 elsewhere in the code).
for i in range(output_samples.size()):
var freq = min_freq + interval * i
var freqrange_low = float(freq - min_freq) / float(max_freq - min_freq)
freqrange_low = pow(freqrange_low, 2.0)
freqrange_low = lerp(min_freq, max_freq, freqrange_low)
freq += interval
var freqrange_high = float(freq - min_freq) / float(max_freq - min_freq)
freqrange_high = pow(freqrange_high, 2.0)
freqrange_high = lerp(min_freq, max_freq, freqrange_high)
var mag = analyzer.get_magnitude_for_frequency_range(freqrange_low, freqrange_high)
mag = linear2db(mag.length())
mag = (mag - min_db) / (max_db - min_db)
mag += 0.3 * (freq - min_freq) / (max_freq - min_freq)
mag = clamp(mag, 0.00, 1)
if mag > output_samples[i]:
output_samples.set(i, mag)