Project tutorial
Arpeggino - MIDI Arpeggiator, Sequencer, Recorder and Looper

Arpeggino - MIDI Arpeggiator, Sequencer, Recorder and Looper © MIT

Play MIDI arpeggios in every scale and pattern. Record and loop your sequences. Modify your sequences even after recording and be creative.

  • 5,018 views
  • 5 comments
  • 8 respects

Components and supplies

About this project

What is Arpeggino

Arpeggino is an Arduino based MIDI arpeggiator, sequencer, recorder and looper. By clicking on one of the keys, an arpeggio of the selected scale degree will start playing.

With Arpeggino you can configure every aspect of the arpeggios while seeing the configuration on the LCD screen:

  • Root note of the scale (e.g. C, D#, E, Fb)
  • Scale mode: Ionian, Dorian, Phrygian, Lydian, Mixolydian, Aeolian, Locrian
  • Octave (e.g. 2, 3, 4)
  • Permutation (i.e. the order in which the arpeggio notes are being played)
  • Steps (i.e. the number of notes of the arpeggio)
  • Rhythm (i.e. the musical rhythm in which the arpeggio is being played)
  • BPM

Arpeggino lets you record your sequences, for any number of bars you want. You can delete recorded layers, record new ones on top of the sequence, and even reconfigure recorded layers. This lets you focus on your musical creativity and experiment with different scales, styles, and rhythms.

Demo

Here's a short video that demonstrates the basic features of Arpeggino.

Note that Arpeggino is here at its full board configuration. In the tutorial we will be implementing much simpler configurations at first.

Arpeggino Teaser

The Project Code

Arpeggino consists of three sub-projects:

  • Controlino - An Arduino library for using complex gestures of input controls behind a multiplexer. It is the library used in Arpeggino to easily control buttons and potentiometers behind a multiplexer. It allows the detection of complex click gestures such as click/double-click/long click (press)/etc.
  • Midier - An Arduino library for playing, recording, looping and programming MIDI notes, arpeggios and sequences. This is the engine behind Arpggino, that lets it do all the musical and rhythmical stuff.
  • Arpeggino - The Arduino sketch project. This project is responsible for handling all I/O controls (keys, buttons, potentiometers, LCD), configuring Midier and using it for recording and playing the sequences.

All three projects (Arpeggino, Midier, and Controlino) are available on GitHub. They are comprehensively documented and feature plenty of examples you can check out and listen to right from your browser.

Both Midier and Controlino libraries are officially published and can be installed directly from the Arduino IDE Library Manager (here's a short guide).

Project Setup

Arpeggino sends MIDI commands over the Arduino's serial connection. These MIDI commands can be the input to any device that supports MIDI as input. A computer software can accept MIDI commands as well. Therefore, the computer can be the device we connect the Arduino to. There are two main ways you can connect Arpeggino to your computer: (1) over the USB connection using the help of some software (I use Hairless MIDI-Serial Bridge) (2) using a MIDI-to-USB cable. The two ways are fully documented here in Midier GitHub repository.

Note that you need to set the baud rate properly in the Arduino sketch (the parameter passed to the Arduino serial module upon initialization) according to the way you are connecting your Arduino to the MIDI device. This is also fully described here in Midier GitHub repository.

How It All Started

Having studied Computer Science, I wanted to experiment with technology other than software. I love music, and I was intrigued to know how it is represented digitally. This led me to learn DSP, and after a few weeks of studying, I started to look for a project idea that would involve both music and technology.

I ran into this Arduino project and even though MIDI is not proper DSP, it was immediately clear to me that I want to build a MIDI sequencer myself.

Back then I knew nothing about Arduino or electronics. I did not know how to begin the project, and I was certain that I would not get to the level of the project I took inspiration from. I started by playing a single note via MIDI, then chords, scales, and arpeggios. By the time more and more ideas came to me, such as having an LCD, supporting dynamic configuration, recording and looping, etc.

During the development of Arpeggino, I learned a lot about Arduino and electronics, MIDI, and music theory, and I was fascinated by how code and music theory seamlessly integrate.

Next Steps

I plan to design and print a closure for Arpeggino some time, and will update the project site as well.

Tutorial

This tutorial is 100% plug and play. Code (sketches), schemas, and illustrations of all steps are on GitHub.

You can even stop reading the tutorial right here if you want, and just do it on your own by following the schema of each step and uploading its sketch as-is.

We will be covering the main additions of every step and will have a look at code samples for some explanation.

Tutorial: Step Zero - Prerequisites

1. Clone the Arpeggino repository from GitHub to have all source files on your computer.

2. Install Midier and Controlino using the Arduino Library Manager (here's a short guide).

3. Pick your MIDI-to-Serial bridge software. I personally use Hairless MIDI-Serial Bridge for this (check this out if you are using macOS Catalina or later and there's still no official 64-bit version of the application). Make sure you choose the same baud rate as you will be using in the Arduino sketch (I use 9600).

4. Pick your favorite software for playing MIDI notes. A few that I know of: GarageBand (my choice), Logic Pro X, Ableton, Cubase, LMMS, etc.

5. (Optional) Check out some of Midier examples on GitHub and verify your setup works.

Tutorial: Step One - Playing Arpeggios

Here's a video demonstrating what we will achieve in this step of the tutorial:

Tutorial Demo - Step One - Playing Arpeggios

First we need to connect a few buttons to the Arduino. Having eight buttons is optimal as it is the number of notes in diatonic scales (including one for the root at an octave higher).

To simplify the development, it's better to connect all buttons to sequential pins on the Arduino (e.g. pins 2-9).

We use INPUT_PULLUP for buttons (here's an explanation) so one leg of the button should be plugged to GND and the other one to the Arduino.

Here's a schema you can use:

Here are some pictures of my Arduino in the current setup:

Now the setup is ready, and we can start coding.

You can use the existing Arduino sketch from GitHub, or you can code it yourself to see how easy it is.

For the sake of the tutorial, I'll assume you'll be coding yourself.

Open up a new Arduino sketch and add the following imports:

#include <Controlino.h>
#include <Midier.h>

We now need to create a midier::Sequencer to play arpeggios, as Midier is the engine behind Arpeggino. A sequencer should be initialized with some layers (which dictate the maximum number of concurrent layers that could be played) upon creation.

The sequencer should be declared in a global scope for its state to be preserved all the time, and should not be declared inside a method.

midier::Layers<8> layers;
midier::Sequencer sequencer(layers);

We also need to initialize the Arduino Serial module with the correct baud rate (the one you are using in the software as well). I'm using 9600.

void setup()
{
    Serial.begin(9600);
}

Now comes the real action. Every iteration, we want to check if any of the keys was pressed or released, and start or stop playing the arpeggio respectively. After checking for I/O, we have to "click" Midier.

The following code might seem complicated at first sight but it is really not. We define a structure that extends controlino::Key that can hold the handler of an arpeggio that is being played by Midier. We use this structure and initialize an array of keys using with the pin numbers to which the buttons are connected (here the buttons are connected to pins 2-9). We then check if the key was pressed (Down event) or released (Up event) and start or stop the arpeggio of the respective scale degree. Eventually we "click" the Midier sequencer for it to play and stop the MIDI notes.

void loop()
{
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(pin)
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 2, 3, 4, 5, 6, 7, 8, 9 }; // pin numbers

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue;
        }

        if (event == Key::Event::Down)
        {
            key.h = sequencer.start(i + 1); // start playing the arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up)
        {
            sequencer.stop(key.h);
        }
    }

    sequencer.click(midier::Sequencer::Run::Async);
}

That's it! Upload it onto your Arduino, start the MIDI-to-Serial bridge, open up your DAW (or any software that plays MIDI notes) and press the keys! You should hear arpeggios from the C major scale as you press the keys.

Tutorial: Step Two - Configuring the Arpeggios

Now that we have successfully played some arpeggios, it's time for us to start configuring them.

Here's a video demonstrating what we will achieve in this step of the tutorial:

Tutorial Demo - Step Two - Configuring the Arpeggios

As listed above, every aspect of the arpeggio can be configured and changed. Therefore, we are going to have an I/O control for every configuration parameter.

We will use a potentiometer for controlling the BPM and six buttons for controlling all other configuration parameters.

Here's a schema you can use:

Here are some pictures of my Arduino in the current setup:

Now let's do some coding again. We will extend our Arduino sketch from the previous step, but in this step of the tutorial I'll not go through all the code that needs to be written but just some of it that covers the main concepts.

Please refer to the Arduino sketch of this step for the full implementation or just use it as-is.

First of all, we will have to create an object for every I/O control we are using. We will have a controlino::Potentiometer for the BPM I/O control and limit its value to be at least 20 and at most 230. We will have a controlino::Key for all other I/O controls.

Here is the I/O control object declarations, using the pin numbers as shown in the schema:

namespace io
{

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]
controlino::Key Note(10);
controlino::Key Mode(11);
controlino::Key Octave(12);
controlino::Key Perm(A5);
controlino::Key Steps(A4);
controlino::Key Rhythm(A3);

} // io

If you have connected fewer I/O controls, make sure to comment out all the code that uses this I/O control. A good way is to comment out the I/O control object declaration and comment out all code that fails to compile.

After creating the I/O objects, we are going to use them. Every time loop() gets called, we want to check for any I/O activity, and change the configuration parameter of the respective I/O control.

We will declare a Configurer for every I/O control. A Configurer will be a method that is responsible for updating a single configuration parameter according to changes of an I/O control. We will use the check() method of our Controlino I/O controls to check for I/O events.

Let's take a look at the BPM configurer:

namespace configurer
{

void BPM()
{
    // if the value of the potentiometer has changed
    // we read the new value of the potentiometer
    // and set it as the current BPM of the sequencer
    if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
    {
        sequencer.bpm = io::BPM.read();
    }
}

} // configurer

Let's now take a look at the octave configurer:

namespace configurer
{

void Octave()
{
    // if the key was pressed, we increase the
    // and set it as the new configuration value
    if (io::Octave.check() == controlino::Key::Event::Down)
    {
        const auto current = sequencer.config.octave();
        const auto next = (current % 7) + 1;
        sequencer.config.octave(next);
    }
}

} // configurer

We will have such Configurer methods for every I/O control. Take a look at them in the Arduino sketch of this step.

We will now have an array to hold all the Configurer methods so we could iterate through all of them easily:

namespace configurer
{

// a configurer is a method that is responsible 
//
// according to changes of an I/O control
using Configurer = void(*)();

Configurer All[] =
    {
        BPM,
        Octave,
        // all other configurer methods
    };

} // configurer

Now we have to call each and every Configurer method every time loop() gets called. We will declare a helper method to do so:

namespace handle
{

void configurers()
{
    // configurers will update the configuration on I/O events
    for (const auto & configurer : configurer::All)
    {
        configurer();
    }
}

} // handle 

And call it in loop(), by adding the following line:

void loop()
{
    handle::configurers();
    // everything else..
}

That's it! We declared objects for our I/O controls, implemented Configurer methods to update the configuration when an I/O event was received on each I/O control, and called all Configurer methods every time loop() gets called.

Go ahead and play some arpeggios. This time, play around with the new buttons and the potentiometer we have just added, and listen how they affect the arpeggios that you are playing.

Tutorial: Step Three - LCD

In this step we will add an LCD to our setup for presenting the configuration.

Here's a video demonstrating what we will achieve in this step of the tutorial:

Tutorial Demo - Step Three - LCD

Connecting an LCD to the Arduino requires 6 (digital) I/O pins. I'm using and Arduino UNO and in the current setup, with all keys and configuration buttons, there are no six more available I/O pins. Therefore, I'm using a 16-channel multiplexer (here's a great explanation) in order to significantly reduce the number of I/O pins that will be used for all the keys and configuration buttons. This will make plenty more I/O pins available on the Arduino, and will allow to connect an LCD to it.

I'm also using two more mini breadboards to help me organize the main breadboard. There are a lot of possibilities here. You can use a single breadboard. I did it at first, it's possible but might be suboptimal. You can use an extra mini breadboard or two. You can use an extra full breadboard. So just use whatever gear you have and choose your preferred setup layout. I use more than a single breadboard because connecting a lot of buttons, potentiometers, and an LCD results in a pretty cluttered breadboard. Alternative schemas are available on GitHub.

Don't worry if you don't have all this hardware. All the new components we will be adding are probably not available to everyone.

First of all, I'd recommend you to order those parts so you could eventually build your full Arpeggino. In the meantime, you can skip this step of the tutorial entirely and continue to the next steps without an LCD for now.

If you have an LCD but you don't have a multiplexer, you can get rid of some of the keys to free up some pins for the LCD.

If you now have all the needed hardware components (an LCD and a multiplexer), we can continue with this step of the tutorial.

Adding the LCD to our project requires many changes, both in hardware and software. To ease the tutorial, and decrease the amount of changes needed to be done before seeing some feedback, this step is split to three sub-steps. You can follow them one by one to get some feedback from the Arduino that you are headed in the right direction. You can also just go ahead and follow the last one (part 3) if you are feeling in control.

Part 1 - Multiplexer

First, we will add the 16-channel multiplexer to our setup. The multiplexer itself takes some space on the breadboard, so I'm using an extra mini breadboard for it to have a place for itself. Again, this is not mandatory and just go on with the hardware you have (here's a schema I used when I did not have an extra mini breadboard).

In addition, we will reorganize the buttons and the potentiometer a bit. This is for the laying the groundwork for the next steps.

Here's the schema of my setup:

Here's a picture of my current setup:

Now to the software side. We will create a controlino::Multiplexer object for the usage of the multiplexer. Upon creation, we will pass a controlino::Selector object to it as an argument that encapsulates the selection pins of a multiplexer.

namespace io
{

controlino::Selector Selector(/* s0 = */ 6, /* s1 = */ 5, /* s2 = */ 4, /* s3 = */ 3);
controlino::Multiplexer Multiplexer(/* sig = */ 2, Selector);

} // io

Now, all configuration buttons are behind the multiplexer. We will pass io::Multiplexer to the creation of each controlino::Key, and use the channel number as the pin number.

namespace io
{

controlino::Key Note(Multiplexer, 7);
controlino::Key Mode(Multiplexer, 6);
controlino::Key Octave(Multiplexer, 5);
controlino::Key Perm(Multiplexer, 4);
controlino::Key Steps(Multiplexer, 3);
controlino::Key Rhythm(Multiplexer, 2);

} // io

In addition, all keys were moved to behind the multiplexer as well. Therefore, we will pass io::Multiplexer upon their creation as well, and will use the channel numbers as pin numbers.

namespace handle
{

void keys()
{
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(io::Multiplexer, pin) // keys are behind the multiplexer
        {}

        // ...
    };

    static Key __keys[] = { 15, 14, 13, 12, 11, 10, 9, 8 }; // channel numbers of the multiplexer
    // ...
}

} // handle 

Go ahead and upload the sketch onto your Arduino to see that everything is still working.

Part 2 - Basic LCD

Now it's time for the real thing. We freed up some I/O pins in the Arduino, and we can now add the LCD to our setup.

I'm using a second extra mini breadboard for this, but again, this is not mandatory (here's a schema I used when I did not have a second extra mini breadboard).

