项目作者: step-sequencer

项目描述 :
The step sequencer allows you to create a repeating sequence of tones and to change the resulting tone sequence with the buttons and potentiometers. It supports the MIDI protocol and has a DIN connector. This allows you to use the step sequencer together with other instruments or use it as an input for software instruments in a Digital Audio Workstation (DAW). For the seccond approach you might need to use a MIDI to USB cable.
高级语言: C
项目地址: git://github.com/step-sequencer/step-sequencer.git
创建时间: 2021-02-17T15:42:19Z
项目社区:https://github.com/step-sequencer/step-sequencer

开源协议:MIT License

下载


Step Sequencer

Video

A demo video of the step sequencer can be found on YouTube.

Features

The step sequencer allows you to create a repeating sequence of tones and to change the resulting tone sequence with
the buttons and potentiometers. It supports the MIDI protocol and has a DIN connector. This allows you to use the step
sequencer together with other instruments or use it as an input for software instruments in a Digital Audio
Workstation (DAW). For the seccond approach you might need to use a MIDI to USB cable.

The step sequencer has five buttons, five LEDs and four potentiometers. The first button is the start/stop button with
which the step sequencer can be turned on and off. The LED above indicates the status. The other four buttons stand
for the four tones of the tone sequence. When the step sequencer is turned on, all LEDs are lighted, which means that
all tones are played. The individual tone buttons can be used to turn the individual tones on or off.

The tone sequence can be adjusted with the potentiometers. The different potentiometers have the following functions:

  • Frequency: setting the tone frequency of all tones depending on the MIDI note range
  • Velocity: setting how hard the note is struck
  • Length: setting the length of the notes
  • Tempo: setting the tempo in BPM

Requirements

Hardware / Materials

:warning: Most of the parts listed are not breadboard compatible. If development with the breadboard is desired, take
care to purchase the appropriate parts.

Additional parts / tools:

  • MIDI-USB cable (Amazon)
  • Wires
  • Connectors
  • Resistors
  • Copper strand
  • Heat shrink tubing
  • Soldering iron + solder

Housing

  • Filament for 3D-printer
  • Remaining piece metal sheet
  • Threaded bushings (Amazon)
  • Screws (Amazon)

Software

The step sequencer we built is based on the AVR toolchain including:

These packages contain the libraries to write code for the Atmega chips. Besides that they provide command line
interfaces to build your software and upload the result to your microcontroller.

On MacOs you can install avr-gcc and avrdude via homebrew:

  1. $ brew install avr-gcc
  2. $ brew install avrdude

If you are using a linux distribution like Ubuntu that is based on Debian you can install the packages as common:

  1. $ apt update && apt install -y gcc-avr && apt install -y avrdude

Besides this you can find a Docker image including all packages necessary to build avr-gcc projects. We used the image
for building the project in a CI/CD pipeline. The image can be found on
Docker Hub. The corresponding Dockerfile is located
here.

Configuration

The step sequencer’s configuration can be edited by adjusting the values of the source/globals.h file. Here you can
change hardware related settings, like the used pins and sequencer specific configurations like the note ranges or BPM
(beats per minute) settings.

BPM

If you want to change the BPM range for the tempo potentiometer you can do this by adjusting the MIN_TEMPO_BPM and
MAX_TEMPO_BPM values. The maximum value the implementation supports is 600 BPM.

MIDI note range

During our development process we recognized that not all software instruments support the whole range of MIDI notes.
Due to this we decided to implement the possibility to edit the range of MIDI notes that will be played. If you want
to change the minimum MIDI note number that the step sequencer supports you have to adjusts the value of the
MIN_NOTE_NUMBER property. For the max MIDI note number the MAX_NOTE_NUMBER has to be adapted. For more
information about the MIDI protocol click here.

Buttons, LEDs, potentiometers and the DIN jack (MIDI jack)

If you decide to change the pins for the buttons, leds, potentiometers or the DIN jack that will be used to transfer
the MIDI messages you can modify the values in the source/globals.h as well. It’s important that the buttons are
connected to the same register, otherwise the implementation will not work. This can be applied to the LEDs and
potentiometers. Here we want to mention that the potentiometer will have to be connected to pins that support the
Arduinos built in ADC (analog digital converter).

