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

// 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:

8 Likes

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

2 Likes

This has been a long time coming. But this awesome technical feat (FM library), Pomifactory game and other contributions made the Batcomputer sit up, turn its ears and print out a much deserved prize.

Hereby awarded, the highest honour of technical prowess on Pokitto…

wizard king
@jpfli

Congratulations!!!

5 Likes

Wow you are handing out awards at a great rate. Soon you will handing out pardons (like Trump is about to) …

1 Like

Wouldn’t be nice have this project embedded on Pokitto Lib for an easy #define integration?
And avoid missing such a great functionality?

2 Likes

Cool, thanks!

A fanfare to celebrate the badge:

Show code
constexpr auto fanfare_patch = Audio::FMPatch().algorithm(11).volume(80).feedback(20).glide(0).attack(0).decay(75).sustain(0).release(60)
    .initOperator(0, Audio::FMPatch::Operator().level(70).fixed(false).coarse(1).fine(0).detune(0).attack(0).decay(45).sustain(0).loop(true))
    .initOperator(1, Audio::FMPatch::Operator().level(70).fixed(false).coarse(1).fine(25).detune(0).attack(0).decay(40).sustain(0).loop(true))
    .initOperator(2, Audio::FMPatch::Operator().level(70).fixed(false).coarse(1).fine(50).detune(0).attack(0).decay(35).sustain(0).loop(true))
    .initOperator(1, Audio::FMPatch::Operator().level(100).fixed(false).coarse(0).fine(50).detune(0).attack(30).decay(30).sustain(80).loop(false));

constexpr auto fanfare_tune = SIMPLE_TUNE_FM(G-4*4, G-4*4, E-4*2, G-4*4, A-4*10, B-4*2, B-4*4, B-4*2, A-4*4, B-4*4, B-5*8)
    .patch(fanfare_patch).tempo(16*240);

That’d be great. I wrote the FMSynthSource in a similar style to other LibAudio sources, and SimpleTuneFM is basically just a copy of SimpleTune with a few changes, so they should fit in nicely.

6 Likes

I am actually just implementing audio using this in my incoming game.

2 Likes

Works and sounds great! These are the 8-bit effects I have used as sampled audio before, like the coin sound in Super Mario. This, FMSynth, is how I will make the effects from now on!

The only problem is how to find the right parameters :wink: It is just not very easy even with the FMSynth app. @jpfli How did you make those effects? Are there any web pages to help?

I don’t have any particular tutorials to recommend, but there are many written and video tutorials online. Search for fm synth(esis) basics/tutorial, but ignore all tutorials that have ANY math formulas or too much theoretical explanations.

When replicating an existing sound, I look at the waveform and spectrum and tweak the parameters until I get similar result.

It’s often sufficient to only use modulator frequency ratios 1:1 and 2:1. With 1:1 ratio you get a “saw-type” waveform (1st, 2nd, 3rd etc. harmonics) and 2:1 ratio gives you a “square-type” waveform (1st, 3rd, 5th etc. harmonics). Then use the modulator amplitude to adjust how much harmonics you want.

In the image below OP1 & OP2 generate a saw-type and OP3 & OP4 a square-type wave. The fundamental frequency is actually same in both waveforms, but OP4 amplitude is relatively high so the square-type sound is brighter with much more harmonics.

And remember that you can use operator ADS-envelopes to change the modulation over time and make the sound less monotonic, adding a sharp bright attack or making a sound where harmonic content slowly increases or decreases.

Let me know if you have any further questions or problems with a particular sound effect.

5 Likes

Would it be possible to play multiple instruments at the same time to have a more complex song (compared with a simpletune)?

Yes, you can play multiple FMSynthSources by giving different channel numbers as template parameter.

std::int8_t note;
Audio::FMSynthSource* chn0 = &drone_patch.play<0>(note=25);  // Drone on channel 0
Audio::FMSynthSource* chn1 = &piano_patch.play<1>(note=41);  // Piano on channel 1

But there is something weird when I tested this with 4 channels. You can play instruments on channel 0 and only one of the channels 1, 2 and 3 at a time. For example, if you play instruments on channels 0 and 2, and then play a new instrument on channel 1, the instrument on channel 2 stops.

I didn’t yet find where the problem is. At least a new instance of FMSynthSource is correctly created for each channel.

3 Likes

Mystery solved! In the code example in the first post, this line is useless:

Audio::Sink<4, PROJ_AUD_FREQ> audio;

, because an instance of Audio::Sink is already created in POKITTO_CORE/PokittoSound.cpp, and it uses NUM_CHANNELS define, which defaults to 2.

So to change channel count to 4, this should be added to My_settings.h:

#define NUM_CHANNELS 4

@sbmrgd, I hadn’t thought about it earlier, but you can play multiple simpletunes at the same time and assign them to different audio channels in the same way as synth source. It’s still simpletune, but you can have e.g. 4 simultaneous simpletunes each playing different track.

#include <Pokitto.h>
#include "FMSynthSource.h"
#include "SimpleTuneFMSource.h"