Here's the schema of my setup:

Here are some pictures of my current setup:

And again to coding. We are using LiquidCrystal library to control the LCD. We will be extending the LiquidCrystal to fit better to our needs (have a look at io::LCD to see the implementation) and will create io::lcd using the pins shown in the schema.

Our LCD layout consists of a component for every configuration parameter. Every component will be printed in a specific position in the LCD, and is responsible for printing its configuration parameter. Some of the components have a prefix or suffix as well. We call this prefix or suffix the title, and the configuration parameter value is the data. We want to print the titles once at the beginning, and to reprint the data every time the configuration parameter changes.

We will have a viewer::Viewer method that will be responsible for printing both the title and data of a single configuration parameter. This is just like we have a configurer::Configurer which is responsible for updating the configuration parameter according to changes of an I/O control.

Let's take a look at the BPM viewer for example:

namespace viewer
{

void BPM(What what)
{
    if (what == What::Title)
    {
        io::lcd.print(13, 1, "bpm");
    }
    if (what == What::Data)
    {
        io::lcd.print(9, 1, 3, state::sequencer.bpm);
    }
}

} // viewer

We will replace our array of Configurers with a new structure to which we call Component that holds both a configurer::Configurer and a viewer::Viewer.

namespace component
{

struct Component
{
    configurer::Configurer configurer;
    viewer::Viewer viewer;
};

Component All[] =
    {
        { configurer::BPM, viewer::BPM },
        { configurer::Note, viewer::Note },
        { configurer::Mode, viewer::Mode },
        { configurer::Octave, viewer::Octave },
        { configurer::Perm, viewer::Style },
        { configurer::Steps, viewer::Style },
        { configurer::Rhythm, viewer::Rhythm },
    };

} // component

To support reprinting the configuration parameter data when changes, we will have all Configurers returning a boolean value that will indicate whether the parameter has changed. If so, we will call its viewer to reprint the data.

namespace handle
{

void configurers()
{
    for (const auto & component : component::All)
    {
        if (component.configurer())
        {
            component.viewer(viewer::What::Data); // reprint the value on the LCD if changed
        }
    }
}

} // handle

The last thing we have to do is to initialize our LCD at startup and print all titles and initial configuration values. This is done in setup().

void setup()
{
    // ...

    io::lcd.begin(16, 2);

    for (const auto & component : component::All)
    {
        component.viewer(viewer::What::Title);
        component.viewer(viewer::What::Data);
    }
}

Part 3 - Advanced LCD

After spending some time with the current setup, you might feel like we are not utilizing the LCD properly. Meaning, it's only full of short summarization of the configuration parameters, but in a not really informative way. For example, the rhythm and style are presented as numbers that could tell us pretty much nothing, the scale mode is not in its full name, and more.

This step will improve what the LCD has to offer in Arpeggino. This step requires no hardware changes from the previous part ("basic LCD"), so just follow its schema if you skipped it. In case you implemented the previous step, there's nothing more for you to do now with the hardware.

We are introducing a new "focus" mode for viewers to use the entire LCD for printing a longer description of the parameter while being configured. This is not used by all viewers.

In addition, the previous summary view is kept and is the one being used when not currently configuring parameters. To support this, we also introduce a helper class utils::Timer to allow us to easily check for how much time elapsed and if we should go back to summary view.

We will not have a look at some specific code samples but I encourage you to go ahead and explore the code by yourself.

Tutorial: Step Four - Recording

One of the major features of Arpeggino is the ability to record and playback MIDI sequences. In this step we will add the support for that.

Here's a video demonstrating what we will achieve in this step of the tutorial:

Tutorial Demo - Step Four - Recording

The only hardware addition that is really needed is a single button. We will add two more LEDs, a green one and a red one, to improve the user experience. The red one will indicate whether Arpeggino is recording, and the green one will blink when entering a new bar in the sequence.

If you skipped the previous step and did not add a multiplexer and an LCD to your setup, or diverged a bit from the schemas we are using, just add a single button to your setup and you are good to go.

Here's the schema of my setup:

Here are some pictures of my current setup:

We will declare our I/O object, and now we will use controlino::Button as we will use a few more click gestures, such as long-press and a click-and-press.

We will add a control method that will handle I/O events from this button. Upon each event we will call a method of our Midier Sequencer. On click, we will toggle the recording mode using record(). This is fully documented here on Midier repository. On long click (press), we will revoke the last recorded layer. On click and press, we will stop the recording entirely.

namespace handle
{

void record()
{
    const auto event = io::Record.check();

    if (event == controlino::Button::Event::Click)
    {
        state::sequencer.record();
    }
    else if (event == controlino::Button::Event::Press)
    {
        state::sequencer.revoke();
    }
    else if (event == controlino::Button::Event::ClickPress)
    {
        state::sequencer.wander();
    }
}

} // handle

We also want to print the bar index within the current loop. We will use the return value of sequencer.click() for this. It returns the bar index if just entered a new bar in a loop, Bar::Same if there's no change, and Bar::None if the loop was just stopped. In case a bar index is returned, we will blink the green LED, and print it using a new method control::view::bar(). In case the loop was stopped, Bar::None will be returned and we will clear this part of the screen.

namespace handle
{

void click()
{
    const auto bar = state::sequencer.click(midier::Sequencer::Run::Async);

    if (bar != midier::Sequencer::Bar::Same)
    {
        control::flash();

        if (viewer::focused == nullptr)
        {
            control::view::bar(bar);
        }
    }
}

} // handle 

The sketch of this step adds some more code, but we will not cover it here as it is pretty straightforward.

Tutorial: Step Five - Layers

In the previous step we added the support to record sequences in Arpeggino. In this step we will add the support to iterate recorded layers and control them individually.

Here's a video demonstrating what we will achieve in this step of the tutorial:

Tutorial Demo - Step Five - Layers

While recording, every key press creates a new layer in the sequence with the respective scale degree. Along with its scale degree, each layer has its configuration.

By default, all layers point to the common configuration (also called the global configuration) - the one that is shown on the LCD. This means that even after recording, you can change the common configuration, and all recorded layers will follow along your changes. This allows us to play around different styles and rhythms even after recording a sequence.

A layer can also detach from the common configuration and have its own private configuration. After detaching from the common configuration, any changes to the common configuration will not affect the layer.

When a layer points to the common configuration it is called "dynamic", and when it's detached from it and has its own private configuration, it is called "static".

These two possibilities together significantly expand the boundaries of our recorded sequences. We can combine both static and dynamic layers in our sequence. For example, we can have some static layers to play the role of a bass, configured to a certain octave, note, style, or rhythm. At the same time, we can have some dynamic layers with the melody itself, and playing around with their configuration post recording.

In this step of the tutorial, we will add the support to iterate recorded layers, and convert dynamic layers to be static layers and vice versa. While iterating layers, the layer number will be printed on the LCD, and its configuration - whether it's the common one or a private one - will be the one shown on the LCD. Also, while iterating layers, we will decrease the volume of all the other layers, so we could use our ears and easily understand which layer is selected at the moment. When a layer is selected, changing the configuration will cause the layer to detach from the common configuration, and the shown configuration will be set to its private configuration. Any changes while a layer is selected will affect only the selected layer.

A short detour if we are already talking of layers:

Layers can be either finite or infinite. A finite layer is a layer that starts some time within the sequence and stops at another time, before the sequence has fully looped. An infinite layer is a layer that is played all along the sequence.

For example, if we have a sequence of 4 bars, and we pressed a certain key for 2 bars, then its layer is finite. In contrast, if we pressed the key and did not release it until all 4 bars played entirely, and the sequence started to loop itself, then the layer is infinite.

Infinite layers will always be continuous, regardless of the number of bars in the sequence, the number of steps of the arpeggio, and the rhythm. Finite layers are not continuous, they stop when the key was released, and start again when it was pressed in the next loop of the sequence.

Let's take an extreme example, and say that we have a sequence of a single bar. Let's also say that we are playing in the rhythm of triplets, meaning that we have three notes in a bar. What happens if we played an arpeggio with more than three steps? The answer is that it depends.