Build and Logging

USB port configuration

In the source/makefile you will have to configure the USB port your Arduino is connected to. On MacOS systems the
right port is located at /dev/tty.usbmodem* or /dev/tty.usbserial*. On Linux it is /dev/ttyUSB* or
/dev/ttyACM*. The port number is represented with *. To get a list of all connected USB devices execute the
following command:

  1. $ ls -al /dev | grep tty

The output should look something like this:

  1. crw-rw-rw- 1 root wheel 22, 12 Feb 17 15:34 tty.usbmodem14501

After finding out the right port, the PORT value of the source/makefile needs to be adjusted. In our example the
PORT value would look like that:

  1. ...
  2. # Add used USB port here (e.g. /dev/ttyACM0)
  3. PORT=/dev/tty.usbmodem14501
  4. ...

Reading Logs

If you want to debug the application you can use our logging library. For more information on how to write log
messages take a look at the Logging library.

To read the transfered log data you will have to set the right baud rate for the used USB port. The baud rate can be
read with the following commands. <port> is the USB port your Arduino is connected to (e.g. /dev/tty.usbmodem14501
or /dev/ttyUSB123).

MacOs:

  1. $ stty -f <port>

Linux:

  1. $ stty -F <port>

If the baud rate is 9600 you are fine. If not execute the following commands to set the baud rate.

  1. $ stty -f <port> 9600

Linux:

  1. $ stty -F <port> 9600

To read the logs, sent by the step sequecer you have to listen for messages on the corresponding port. On Linux systems
this is possible with the cat command. On MacOS you can use the screen command.

MacOs: (To leave press ctl + a, then k and then y)

  1. $ screen <port>

Linux: (To leave press ctl + c)

  1. $ cat <port>

Installation Guide

The step sequencer’s heart, the Arduino, will be flashed using the Make
toolchain. Therefore we created a corresponding makefile that is located at source/makefile.

Due to the logging functionality you have to configure your used USB port first. Therefore take a look into the
configuration chapter. Besides this you should navigate in the makefiles folder:

  1. $ cd source

With the help of Make we provide the following functionalities:

  • Flash the step sequencers source code to the Arduino (includes build, size and clean)
    1. $ make flash
  • Print size information for the programm and the data
    1. $ make size
  • Build the source code (includes size)
    1. $ make build
  • Delete the files that were created during the build
    1. $ make clean

Circuit Diagram

Here you can see the breadboard diagram of our stepsequencer. For more details, please take a look at our schematic view.
Both circuit diagrams have been created with Fritzing.

Housing

The complete housing

Base plate

All buttons, LEDs and potentiometers have to be mounted to a plate. In our case we used a metal one to guarantee
stability. The plate’s dimensions are 16,3 cm * 6,7 cm. Feel free to mount the parts wherever you want to but be aware
of leaving enough space on the sides for the screws and the casing. We recommend 1 cm on each side.

To mount the plate to the casing you have to drill four holes. Two of them will have to be drilled to the longer bottom
side of the base plate. They have to have a 0.5 cm gap to the longer side of the base plate and a 3.5 cm gap to the
short side of the plate. The two holes in the top edges have to have a 0.7 cm gap to each side.

Casing

The complete housing

We created CAD drawings, to print the rest of the housing with a 3D-Printer. The drawings can be found in the
repository’s housing folder. There are two .stl files:

Additionally we uploaded the files to Thingiverse.

Implementation

As we needed to implement different functionalities for our sequencer, we decided to create our own libraries and group
them in a folder called libs which can be found in the /source directory. The functionality we implemented there is
used in our main file which runs the sequencer. The following listing shows how our code is structured:

Project Structure

For details regarding globals and building our project with the makefile take a look at the
Configuration and the Installation Guide.

Logging library

The Logging library allows to send log messages via the serial USB port of your Arduino. Using this library is very
easy. You just have to add the library to your project and you are ready to send log messages. The connection will be
established on the first attempt.

Here is a small example on how to use the library:

  1. #include "libs/logger.h"
  2. int main(){
  3. ...
  4. usb_log("Hello I am a debugging message\n");
  5. ...
  6. }

If you want to read the log messages take a look at the reading logs chapter.

SUART library for sending MIDI messages