// Instruments
constexpr auto bass_patch = Audio::FMPatch().algorithm(9).volume(80).feedback(0).glide(0).attack(0).decay(85).sustain(0).release(50)
    .initOperator(0, Audio::FMPatch::Operator().level(60).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(1).fine(25).detune(0).attack(0).decay(0).sustain(100).loop(false))
    .initOperator(2, Audio::FMPatch::Operator().level(60).fixed(false).coarse(1).fine(50).detune(0).attack(0).decay(0).sustain(100).loop(false))
    .initOperator(3, Audio::FMPatch::Operator().level(55).fixed(false).coarse(3).fine(0).detune(0).attack(0).decay(45).sustain(0).loop(false));
constexpr auto chimes_patch = Audio::FMPatch().algorithm(6).volume(80).feedback(0).glide(0).attack(0).decay(0).sustain(100).release(70)
    .initOperator(0, Audio::FMPatch::Operator().level(80).fixed(false).coarse(2).fine(0).detune(0).attack(0).decay(65).sustain(0).loop(false))
    .initOperator(1, Audio::FMPatch::Operator().level(80).fixed(false).coarse(2).fine(0).detune(7).attack(0).decay(75).sustain(0).loop(false))
    .initOperator(2, Audio::FMPatch::Operator().level(15).fixed(false).coarse(3).fine(0).detune(0).attack(0).decay(80).sustain(0).loop(false))
    .initOperator(3, Audio::FMPatch::Operator().level(30).fixed(false).coarse(10).fine(0).detune(0).attack(1).decay(85).sustain(0).loop(false));
constexpr auto theremin_patch = Audio::FMPatch().algorithm(1).volume(80).feedback(0).glide(45).attack(40).decay(0).sustain(100).release(60)
    .initLFO(Audio::FMPatch::LFO().speed(70).attack(60).pmd(5))
    .initOperator(0, Audio::FMPatch::Operator().level(80).fixed(false).coarse(1).fine(0).detune(0).attack(0).decay(0).sustain(100).loop(false))
    .initOperator(1, Audio::FMPatch::Operator().level(20).fixed(false).coarse(1).fine(0).detune(1).attack(0).decay(0).sustain(100).loop(false));

// Tracks
constexpr auto bass_track = SIMPLE_TUNE_FM(C-3*32, G-3*32, E-3*32, D-3*32, C-3*64)
    .patch(bass_patch).tempo(120*16);  // Tempo 120, use 16th note resolution for note lengths
constexpr auto chimes_track = SIMPLE_TUNE_FM(
    X*8, E-4*16, E-4*2, D-4*2, C-4*2, E-4*2, 
    D-4*24, D-4*2, C-4*2, A-3*2, D-4*2, 
    C-4*24, D-4*4, G-3*2, A-3*2,
    C-4*32)
    .patch(chimes_patch).tempo(120*16);
constexpr auto theremin_track = SIMPLE_TUNE_FM(
    X*4, C-5*16, E-5*16, 
    D-5*16, G-5*16, 
    A-5*16, D-5*16, 
    C-5*40)
    .patch(theremin_patch).tempo(120*16);

int main() {
    using Pokitto::Core;
    
    Core::begin();
    
    Audio::playSimpleTuneFM<0>(bass_track);     // Play bass track on channel 0
    Audio::playSimpleTuneFM<1>(chimes_track);   // Play chimes track on channel 1
    Audio::playSimpleTuneFM<2>(theremin_track); // Play theremin track on channel 2
    
    while(Core::isRunning()) {
        if(!Core::update()) {
            continue;
        }
    }
}

6 Likes

That is epic.

Great news!

Two more tricks you can do with simpletune: Looping and multiple instruments on one channel.

Multiple tunes/instruments can be easily played on one channel. Only thing to notice is that tunes use LibSchedule internally and they choose a timer ID based on audio channel number, so you must manually provide unique timer ID as template parameter. Otherwise you will only hear one tune.

Although simpletune doesn’t support looping, LibSchedule::repeat() can be used to loop tunes. Just set the correct interval based on tempo and length of the tune. Looping automatically stops if you return false from the repeat function. To manually stop the song, you need to cancel the loop and tune timers and optionally also stop audio channels. See the code example below.

Here is an example that plays kick and snare drum on channel 0 and bass on channel 1. The drum track loops every 2 bars and bass track every 4 bars.

#include <cstdint>
#include <Pokitto.h>
#include <LibSchedule>
#include "FMSynthSource.h"
#include "SimpleTuneFMSource.h"

constexpr std::uint32_t TEMPO = 120;
constexpr std::uint32_t MILLIS_PER_BAR = (4*1000*60)/TEMPO;

// Instruments
constexpr auto kick_patch = Audio::FMPatch().algorithm(8).volume(80).feedback(0).glide(0).attack(0).decay(0).sustain(100).release(50)
    .initOperator(0, Audio::FMPatch::Operator().level(80).fixed(false).coarse(0).fine(50).detune(0).attack(0).decay(30).sustain(0).loop(false))
    .initOperator(1, Audio::FMPatch::Operator().level(80).fixed(true).coarse(12).fine(45).detune(0).attack(0).decay(10).sustain(0).loop(false))
    .initOperator(2, Audio::FMPatch::Operator().level(80).fixed(false).coarse(0).fine(50).detune(0).attack(0).decay(50).sustain(0).loop(false))
    .initOperator(3, Audio::FMPatch::Operator().level(80).fixed(false).coarse(0).fine(50).detune(0).attack(0).decay(5).sustain(25).loop(false));
