Skip to content

For Developers

Jacob Luvisi edited this page Sep 21, 2021 · 14 revisions

Developers Guide

Prologue

Welcome to the official TuneStudio2560 guide for developers.
This guide goes into very specific details on how the software of TuneStudio2560 works and how it interacts with the various hardware components of TuneStudio2560.
By the end of this guide you should have a firm understanding of the code structure and how TuneStudio displays information to the user.

TuneStudio2560 is already very well documented with comments in the code but this wiki page will go in depth on how TuneStudio2560 functions.

NOTE: An official Product Brief for TuneStudio2560 has been released. This product brief is a white paper PDF document on TuneStudio2560 which contains important information regarding the project on both a high and low level approach. Although this developers guide is still recomended, reading the Product Brief is highly recomended as it provides additional and direct details on TuneStudio2560's functions.

Make sure to check out the Doxygen Documentation.

Examples of code blocks will frequently be used to convey examples.
Note that in each file in TuneStudio2560 there are comments on what the file is for and how it functions. It is recommended to read those as well.

It is recommended to read the Users Guide first before reading this.

Important pre-notes

There are some quick notes to understand before continuing on how TuneStudio2560 software works.

  • All major constants are declared in the tune_studio.h header file. This includes pins as well as global variables such as the LCD or Segment Display. This file also includes all of the "global" methods to the entire program. Each global method is defined in this header file and can be used throughout different files in TuneStudio.
  • The main.cpp file is both where the program begins but can also be thought of as a utility class. Most methods in main.cpp can be used outside of main.cpp and are useful for other classes to perform functions relating to the LCD, SD Card, segment display, etc.

Software Explanation

Initial Setup

First, the setup() function begins by initializing all of the hardware on TuneStudio2560.
There are two flags in TuneStudio2560: #DEBUG and #PERF_METRICS. If #DEBUG is enabled then the Serial monitor will begin and messages will be printed when flags in the code are crossed. PERF_METRICS displays occasional RAM usage including HEAP fragmentation as well as stack usage (requires DEBUG to be true). Sometimes PERF_METRICs also might display how long a function takes to execute.
The pins are setup, the lcd is started, and the 7 segment display is initialized.
When the lcd is being created, special characters are also added to the LCD. These characters, defined in tune_studio.h are retrieved from PROGMEM and then copied into memory where they are added.
The SD card module is also checked for functionality. If the SD module does not work then the program gives an error message and will not boot.

Program States

General Description: In TuneStudio2560 there are different "states" that the program can be in at any point in time.
Different states change how button presses are handled, information that appears on the screen, and general code execution.
For example, one state might read a SELECT button press as an indication to add a note to a song while another state might see it as a way of pausing the current song being played.

Each state inherits from the state.h header which holds the ProgramState class.
The program state defines the basic methods that every state shares.
Every state inherits from ProgramState because it allows the destruction of dynamically allocated states, which may be of different sizes, without heap fragmentation.

Every state is defined in the states.h header along with each states own unique global variables and methods.

Every state also has a StateID enum associated with it. This helps the program compare states to each other in a quick way.

Lifecycle of a State

The current state the program is in can be seen from the global prgmState variable.

static ProgramState* prgmState;

A state is then assigned to the prgmState by dynamic memory allocation new .
Each state has one init() method and one loop() method.
The init() method is run only when the state first loads.
The loop() method is repeated as long as the state is active.
The main loop() function in main.cpp runs a execute() method which runs the init and then the loop once the init has been run.
When a state is destroyed via delete prgmState then all of the variables and data in the state are destroyed and a new state must be assigned.

Custom Methods & Variables

Every state in TuneStudio can have its own personal methods and private variables.
Private variables defined in each state can be thought of as global variables for the init(), loop() and any other methods that state has.
All global variables should be defined private in the header file.

Types of States

TuneStudio2560 has 5 standard states the program can be in.
Main Menu: Controls the main menu text and loops through some basic controls.
Creator Mode Menu: A menu that lists the instructions for creator mode.
Creator Mode: A state which allows the creation, saving, and simple playback of songs.
Listening Mode Menu: Gives instructions for listening mode. Also allows a user to select a song when the instructions have finished or been skipped.
Listening Mode: Allows the user to playback their selected song as well as pause, rewind, and go forward.

