-
-
Notifications
You must be signed in to change notification settings - Fork 3
Creating Your Own Instrument
Let's say you're a fellow modder who'd like to use this mod as a dependency for making a new instrument. While I heavily doubt anyone would actually do it, I still left a pretty neat API for you to use. In any case, this section is mostly for me to remember my stuff :P
Anywho, this tutorial will assume that you are a complete beginner to the subject of modding and such in general, as it makes it easier for me to write it that way.
Of course, you will still need basic Java, Forge (and Minecraft) knowledge.
-
Instrument mod extensions are untested!
-
Modifying the number of rows/columns in an instrument is highly experimental. You may, however, do so by overriding the
columns
/rows
method respectively.
You will then be required to re-map your instruments. For that, you may override thenoteMap
method.
Grid instruments will have an easier time, since they just need to pass in tonoteGrid.genKeyboardMap
their key mappings of choice in the form of a 2D array; and simply return its value.- A note map MUST have all note buttons in it!
If you encounter any issues with the above points and is sure that it is nothing but my stupidity at rise, make sure to report it to this project's issue tracker.
All resources for any instrument's screen must be kept under assets\<modid>\textures\gui\instrument\<instrument>
. If this file structure is not followed, the API will not be able to detect your resources.
We're going to start simple, and assume that all you have in mind is a simple grid instrument that inherits the 21 notes style - per se, a piano.
*Quick sidenote, I'd actually love to implement a piano into this mod but I just don't have the sounds for it. If you actually make it that'd be very epic and I'll be in a life's debt to you.
NOTE: If you wish to implement an instrument that does not inherit this style, I suggest seeing the implementation of AratakisGreatAndGloriousDrumScreen.
More than that - just go see any of the present instruments' implementations to gain better understanding of the API. You're more than welcome to do so.
Firstly, we'll need to create an instrument screen. As the name may suggest, it is the container for our instrument's GUI. Such a screen must extend the AbstractInstrumentScreen
class.
NOTE: This is a client only class; screens do not exist on dedicated servers.
This mod has a convenient implementation for grid-specific instruments called 'AbstractGridInstrument` - so let us use this in our example.
We will therefore annotate it as such:
@OnlyIn(Dist.CLIENT)
public class PianoScreen extends AbstractGridInstrumentScreen {
public static final String INSTRUMENT_ID = "piano";
//...
}
The INSTRUMENT_ID
field is not required, however we will have to reference the ID more than once, so it is recommended to store it there.
The ID must match that of your item's ID.
This class will kindly ask you to implement some methods. Let's explore their purposes and how you should handle them:
All that the AbstractInstrumentScreen
constructor requires is an InterationHand
instance. This hand represents the hand of which the player currently plays the instrument with.
Pretty self-explanatory. Just return the INSTRUMENT_ID
we made, paired with your mod ID:
@Override
public ResourceLocation getInstrumentId() {
return new ResourceLocation(Main.MODID, INSTRUMENT_ID);
}
Yet again, this ID (location) must match that of your item's.
This method should return an InstrumentThemeLoader
.
This class is responsible for handling the color palettes of the instrument, and will automatically assign them according to the JSON styler associated in the instrument's resources directory, under the name of instrument_style.json
. We will explore its contents later.
For now, just know that our lad AbstractInstrumentScreen
has got our back, and stores in it a neat method to create one for you. Just call initThemeLoader
, passing both your mod and instrument's ID.
The initiator for this class must be called in about the same time as of the loading of the Mod event bus
. This is so because when you call the initiator, it automatically subscribes the given JSON for any resource load event on the client, and the first one is just about when the game fires.
This means that if you do not call it at that time, your mod's and any resource pack that is modifying your piano will not have its effects applied until a resource load event occurs (such F3+T being pressed).
The trick I use for it is to make the theme loader a static property of the screen class, and annotate the screen class as an EventBusSubscriber
for the mod event bus.
This makes the FML load the class early on in search for any methods trying to subscribe to the specified event bus.
We must also declare the value
property to be of CLIENT
for it to not attempt to load on the server.
Thus, we end up with the following snippet:
@OnlyIn(Dist.CLIENT)
//NOTE: There just to make it load on mod setup
@EventBusSubscriber(bus = Bus.MOD, value = Dist.CLIENT)
public class PianoScreen extends AbstractGridInstrumentScreen {
//...
private static final InstrumentThemeLoader THEME_LOADER = initThemeLoader(PianoMod.MODID, INSTRUMENT_ID);
@Override
public InstrumentThemeLoader getThemeLoader() {
return THEME_LOADER;
}
//...
NOTE: If you want to add any other design element to the styler, you may subscribe to a theme load event via
InstrumentThemeLoader#addListener
.
It is recommended that you make use ofInstrumentOptionsScreen
for most options, though - simply overrideinitInstrumentOptionsScreen
.
Use the styler for properties that make sense that only a resource pack needs to modify.
This method should return an array of NoteSound
s. Their length must be equal to the total notes that can be pressed in such instrument - by default 21.
Each one will be taken in the order of the note pressed by the player, as specified in the associated NoteGrid
.
For now, at least, we don't have such array, so let's help ourselves by simply returning null
.
With all said and done, we end up with this class at hand:
@OnlyIn(Dist.CLIENT)
//NOTE: There just to make it load on mod setup
@EventBusSubscriber(bus = Bus.MOD, value = Dist.CLIENT)
public class PianoScreen extends AbstractGridInstrumentScreen {
public static final String INSTRUMENT_ID = "piano";
public PianoScreen(InteractionHand hand) {
super(hand);
}
@Override
public ResourceLocation getInstrumentId() {
return new ResourceLocation(Main.MODID, INSTRUMENT_ID);
}
@Override
public NoteSound[] getSounds() {
return null;
}
private static final InstrumentThemeLoader THEME_LOADER = initThemeLoader(Main.MODID, INSTRUMENT_ID);
@Override
public InstrumentThemeLoader getThemeLoader() {
return THEME_LOADER;
}
}
NOTE: Keep in mind that, according to this very clever Reddit user, quote:
The Genshin lyre is in C major, so transpose everything into 1-4-5 chords and play in C major
Because of some annoying specification by OpenAL, the sound library used by Minecraft - all sounds produced on Stereo will be stuck on 100%.
I have thus made a system to balance quality and your bloody ears, by implementing the NoteSound
class. There, you may define one mono sound and an optional surround version.
Which one is chosen depends on multiple factors, but ultimately boils down to the user's preference, as set by their instrument's options.
If the player has chosen either Mono or Stereo sounds only, all instrument sounds are forced into the respected version - no matter the factors.
However, when Mixed (the default options) is chosen:
- If the instruments volume is lesser than 100%, mono sounds will always play. This does not account for the master volume.
- When requested by the client, stereo sounds will always by chosen.
- When requested by the server, only if the distance between the player and the client is lesser than the set stereo range (5 blocks), stereo will be chosen.
Note that if a surround is not present, the quite obviously - mono will always be chosen.
All instrument sounds should be kept under: assets/<modid>/sounds/<instrument>/<note_number>.ogg
. If you have their stereo version too, name them <note_number>.stereo.ogg
by convention.
NOTE: The sounds, in a grid, must be ordered from the first visual row to the last, as described here:
In our case, we would need 21 piano key sounds. Let us assume that we also happen to have a stereo version of the piano as well - we will thus include all 42 audio files in our instruments folder, as described earlier.
NOTE: If you only have the stereo versions of your sounds, you may use FFMPEG to channel them down to a singular mono channel. This command was generated by ChatGPT, and it worked quite well for me. Make sure to run it in your instrument sounds directory as the active directory.
PowerShell:
Get-ChildItem -Filter "*.ogg" | ForEach-Object { ffmpeg -i $_.FullName -ac 1 -c:v copy ($_.BaseName + "_mono" + $_.Extension) }Unix Bash (Untested!):
for file in *.ogg; do ffmpeg -i "$file" -ac 1 -c:v copy "${file%.*}_mono.${file##*.}"; doneIn summary, the command takes all the ".ogg" files in the current directory and converts each file's audio to mono using ffmpeg. The converted files will have "_mono" appended to their filenames before the original file extension.
It is still recommended to rename your mono output files to*.ogg
and the stereo ones to*.stereo.ogg
in order to follow the mod's conventions, though.
Forge requires you make a JSON file describing all your sounds that the mod adds, as described here.
The keys of these sounds will be what Minecraft and the GI-API will use to reference your sounds. The naming of the sounds must follow this naming principal: <instrument>_note_<note number>
, and for stereo types: <instrument>_note_<note number>_stereo
.
All instrument sounds should channel to the records
category.
So, your file should be constructed as something like this:
{
"piano_note_0": {
"catrgory": "record",
"sounds": ["pianomod:piano/0"]
},
"piano_note_1": {
"category": "record",
"sounds": ["pianomod:piano/1"]
},
//...
"piano_note_20": {
"category": "record",
"sounds": ["pianomod:piano/1"]
},
// If you have stereo versions:
"piano_note_0_stereo": {
"catrgory": "record",
"sounds": ["pianomod:piano/0.stereo"]
},
"piano_note_1_stereo": {
"category": "record",
"sounds": ["pianomod:piano/1.stereo"]
},
//...
"piano_note_20_stereo": {
"category": "record",
"sounds": ["pianomod:piano/1.stereo"]
}
}
Do notice how the sounds
array refers to the location inside of our sounds directory: piano
's the parent directory and the files range from 0 to 20.
Registering the sounds is made extremely easy on your end. And I really mean it - all you need is but a single method to handle it all.
You will need the general registration class, of type SoundEvent, and its contents will be as followed:
public class ModSounds {
public static final DeferredRegister<SoundEvent> SOUNDS = DeferredRegister.create(ForgeRegistries.SOUND_EVENTS, PianoMod.MODID);
public static void register(final IEventBus bus) {
SOUNDS.register(bus);
}
public static final NoteSound[] PIANO_NOTE_SOUNDS = NoteSoundRegistrer.createInstrumentNotes(
SOUNDS, new ResourceLocation(PianoMod.MODID, "piano"), true
)
}