The Arduino Uno has one hardware component, that is called UART. This is using the Tx/Rx pins to transmit data via
the USB port. In our case the USB port is already in use for sending debug messages. Therefore, in order to send MIDI
messages to the computer, we implemented a software UART (SUART).

The SUART library is the base on which we built the MIDI library. This library takes the MIDI messages and sends it to
the MIDI jack using the right configuration specified by the MIDI protocol.

This library consists of three functions which will be further explained in the following.

void suart_midi_init()

As the name of the function already describes, the SUART library is initialized here. Since UART is an asynchronous
communication, the sender and receiver must use the same baud rate. That way the receiver knows in which clock it must
sample the line, in order to be able to read the message correctly. In this case the baud rate indicates how many bits
per second are sent. For MIDI there is a fixed baud rate of 31250. To be able to send in this rate the timer0 is used
in CTC (Clear Time on Compare) mode. This means that the timer value is incremented with every cycle of the CPU
(16MHz) until it matches the compare register. Then the timer is set to zero again and an interrupt
ISR(TIMER0_COMPA_vect) is triggered.

Furthermore, in this function the pin over which the data is transmitted is declared as output pin and the line is set
to high in idle state. In addition, the variable outframe is declared, which is later used to store the message to
be sent.

void suart_put_byte(const uint8_t dataByte)

This function is called to send a byte which is passed as a parameter. This byte represents a MIDI message. If at
moment of the funciton call the transmission of another byte is still running, we have to wait. After that the byte to
be sent is written into the outframe variable, and the start/stop bits are added. After this the message will be
converted to its final structure.

Finally, the timer count value is reset to ensure that no value from a previous transmission is still stored in this
register. Then the timer0 is activated to send the message.

ISR(TIMER0_COMPA_vect)

The actual sending of the message takes place in the interrupt function of timer0. Every time the function is triggered
the first bit of the message stored in the outframe variable will be sent. After this the sent bit will be removed
from the outframe variable. This process is repeated until all bits from the outframe have been sent. If this
happened the timer is deactivated and the transmission of the message is successfully completed.

MIDI library

The MIDI library is used to send MIDI messages. To use this library the method init_midi(MidiChannel ch) needs to be
called. In this method the MIDI channel is set and the underlying
SUART library is initialized. The library is used to send the actual bytes
of a MIDI message. Additionally the MIDI library contains the following methods:

void set_midi_channel(MidiChannel ch)

The MIDI channel is set when the MIDI library is initialized. If you want to change the MIDI channel at any time this
method can be used. As parameter this method accepts a value of the MidiChannel enum.

void send_midi_note_on(MidiNote *note)

This method takes a MidiNote pointer as parameter which is a custom declared struct which has all parameters that
are necessary to send a MIDI message. The struct looks as follows:

  1. typedef struct {
  2. // Midi note number (0 - 127)
  3. uint8_t number;
  4. // Midi note velocity (0 - 127)
  5. uint8_t velocity;
  6. // After *length* time ticks, note_off has to be sent
  7. uint8_t length;
  8. } MidiNote;

This method calls the internal method void send_midi_note(MidiMessageType type, MidiNote *note) for sending a MIDI
message via the suart library. In the parameters type specifies whether a note-on or a note-off MIDI message is sent
and the pointer MidiNote contains information about which note should be played with which velocity.

void send_midi_note_off(MidiNote *note)

This method works the same as the previously described method. With the only difference that in this case a note-off
message is sent.

Potentiometer library

The potentiometer library can be used to read the values of potentiometers using the Arduino’s built in analog digital
converter (ADC). The library has to get initialized by calling the init_poti_adc(uint8_t pin) method. After this you
can read the power level of the pin that is connected to the potentiometer. The function that reads the analog values
is called adc_read_poti(uint8_t pin). If you want to execute the method, you have to pass the pin of the Arduino
where your potentiometer is connected to. The pins you can use are A0 to A5 but while we are using the avr-gcc
toolchain you have to pass P0 to P5. After measuring the input power of the potentiometer the method returns a value
between 0 and 1023.

During our development process we realized that there might happen inaccuracy during the measurement. Due to this
we implemented another method to calculate the average from 64 reads. The method is called
get_poti_average(uint8_t pin).

