Here is a LibAudio extension for playing FM sound effects and tunes.
libaudio-fmsynth.zip (12.4 KB)
Copy the contents of the zip file to your project and you are ready to use FM sound in your game. There are two main classes: FMSynthSource and SimpleTuneFMSource, which is like SimpleTune, but using FMSynth.
FMSynthSource
To play a sound effect, first create FMPatch:
constexpr auto fmpatch = Audio::FMPatch().algorithm(1).volume(100).feedback(0).glide(0).attack(20).decay(0).sustain(100).release(40)
.initLFO(Audio::FMPatch::LFO().speed(70).attack(60).pmd(5))
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(1).fine(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(25).fixed(false).coarse(2).fine(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(2, Audio::FMPatch::Operator().level(0))
.initOperator(3, Audio::FMPatch::Operator().level(0));
Each FMPatch is 47 bytes in size. You can use the FMSynth to create patches and then copy the parameters to your code.
To play the patch:
Audio::FMSynthSource& source = fmpatch.play(notenumber, velocity);
// Or
//Audio::FMSynthSource& source = Audio::play(fmpatch.patch(), notenumber, velocity);
Unlike Pokitto Synth, notes do not automatically stop in FMSynth. You have to manually trigger the release stage of the ADSR envelope:
source.release();
SimpleTuneFMSource
With SimpleTuneFM you can play tunes or create sound effects that are made up of multiple notes. It would also make sense to play just a single note, beacuse then you don’t need to manually call release() as with FMSynthSource.
You initialize a tune in a similar way to LibAudio SimpleTune, just use SIMPLE_TUNE_FM instead of SIMPLE_TUNE:
constexpr auto tune = SIMPLE_TUNE_FM(E-5/3, X/6, A#4/2, B-3*2).patch(fmpatch).tempo(120);
Tempo and FMpatch can be set at initialization or later when you play the tune.
X marks a rest/pause. When playing a patch with glide enabled, X can be used to stop the previous note, so that the next note plays without pitch glide from previous one.
To play a tune:
auto& source = Audio::playSimpleTuneFM(tune);
Tempo and FMPatch can be changed using the source. SimpleTuneFM holds its own copy of FMPatch so changing the parameters doesn’t modify the original FMPatch.
source.tempo(180);
source.patch(other_patch); // FMPatch can be replaced
source.patch().attack(30).decay(50).sustain(30); // or you can modify parameters individually
Example
Below is an example that plays couple of different Super Mario and Commander Keen sound effects using SimpleTuneFM.
Example code
#include <Pokitto.h>
#include <LibAudio>
#include "FMSynthSource.h"
#include "SimpleTuneFMSource.h"
constexpr auto square_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(0).attack(20).decay(0).sustain(100).release(10)
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(1).fine(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(25).fixed(false).coarse(2).fine(0).attack(0).decay(0).sustain(100).loop(false));
// Super Mario sfx
constexpr auto mario_1up_tune = SIMPLE_TUNE_FM(E-6/16, G-6/16, E-7/16, C-7/16, D-7/16, G-7/16)
.patch(square_patch).tempo(120);
constexpr auto mario_coin_tune = SIMPLE_TUNE_FM(B-5/26, E-6)
.patch(square_patch).tempo(120);
constexpr auto mario_jump_tune = SIMPLE_TUNE_FM(D-4*1, X/128, C#4/128, D-5*32) // First a short D-4, then glide from C#4 to D-5
.patch(square_patch).tempo(8*180*4); // 8th notes, tempo 180 bpm (4th note is one beat)
// Commander Keen sfx
constexpr auto keen_item_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(0).attack(0).decay(35).sustain(50).release(1)
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(1).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(40).fixed(false).coarse(4).fine(0).detune(0).attack(1).decay(30).sustain(0).loop(false));
constexpr auto keen_item_tune = SIMPLE_TUNE_FM(D#5*1, X*2, E-5*2, X*2, F-5*3, X*2, F#5*2)
.patch(keen_item_patch).tempo(32*120*4); // 32th notes, tempo 120 bpm (4th note is one beat)
constexpr auto keen_ammo_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(0).attack(10).decay(0).sustain(100).release(1)
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(1).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(50).fixed(false).coarse(1).fine(0).detune(0).attack(15).decay(0).sustain(100).loop(false));
constexpr auto keen_ammo_tune = SIMPLE_TUNE_FM(C#5*1, X*2, E-5*2, X*2, G-5*3, X*2, C#5*2, X*2, G-5*3)
.patch(keen_ammo_patch).tempo(16*120*4);
constexpr auto keen_enter_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(0).attack(0).decay(30).sustain(65).release(1)
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(3).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(25).fixed(false).coarse(2).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false));
constexpr auto keen_enter_tune = SIMPLE_TUNE_FM(D-4*2, X*2, D-4*3, X*2, D-4*4, X*2, D-4*6, X*5, E-3*4, X*2, E-3*4, X*2, G-3*2, X*2, G-3*6, X*2, C#4*10, D-4*11, D#4*10, F-4*3, X*2, F-4*2, X*2, F-4*3, X*2, F-4*6)
.patch(keen_enter_patch).tempo(32*120*4);
constexpr auto keen_noway_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(30).attack(10).decay(0).sustain(100).release(1)
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(1).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(50).fixed(false).coarse(1).fine(0).detune(0).attack(15).decay(0).sustain(100).loop(false));
constexpr auto keen_noway_tune = SIMPLE_TUNE_FM(D-3/128, E-3*2, X*2, D#3/128, A-2*2) // Glide from D-3 to E-3, then from D#3 to A-2
.patch(keen_noway_patch).tempo(16*120*4);
constexpr auto keen_die_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(40).attack(25).decay(50).sustain(50).release(1)
.initOperator(0, Audio::FMPatch::Operator().level(100).fixed(false).coarse(1).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false))
.initOperator(1, Audio::FMPatch::Operator().level(60).fixed(false).coarse(2).fine(0).detune(0).attack(0).decay(55).sustain(50).loop(false));
constexpr auto keen_die_tune = SIMPLE_TUNE_FM(B-1*4, X*1, B-1*7, X*1, F-2*13, X*1, E-3/128, A#2*9, X, A#2/128, D-2*10, X, B-1/128, B-0*17)
.patch(keen_die_patch).tempo(16*120*4);
constexpr char* TUNE_NAMES[] = {"MARIO 1UP", "MARIO COIN", "MARIO JUMP", "KEEN ITEM", "KEEN AMMO", "KEEN ENTER", "KEEN NOWAY", "KEEN DIE"};
// This line is useless now, use #define NUM_CHANNELS 4 in `My_settings.h` instead
// Audio::Sink<4, PROJ_AUD_FREQ> audio;
int main() {
using Pokitto::Core;
using Pokitto::Display;
using Pokitto::Buttons;
Core::begin();
unsigned int tune_idx = 0;
Audio::FMSynthSource* source = nullptr;
while(Core::isRunning()) {
if(!Core::update()) {
continue;
}
if(Buttons::pressed(BTN_LEFT)) {
tune_idx = (tune_idx - 1) & 7;
}
if(Buttons::pressed(BTN_RIGHT)) {
tune_idx = (tune_idx + 1) & 7;
}
Display::clear();
Display::print("\nTUNE:\n");
Display::print(TUNE_NAMES[tune_idx]);
// Pressing button A plays the selected tune
if(Buttons::pressed(BTN_A)) {
switch(tune_idx) {
// Mario sound effects use the same square wave patch but glide and ADSR settings are customized in run-time
case 0:
{
auto& tunesource = Audio::playSimpleTuneFM(mario_1up_tune);
tunesource.patch().glide(0).attack(0).decay(40).sustain(0);
break;
}
case 1:
{
auto& tunesource = Audio::playSimpleTuneFM(mario_coin_tune);
tunesource.patch().glide(1).attack(0).decay(57).sustain(0);
break;
}
case 2:
{
auto& tunesource = Audio::playSimpleTuneFM(mario_jump_tune);
tunesource.patch().glide(48).attack(0).decay(52).sustain(0);
break;
}
case 3: Audio::playSimpleTuneFM(keen_item_tune); break;
case 4: Audio::playSimpleTuneFM(keen_ammo_tune); break;
case 5: Audio::playSimpleTuneFM(keen_enter_tune); break;
case 6: Audio::playSimpleTuneFM(keen_noway_tune); break;
case 7: Audio::playSimpleTuneFM(keen_die_tune); break;
}
}
// Play a square wave when button B is pressed and stop when it is released
if(Buttons::pressed(BTN_B)) {
source = &square_patch.play(37); // Play note 37 (C-4) using default velocity (127)
}
else if(Buttons::released(BTN_B)) {
if(source) source->release();
}
}
return 0;
}
This is what the example sounds like:

It is just not very easy even with the FMSynth app. 