LibAudio FMSynth

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"};

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:

8 Likes

Sounds great! I will likely use this in the next game instead of digitized samples.

2 Likes