If the key was pressed all along the sequence, then the layer is infinite, and the entire arpeggio will be played, with the correct number of steps. If, for example, we use six steps, then it will be played across two bars, even that our sequence is a single bar long. This might seem odd when you think about it, but your ears and feeling would suggest that it is clearly necessary, and it's the native and expected behavior if you continuously pressed a key along your sequence.

In contrast, if we released the key sometime before the loop was fully completed, then the layer would stop and start again in the next loop, meaning that not all steps would be heard. This is again the expected behavior if you released the key within the loop. Play with it for some time and you'll get the feeling.

Now that we have much more information about layers, we could get back to the tutorial.

On the hardware side, a single button is needed. I added it next to the recording button.

Here's the schema of my setup:

Here are some pictures of my current setup:

Now to the software side. Until now, we only referred to the common configuration. We now want to point to other configurations sometimes - the selected layer's configuration. Therefore, we will have a pointer that will point to the currently shown configuration, and we will refer to it in all places we referred to the common configuration. It will be initialized by pointing to the common configuration.

namespace state
{

midier::Config * config = &sequencer.config;

} // state 

We will also have to handle I/O events from the newly added button. Here's a summary of the supported gestures and their actions:

  • Click - Selects the next layer. This selects the first layer if currently no layer is selected, or the next one if one is currently selected. After the last layer, this will go back to the common configuration. We will set state::configuration to point to the configuration of the selected layer and print it in summary view.
  • Press - If a layer is selected, and is statically configured, we will make it dynamic again, so it will follow the common configuration again. If no layer is selected, we will convert all dynamic layers to be statically configured.
  • ClickPress - This will convert all layers to be dynamically configured. The common configuration will be printed in summary view.

The sketch of this step has more code additions, but we will not cover anymore. They contain the printing of the layer number in summary view, the handling of timers (going back to the common configuration after a few seconds), and some more minor changes.

Code

Tutorial: Step One - Playing Arpeggios - SketchC/C++
#include <Controlino.h>
#include <Midier.h>

namespace arpeggino
{

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

} // state

namespace handle
{

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(pin)
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 2, 3, 4, 5, 6, 7, 8, 9 }; // initialize with pin numbers

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    state::sequencer.click(midier::Sequencer::Run::Async);
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);
}

extern "C" void loop()
{
    handle::keys();
    handle::click();
}

} // arpeggino
Tutorial: Step Two - Configuring the Arpeggios - SketchC/C++
#include <Controlino.h>
#include <Midier.h>

namespace arpeggino
{

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

} // state

namespace io
{

// here we declare all I/O controls with their corresponding pin numbers

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]
controlino::Key Note(10);
controlino::Key Mode(11);
controlino::Key Octave(12);
controlino::Key Perm(A5);
controlino::Key Steps(A4);
controlino::Key Rhythm(A3);

} // io

namespace configurer
{

// a configurer is a method that is responsible for updating a single
// configuration parameter according to changes of an I/O control
using Configurer = void(*)();

void BPM()
{
    if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
    {
        state::sequencer.bpm = io::BPM.read();
    }
}

void Note()
{
    if (io::Note.check() != controlino::Key::Event::Down)
    {
        return; // nothing to do
    }

    // the key was just pressed

    auto & config = state::sequencer.config; // a shortcut

    if (config.accidental() == midier::Accidental::Flat)
    {
        config.accidental(midier::Accidental::Natural);
    }
    else if (config.accidental() == midier::Accidental::Natural)
    {
        config.accidental(midier::Accidental::Sharp);
    }
    else if (config.accidental() == midier::Accidental::Sharp)
    {
        config.accidental(midier::Accidental::Flat);

        if      (config.note() == midier::Note::C) { config.note(midier::Note::D); }
        else if (config.note() == midier::Note::D) { config.note(midier::Note::E); }
        else if (config.note() == midier::Note::E) { config.note(midier::Note::F); }
        else if (config.note() == midier::Note::F) { config.note(midier::Note::G); }
        else if (config.note() == midier::Note::G) { config.note(midier::Note::A); }
        else if (config.note() == midier::Note::A) { config.note(midier::Note::B); }
        else if (config.note() == midier::Note::B) { config.note(midier::Note::C); }
    }
}

void Mode()
{
    if (io::Mode.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.mode();
        const auto next = (midier::Mode)(((unsigned)current + 1) % (unsigned)midier::Mode::Count);

        state::sequencer.config.mode(next);
    }
}

void Octave()
{
    if (io::Octave.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.octave();
        const auto next = (current % 7) + 1;

        state::sequencer.config.octave(next);
    }
}

void Perm()
{
    if (io::Perm.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.perm();
        const auto next = (current + 1) % midier::style::count(state::sequencer.config.steps());

        state::sequencer.config.perm(next);
    }
}

void Steps()
{
    if (io::Steps.check() == controlino::Key::Event::Down)
    {
        auto & config = state::sequencer.config; // a shortcut

        if (config.looped() == false) // we set to loop if currently not looping
        {
            config.looped(true);
        }
        else
        {
            unsigned steps = config.steps() + 1;

            if (steps > 6)
            {
                steps = 3;
            }

            config.steps(steps);
            config.perm(0); // reset the permutation
            config.looped(false); // set as non looping
        }
    }
}

void Rhythm()
{
    if (io::Rhythm.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.rhythm();
        const auto next = (midier::Rhythm)(((unsigned)current + 1) % (unsigned)midier::Rhythm::Count);

        state::sequencer.config.rhythm(next);
    }
}

Configurer All[] =
    {
        BPM,
        Note,
        Mode,
        Octave,
        Perm,
        Steps,
        Rhythm,
    };

} // configurer

namespace handle
{

void configurers()
{
    // configurers will update the configuration on I/O events

    for (const auto & configurer : configurer::All)
    {
        configurer();
    }
}

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(pin)
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 2, 3, 4, 5, 6, 7, 8, 9 }; // initialize with pin numbers

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    state::sequencer.click(midier::Sequencer::Run::Async);
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);
}

extern "C" void loop()
{
    handle::configurers();
    handle::keys();
    handle::click();
}

} // arpeggino
Tutorial: Step Three - LCD - Part 1 - SketchC/C++
#include <Controlino.h>
#include <Midier.h>

namespace arpeggino
{

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

} // state

namespace io
{

// here we declare all I/O controls with their corresponding pin numbers

controlino::Selector Selector(/* s0 = */ 6, /* s1 = */ 5, /* s2 = */ 4, /* s3 = */ 3);
controlino::Multiplexer Multiplexer(/* sig = */ 2, Selector);

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]

// all configuration keys are behind the multiplexer
controlino::Key Note(Multiplexer, 7);
controlino::Key Mode(Multiplexer, 6);
controlino::Key Octave(Multiplexer, 5);
controlino::Key Perm(Multiplexer, 4);
controlino::Key Steps(Multiplexer, 3);
controlino::Key Rhythm(Multiplexer, 2);

} // io

namespace configurer
{

// a configurer is a method that is responsible for updating a single
// configuration parameter according to changes of an I/O control
using Configurer = void(*)();

void BPM()
{
    if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
    {
        state::sequencer.bpm = io::BPM.read();
    }
}

void Note()
{
    if (io::Note.check() != controlino::Key::Event::Down)
    {
        return; // nothing to do
    }

    // the key was just pressed

    auto & config = state::sequencer.config; // a shortcut

    if (config.accidental() == midier::Accidental::Flat)
    {
        config.accidental(midier::Accidental::Natural);
    }
    else if (config.accidental() == midier::Accidental::Natural)
    {
        config.accidental(midier::Accidental::Sharp);
    }
    else if (config.accidental() == midier::Accidental::Sharp)
    {
        config.accidental(midier::Accidental::Flat);

        if      (config.note() == midier::Note::C) { config.note(midier::Note::D); }
        else if (config.note() == midier::Note::D) { config.note(midier::Note::E); }
        else if (config.note() == midier::Note::E) { config.note(midier::Note::F); }
        else if (config.note() == midier::Note::F) { config.note(midier::Note::G); }
        else if (config.note() == midier::Note::G) { config.note(midier::Note::A); }
        else if (config.note() == midier::Note::A) { config.note(midier::Note::B); }
        else if (config.note() == midier::Note::B) { config.note(midier::Note::C); }
    }
}

void Mode()
{
    if (io::Mode.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.mode();
        const auto next = (midier::Mode)(((unsigned)current + 1) % (unsigned)midier::Mode::Count);

        state::sequencer.config.mode(next);
    }
}

void Octave()
{
    if (io::Octave.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.octave();
        const auto next = (current % 7) + 1;

        state::sequencer.config.octave(next);
    }
}

void Perm()
{
    if (io::Perm.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.perm();
        const auto next = (current + 1) % midier::style::count(state::sequencer.config.steps());

        state::sequencer.config.perm(next);
    }
}

void Steps()
{
    if (io::Steps.check() == controlino::Key::Event::Down)
    {
        auto & config = state::sequencer.config; // a shortcut

        if (config.looped() == false) // we set to loop if currently not looping
        {
            config.looped(true);
        }
        else
        {
            unsigned steps = config.steps() + 1;

            if (steps > 6)
            {
                steps = 3;
            }

            config.steps(steps);
            config.perm(0); // reset the permutation
            config.looped(false); // set as non looping
        }
    }
}

void Rhythm()
{
    if (io::Rhythm.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.rhythm();
        const auto next = (midier::Rhythm)(((unsigned)current + 1) % (unsigned)midier::Rhythm::Count);

        state::sequencer.config.rhythm(next);
    }
}

Configurer All[] =
    {
        BPM,
        Note,
        Mode,
        Octave,
        Perm,
        Steps,
        Rhythm,
    };

} // configurer

namespace handle
{

void configurers()
{
    // configurers will update the configuration on I/O events

    for (const auto & configurer : configurer::All)
    {
        configurer();
    }
}

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(io::Multiplexer, pin) // keys are behind the multiplexer
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 15, 14, 13, 12, 11, 10, 9, 8 }; // channel numbers of the multiplexer

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    state::sequencer.click(midier::Sequencer::Run::Async);
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);
}

extern "C" void loop()
{
    handle::configurers();
    handle::keys();
    handle::click();
}

} // arpeggino
Tutorial: Step Three - LCD - Part 2 - SketchC/C++
#include <Controlino.h>
#include <LiquidCrystal.h>
#include <Midier.h>

namespace arpeggino
{

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

} // state

namespace io
{

// here we declare all I/O controls with their corresponding pin numbers

controlino::Selector Selector(/* s0 = */ 6, /* s1 = */ 5, /* s2 = */ 4, /* s3 = */ 3);
controlino::Multiplexer Multiplexer(/* sig = */ 2, Selector);

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]

// all configuration keys are behind the multiplexer
controlino::Key Note(Multiplexer, 7);
controlino::Key Mode(Multiplexer, 6);
controlino::Key Octave(Multiplexer, 5);
controlino::Key Perm(Multiplexer, 4);
controlino::Key Steps(Multiplexer, 3);
controlino::Key Rhythm(Multiplexer, 2);

struct LCD : LiquidCrystal
{
    LCD(uint8_t rs, uint8_t e, uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7) : LiquidCrystal(rs, e, d4, d5, d6, d7)
    {}

    template <typename T>
    char print(const T & arg)
    {
        return LiquidCrystal::print(arg);
    }

    template <typename T>
    char print(char col, char row, const T & arg)
    {
        setCursor(col, row);
        return print(arg);
    }

