I realized one of the reasons I was so confused yesterday about the SetWaveParameter was that it’s actually a function of the AudioComponent.
This is significant, because in order to dynamically set these variables for a single Metasound, I need to do it on an object that has an AudioComponent.
This is actually a good thing, because there is only really one place that has an AudioComponent now- the AudioCuePlayer
.
This reinforced a lesson from Building The Core Gameplay Loop, where you should start your refactor/modification at the place that will consume the new change. I had started at the other end, deleting the 48 metasounds and changing them to USoundCue
. That’s great, except I actually need USoundWave
. A costly mistake if one made 48 times.
Starting with the AudioCuePlayer
, I now have
// MetaSoundPlayer.h
// This is set in editor to point to my single parameterized Metasound
UPROPERTY(BlueprintReadWrite, EditDefaultsOnly)
UMetaSoundSource* MetaSoundPlayer;
// Iniitialize used to take a MetasoundSource*
// It now takes data to initialize the above Metasound
UFUNCTION()
void Initialize(const FMetaSoundPlayerData& MetaSoundData, UQuartzClockHandle* ParentClock, const FQuartzQuantizationBoundary& ParentQuartzQuantizationBoundary);
// I also set a specific func to map data to functionality
// This could just be done in Initialize
// But I read that Metasound parameters need to be set *after* Play.
// In that case having a separate function makes it easier to move.
UFUNCTION()
void InitializeMetaSoundPlayer(const FMetaSoundPlayerData& Data) const;
// MetaSoundPlayer.cpp
// The actual code here is fairly straight forward for now
// Eventually this could become more complex as more effects are implemented
void AAudioCuePlayer::InitializeMetaSoundPlayer(const FMetaSoundPlayerData& Data) const
{
AudioComponent->SetWaveParameter(Data.WaveAssetName, Data.WaveAsset);
AudioComponent->SetFloatParameter(Data.OutputVolumeName, Data.OutputVolume);
}
The MetaSoundPlayerData is a fairly straight forward struct:
// FMetaSoundPlayerData.h
// This must be exact match for the parameterized MetaSound
UPROPERTY()
const FName WaveAssetName = FName("WaveAsset");
// Same with this
UPROPERTY()
const FName OutputVolumeName = FName("OutputVolume");
UPROPERTY()
USoundWave* WaveAsset;
UPROPERTY()
float OutputVolume = 1.0f;
So, our MetaSoundPlayerData represents the data necessary to control our single Metasound object.
This change of replacing the MetaSoundSource
to MetaSoundData
will now break anything using AudioCuePlayer
.
This essentially triggers a refactoring chain reaction where I have to percolate up the change to the data model throughout the codebase.
Specifically, this breaks the all important Instrument
Actor that Initializes a set of AudioCuePlayers on every bar- see Building A Better Drum Machine.
Now on every bar, its trying to initialize the AudioCuePlayer with the MetasoundSource, not my new MetaSoundData.
So, I have to update Instrument to contain a Data object, which will break both DrumData
and CheeseKeyData
, forcing me to refactor those to contain MetaSoundData instead of MetasoundSource.
It’s some tedious bullshit, but alas, all human endeavor is the passion and survival of tedious bullshit.
Instrument receives a FInstrumentSchedule
, a struct containing a set of FNotesPerBar
FNotesPerBar
is a struct containing all the data required to play a set of notes in a bar- if there’s 4 beats in a bar, you could expect 4 pieces of data expressing which note to play on which beat.
NotesPerBar contains a MetasoundSource reference, and is the place I need to rework.
Update this, and Instrument should be able to pass through the data to the AudioCuePlayer easily.
This triggers the next refactoring chain reaction: I define InstrumentSchedule
in two places: One for the Drums- snare, kick, hi-hat, etc; and one for a sampled synth instrument.
Once I change the spec for NotesPerBar, I will have to update the InstrumentSchedule in these data objects.