Programming lesson
Building a MIDI Scale Player in C++: A Step-by-Step Tutorial for NFE2140
Learn to build a console-based MIDI scale player in C++ with file I/O, user input validation, and modular functions. This tutorial covers core concepts like vectors, classes, and MIDI messaging, using a TikTok-inspired analogy for scale types.
Introduction to the NFE2140 MIDI Scale Player
Welcome to this hands-on tutorial for building a MIDI Scale Player in C++. This project is a classic assignment for students learning C++ programming, focusing on console applications, user input, file I/O, and MIDI messaging. By the end, you'll have a fully functional program that lets users create, play, and save musical scales—from chromatic to major and minor—with customizable parameters like note length, instrument, and direction. Whether you're a beginner or brushing up for your NFE2140 course, this guide will help you write efficient, modular code that meets high standards.
Understanding the Requirements
Your program must present a main menu with three options: Enter new data and play scale, Load data from file and play scale, or Quit. For options 1 and 2, you'll output MIDI messages to play the scale. The user can choose:
- Scale type: chromatic, major, or minor
- Starting note: any valid MIDI note (e.g., C4 = 60)
- Ascending or descending
- Note length: 100–2000 ms
- Instrument: piano, trumpet, guitar, or violin
- Repeats: 1 to 5 times
After playing, option 1 allows saving inputs to a file. Option 2 loads and plays from a file. All I/O uses the current directory. Higher marks reward efficient code, functions, loops, arrays/vectors, classes, and error checking.
Setting Up Your C++ Project
Create a new console application in your favorite IDE (Visual Studio, Code::Blocks, or CLion). Include necessary headers: <iostream>, <fstream>, <vector>, <string>, <windows.h> (for MIDI on Windows) or use a cross-platform library like RtMidi. We'll assume Windows for this tutorial, using the midiOut API. Remember to link winmm.lib.
Designing the Scale Class
Use a class to encapsulate scale data. This aligns with modern C++ practices and makes your code reusable. Here's a skeleton:
class Scale {
public:
std::string type; // chromatic, major, minor
int startNote; // MIDI note number (0-127)
bool ascending; // true if ascending
int noteLength; // in milliseconds
std::string instrument; // piano, trumpet, etc.
int repeats; // 1-5
std::vector<int> notes; // generated notes
void generateNotes();
void play();
void saveToFile(const std::string& filename);
void loadFromFile(const std::string& filename);
};The generateNotes() method will populate the notes vector based on type and direction. For example, a major scale follows whole-whole-half-whole-whole-whole-half steps. Use arrays or vectors to store intervals.
Generating Scale Notes
Define intervals for each scale type:
const int chromaticIntervals[] = {1,1,1,1,1,1,1,1,1,1,1,1};
const int majorIntervals[] = {2,2,1,2,2,2,1};
const int minorIntervals[] = {2,1,2,2,1,2,2};In generateNotes(), start from startNote and add intervals. For descending, reverse the order. Repeat the scale according to repeats (concatenate the same sequence). For example, a C major scale ascending: C4 (60), D4 (62), E4 (64), F4 (65), G4 (67), A4 (69), B4 (71), C5 (72).
Playing MIDI Notes
On Windows, use midiOutOpen and midiOutShortMsg. A MIDI note-on message is: 0x90 | channel (note on), note number, velocity. Note-off: 0x80 | channel, note, 0. Wrap this in a play() method:
void Scale::play() {
HMIDIOUT handle;
midiOutOpen(&handle, 0, 0, 0, CALLBACK_NULL);
// Set instrument using program change (0xC0 | channel, instrument number)
int instrumentNum = getInstrumentNumber(instrument);
midiOutShortMsg(handle, 0xC0 | instrumentNum);
for (int note : notes) {
midiOutShortMsg(handle, 0x90 | (note << 8) | 100);
Sleep(noteLength);
midiOutShortMsg(handle, 0x80 | (note << 8));
}
midiOutClose(handle);
}Map instruments to MIDI program numbers: Piano=1, Trumpet=57, Guitar=25, Violin=41.
User Input and Validation
Use functions to get validated input. For example, to get a scale type:
std::string getScaleType() {
std::string input;
while (true) {
std::cout << "Enter scale type (chromatic, major, minor): ";
std::cin >> input;
if (input == "chromatic" || input == "major" || input == "minor")
return input;
std::cout << "Invalid. Try again.\n";
}
}Similarly for starting note (0-127), note length (100-2000), repeats (1-5), and instrument. Use std::cin.fail() to handle non-numeric inputs.
File I/O (Save and Load)
Save scale parameters to a text file. Use a simple format: each parameter on its own line. For example:
void Scale::saveToFile(const std::string& filename) {
std::ofstream file(filename);
file << type << "\n";
file << startNote << "\n";
file << ascending << "\n";
file << noteLength << "\n";
file << instrument << "\n";
file << repeats << "\n";
file.close();
}Loading is the reverse. Ensure you check if the file opens successfully.
Main Menu and Program Flow
Use a loop to display the menu and handle choices. For option 1, create a Scale object, get user input, generate notes, play, then ask to save. For option 2, ask for filename, load, and play. Option 3 exits. Here's a snippet:
int main() {
int choice;
do {
std::cout << "1. Enter new data and play\n2. Load from file and play\n3. Quit\nChoice: ";
std::cin >> choice;
if (choice == 1) {
Scale s;
// get input...
s.generateNotes();
s.play();
// ask to save...
} else if (choice == 2) {
// load and play
}
} while (choice != 3);
return 0;
}Trend-Inspired Analogy: TikTok Music Trends and Scale Types
Think of scale types like TikTok audio trends. A chromatic scale is like a viral sound that slides through every note—think of the "Oh No" meme where each note is a step. A major scale feels happy and upbeat, like the background music for a "get ready with me" video. A minor scale gives a dramatic or sad vibe, perfect for "plot twist" edits. Just as creators choose the right audio for their content, your program lets users pick the "mood" of their scale. This connection makes the concept more relatable!
Error Handling and Edge Cases
Always validate user input. For file loading, if the file doesn't exist, display an error and return to menu. For MIDI output, check if midiOutOpen succeeds. Use try-catch blocks if needed. Ensure note numbers stay within 0-127 after generation (wrap or limit). For example, if starting note is 120 and you add intervals, you might exceed 127; cap at 127 or loop back.
Optimizing Code Efficiency
Use vectors instead of raw arrays for flexibility. Avoid magic numbers; use constexpr or enum for constants. Break down tasks into small functions (e.g., getValidInt(), getValidString()). Use loops to handle repeats without duplicating code. For saving/loading, consider using a class method to serialize/deserialize.
Testing Your Program
Test with various inputs: chromatic scales from C0 to G#8, major scales ascending/descending, different instruments. Verify file save/load works. Try invalid inputs like negative note length or "guitar" misspelled. Your error checking should catch these. Also test edge cases: repeat count = 1, note length = 100 ms, etc.
Conclusion
You've now built a robust MIDI Scale Player in C++. This project teaches you essential skills: classes, file I/O, input validation, and MIDI programming. By following this tutorial, you've created a program that meets NFE2140 requirements and goes beyond with efficient, modular design. Now go ahead and experiment—add more instruments, scale types, or even a graphical interface. Happy coding!