    template <typename T>
    char print(char col, char row, char max, const T & arg)
    {
        const auto written = print(col, row, arg);

        for (unsigned i = 0; i < max - written; ++i)
        {
            write(' '); // make sure the non-used characters are clear
        }

        return written;
    }
};

LCD lcd(/* rs = */ 7, /* e = */ 8,  /* d4 = */ 9, /* d5 = */ 10, /* d6 = */ 11, /* d7 = */ 12);

} // io

namespace configurer
{

// a configurer is a method that is responsible for updating a single
// configuration parameter according to changes of an I/O control
using Configurer = bool(*)();

bool BPM()
{
    if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
    {
        state::sequencer.bpm = io::BPM.read();
        return true;
    }

    return false;
}

bool Note()
{
    if (io::Note.check() != controlino::Key::Event::Down)
    {
        return false; // nothing to do
    }

    // the key was just pressed

    auto & config = state::sequencer.config; // a shortcut

    if (config.accidental() == midier::Accidental::Flat)
    {
        config.accidental(midier::Accidental::Natural);
    }
    else if (config.accidental() == midier::Accidental::Natural)
    {
        config.accidental(midier::Accidental::Sharp);
    }
    else if (config.accidental() == midier::Accidental::Sharp)
    {
        config.accidental(midier::Accidental::Flat);

        if      (config.note() == midier::Note::C) { config.note(midier::Note::D); }
        else if (config.note() == midier::Note::D) { config.note(midier::Note::E); }
        else if (config.note() == midier::Note::E) { config.note(midier::Note::F); }
        else if (config.note() == midier::Note::F) { config.note(midier::Note::G); }
        else if (config.note() == midier::Note::G) { config.note(midier::Note::A); }
        else if (config.note() == midier::Note::A) { config.note(midier::Note::B); }
        else if (config.note() == midier::Note::B) { config.note(midier::Note::C); }
    }

    return true;
}

bool Mode()
{
    if (io::Mode.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.mode();
        const auto next = (midier::Mode)(((unsigned)current + 1) % (unsigned)midier::Mode::Count);

        state::sequencer.config.mode(next);
        return true;
    }

    return false;
}

bool Octave()
{
    if (io::Octave.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.octave();
        const auto next = (current % 7) + 1;

        state::sequencer.config.octave(next);
        return true;
    }

    return false;
}

bool Perm()
{
    if (io::Perm.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.perm();
        const auto next = (current + 1) % midier::style::count(state::sequencer.config.steps());

        state::sequencer.config.perm(next);
        return true;
    }

    return false;
}

bool Steps()
{
    if (io::Steps.check() == controlino::Key::Event::Down)
    {
        auto & config = state::sequencer.config; // a shortcut

        if (config.looped() == false) // we set to loop if currently not looping
        {
            config.looped(true);
        }
        else
        {
            unsigned steps = config.steps() + 1;

            if (steps > 6)
            {
                steps = 3;
            }

            config.steps(steps);
            config.perm(0); // reset the permutation
            config.looped(false); // set as non looping
        }

        return true;
    }

    return false;
}

bool Rhythm()
{
    if (io::Rhythm.check() == controlino::Key::Event::Down)
    {
        const auto current = state::sequencer.config.rhythm();
        const auto next = (midier::Rhythm)(((unsigned)current + 1) % (unsigned)midier::Rhythm::Count);

        state::sequencer.config.rhythm(next);
        return true;
    }

    return false;
}

} // configurer

namespace viewer
{

enum class What
{
    Title,
    Data,
};

using Viewer = void(*)(What);

void BPM(What what)
{
    if (what == What::Title)
    {
        io::lcd.print(13, 1, "bpm");
    }

    if (what == What::Data)
    {
        io::lcd.print(9, 1, 3, state::sequencer.bpm);
    }
}

void Note(What what)
{
    if (what == What::Data)
    {
        io::lcd.setCursor(0, 0);

        const auto & config = state::sequencer.config; // a shortcut

        if      (config.note() == midier::Note::A) { io::lcd.print('A'); }
        else if (config.note() == midier::Note::B) { io::lcd.print('B'); }
        else if (config.note() == midier::Note::C) { io::lcd.print('C'); }
        else if (config.note() == midier::Note::D) { io::lcd.print('D'); }
        else if (config.note() == midier::Note::E) { io::lcd.print('E'); }
        else if (config.note() == midier::Note::F) { io::lcd.print('F'); }
        else if (config.note() == midier::Note::G) { io::lcd.print('G'); }

        if      (config.accidental() == midier::Accidental::Flat)    { io::lcd.print('b'); }
        else if (config.accidental() == midier::Accidental::Natural) { io::lcd.print(' '); }
        else if (config.accidental() == midier::Accidental::Sharp)   { io::lcd.print('#'); }
    }
}

void Mode(What what)
{
    if (what == What::Data)
    {
        midier::mode::Name name;
        midier::mode::name(state::sequencer.config.mode(), /* out */ name);
        name[3] = '\0'; // trim the full name into a 3-letter shortcut
        io::lcd.print(0, 1, name);
    }
}

void Octave(What what)
{
    if (what == What::Title)
    {
        io::lcd.print(3, 0, 'O');
    }
    else if (what == What::Data)
    {
        io::lcd.print(4, 0, state::sequencer.config.octave());
    }
}

void Style(What what)
{
    if (what == What::Title)
    {
        io::lcd.print(6, 0, 'S');
    }
    else if (what == What::Data)
    {
        const auto & config = state::sequencer.config; // a shortcut

        io::lcd.print(7, 0, config.steps());
        io::lcd.print(8, 0, config.looped() ? '+' : '-');
        io::lcd.print(9, 0, 3, config.perm() + 1);
    }
}

void Rhythm(What what)
{
    if (what == What::Title)
    {
        io::lcd.print(4, 1, 'R');
    }
    else if (what == What::Data)
    {
        io::lcd.print(5, 1, 2, (unsigned)state::sequencer.config.rhythm() + 1);
    }
}

} // viewer

namespace component
{

struct Component
{
    configurer::Configurer configurer;
    viewer::Viewer viewer;
};

Component All[] =
    {
        { configurer::BPM, viewer::BPM },
        { configurer::Note, viewer::Note },
        { configurer::Mode, viewer::Mode },
        { configurer::Octave, viewer::Octave },
        { configurer::Perm, viewer::Style },
        { configurer::Steps, viewer::Style },
        { configurer::Rhythm, viewer::Rhythm },
    };

} // component

namespace handle
{

void components()
{
    // components will update the configuration on I/O events

    for (const auto & component : component::All)
    {
        if (component.configurer())
        {
            component.viewer(viewer::What::Data); // reprint the value on the LCD if changed
        }
    }
}

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(io::Multiplexer, pin) // keys are behind the multiplexer
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 15, 14, 13, 12, 11, 10, 9, 8 }; // channel numbers of the multiplexer

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    state::sequencer.click(midier::Sequencer::Run::Async);
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);

    // initialize the LCD
    io::lcd.begin(16, 2);

    // print the initial configuration
    for (const auto & component : component::All)
    {
        component.viewer(viewer::What::Title);
        component.viewer(viewer::What::Data);
    }
}

extern "C" void loop()
{
    handle::components();
    handle::keys();
    handle::click();
}

} // arpeggino
Tutorial: Step Three - LCD - Part 3 - SketchC/C++
#include <Controlino.h>
#include <LiquidCrystal.h>
#include <Midier.h>

#include <assert.h>

namespace arpeggino
{

namespace utils
{

struct Timer
{
    // control

    void start() // start (or restart)
    {
        _millis = millis();
    }

    void reset() // restart only if ticking
    {
        if (ticking())
        {
            start();
        }
    }

    void stop()
    {
        _millis = -1;
    }

    // query

    bool elapsed(unsigned ms) const // only if ticking
    {
        return ticking() && millis() - _millis >= ms;
    }

    bool ticking() const
    {
        return _millis != -1;
    }

private:
    unsigned long _millis = -1;
};

} // utils

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

} // state

namespace io
{

// here we declare all I/O controls with their corresponding pin numbers

controlino::Selector Selector(/* s0 = */ 6, /* s1 = */ 5, /* s2 = */ 4, /* s3 = */ 3);
controlino::Multiplexer Multiplexer(/* sig = */ 2, Selector);

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]

// all configuration keys are behind the multiplexer
controlino::Key Note(Multiplexer, 7);
controlino::Key Mode(Multiplexer, 6);
controlino::Key Octave(Multiplexer, 5);
controlino::Key Perm(Multiplexer, 4);
controlino::Key Steps(Multiplexer, 3);
controlino::Key Rhythm(Multiplexer, 2);

struct LCD : LiquidCrystal
{
    LCD(uint8_t rs, uint8_t e, uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7) : LiquidCrystal(rs, e, d4, d5, d6, d7)
    {}

    template <typename T>
    char print(const T & arg)
    {
        return LiquidCrystal::print(arg);
    }

    template <typename T>
    char print(char col, char row, const T & arg)
    {
        setCursor(col, row);
        return print(arg);
    }

    template <typename T>
    char print(char col, char row, char max, const T & arg)
    {
        const auto written = print(col, row, arg);

        for (unsigned i = 0; i < max - written; ++i)
        {
            write(' '); // make sure the non-used characters are clear
        }

        return written;
    }
};

LCD lcd(/* rs = */ 7, /* e = */ 8,  /* d4 = */ 9, /* d5 = */ 10, /* d6 = */ 11, /* d7 = */ 12);

} // io

namespace configurer
{

enum class Action
{
    None,