Buttons, Interrupts & Navigation

Buttons in TuneStudio2560 use a debounce rate to decide whether or not to register a button press. The debounce rate is about 500ms by default.
Some checks for button press in TuneStudio2560 do not use a debounce rate. This is to allow for fast access when a button is pressed.

The SELECT and CANCEL buttons are automatically tied to interrupts. The isr_btn_handle() is tied to both of these buttons.
Some states will not do anything with these interrupts but others may use them to exit the menu and load a new one, or skip instructions.

If a state decides to utilize these interrupts then the immediateInterrupt global variable will be toggled on.
While on, many methods in TuneStudio2560 will skip or ignore there execution to complete as fast as possible as long as the variable is true.
This allows the fast skipping of text for example.

If a state does not use interrupts then it may navigate using the void update_state(StateID state) which allows the switching of states without using interrupts.

How Songs Work

Definitions

  • Note Structure: A C++ struct which contains a const char* pitch (human readable, ex. GS2) and a corresponding frequency in hertz uint16_t frequency. A note can be represented as a single tone in a song.
  • Button Frequency Structure: A C++ struct with a 8 bit integer signifying a button and an array of notes (17 in length) which signifies all possible notes for that tune button.
  • Tone Frequencies Array: A PROGMEM array which stores 5 button frequency structures for each tune button. Contains all of the possible pitches and frequencies for each tone button.
  • MAX_SONG_LENGTH: The maximum allowed size of a song (255 default).
  • MAX_SONG_AMOUNT: The maximum allowed number of songs on an SD card.
  • EMPTY_NOTE: A note struct with "0000" as its pitch and 0 as its frequency. Is skipped by songs.
  • PAUSE_NOTE: A note stuct with "PS" as its pitch and 1 as its frequency. Used to signify a pause in the song.

Description

A song is a class in TuneStudio2560 which can symbolize a series of tones that the application should play.
Each tone playes for a specified amount of time TONE_LENGTH. After the tone has been played then the song waits TONE_DELAY milliseconds before playing the next tone.
Every song class has an array of 16 bit unsigned integers which signify the frequency (hz) that each note in the song has. (Note that the song class does not store any note structs)

Songs can also be parsed from files via the "sd_" methods in the main.cpp.

The max possible length of each song changes a song_size_t datatype which modifies its typing depending on the setting of PRGM_MODE. This allows a fast way to change max song size above 255 without having to change every uint8_t datatype.

As of V1.2.0-R3 The software only utilizes one instance of the Song object and no longer dynamically allocates them. This object is located in tune_studio.h and is used as the primary reference for all songs in TuneStudio2560.
When a program state has finished with a song, it should be cleared using clear().

Adding/Removing Notes

Frequencies (16-bit unsigned integers) can be added to the song via the add_note(uint16_t note) and the remove_note(). Songs cannot go below 0 in size and cannot go above MAX_SONG_LENGTH.

Adding notes from the Tone Frequencies Array can be done by using the note get_current_tone(uint8_t toneButton)method which searches the array for a matching value depending on the tune button passed as well as the current value of the potentiometer.
Methods such as note get_note_from_freq(const uint16_t frequency) and note get_note_from_pitch(const char* pitch) are used to retrieve note structs from the Tone Frequencies Array just by passing a pitch or a frequency.

Playing Back

Songs can be played back using the void play_song()function. However, this method is BLOCKING and will stop code execution.
As a result, the Listening Mode state uses a different non-blocking way to play the song back (more information in lm_playing_song.cpp).

When playing back a song a progress bar is displayed. This progress bar updates every iteration using a simple algorithm but only works when moving forward in a song.
When the user moves back in the song, the progress bar is removed and regenerated.

LCD Functions

Printing on the LCD is important to tell the user instructions and provide an easy to read GUI.
TuneStudio2560 has some custom ways to interact with the LCD.
Each method to print text to the LCD prints char by char in TuneStudio2560 but the default lcd.print("") method is still widely used.

Printing Full Screen Text