Here is a short example on how to use the library:

  1. #include "globals.h"
  2. #include "libs/poti.h"
  3. int main(){
  4. init_poti_adc();
  5. resultPotiFrquency = get_poti_average(POTI_FREQUENCY);
  6. // Do something
  7. }

If you are more interested in ADC and how to use it with the avr-gcc toolchain take a closer look at
this article.

Button abstraction

To declutter the main.c we decided to outsource the logic for reading the buttons into its own file.
The button.c implements two methods, init_button_ports_mcu() and button_pressed(). The
init_button_ports_mcu() function has to be executed to initialize the pins the buttons are connected to. If you want
to learn more about the configuration of the buttons take a closer look at the Configuration chapter.

The button_pressed() method can be used can be used to check if a button was pressed. The method returns 1 if the
button was pressed, else 0 is returned. The method takes two arguments. The first one is the pin the button is
connected to. The second one indicates if the button is an opener or a closer. If your button is from type closer you
will have to pass 0, otherwise 1.

If you want to read a button the code would look like that:

  1. #include "globals.h"
  2. #include "buttons.h"
  3. int main(){
  4. init_button_ports_mcu();
  5. if(button_pressed(BUTTON_START_STOP, 0)){
  6. // Do something, the button was pressed
  7. }
  8. }

Due to button debouncing you should lock the button read for a specific time. In our case we wait for six ticks before
reading the button again. To understand what we mean with ticks you can take a closer look at the
Implementation Details chapter.

Implementation Details

The core functionality of our sequencer is to play all notes repeatedly in sequence.
To achieve that, we make use of a timer(specifically Timer1).
It is programmed to call an interrupt routine in a fixed defined frequency and
decreases a variable repeatedly to 0, which is the event when the next note can be played.
As our sequencer also allows to modify the beats per minute and note length,
we also implemented counters for that. The general concept behind our counters is that
they are continuously counted down by the interrupt routine and if they reach 0,
a specific event occurs. After that, the counter value is updated and the process repeats.

All counters are members of a struct called SequencerState (defined in main.c).
This struct is instanciated only once in our program and holds all variables related to general sequencer information.

In the following we will explain how notes are continuously sent at the right time with respect to the tempo (BPM)
which is controlled by a potentiometer. This should give you a better understanding of our counters.

Detailed Example

The SequencerState instance has a variable tempoTicks which holds the number of ticks
(interrupts which trigger the interrupt routine) that are needed until the next note can be sent.
Its value is updated when the potentiometer that is used to controll the tempo is being turned.
The functionality to read values from the potentiometer and convert them to their digital equivalent is implemented
in our poti library. The method calculating the amount of ticks needed, also takes the
configuration of minimum and maximum BPM into consideration.

The counter used to trigger a send-note-event is named tempoTicksCounter. It is set to the value of the tempoTicks
variable and is then decreased by the interrupt routine, until it reaches 0. When it reaches 0, the next note will be
sent. After that the value is set to the current value of the tempoTicks variable and the process repeats.

The condition to send a note is constantly monitored inside the main loop of our program (main.c:main()) and when
it is detected (tempoTicksCounter == 0), the note is sent.

Sending Notes

Sending notes and controlling their length is implemented by our MIDI library.
This library provides two methods for this purpose. They are called send_midi_note_on() and send_midi_note_off().
This is necessary because the MIDI protocol is working with messages that indicate the beginning and the ending of a
note. If you press a key on a MIDI keyboard a NOTE_ON for this note will be sent. If you release the key the
NOTE_OFF MIDI message will be send. The time between the NOTE_ON and NOTE_OFF message is the length of our note.
For more information about the MIDI protocol click here.

Both methods require a pointer to a MidiNote instance (defined in midi.h).
MidiNote is a struct which holds all information needed by a MIDI note (e.g.: note number and velocity).
The MIDI message is constructed by the method sendMidiNote() and consists of three parts (three times one byte).
Those bytes are send to our MIDI/USB output with our software UART implementation which is another one of our libraries
and can be found here.
This library internally also uses a timer (Timer0) to send the bytes (in single bits) with the correct baude rate
(31250 for MIDI)

This section gave an overview of the most important implementation and architecture details.
For more details see the comments in our source code.