    Summary,
    Focus,
};

// a configurer is responsible for updating a single configuration
// parameter according to changes of an I/O control

struct Configurer
{
    Action(*check)();
    void(*update)();
};

Configurer BPM =
    {
        .check = []()
            {
                if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                state::sequencer.bpm = io::BPM.read();
            },
    };

Configurer Note =
    {
        .check = []()
            {
                if (io::Note.check() == controlino::Key::Event::Down)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                auto & config = state::sequencer.config; // a shortcut

                if (config.accidental() == midier::Accidental::Flat)
                {
                    config.accidental(midier::Accidental::Natural);
                }
                else if (config.accidental() == midier::Accidental::Natural)
                {
                    config.accidental(midier::Accidental::Sharp);
                }
                else if (config.accidental() == midier::Accidental::Sharp)
                {
                    config.accidental(midier::Accidental::Flat);

                    if      (config.note() == midier::Note::C) { config.note(midier::Note::D); }
                    else if (config.note() == midier::Note::D) { config.note(midier::Note::E); }
                    else if (config.note() == midier::Note::E) { config.note(midier::Note::F); }
                    else if (config.note() == midier::Note::F) { config.note(midier::Note::G); }
                    else if (config.note() == midier::Note::G) { config.note(midier::Note::A); }
                    else if (config.note() == midier::Note::A) { config.note(midier::Note::B); }
                    else if (config.note() == midier::Note::B) { config.note(midier::Note::C); }
                }
            },
    };

Configurer Mode =
    {
        .check = []()
            {
                if (io::Mode.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.mode();
                const auto next = (midier::Mode)(((unsigned)current + 1) % (unsigned)midier::Mode::Count);

                state::sequencer.config.mode(next);
            },
    };

Configurer Octave =
    {
        .check = []()
            {
                if (io::Octave.check() == controlino::Key::Event::Down)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.octave();
                const auto next = (current % 7) + 1;

                state::sequencer.config.octave(next);
            },
    };

Configurer Perm =
    {
        .check = []()
            {
                if (io::Perm.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.perm();
                const auto next = (current + 1) % midier::style::count(state::sequencer.config.steps());

                state::sequencer.config.perm(next);
            },
    };

Configurer Steps =
    {
        .check = []()
            {
                if (io::Steps.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                auto & config = state::sequencer.config; // a shortcut

                if (config.looped() == false) // we set to loop if currently not looping
                {
                    config.looped(true);
                }
                else
                {
                    unsigned steps = config.steps() + 1;

                    if (steps > 6)
                    {
                        steps = 3;
                    }

                    config.steps(steps);
                    config.perm(0); // reset the permutation
                    config.looped(false); // set as non looping
                }
            },
    };

Configurer Rhythm =
    {
        .check = []()
            {
                if (io::Rhythm.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.rhythm();
                const auto next = (midier::Rhythm)(((unsigned)current + 1) % (unsigned)midier::Rhythm::Count);

                state::sequencer.config.rhythm(next);
            }
    };

} // configurer

namespace viewer
{

enum class What
{
    Title,
    Data,
};

enum class How
{
    Summary,
    Focus,
};

using Viewer = void(*)(What, How);

struct : utils::Timer
{
    // query
    bool operator==(Viewer other) const { return _viewer == other; }
    bool operator!=(Viewer other) const { return _viewer != other; }

    // assignment
    void operator=(Viewer other) { _viewer = other; }

    // access
    void print(What what, How how) { _viewer(what, how); }

private:
    Viewer _viewer = nullptr;
} focused;

void BPM(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Title)
    {
        io::lcd.print(13, 1, "bpm");
    }

    if (what == What::Data)
    {
        io::lcd.print(9, 1, 3, state::sequencer.bpm);
    }
}

void Note(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Data)
    {
        io::lcd.setCursor(0, 0);

        const auto & config = state::sequencer.config; // a shortcut

        if      (config.note() == midier::Note::A) { io::lcd.print('A'); }
        else if (config.note() == midier::Note::B) { io::lcd.print('B'); }
        else if (config.note() == midier::Note::C) { io::lcd.print('C'); }
        else if (config.note() == midier::Note::D) { io::lcd.print('D'); }
        else if (config.note() == midier::Note::E) { io::lcd.print('E'); }
        else if (config.note() == midier::Note::F) { io::lcd.print('F'); }
        else if (config.note() == midier::Note::G) { io::lcd.print('G'); }

        if      (config.accidental() == midier::Accidental::Flat)    { io::lcd.print('b'); }
        else if (config.accidental() == midier::Accidental::Natural) { io::lcd.print(' '); }
        else if (config.accidental() == midier::Accidental::Sharp)   { io::lcd.print('#'); }
    }
}

void Mode(What what, How how)
{
    if (what == What::Data)
    {
        midier::mode::Name name;
        midier::mode::name(state::sequencer.config.mode(), /* out */ name);

        if (how == How::Summary)
        {
            name[3] = '\0'; // trim the full name into a 3-letter shortcut
            io::lcd.print(0, 1, name);
        }
        else if (how == How::Focus)
        {
            io::lcd.print(0, 1, sizeof(name), name);
        }
    }
    else if (what == What::Title && how == How::Focus)
    {
        io::lcd.print(0, 0, "Mode: ");
    }
}

void Octave(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Title)
    {
        io::lcd.print(3, 0, 'O');
    }
    else if (what == What::Data)
    {
        io::lcd.print(4, 0, state::sequencer.config.octave());
    }
}

void Style(What what, How how)
{
    if (how == How::Summary)
    {
        if (what == What::Title)
        {
            io::lcd.print(6, 0, 'S');
        }
        else if (what == What::Data)
        {
            const auto & config = state::sequencer.config; // a shortcut

            io::lcd.print(7, 0, config.steps());
            io::lcd.print(8, 0, config.looped() ? '+' : '-');
            io::lcd.print(9, 0, 3, config.perm() + 1);
        }
    }
    else if (how == How::Focus)
    {
        if (what == What::Title)
        {
            io::lcd.print(0, 0, "Style: ");
        }
        else if (what == What::Data)
        {
            const auto & config = state::sequencer.config; // a shortcut

            io::lcd.print(7, 0, config.steps());
            io::lcd.print(8, 0, config.looped() ? '+' : '-');
            io::lcd.print(9, 0, 3, config.perm() + 1);

            midier::style::Description desc;
            midier::style::description(config.steps(), config.perm(), /* out */ desc);
            io::lcd.print(0, 1, 16, desc); // all columns in the LCD

            if (config.looped())
            {
                io::lcd.setCursor(strlen(desc) + 1, 1);

                for (unsigned i = 0; i < 3; ++i)
                {
                    io::lcd.print('.');
                }
            }
        }
    }
}

void Rhythm(What what, How how)
{
    if (how == How::Summary)
    {
        if (what == What::Title)
        {
            io::lcd.print(4, 1, 'R');
        }
        else if (what == What::Data)
        {
            io::lcd.print(5, 1, 2, (unsigned)state::sequencer.config.rhythm() + 1);
        }
    }
    else if (how == How::Focus)
    {
        if (what == What::Title)
        {
            io::lcd.print(0, 0, "Rhythm #");
        }
        else if (what == What::Data)
        {
            io::lcd.print(8, 0, 2, (unsigned)state::sequencer.config.rhythm() + 1);

            midier::rhythm::Description desc;
            midier::rhythm::description(state::sequencer.config.rhythm(), /* out */ desc);
            io::lcd.print(0, 1, desc);
        }
    }
}

} // viewer

namespace component
{

struct Component
{
    configurer::Configurer configurer;
    viewer::Viewer viewer;
};

Component All[] =
    {
        { configurer::BPM, viewer::BPM },
        { configurer::Note, viewer::Note },
        { configurer::Mode, viewer::Mode },
        { configurer::Octave, viewer::Octave },
        { configurer::Perm, viewer::Style },
        { configurer::Steps, viewer::Style },
        { configurer::Rhythm, viewer::Rhythm },
    };

} // component

namespace control
{

namespace view
{

void summary(viewer::Viewer viewer = nullptr) // 'nullptr' means all components
{
    if (viewer::focused != nullptr) // some viewer is currently in focus
    {
        viewer::focused.stop(); // stop the timer
        viewer::focused = nullptr; // mark as there's no viewer currently in focus
        io::lcd.clear(); // clear the screen entirely
        viewer = nullptr; // mark to print all titles and values
    }

    if (viewer == nullptr)
    {
        for (const auto & component : component::All)
        {
            component.viewer(viewer::What::Title, viewer::How::Summary);
            component.viewer(viewer::What::Data, viewer::How::Summary);
        }
    }
    else
    {
        viewer(viewer::What::Data, viewer::How::Summary);
    }
}

void focus(viewer::Viewer viewer)
{
    if (viewer::focused != viewer) // either in summary mode or another viewer is currently in focus
    {
        io::lcd.clear(); // clear the screen entirely
        viewer::focused = viewer; // mark this viewer as the one being in focus
        viewer::focused.print(viewer::What::Title, viewer::How::Focus); // print the title (only if just became the one in focus)
    }

    viewer::focused.print(viewer::What::Data, viewer::How::Focus); // print the data anyways
    viewer::focused.start(); // start the timer or restart it if ticking already
}

} // view

} // control

namespace handle
{

void focus()
{
    if (viewer::focused.elapsed(3200))
    {
        control::view::summary(); // go back to summary view
    }
}

void components()
{
    // components will update the configuration on I/O events

    for (const auto & component : component::All)
    {
        const auto action = component.configurer.check();

        if (action == configurer::Action::None)
        {
            continue; // nothing to do
        }

        // update the configuration only if in summary mode or if this configurer is in focus

        if ((action == configurer::Action::Summary && viewer::focused == nullptr) ||
            (action == configurer::Action::Focus && viewer::focused == component.viewer))
        {
            component.configurer.update();
        }

        if (action == configurer::Action::Summary)
        {
            control::view::summary(component.viewer);
        }
        else if (action == configurer::Action::Focus)
        {
            control::view::focus(component.viewer);
        }
    }
}

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(io::Multiplexer, pin) // keys are behind the multiplexer
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 15, 14, 13, 12, 11, 10, 9, 8 }; // channel numbers of the multiplexer

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    state::sequencer.click(midier::Sequencer::Run::Async);
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);

    // initialize the LCD
    io::lcd.begin(16, 2);

    // print the initial configuration
    control::view::summary();
}

extern "C" void loop()
{
    handle::focus();
    handle::components();
    handle::keys();
    handle::click();
}

} // arpeggino
Tutorial: Step Four - Recording - SketchC/C++
#include <Controlino.h>
#include <LiquidCrystal.h>
#include <Midier.h>

#include <assert.h>

namespace arpeggino
{

namespace utils
{

struct Timer
{
    // control

    void start() // start (or restart)
    {
        _millis = millis();
    }

    void reset() // restart only if ticking
    {
        if (ticking())
        {
            start();
        }
    }

    void stop()
    {
        _millis = -1;
    }

    // query

    bool elapsed(unsigned ms) const // only if ticking
    {
        return ticking() && millis() - _millis >= ms;
    }

    bool ticking() const
    {
        return _millis != -1;
    }

private:
    unsigned long _millis = -1;
};

} // utils

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

} // state

namespace io
{

// here we declare all I/O controls with their corresponding pin numbers

controlino::Selector Selector(/* s0 = */ 6, /* s1 = */ 5, /* s2 = */ 4, /* s3 = */ 3);
controlino::Multiplexer Multiplexer(/* sig = */ 2, Selector);

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]

// all configuration keys are behind the multiplexer
controlino::Key Note(Multiplexer, 7);
controlino::Key Mode(Multiplexer, 6);
controlino::Key Octave(Multiplexer, 5);
controlino::Key Perm(Multiplexer, 4);
controlino::Key Steps(Multiplexer, 3);
controlino::Key Rhythm(Multiplexer, 2);

// control buttons
controlino::Button Record(Multiplexer, 0);

struct LCD : LiquidCrystal
{
    LCD(uint8_t rs, uint8_t e, uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7) : LiquidCrystal(rs, e, d4, d5, d6, d7)
    {}

    template <typename T>
    char print(const T & arg)
    {
        return LiquidCrystal::print(arg);
    }

    template <typename T>
    char print(char col, char row, const T & arg)
    {
        setCursor(col, row);
        return print(arg);
    }

    template <typename T>
    char print(char col, char row, char max, const T & arg)
    {
        const auto written = print(col, row, arg);

        for (unsigned i = 0; i < max - written; ++i)
        {
            write(' '); // make sure the non-used characters are clear
        }

        return written;
    }
};

LCD lcd(/* rs = */ 7, /* e = */ 8,  /* d4 = */ 9, /* d5 = */ 10, /* d6 = */ 11, /* d7 = */ 12);

utils::Timer flashing;

} // io

namespace configurer
{

enum class Action
{
    None,