If the programmer wants to display a very large portion of text to the user at once they can use the void print_lcd(const __FlashStringHelper* text, uint8_t charDelay) method.
This method clears the screen and takes a flash string F() of any size and prints it to the lcd.
The method prints char by char and will automatically go to the next line when it detects the current line can no longer fit any additional characters.
In addition, the method also removes unneeded spaces on new lines and will continue until the entire flash string has been printed.

An optional _charDelay can be added to increase or decrease the speed of the printing text.

Printing Scrolling Text

The void print_scrolling(const __FlashStringHelper* text, uint8_t cursorY, uint8_t charDelay) allows the printing of scrolling text on one specific line of the lcd.
Note that this method is blocking and will stop execution.

The method can use a flash string F() to print in order to save SRAM space.
The method also allows a user to make any line of the LCD scroll (cursorY) and does not clear the lcd before printing the scrolling text.
The _charDelay allows a programmer to adjust the delay between each character printing on the lcd.

Clearing Lines

The LCD can be cleared both using the lcd.clear() method in the LiquidCrystal_I2C library as well as cleared line by line using void lcd_clear_row(uint8_t row).

The latter is preferred when you must constantly update one part of the screen in a loop in order to prevent flicker.

Custom Characters

The lcd uses custom characters that have been generated using bytes.
Every custom character is stored in PROGMEM in tune_studio.h.

When the setup() function is run the custom characters are each copied from PROGMEM and sent to the LCD.

Seven Segment Display

Interaction with the 4 digit 7 segment display is simple for TuneStudio2560.
There are no custom methods made for interacting with the segment display and the SevSegShift library is sufficient enough to provide good interaction.
It is of note that the segment display refreshDisplay() is commonly used in order to refresh the display. If this method is not used then the display may show invalid or random characters due to its relationship with the two shift registers it communicates with having garbage data.

SD Card

TuneStudio2560 provides many methods for interacting with the SD card using the SD library by Adafruit.
All methods which interact directly with the SD card can be found in the main.cpp file.
IMPORTANT: Remember that ALL file names must be 8 characters or less (not including extension) when saving due to the 8.3 file format.

Saving Songs

void sd_save_song(char* fileName, Song* song)

Songs can be saved using the above method.
Songs should be at least 8 notes in length when being saved to ensure program compatiblity.

Functionality

This method will first save the default tone delay and default tone length to the song in easy to read variables.
Then this song will parse each frequency in the song pointer to a human readable pitch using the get_note_from_freq(song->get_note(i)).pitch instruction.

Removing Songs

void sd_rem(const char* fileName)
Functionality

Will search the SD card for a file with fileName and will remove it if found.
Utilizes debug console to print name of file deleted if DEBUG macro is enabled.

Getting Files on SD

const char* sd_get_file(uint8_t index)
Functionality

Will search the entire SD card and iterate through every file on the root directory.
Every file the method iterates through will be given a "number" that increments.
When the current number is equal to index then the file name at the index is returned.

Method ignores directories, files without .txt extension, and "README.TXT" files.

Copying a Song from SD to RAM

bool sd_songcpy(Song* song, const char* fileName)

The most complicated custom SD card method will parse information in a file and copy it onto a song pointer.

Functionality

Will open a file on the SD card with fileName as its name.
The method will first ignore all comments "#" found in the file.
If an '=' sign is encountered then the method will set the tone delay and will set isToneDelay to false. If isToneDelay is already false then the tone length will be set instead.
The method will then continue to cycle through the entire file where a '-' character is found.
The line will be parsed by removing spaces and converting it into a char string that get_note_from_pitch() can understand.
When a note struct is returned from get_note_from_pitch() its frequency is added to the song.
At the end, the method will verify the song length is accurate (8-255) and if any errors are encountered the method will return false.
If the method is a success then the song pointer will have its tone delay and tone length attributes replaced with the ones read from the file.

Generating README

void sd_make_readme()

The README.TXT file which is generated by the program is quite simple.
First, if "README.TXT" does not exist on the SD card the SD card creates a new file then prints a large portion of text to it.

Dynamic Allocation & Heap Fragmentation

Dynamic allocation on Arduinos can be alarming as the Arduino has no operating system to manage memory leakages and corrupted memory addresses.
TuneStudio2560 utilizes dynamic allocation as an easy to understand way of updating the program state.
In order to make dynamic allocation as safe as possible there are some notes and warnings.
Remember that dynamic allocation can be measured by developers by toggling the PERF_METRICS macro