constexpr auto snare_patch = Audio::FMPatch().algorithm(6).volume(80).feedback(50).glide(0).attack(0).decay(0).sustain(100).release(50)
    .initOperator(0, Audio::FMPatch::Operator().level(80).fixed(false).coarse(1).fine(29).detune(0).attack(5).decay(30).sustain(0).loop(false))
    .initOperator(1, Audio::FMPatch::Operator().level(70).fixed(false).coarse(1).fine(0).detune(0).attack(1).decay(40).sustain(0).loop(false))
    .initOperator(2, Audio::FMPatch::Operator().level(75).fixed(false).coarse(2).fine(60).detune(0).attack(5).decay(0).sustain(100).loop(false))
    .initOperator(3, Audio::FMPatch::Operator().level(60).fixed(false).coarse(5).fine(0).detune(0).attack(10).decay(0).sustain(100).loop(false));
constexpr auto bass_patch = Audio::FMPatch().algorithm(9).volume(50).feedback(0).glide(0).attack(0).decay(85).sustain(0).release(50)
    .initOperator(0, Audio::FMPatch::Operator().level(60).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(1).fine(25).detune(0).attack(0).decay(0).sustain(100).loop(false))
    .initOperator(2, Audio::FMPatch::Operator().level(60).fixed(false).coarse(1).fine(50).detune(0).attack(0).decay(0).sustain(100).loop(false))
    .initOperator(3, Audio::FMPatch::Operator().level(55).fixed(false).coarse(3).fine(0).detune(0).attack(0).decay(45).sustain(0).loop(false));

// Tracks
constexpr auto kick_track = SIMPLE_TUNE_FM(
    E-3*2, E-3*4, E-3*2, E-3*1, E-3*3, E-3*4, E-3*2, E-3*4, E-3*2, E-3*1, E-3*3, E-3*4)
    .patch(kick_patch).tempo(TEMPO*16);
constexpr auto snare_track = SIMPLE_TUNE_FM(
    X*4, E-3*6, E-3*4, E-3*6, E-3*6, E-3*4, E-3*2)
    .patch(snare_patch).tempo(TEMPO*16);
constexpr auto bass_track = SIMPLE_TUNE_FM(
    F#3*6, E-4*4, A-3*4, A-3*6, G#3*4, F#3*4, E-3*4, 
    F-3*6, F#3*4, A-3*4, A-3*4, C-4*6, C#4*8)
    .patch(bass_patch).tempo(TEMPO*16);

std::uint32_t counter0;
std::uint32_t counter1;

int main() {
    // Unique timer ID needed to play multiple tunes on one channel
    constexpr std::uint32_t TIMER_ID_KICK = 100;
    constexpr std::uint32_t TIMER_ID_SNARE = 101;
    constexpr std::uint32_t TIMER_ID_BASS = 102;  // Only needed for stopping the bass tune
    
    using Pokitto::Core;
    using Pokitto::Buttons;
    
    Core::begin();
    
    while(Core::isRunning()) {
        if(!Core::update()) {
            continue;
        }
        
        if(Buttons::pressed(BTN_A)) {
            // Start kick and snare drum tracks on channel 0
            Audio::playSimpleTuneFM<0, TIMER_ID_KICK>(kick_track);
            Audio::playSimpleTuneFM<0, TIMER_ID_SNARE>(snare_track);
            counter0 = 1;
            // And repeat them every 2 bars
            Schedule::repeat<0>(2*MILLIS_PER_BAR, [](){
                Audio::playSimpleTuneFM<0, TIMER_ID_KICK>(kick_track);
                Audio::playSimpleTuneFM<0, TIMER_ID_SNARE>(snare_track);
                ++counter0;
                return counter0 < 4;  // Loop 4 times
            });
            
            // Start bass track on channel 1
            Audio::playSimpleTuneFM<1, TIMER_ID_BASS>(bass_track);
            counter1 = 1;
            // And repeat them every 4 bars
            Schedule::repeat<1>(4*MILLIS_PER_BAR, [](){
                Audio::playSimpleTuneFM<1, TIMER_ID_BASS>(bass_track);
                ++counter1;
                return counter1 < 2;  // Loop 2 times
            });
        }
        
        if(Buttons::pressed(BTN_B)) {
            // Stop looping
            Schedule::cancel<0>();
            Schedule::cancel<1>();
            // Stop tunes
            Schedule::cancel<TIMER_ID_KICK>();
            Schedule::cancel<TIMER_ID_SNARE>();
            Schedule::cancel<TIMER_ID_BASS>();
            // Stop audio channels
            Audio::stop<0>();
            Audio::stop<1>();
        }
    }
}

2 Likes