    Summary,
    Focus,
};

// a configurer is responsible for updating a single configuration
// parameter according to changes of an I/O control

struct Configurer
{
    Action(*check)();
    void(*update)();
};

Configurer BPM =
    {
        .check = []()
            {
                if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                state::sequencer.bpm = io::BPM.read();
            },
    };

Configurer Note =
    {
        .check = []()
            {
                if (io::Note.check() == controlino::Key::Event::Down)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                auto & config = state::sequencer.config; // a shortcut

                if (config.accidental() == midier::Accidental::Flat)
                {
                    config.accidental(midier::Accidental::Natural);
                }
                else if (config.accidental() == midier::Accidental::Natural)
                {
                    config.accidental(midier::Accidental::Sharp);
                }
                else if (config.accidental() == midier::Accidental::Sharp)
                {
                    config.accidental(midier::Accidental::Flat);

                    if      (config.note() == midier::Note::C) { config.note(midier::Note::D); }
                    else if (config.note() == midier::Note::D) { config.note(midier::Note::E); }
                    else if (config.note() == midier::Note::E) { config.note(midier::Note::F); }
                    else if (config.note() == midier::Note::F) { config.note(midier::Note::G); }
                    else if (config.note() == midier::Note::G) { config.note(midier::Note::A); }
                    else if (config.note() == midier::Note::A) { config.note(midier::Note::B); }
                    else if (config.note() == midier::Note::B) { config.note(midier::Note::C); }
                }
            },
    };

Configurer Mode =
    {
        .check = []()
            {
                if (io::Mode.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.mode();
                const auto next = (midier::Mode)(((unsigned)current + 1) % (unsigned)midier::Mode::Count);

                state::sequencer.config.mode(next);
            },
    };

Configurer Octave =
    {
        .check = []()
            {
                if (io::Octave.check() == controlino::Key::Event::Down)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.octave();
                const auto next = (current % 7) + 1;

                state::sequencer.config.octave(next);
            },
    };

Configurer Perm =
    {
        .check = []()
            {
                if (io::Perm.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.perm();
                const auto next = (current + 1) % midier::style::count(state::sequencer.config.steps());

                state::sequencer.config.perm(next);
            },
    };

Configurer Steps =
    {
        .check = []()
            {
                if (io::Steps.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                auto & config = state::sequencer.config; // a shortcut

                if (config.looped() == false) // we set to loop if currently not looping
                {
                    config.looped(true);
                }
                else
                {
                    unsigned steps = config.steps() + 1;

                    if (steps > 6)
                    {
                        steps = 3;
                    }

                    config.steps(steps);
                    config.perm(0); // reset the permutation
                    config.looped(false); // set as non looping
                }
            },
    };

Configurer Rhythm =
    {
        .check = []()
            {
                if (io::Rhythm.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::sequencer.config.rhythm();
                const auto next = (midier::Rhythm)(((unsigned)current + 1) % (unsigned)midier::Rhythm::Count);

                state::sequencer.config.rhythm(next);
            }
    };

} // configurer

namespace viewer
{

enum class What
{
    Title,
    Data,
};

enum class How
{
    Summary,
    Focus,
};

using Viewer = void(*)(What, How);

struct : utils::Timer
{
    // query
    bool operator==(Viewer other) const { return _viewer == other; }
    bool operator!=(Viewer other) const { return _viewer != other; }

    // assignment
    void operator=(Viewer other) { _viewer = other; }

    // access
    void print(What what, How how) { _viewer(what, how); }

private:
    Viewer _viewer = nullptr;
} focused;

void BPM(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Title)
    {
        io::lcd.print(13, 1, "bpm");
    }

    if (what == What::Data)
    {
        io::lcd.print(9, 1, 3, state::sequencer.bpm);
    }
}

void Note(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Data)
    {
        io::lcd.setCursor(0, 0);

        const auto & config = state::sequencer.config; // a shortcut

        if      (config.note() == midier::Note::A) { io::lcd.print('A'); }
        else if (config.note() == midier::Note::B) { io::lcd.print('B'); }
        else if (config.note() == midier::Note::C) { io::lcd.print('C'); }
        else if (config.note() == midier::Note::D) { io::lcd.print('D'); }
        else if (config.note() == midier::Note::E) { io::lcd.print('E'); }
        else if (config.note() == midier::Note::F) { io::lcd.print('F'); }
        else if (config.note() == midier::Note::G) { io::lcd.print('G'); }

        if      (config.accidental() == midier::Accidental::Flat)    { io::lcd.print('b'); }
        else if (config.accidental() == midier::Accidental::Natural) { io::lcd.print(' '); }
        else if (config.accidental() == midier::Accidental::Sharp)   { io::lcd.print('#'); }
    }
}

void Mode(What what, How how)
{
    if (what == What::Data)
    {
        midier::mode::Name name;
        midier::mode::name(state::sequencer.config.mode(), /* out */ name);

        if (how == How::Summary)
        {
            name[3] = '\0'; // trim the full name into a 3-letter shortcut
            io::lcd.print(0, 1, name);
        }
        else if (how == How::Focus)
        {
            io::lcd.print(0, 1, sizeof(name), name);
        }
    }
    else if (what == What::Title && how == How::Focus)
    {
        io::lcd.print(0, 0, "Mode: ");
    }
}

void Octave(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Title)
    {
        io::lcd.print(3, 0, 'O');
    }
    else if (what == What::Data)
    {
        io::lcd.print(4, 0, state::sequencer.config.octave());
    }
}

void Style(What what, How how)
{
    if (how == How::Summary)
    {
        if (what == What::Title)
        {
            io::lcd.print(6, 0, 'S');
        }
        else if (what == What::Data)
        {
            const auto & config = state::sequencer.config; // a shortcut

            io::lcd.print(7, 0, config.steps());
            io::lcd.print(8, 0, config.looped() ? '+' : '-');
            io::lcd.print(9, 0, 3, config.perm() + 1);
        }
    }
    else if (how == How::Focus)
    {
        if (what == What::Title)
        {
            io::lcd.print(0, 0, "Style: ");
        }
        else if (what == What::Data)
        {
            const auto & config = state::sequencer.config; // a shortcut

            io::lcd.print(7, 0, config.steps());
            io::lcd.print(8, 0, config.looped() ? '+' : '-');
            io::lcd.print(9, 0, 3, config.perm() + 1);

            midier::style::Description desc;
            midier::style::description(config.steps(), config.perm(), /* out */ desc);
            io::lcd.print(0, 1, 16, desc); // all columns in the LCD

            if (config.looped())
            {
                io::lcd.setCursor(strlen(desc) + 1, 1);

                for (unsigned i = 0; i < 3; ++i)
                {
                    io::lcd.print('.');
                }
            }
        }
    }
}

void Rhythm(What what, How how)
{
    if (how == How::Summary)
    {
        if (what == What::Title)
        {
            io::lcd.print(4, 1, 'R');
        }
        else if (what == What::Data)
        {
            io::lcd.print(5, 1, 2, (unsigned)state::sequencer.config.rhythm() + 1);
        }
    }
    else if (how == How::Focus)
    {
        if (what == What::Title)
        {
            io::lcd.print(0, 0, "Rhythm #");
        }
        else if (what == What::Data)
        {
            io::lcd.print(8, 0, 2, (unsigned)state::sequencer.config.rhythm() + 1);

            midier::rhythm::Description desc;
            midier::rhythm::description(state::sequencer.config.rhythm(), /* out */ desc);
            io::lcd.print(0, 1, desc);
        }
    }
}

} // viewer

namespace component
{

struct Component
{
    configurer::Configurer configurer;
    viewer::Viewer viewer;
};

Component All[] =
    {
        { configurer::BPM, viewer::BPM },
        { configurer::Note, viewer::Note },
        { configurer::Mode, viewer::Mode },
        { configurer::Octave, viewer::Octave },
        { configurer::Perm, viewer::Style },
        { configurer::Steps, viewer::Style },
        { configurer::Rhythm, viewer::Rhythm },
    };

} // component

namespace control
{

void flash()
{
    if (io::flashing.ticking())
    {
        return; // already flashing
    }

    digitalWrite(13, HIGH);
    io::flashing.start();
}

namespace view
{

void summary(viewer::Viewer viewer = nullptr) // 'nullptr' means all components
{
    if (viewer::focused != nullptr) // some viewer is currently in focus
    {
        viewer::focused.stop(); // stop the timer
        viewer::focused = nullptr; // mark as there's no viewer currently in focus
        io::lcd.clear(); // clear the screen entirely
        viewer = nullptr; // mark to print all titles and values
    }

    if (viewer == nullptr)
    {
        for (const auto & component : component::All)
        {
            component.viewer(viewer::What::Title, viewer::How::Summary);
            component.viewer(viewer::What::Data, viewer::How::Summary);
        }
    }
    else
    {
        viewer(viewer::What::Data, viewer::How::Summary);
    }
}

void focus(viewer::Viewer viewer)
{
    if (viewer::focused != viewer) // either in summary mode or another viewer is currently in focus
    {
        io::lcd.clear(); // clear the screen entirely
        viewer::focused = viewer; // mark this viewer as the one being in focus
        viewer::focused.print(viewer::What::Title, viewer::How::Focus); // print the title (only if just became the one in focus)
    }

    viewer::focused.print(viewer::What::Data, viewer::How::Focus); // print the data anyways
    viewer::focused.start(); // start the timer or restart it if ticking already
}

void bar(midier::Sequencer::Bar bar)
{
    io::lcd.setCursor(14, 0);

    char written = 0;

    if (bar != midier::Sequencer::Bar::None)
    {
        written = io::lcd.print((unsigned)bar);
    }

    while (written++ < 2)
    {
        io::lcd.write(' ');
    }
}

} // view

} // control

namespace handle
{

void flashing()
{
    if (io::flashing.elapsed(70))
    {
        digitalWrite(13, LOW);
        io::flashing.stop();
    }
}

void recording()
{
    static bool __recording = false;

    const auto recording = state::sequencer.recording(); // is recording at the moment?

    if (__recording != recording)
    {
        digitalWrite(A1, recording ? HIGH : LOW);
        __recording = recording;
    }
}

void focus()
{
    if (viewer::focused.elapsed(3200))
    {
        control::view::summary(); // go back to summary view
    }
}

void components()
{
    // components will update the configuration on I/O events

    for (const auto & component : component::All)
    {
        const auto action = component.configurer.check();

        if (action == configurer::Action::None)
        {
            continue; // nothing to do
        }

        // update the configuration only if in summary mode or if this configurer is in focus

        if ((action == configurer::Action::Summary && viewer::focused == nullptr) ||
            (action == configurer::Action::Focus && viewer::focused == component.viewer))
        {
            component.configurer.update();
        }

        if (action == configurer::Action::Summary)
        {
            control::view::summary(component.viewer);
        }
        else if (action == configurer::Action::Focus)
        {
            control::view::focus(component.viewer);
        }
    }
}

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(io::Multiplexer, pin) // keys are behind the multiplexer
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 15, 14, 13, 12, 11, 10, 9, 8 }; // channel numbers of the multiplexer

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void record()
{
    const auto event = io::Record.check();

    if (event == controlino::Button::Event::Click)
    {
        state::sequencer.record();
    }
    else if (event == controlino::Button::Event::Press)
    {
        state::sequencer.revoke(); // revoke the last recorded layer
    }
    else if (event == controlino::Button::Event::ClickPress)
    {
        state::sequencer.wander();
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    const auto bar = state::sequencer.click(midier::Sequencer::Run::Async);

    if (bar != midier::Sequencer::Bar::Same)
    {
        control::flash();

        if (viewer::focused == nullptr)
        {
            control::view::bar(bar);
        }
    }
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);

    // initialize the LEDs
    pinMode(13, OUTPUT);
    pinMode(A1, OUTPUT);

    // initialize the LCD
    io::lcd.begin(16, 2);

    // print the initial configuration
    control::view::summary();
}

extern "C" void loop()
{
    handle::flashing();
    handle::recording();
    handle::focus();
    handle::components();
    handle::keys();
    handle::record();
    handle::click();
}

} // arpeggino
Tutorial: Step Five - Layers - SketchC/C++
#include <Controlino.h>
#include <LiquidCrystal.h>
#include <Midier.h>

#include <assert.h>

namespace arpeggino
{

namespace utils
{

struct Timer
{
    // control

    void start() // start (or restart)
    {
        _millis = millis();
    }

    void reset() // restart only if ticking
    {
        if (ticking())
        {
            start();
        }
    }

    void stop()
    {
        _millis = -1;
    }