Warnings

Heap fragmentation can cause the program to produce garbage data and override variables.
When variables crucial to the program execution are overridden the program crashes.

Prevention

  • Make sure to deletedynamic objects (like Songs) when they are no longer needed. If needed, you can also add additional code to the states overridden deconstructor to delete some of these objects.
  • Make sure to close() files when using the SD card.
  • Make sure to delete new states when transitioning to another state.
  • Debug your code frequently.

Delays

TuneStudio2560 utilizes custom delay methods in order to halt program execution.
There are two types of delays used blocking delays and non-blocking delays.

Blocking Delays will halt all code execution until the program has waited a specific amount of time. If needed, TuneStudio2560 provides a custom blocking delay method in which code can be executed while the program waits.
Example:

lcd.print(F("Hello World!"));
delay_ms(500); // Delay for 500ms (one half a second)
lcd.clear();

Non-Blocking Delays will not halt the code while waiting for time to pass. This is done using millis() in tandem with an unsigned long variable which saves the current time and constantly checks to see if enough time has passed. Example:

const unsigned long interval = 5000; // 5 seconds.
unsigned long lastPress = 0;
void loop() {
// Will only run when BTN_TONE_1 is low and over 5 seconds have passed.
if(digitalRead(BTN_TONE_1) == LOW && millis() - lastPress > interval) {
    Serial.println(F("Button Pressed!");
    lastPress = millis();
  }
  // Any code here will run every iteration without a block.
  ...
}

Blocking Delays

Blocking delays are done using the custom void delay_ms(const unsigned long milliseconds).

This method runs an infinite while loop until the milliseconds passed are greater then or equal to the milliseconds given in the argument.
If needed, the delay method can also be altered to execute special code on every while loop iteration while the program is waiting.

Non-Blocking Delays

Non-Blocking delays rely on an out of scope unsigned long which keeps track of the last time an event occured.
When the event does occur the variable is set to the current time.
Each time an event is checked for the current time is subtracted by the variable to check if it is greater than the interval.

Debugging & Performance Metrics

TuneStudio2560 provides ways for developers to run checkpoints in their code without having to delete all of them before release. A handy #DEBUG flag allows this.

Note that any DEBUG and PERF_METRICS flags should be disabled on GitHub commit and for any release because they will slow the application down.

Serial Monitor

Enabling the #DEBUG flag will tell the program to enable the Serial monitor.
There are multiple statements in TuneStudio2560 under #if conditions to check if DEBUG is enabled.
When enabled, messages at these points will print information to the console which can help a developer make note of "checkpoints" in the code that have been passed.

Checking Performance

Performance monitoring can be enabled via #PERF_METRICS flag. This macro requires that debug is enabled.
When enabled, information about RAM usage, heap fragmentation, and stack size will be printed to the serial monitor occasionally.

Some methods also print the amount of micro/milliseconds a method took to execute.

State Looping Performance

As of v1.1.0-R2 there is now support for viewing performance data for ProgramState loop() method execution!
The main loop() class in main.cpp now allows additional viewing of performance data when PERF_METRICS macro is enabled:

  • Microseconds for loop completion time.
  • Current program RAM usage.
  • Percent of RAM being utilized.
  • Amount of clock cycles to complete program loop.
  • Amount of iterations per second (IPS) the loop is doing. A higher IPS means the loop is executing more times per second.

Example

To add your own debug flags you would add something similar to this.

void Song::clear() {

#if DEBUG == true
song_size_t size = 0;
#endif

    for (song_size_t i = this->_maxLength; i > 0; i--) {
        if (_songData[i] != EMPTY_FREQ) {
            _songData[i] = EMPTY_FREQ;
        }
        #if DEBUG == true
        size++;
        #endif
    }
    
#if DEBUG == true
    Serial.print(get_active_time());
    Serial.print(F(" Removed a song of size: "));
    Serial.println(size);
#endif

}

Other

  • Make sure to check each of the files in TuneStudio2560 as there are many comments and Doxygen method comments which describe how methods/variables work and their purpose in the program cycle.