    // query

    bool elapsed(unsigned ms) const // only if ticking
    {
        return ticking() && millis() - _millis >= ms;
    }

    bool ticking() const
    {
        return _millis != -1;
    }

private:
    unsigned long _millis = -1;
};

} // utils

namespace state
{

midier::Layers<8> layers; // the number of layers chosen will affect the global variable size
midier::Sequencer sequencer(layers);

struct : utils::Timer
{
    midier::Layer * layer = nullptr;
    unsigned char id;
} layer;

midier::Config * config = &sequencer.config;

} // state

namespace io
{

// here we declare all I/O controls with their corresponding pin numbers

controlino::Selector Selector(/* s0 = */ 6, /* s1 = */ 5, /* s2 = */ 4, /* s3 = */ 3);
controlino::Multiplexer Multiplexer(/* sig = */ 2, Selector);

controlino::Potentiometer BPM(A0, /* min = */ 20, /* max = */ 230); // we limit the value of BPM to [20,230]

// all configuration keys are behind the multiplexer
controlino::Key Note(Multiplexer, 7);
controlino::Key Mode(Multiplexer, 6);
controlino::Key Octave(Multiplexer, 5);
controlino::Key Perm(Multiplexer, 4);
controlino::Key Steps(Multiplexer, 3);
controlino::Key Rhythm(Multiplexer, 2);

// control buttons
controlino::Button Layer(Multiplexer, 1);
controlino::Button Record(Multiplexer, 0);

struct LCD : LiquidCrystal
{
    LCD(uint8_t rs, uint8_t e, uint8_t d4, uint8_t d5, uint8_t d6, uint8_t d7) : LiquidCrystal(rs, e, d4, d5, d6, d7)
    {}

    template <typename T>
    char print(const T & arg)
    {
        return LiquidCrystal::print(arg);
    }

    template <typename T>
    char print(char col, char row, const T & arg)
    {
        setCursor(col, row);
        return print(arg);
    }

    template <typename T>
    char print(char col, char row, char max, const T & arg)
    {
        const auto written = print(col, row, arg);

        for (unsigned i = 0; i < max - written; ++i)
        {
            write(' '); // make sure the non-used characters are clear
        }

        return written;
    }
};

LCD lcd(/* rs = */ 7, /* e = */ 8,  /* d4 = */ 9, /* d5 = */ 10, /* d6 = */ 11, /* d7 = */ 12);

utils::Timer flashing;

} // io

namespace configurer
{

enum class Action
{
    None,

    Summary,
    Focus,
};

// a configurer is responsible for updating a single configuration
// parameter according to changes of an I/O control

struct Configurer
{
    Action(*check)();
    void(*update)();
};

Configurer BPM =
    {
        .check = []()
            {
                if (io::BPM.check() == controlino::Potentiometer::Event::Changed)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                state::sequencer.bpm = io::BPM.read();
            },
    };

Configurer Note =
    {
        .check = []()
            {
                if (io::Note.check() == controlino::Key::Event::Down)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                if (state::config->accidental() == midier::Accidental::Flat)
                {
                    state::config->accidental(midier::Accidental::Natural);
                }
                else if (state::config->accidental() == midier::Accidental::Natural)
                {
                    state::config->accidental(midier::Accidental::Sharp);
                }
                else if (state::config->accidental() == midier::Accidental::Sharp)
                {
                    state::config->accidental(midier::Accidental::Flat);

                    if      (state::config->note() == midier::Note::C) { state::config->note(midier::Note::D); }
                    else if (state::config->note() == midier::Note::D) { state::config->note(midier::Note::E); }
                    else if (state::config->note() == midier::Note::E) { state::config->note(midier::Note::F); }
                    else if (state::config->note() == midier::Note::F) { state::config->note(midier::Note::G); }
                    else if (state::config->note() == midier::Note::G) { state::config->note(midier::Note::A); }
                    else if (state::config->note() == midier::Note::A) { state::config->note(midier::Note::B); }
                    else if (state::config->note() == midier::Note::B) { state::config->note(midier::Note::C); }
                }
            },
    };

Configurer Mode =
    {
        .check = []()
            {
                if (io::Mode.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::config->mode();
                const auto next = (midier::Mode)(((unsigned)current + 1) % (unsigned)midier::Mode::Count);

                state::config->mode(next);
            },
    };

Configurer Octave =
    {
        .check = []()
            {
                if (io::Octave.check() == controlino::Key::Event::Down)
                {
                    return Action::Summary;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::config->octave();
                const auto next = (current % 7) + 1;

                state::config->octave(next);
            },
    };

Configurer Perm =
    {
        .check = []()
            {
                if (io::Perm.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::config->perm();
                const auto next = (current + 1) % midier::style::count(state::config->steps());

                state::config->perm(next);
            },
    };

Configurer Steps =
    {
        .check = []()
            {
                if (io::Steps.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                if (state::config->looped() == false) // we set to loop if currently not looping
                {
                    state::config->looped(true);
                }
                else
                {
                    unsigned steps = state::config->steps() + 1;

                    if (steps > 6)
                    {
                        steps = 3;
                    }

                    state::config->steps(steps);
                    state::config->perm(0); // reset the permutation
                    state::config->looped(false); // set as non looping
                }
            },
    };

Configurer Rhythm =
    {
        .check = []()
            {
                if (io::Rhythm.check() == controlino::Key::Event::Down)
                {
                    return Action::Focus;
                }

                return Action::None;
            },
        .update = []()
            {
                const auto current = state::config->rhythm();
                const auto next = (midier::Rhythm)(((unsigned)current + 1) % (unsigned)midier::Rhythm::Count);

                state::config->rhythm(next);
            }
    };

} // configurer

namespace viewer
{

enum class What
{
    Title,
    Data,
};

enum class How
{
    Summary,
    Focus,
};

using Viewer = void(*)(What, How);

struct : utils::Timer
{
    // query
    bool operator==(Viewer other) const { return _viewer == other; }
    bool operator!=(Viewer other) const { return _viewer != other; }

    // assignment
    void operator=(Viewer other) { _viewer = other; }

    // access
    void print(What what, How how) { _viewer(what, how); }

private:
    Viewer _viewer = nullptr;
} focused;

void BPM(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Title)
    {
        io::lcd.print(13, 1, "bpm");
    }

    if (what == What::Data)
    {
        io::lcd.print(9, 1, 3, state::sequencer.bpm);
    }
}

void Note(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Data)
    {
        io::lcd.setCursor(0, 0);

        if      (state::config->note() == midier::Note::A) { io::lcd.print('A'); }
        else if (state::config->note() == midier::Note::B) { io::lcd.print('B'); }
        else if (state::config->note() == midier::Note::C) { io::lcd.print('C'); }
        else if (state::config->note() == midier::Note::D) { io::lcd.print('D'); }
        else if (state::config->note() == midier::Note::E) { io::lcd.print('E'); }
        else if (state::config->note() == midier::Note::F) { io::lcd.print('F'); }
        else if (state::config->note() == midier::Note::G) { io::lcd.print('G'); }

        if      (state::config->accidental() == midier::Accidental::Flat)    { io::lcd.print('b'); }
        else if (state::config->accidental() == midier::Accidental::Natural) { io::lcd.print(' '); }
        else if (state::config->accidental() == midier::Accidental::Sharp)   { io::lcd.print('#'); }
    }
}

void Mode(What what, How how)
{
    if (what == What::Data)
    {
        midier::mode::Name name;
        midier::mode::name(state::config->mode(), /* out */ name);

        if (how == How::Summary)
        {
            name[3] = '\0'; // trim the full name into a 3-letter shortcut
            io::lcd.print(0, 1, name);
        }
        else if (how == How::Focus)
        {
            io::lcd.print(0, 1, sizeof(name), name);
        }
    }
    else if (what == What::Title && how == How::Focus)
    {
        io::lcd.print(0, 0, "Mode: ");
    }
}

void Octave(What what, How how)
{
    assert(how == How::Summary);

    if (what == What::Title)
    {
        io::lcd.print(3, 0, 'O');
    }
    else if (what == What::Data)
    {
        io::lcd.print(4, 0, state::config->octave());
    }
}

void Style(What what, How how)
{
    if (how == How::Summary)
    {
        if (what == What::Title)
        {
            io::lcd.print(6, 0, 'S');
        }
        else if (what == What::Data)
        {
            io::lcd.print(7, 0, state::config->steps());
            io::lcd.print(8, 0, state::config->looped() ? '+' : '-');
            io::lcd.print(9, 0, 3, state::config->perm() + 1);
        }
    }
    else if (how == How::Focus)
    {
        if (what == What::Title)
        {
            io::lcd.print(0, 0, "Style: ");
        }
        else if (what == What::Data)
        {
            io::lcd.print(7, 0, state::config->steps());
            io::lcd.print(8, 0, state::config->looped() ? '+' : '-');
            io::lcd.print(9, 0, 3, state::config->perm() + 1);

            midier::style::Description desc;
            midier::style::description(state::config->steps(), state::config->perm(), /* out */ desc);
            io::lcd.print(0, 1, 16, desc); // all columns in the LCD

            if (state::config->looped())
            {
                io::lcd.setCursor(strlen(desc) + 1, 1);

                for (unsigned i = 0; i < 3; ++i)
                {
                    io::lcd.print('.');
                }
            }
        }
    }
}

void Rhythm(What what, How how)
{
    if (how == How::Summary)
    {
        if (what == What::Title)
        {
            io::lcd.print(4, 1, 'R');
        }
        else if (what == What::Data)
        {
            io::lcd.print(5, 1, 2, (unsigned)state::config->rhythm() + 1);
        }
    }
    else if (how == How::Focus)
    {
        if (what == What::Title)
        {
            io::lcd.print(0, 0, "Rhythm #");
        }
        else if (what == What::Data)
        {
            io::lcd.print(8, 0, 2, (unsigned)state::config->rhythm() + 1);

            midier::rhythm::Description desc;
            midier::rhythm::description(state::config->rhythm(), /* out */ desc);
            io::lcd.print(0, 1, desc);
        }
    }
}

} // viewer

namespace component
{

struct Component
{
    configurer::Configurer configurer;
    viewer::Viewer viewer;
};

Component All[] =
    {
        { configurer::BPM, viewer::BPM },
        { configurer::Note, viewer::Note },
        { configurer::Mode, viewer::Mode },
        { configurer::Octave, viewer::Octave },
        { configurer::Perm, viewer::Style },
        { configurer::Steps, viewer::Style },
        { configurer::Rhythm, viewer::Rhythm },
    };

} // component

namespace control
{

void flash()
{
    if (io::flashing.ticking())
    {
        return; // already flashing
    }

    digitalWrite(13, HIGH);
    io::flashing.start();
}

namespace view
{

void summary(viewer::Viewer viewer = nullptr) // 'nullptr' means all components
{
    if (viewer::focused != nullptr) // some viewer is currently in focus
    {
        viewer::focused.stop(); // stop the timer
        viewer::focused = nullptr; // mark as there's no viewer currently in focus
        io::lcd.clear(); // clear the screen entirely
        viewer = nullptr; // mark to print all titles and values
    }

    if (viewer == nullptr)
    {
        for (const auto & component : component::All)
        {
            component.viewer(viewer::What::Title, viewer::How::Summary);
            component.viewer(viewer::What::Data, viewer::How::Summary);
        }

        // layers and bars

        io::lcd.setCursor(13, 0);

        char written = 0;

        if (state::layer.layer != nullptr)
        {
            written += io::lcd.print('L');
            written += io::lcd.print(state::layer.id);
        }

        while (written++ < 3)
        {
            io::lcd.write(' ');
        }
    }
    else
    {
        viewer(viewer::What::Data, viewer::How::Summary);
    }
}

void focus(viewer::Viewer viewer)
{
    if (viewer::focused != viewer) // either in summary mode or another viewer is currently in focus
    {
        io::lcd.clear(); // clear the screen entirely
        viewer::focused = viewer; // mark this viewer as the one being in focus
        viewer::focused.print(viewer::What::Title, viewer::How::Focus); // print the title (only if just became the one in focus)
    }

    viewer::focused.print(viewer::What::Data, viewer::How::Focus); // print the data anyways
    viewer::focused.start(); // start the timer or restart it if ticking already
}

void bar(midier::Sequencer::Bar bar)
{
    io::lcd.setCursor(14, 0);

    char written = 0;

    if (bar != midier::Sequencer::Bar::None)
    {
        written = io::lcd.print((unsigned)bar);
    }

    while (written++ < 2)
    {
        io::lcd.write(' ');
    }
}

} // view

namespace config
{

void layer(midier::Layer * layer, unsigned char id) // `nullptr` means go back to global
{
    if (state::layer.layer == nullptr && layer == nullptr)
    {
        return; // nothing to do
    }

    // we allow setting the same layer for updating its config and the timer

    state::layer.layer = layer;
    state::layer.id = id;

    if (layer == nullptr)
    {
        // increase the volume of all layers
        state::sequencer.layers.eval([](midier::Layer & layer)
            {
                layer.velocity = midier::midi::Velocity::High;
            });

        state::layer.stop(); // stop the timer
        state::config = &state::sequencer.config; // point to global configuration
    }
    else
    {
        // lower the volume of all layers
        state::sequencer.layers.eval([](midier::Layer & layer)
            {
                layer.velocity = midier::midi::Velocity::Low;
            });

        // increase the volume of the selected layer
        state::layer.layer->velocity = midier::midi::Velocity::High;

        state::layer.start(); // start ticking
        state::config = layer->config.view(); // point to this layer's configuration
    }

    control::view::summary();
}

void global()
{
    layer(nullptr, 0);
}

} // config

} // control

namespace handle
{

void flashing()
{
    if (io::flashing.elapsed(70))
    {
        digitalWrite(13, LOW);
        io::flashing.stop();
    }
}

void recording()
{
    static bool __recording = false;

    const auto recording = state::sequencer.recording(); // is recording at the moment?

    if (__recording != recording)
    {
        digitalWrite(A1, recording ? HIGH : LOW);
        __recording = recording;
    }
}

void focus()
{
    if (viewer::focused.elapsed(3200))
    {
        state::layer.reset(); // restart the layer timer

        control::view::summary(); // go back to summary view
    }
}

void components()
{
    // components will update the configuration on I/O events

    for (const auto & component : component::All)
    {
        const auto action = component.configurer.check();

        if (action == configurer::Action::None)
        {
            continue; // nothing to do
        }

        const auto layered = (state::layer.layer != nullptr) && (component.viewer != viewer::BPM); // all configurers but BPM are per layer

        if (layered)
        {
            state::layer.start(); // start ticking
        }

        // update the configuration only if in summary mode or if this configurer is in focus

        if ((action == configurer::Action::Summary && viewer::focused == nullptr) ||
            (action == configurer::Action::Focus && viewer::focused == component.viewer))
        {
            if (layered && state::layer.layer->config.outer())
            {
                // the selected layer should now detach from the global configuration as
                // it is being configured specifically.
                state::layer.layer->config = state::sequencer.config; // deep copy the global configuration

                // we also need to point to the configuration of this layer
                state::config = state::layer.layer->config.view();
            }

            component.configurer.update();
        }

        if (action == configurer::Action::Summary)
        {
            control::view::summary(component.viewer);
        }
        else if (action == configurer::Action::Focus)
        {
            control::view::focus(component.viewer);
        }
    }
}

void keys()
{
    // we extend `controlino::Key` so we could hold a Midier handle with every key
    struct Key : controlino::Key
    {
        Key(char pin) : controlino::Key(io::Multiplexer, pin) // keys are behind the multiplexer
        {}

        midier::Sequencer::Handle h;
    };

    static Key __keys[] = { 15, 14, 13, 12, 11, 10, 9, 8 }; // channel numbers of the multiplexer

    for (auto i = 0; i < sizeof(__keys) / sizeof(Key); ++i)
    {
        auto & key = __keys[i];

        const auto event = key.check();

        if (event == Key::Event::None)
        {
            continue; // nothing has changed
        }

        if (event == Key::Event::Down) // a key was pressed
        {
            control::config::global(); // go back to global configutarion when playing new layers

            key.h = state::sequencer.start(i + 1); // start playing an arpeggio of the respective scale degree
        }
        else if (event == Key::Event::Up) // a key was released
        {
            state::sequencer.stop(key.h); // stop playing the arpeggio
        }
    }
}

void record()
{
    const auto event = io::Record.check();

    if (event == controlino::Button::Event::Click)
    {
        state::sequencer.record();
    }
    else if (event == controlino::Button::Event::Press)
    {
        if (state::layer.layer == nullptr)
        {
            state::sequencer.revoke(); // revoke the last recorded layer as no layer is selected
        }
        else
        {
            state::layer.layer->revoke(); // revoke the selected layer
        }
    }
    else if (event == controlino::Button::Event::ClickPress)
    {
        state::sequencer.wander();
    }
    else
    {
        return;
    }

    control::config::global(); // go back to global configuration
}

void layer()
{
    if (state::layer.elapsed(6000))
    {
        control::config::global(); // go back to global configuration after 6 seconds
    }
    else
    {
        const auto event = io::Layer.check();

        if (event == controlino::Button::Event::Click) // iterate layers
        {
            if (viewer::focused != nullptr)
            {
                state::layer.reset(); // reset the layer timer only if there's one selected currently

                control::view::summary(); // go back to summary view
            }
            else
            {
                static const auto __count = state::sequencer.layers.count();

                static unsigned char __index = 0;

                if (state::layer.layer == nullptr || __index >= __count)
                {
                    __index = 0; // search from the start again
                }

                midier::Layer * layer = nullptr;

                while (__index < __count)
                {
                    midier::Layer & prospect = state::sequencer.layers[__index++];

                    if (prospect.running())
                    {
                        layer = &prospect;
                        break;
                    }
                }

                if (layer == nullptr)
                {
                    control::config::global();
                }
                else
                {
                    control::config::layer(layer, __index);
                }
            }
        }
        else if (event == controlino::Button::Event::Press)
        {
            if (state::layer.layer != nullptr) // a layer is selected
            {
                if (state::layer.layer->config.inner())
                {
                    // we make it point to the global configuration
                    state::layer.layer->config = state::config = &state::sequencer.config;

                    // reset the timer
                    state::layer.reset();

                    // print the new (global) configuration
                    control::view::summary();
                }
            }
            else // no layer is selected
            {
                // making all previous dynamic layers static

                state::sequencer.layers.eval([](midier::Layer & layer)
                    {
                        if (layer.config.outer())
                        {
                            layer.config = state::sequencer.config; // make it static and copy the current global configuration
                        }
                    });
            }
        }
        else if (event == controlino::Button::Event::ClickPress)
        {
            // set all layers to be dynamically configured

            state::sequencer.layers.eval([](midier::Layer & layer)
                {
                    layer.config = &state::sequencer.config;
                });

            control::config::global();
        }
    }
}

void click()
{
    // actually click Midier for it to play the MIDI notes
    const auto bar = state::sequencer.click(midier::Sequencer::Run::Async);

    if (bar != midier::Sequencer::Bar::Same)
    {
        control::flash();

        if (viewer::focused == nullptr && state::layer.layer == nullptr)
        {
            control::view::bar(bar);
        }
    }
}

} // handle

extern "C" void setup()
{
    // initialize the Arduino "Serial" module and set the baud rate
    // to the same value you are using in your software.
    // if connected physically using a MIDI 5-DIN connection, use 31250.
    Serial.begin(9600);

    // initialize the LEDs
    pinMode(13, OUTPUT);
    pinMode(A1, OUTPUT);

    // initialize the LCD
    io::lcd.begin(16, 2);

    // print the initial configuration
    control::view::summary();
}

extern "C" void loop()
{
    handle::flashing();
    handle::recording();
    handle::focus();
    handle::components();
    handle::keys();
    handle::record();
    handle::layer();
    handle::click();
}

} // arpeggino
Arpeggino GitHub repository
This is the GitHub repository for the Arpeggino project. It includes the Arduino sketch, all code needed, schemas, and extra files. You can upload it to your Arduino board as-is or you can easily modify the schema to support your own board configuration. The code is written in C++, and you can easily find the places you need to modify the code to adjust it to your boards. A few examples: (1) Instead of having 8 keys, you can start with just a few (2) If you are using a board that has more I/O pins, you can omit the usage of the multiplexer easily (3) Remove the usage of the LCD screen if you don't have one (4) Program your own MIDI sequences and play them when a button gets clicked
Controlino GitHub repository
Controlino is the Arduino library that is used by Arpeggino for complex I/O controls that can be behind a multiplexer. It offers easy control of buttons and potentiometers, and supports both simple and complex clicking gestures such as: (1) Down (2) Up (3) Click (4) Double Click (Click-Click) (5) Long Click (Press) (6) Double Click and Press (Click-Press) It is fully documented and offers plenty of examples. You can use Controlino outside of Arpeggino to integrate complex click gestures in your projects, and control buttons and potentiometers behind a multiplexer.
Midier GitHub repository
Midier is the engine behind Arpeggino. It is a library written in C++ to play, record, loop and program MIDI notes, arpeggios and sequences on Arduino. It is comprehensively documented, and has plenty of plug-and-play examples available. You can use Midier outside Arpeggino, and integrate MIDI sequences and loops to your own projects easily.

Schematics

Arpeggino Final Schema
schema_QzpgVnKNsw.fzz
Tutorial: Step One - Playing Arpeggios - Schema
1__playing_arpeggios_WrvuMN4nfo.fzz
Tutorial: Step Two - Configuring the Arpeggios - Schema
2__configuration_aAFw4025We.fzz
Tutorial: Step Three - LCD - Part 1 - Schema
3_1__multiplexer_8UeU80Mz3e.fzz
Tutorial: Step Three - LCD - Part 2 - Schema
3_2__basic_lcd_0DHe3xaEbF.fzz
Tutorial: Step Four - Recording - Schema
4__recording_k8lJP5SoM5.fzz
Tutorial: Step Five - Layers - Schema
5__layers_VApVxqIkUV.fzz
Tutorial: Step One - Playing Arpeggios - Sketch
1__playing_arpeggios_p99F2QKiy5.ino

Comments

Similar projects you might like

Arduino MIDI Arpeggiator

Project tutorial by Dmitry

  • 46,378 views
  • 63 comments
  • 146 respects

Minimal MIDI Drum Kit with 3D Printer

Project tutorial by ryokosaka

  • 28,623 views
  • 6 comments
  • 52 respects

Arduino Tutorial : Mini Piano

Project tutorial by Rahul Khanna D

  • 43,047 views
  • 9 comments
  • 41 respects

Step Sequencer

Project tutorial by costantinorizzuti

  • 6,936 views
  • 0 comments
  • 14 respects

Cthulhinho: A MIDI Sequencer

Project showcase by primvla

  • 15,842 views
  • 7 comments
  • 37 respects

MIDI Slide Whistle "MEMIDION" Next Stage

Project tutorial by HomeMadeGarbage

  • 5,559 views
  • 1 comment
  • 22 respects
Add projectSign up / Login