CS 312 - Week 1.3
CS 312 Audio Programming Winter 2020
Table of Contents
- 1.3 Class
- Class lab projects
- c131_modular_code
- c132_conditional_compilation
- c133_scope_examples
- c134_char2number.cpp
- c135_casts
- c135_1_narrowing_cast
- c135_2_widening_cast
- c136_1_conditional_logic.cpp \\
- c136_2_bitops.cpp
- c137_function_overloads
- c137_1_overload_by_param_num.cpp
- c137_2_overload_by_param_type.cpp
- c137_3_template.cpp
- c138_vector
- Reading
- 1.3 Homework
- Setup
- hw131_MidiPacket
- hw132_MidiPacket_cout.cpp
- hw133_chromaticScale_vector.cpp
- Submission Format
1.3 Class
Class lab projects
- Mount your course folder using the Go menu in Mac Finder:
smb://courses.ads.carleton.edu/COURSES/cs312-00-w20 - Open Mac Terminal
- Execute:
setup312 <your_carleton_email_name>
Setup
Copy everything/paste everything into Mac Terminal
cd $HOME312/cs312 mkdir hw13 mkdir hw13/class13 c13=$HOME312/cs312/hw13/class13 # c131 mkdir ${c13}/c131_modular_code touch ${c13}/c131_modular_code/c131_circle.cpp touch ${c13}/c131_modular_code/c131_circle.h touch ${c13}/c131_modular_code/c131_main.cpp touch ${c13}/c131_modular_code/c131_rectangle.cpp touch ${c13}/c131_modular_code/c131_rectangle.h touch ${c13}/c131_modular_code/c131_shape.cpp touch ${c13}/c131_modular_code/c131_shape.h # c132 mkdir ${c13}/c132_conditional_compilation touch ${c13}/c132_conditional_compilation/c132_conditional_compilation.cpp # c133 mkdir ${c13}/c133_scope_examples touch ${c13}/c133_scope_examples/c133_1scope.cpp touch ${c13}/c133_scope_examples/c133_2scope.cpp touch ${c13}/c133_scope_examples/c133_3scope.cpp touch ${c13}/c133_scope_examples/c133_4scope.cpp # c134 mkdir ${c13}/c134_char2number touch ${c13}/c134_char2number/c134_char2number.cpp # c135 mkdir ${c13}/c135_casts touch ${c13}/c135_casts/c135_1_narrowing_cast.cpp touch ${c13}/c135_casts/c135_2_widening_cast.cpp touch ${c13}/c135_casts/c135_3_implicit_cast.cpp # c136 mkdir ${c13}/c136_logic_and_bitops touch ${c13}/c136_logic_and_bitops/c136_1_conditional_logic.cpp touch ${c13}/c136_logic_and_bitops/c136_2_bitOps.cpp # c137 mkdir ${c13}/c137_function_overloads touch ${c13}/c137_function_overloads/c137_1_overload_by_param_num.cpp touch ${c13}/c137_function_overloads/c137_2_overload_by_param_type.cpp touch ${c13}/c137_function_overloads/c137_3_template.cpp # c138 mkdir ${c13}/c138_vector touch ${c13}/c138_vector/c138_main.cpp touch ${c13}/c138_vector/c138_vector.cpp touch ${c13}/c138_vector/c138_vector.h
You should see these files.
c131_modular_code
This is a very simple example of writing modular code C++ code. Programs with hundreds of thousands of lines of code may consist of hundreds of .h and .cpp modules following this format.
Setup
Open c131_modular_code folder in vsCode.
Copy/paste this code into the correct files.
- shape.h
// c131_shape.h #ifndef C131_SHAPE_H_ #define C131_SHAPE_H_ class c131_shape { public: float perimiter; float area; c131_shape(); ~c131_shape(); virtual void print(); }; #endif // C131_SHAPE_H_
- circle.h
// c131_circle.h #ifndef C131_CIRCLE_H_ #define C131_CIRCLE_H_ #ifndef C131_SHAPE_H_ #include "c131_shape.h" #endif class c131_circle : public c131_shape { private: const float kPI = 3.14159; const float k2PI = 3.14159 * 2; float r; public: c131_circle(float radius); void print(); }; #endif // C131_CIRCLE_H_
- c131_rectangle.h
// c131_rectangle.h #ifndef C131_RECTANGLE_H_ #define C131_RECTANGLE_H_ #ifndef C131_SHAPE_H_ #include "c131_shape.h" #endif const float kPI = 3.14159; const float k2PI = 3.14159 * 2; class c131_rectangle : public c131_shape { private: float s1; float s2; public: c131_rectangle(float side1, float side2); void print(); }; #endif // C131_RECTANGLE_H_
- shape.cpp
// c131_shape.cpp #ifndef C131_SHAPE_H_ #include "c131_shape.h" #endif #include <iostream> c131_shape::c131_shape() : perimiter{0}, area{0} { } c131_shape::~c131_shape() { } void c131_shape::print() { std::cout << "c131_shape::print()\n"; }
- circle.cpp
// c131_circle.cpp #ifndef C131_CIRCLE_H_ #include "c131_circle.h" #endif #include <iostream> c131_circle::c131_circle(float radius) : r{radius} { } void c131_circle::print() { std::cout << "Circle radius is " << r << '\n'; std::cout << "Circle perimiter = " << k2PI * r << '\n'; std::cout << "Circle area = " << kPI * r * r << '\n'; }
- rectangle.cpp
// c131_rectangle.cpp #ifndef C131_RECTANGLE_H_ #include "c131_rectangle.h" #endif #include <iostream> c131_rectangle::c131_rectangle(float side1, float side2) : s1{side1}, s2{side2} { } void c131_rectangle::print() { std::cout << "Rectangle [" << s1 << ", " << s2 << "]\n"; std::cout << "Rectangle perimiter = " << s1 + s1 + s2 + s2 << '\n'; std::cout << "Rectangle area = " << s1 * s2 << '\n'; }
- main.cpp
// c131_main.cpp #ifndef C131_CIRCLE_H_ #include "c131_circle.h" #endif #ifndef C131_RECTANGLE_H_ #include "c131_rectangle.h" #endif int main() { c131_circle c(5); c131_rectangle s(8.5, 11); c.print(); s.print(); }
Compile
cl c131_main.cpp
Output Errors
Undefined symbols for architecture x86_64: "c131_shape::~c131_shape()", referenced from: c131_rectangle::~c131_rectangle() in c131_main-ad6b08.o c131_circle::~c131_circle() in c131_main-ad6b08.o "c131_circle::print()", referenced from: _main in c131_main-ad6b08.o "c131_circle::c131_circle(float)", referenced from: _main in c131_main-ad6b08.o "c131_rectangle::print()", referenced from: _main in c131_main-ad6b08.o "c131_rectangle::c131_rectangle(float, float)", referenced from: _main in c131_main-ad6b08.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
Compile command
cl c131_main.cpp c131_shape.cpp c131_circle.cpp c131_rectangle.cpp && ./a.out
If you're certain that every required .cpp file is in the working directory and there are no other .cpp files present then you can use this compile command.
cl *.cpp && ./a.out
Output
Circle perimiter = 31.4159 Circle area = 78.5397 Rectangle [8.5, 11] Rectangle perimiter = 39 Rectangle area = 93.5
Header guards
I expect you to follow these conventions in all future homework.
header guards in .h files define a preprocessor name if it hasn't already been defined
#ifndef NAME_OF_FILE_H_ #define NAME_OF_FILE_H_ everything else #endif // NAME_OF_FILE_H_
- header guards in .cpp files include a header file if it hasn't already been included
#ifdef NAME_OF_FILE_H_ #include "name_of_file.h" #endif
- first #include file in .cpp source file is its associated .h header file
c132_conditional_compilation
Setup
Open the cs132_conditional_compilation folder in vsCode.
This is how I do it.
Drag the folder icon from Mac Finder into the vsCode editing window.
Conditional Compilation
Define a DEBUGIT macro to examine function calls at runtime. This is often used while developing software.
// c132_conditional_compilation.cpp #include <iostream> #include <string> #define DEBUGIT // #undef DEBUGIT int add2(int x, int y) { int res = x + y; #ifdef DEBUGIT std::cout << "Entering add2(x, y)" << std::endl; std::cout << " x = " << x << std::endl; std::cout << " y = " << y << std::endl; // compute the value for both #ifdef DEBUG and #undef DEBUG versions std::cout << " return val = " << res << std::endl; std::cout << "Exiting add2(x, y)" << std::endl; #endif return res; } int main() { int x = 16; int y = 10; int res = add2(x, y); std::cout << res << std::endl; }
Compile
# cd $HOME312/cs312/hw13/class13/ cl c132_conditional_compilation.cpp && ./a.out
Output when DEBUGIT is defined
Entering add2(x, y)
x = 16
y = 10
return val = 26
Exiting add2(x, y)
26
Turn off DEBUGIT
Comment out …
// #define DEBUGIT
or use #undef
#define DEBUGIT #undef DEBUGIT
Compile
# cd $HOME312/cs312/hw13/class13/ cl c132_conditional_compilation.cpp && ./a.out
Output when DEBUGIT is not defined
26
Control DEBUGIT from the command line using the -D flag
First comment out both DEBUGIT statements in the source code.
// #define DEBUGIT // #undef DEBUGIT
Then compile using -D DEBUGIT as part of the compile line.
cl -D DEBUGIT c132_conditional_compilation.cpp && ./a.out
Then compile without DEBUGIT.
cl c132_conditional_compilation.cpp && ./a.out
Here's an advanced example using conditional compile macros for Mac, Windows, and Linux. It's part of the RtMidi.h file that we'll be using soon.
#if defined(__MACOSX_CORE__) #if TARGET_OS_IPHONE #define AudioGetCurrentHostTime CAHostTimeBase::GetCurrentTime #define AudioConvertHostTimeToNanos CAHostTimeBase::ConvertToNanos #endif #endif // Default for Windows is to add an identifier to the port names; this // flag can be defined (e.g. in your project file) to disable this behaviour. //#define RTMIDI_DO_NOT_ENSURE_UNIQUE_PORTNAMES // **************************************************************** // // // MidiInApi and MidiOutApi subclass prototypes. // // **************************************************************** // #if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) #define __RTMIDI_DUMMY__ #endif #if defined(__MACOSX_CORE__)
c133_scope_examples
Setup
Open the c133_scope_examples folder in vsCode.
extern
extern is used in a header file to declare variables and functions without defining them. extern means that variable or function will be defined in another file somewhere during the compile process. The compiler permits extern variables and functions to be included by many files but only permits them to be defined once. The definition almost always appears in a .cpp file because .cpp files are not used as #include files.
Variable scope
- global program scope variables in .h files
- Variables declared extern in a .h file need to be defined in a .cpp file.
The .cpp file does not have to share the .h file name but usually does.
Are visible to any other file that #include's the header. - global file scope variables in .cpp files
- Variables declared outside any function or code block in a source file.
Global file variables are usually declared above the first function in that file.
Global file variables are visible to all functions in that file but are not visible to other files. - local function scope variables
- Variables defined as static within a function remember their value previous value the next time the function is called.
Non static variables inside a function are reinitialized each time the function is called.
Both static and non static variables inside a function exist only within that function. - static scope file variables
- Variables defined as static outside a function remember their values in all functions within the file.
Not visible outside the file.
c133_1_scope.cpp
Copy/paste
// c133_1scope.cpp #include <iostream> int x = 1; // global file scope int times2() { x = x * 2; return x; } int main() { int x1 = times2(); int x2 = times2(); int x3 = times2(); std::cout << x1 << std::endl; std::cout << x2 << std::endl; std::cout << x3 << std::endl; std::cout << x << std::endl; }
Read the code before compiling. What do you expect the output to be?
Compile
# cd $HOME312/cs312/hw13/class13/c133_scope_examples cl c133_1scope.cpp && ./a.out
c133_2_scope.cpp
Copy/paste
// c133_2scope.cpp #include <iostream> int x = 1; // global file scope int times2() { int x = 1; // local function scope x = x * 2; return x; } int main() { int x1 = times2(); int x2 = times2(); int x3 = times2(); std::cout << x1 << std::endl; std::cout << x2 << std::endl; std::cout << x3 << std::endl; std::cout << x << std::endl; }
Read the code before compiling. What do you expect the output to be?
Compile
# cd $HOME312/cs312/hw13/class13/c133_scope_examples cl c133_2scope.cpp && ./a.out
c133_3_scope.cpp
Copy/paste
// c133_3scope.cpp #include <iostream> int x = 1; // global file scope int times2() { static int x = 1; // static function scope x = x * 2; return x; } int main() { int x1 = times2(); int x2 = times2(); int x3 = times2(); std::cout << x1 << std::endl; std::cout << x2 << std::endl; std::cout << x3 << std::endl; std::cout << x << std::endl; }
Read the code before compiling. What do you expect the output to be?
Compile
# cd $HOME312/cs312/hw13/class13/c133_scope_examples cl c133_3scope.cpp && ./a.out
c133_4_scope.cpp
Copy/paste
// c133_4scope.cpp #include <iostream> static int x = 1; // global file scope int times2() { x = x * 2; return x; } int main() { int x1 = times2(); int x2 = times2(); int x3 = times2(); std::cout << x1 << std::endl; std::cout << x2 << std::endl; std::cout << x3 << std::endl; std::cout << x << std::endl; }
Read the code before compiling. What do you expect the output to be?
Compile
# cd $HOME312/cs312/hw13/class13/c133_scope_examples cl c133_4scope.cpp && ./a.out
c134_char2number.cpp
char
- There are three char types in C++
char, signed char and unsigned char. - Mac, Windows, and Linux define char as an 8 bit number having \(2^8\) possible values.
- A plain char can be signed or unsigned according to the implementation.
- Signed char values are ascending positive from 0-0x7F (0-127) and descending negative from 0x80-0xFF (-128 to -1).
- Signed 0xFF is -1.
- Unsigned char values are positive from 0 to 255 (0x0 to 0xFF).
This short program reports char as signed on Mac OS.
#include <iostream> #include <climits> int main() { std::cout << CHAR_MIN << std::endl; }
Compile
# cd $HOME312/cs312/hw13/class13/c134_char2number cl c134_char2number.cpp && ./a.out
Output
-128
char and std::cout
Because std::cout outputs all char types as ASCII characters you need to cast them to a larger type to display them as numbers.
There are several methods to print a char as a number.
Setup
Open c134_char2number folder in vsCode
Copy/paste
// c134_char2number.cpp #include <iostream> #include <sstream> void char2num_1(unsigned char uc) { std::cout << "char2num_1 " << uc << " doesn't work" << std::endl; } void char2num_2(unsigned char uc) { std::cout << "char2num_2 " << static_cast<int>(uc) << std::endl; } void char2num_3(unsigned char uc) { int n = uc; std::cout << "char2num_3 " << n << std::endl; } void char2num_4(unsigned char uc) { std::cout << "char2num_4 " << +uc << std::endl; } void char2num_5(unsigned char uc) { std::cout << "char2num_5 " << std::to_string(uc) << std::endl; } int main() { char2num_1(76); char2num_2(76); char2num_3(76); char2num_4(76); char2num_5(76); return 0; }
c135_casts
cast (type coercion)
A cast changes one data type into another data type.
C style - Do not use
Even though C style casts continue to work in C++ their use is discouraged in modern C++.
In older code you'll see two equivalent casts based on parentheses placement
- (type)variable
- type(variable)
unsigned char uc = 0x90; std::cout << int(uc) << std::endl; std::cout << (int)uc << std::endl; // same thing
Output
144 144
C++ style - static_cast<to>(from )
static_cast<new_type>(old_type)
This is the most used of several types of C++ casts.
unsigned char uc = 0x90; std::cout << static_cast<int>(uc) << std::endl;
Output
144
casts can be dangerous
You have to be careful when using static_cast.
There are two cases to worry about:
- a narrowing cast can lose information
- mixing signed and unsigned integer types
Setup
Open c135_casts folder in vsCode
c135_1_narrowing_cast
Converts a type with more bits to a type with fewer bits
Copy/paste
// c135_1_narrowing_cast.cpp #include <iostream> #include <cstdint> // for in8_t, ... #include <cmath> // for M_PI #include <iomanip> // for std::setprecision int main() { const uint32_t n = 0xbeefcafe; const double pi = M_PI; int8_t i8; uint8_t u8; int16_t i16; uint16_t u16; int32_t i32; uint32_t u32; i8 = static_cast<int8_t>(n); u8 = static_cast<uint8_t>(n); i16 = static_cast<int16_t>(n); u16 = static_cast<uint16_t>(n); i32 = static_cast<int32_t>(n); u32 = static_cast<uint32_t>(n); std::cout << "uint32_t n = \t" << std::hex << n << "\t" << std::dec << n << std::endl; std::cout << "static_cast<int8_t>(n): \t" << std::hex << i8 << "\t" << std::dec << i8 << std::endl; std::cout << "static_cast<uint8_t>(n):\t" << std::hex << u8 << "\t" << std::dec << u8 << std::endl; std::cout << "static_cast<int16_t>(n):\t" << std::hex << i16 << "\t" << std::dec << i16 << std::endl; std::cout << "static_cast<uint16_t>(n):\t" << std::hex << u16 << "\t" << std::dec << u16 << std::endl; std::cout << "static_cast<int32_t>(n):\t" << std::hex << i32 << "\t" << std::dec << i32 << std::endl; std::cout << "static_cast<uint32_t>(n):\t" << std::hex << u32 << "\t" << std::dec << u32 << std::endl; std::cout << std::endl; std::cout << std::setprecision(15); // full precision for double std::cout << "double pi = \t" << pi << std::endl; std::cout << "static_cast<double>(pi):\t" << static_cast<double>(pi) << std::endl; std::cout << "static_cast<float>(pi): \t" << static_cast<float>(pi) << std::endl; std::cout << "static_cast<int32_t>(pi):\t" << static_cast<int32_t>(pi) << std::endl; std::cout << "static_cast<int16_t>(pi):\t" << static_cast<int16_t>(pi) << std::endl; std::cout << "static_cast<int8_t>(pi):\t" << static_cast<int8_t>(pi) << std::endl; std::cout << std::endl; }
Compile
# cd $HOME312/cs312/hw13/class13/ cl c135_1_narrowing_cast.cpp && ./a.out
Output can be dangerous with possible loss of information
Lines beginning with ## are my comments
uint32_t n = beefcafe 3203386110 ## loses the left most 24 bits leaving 0xfe that is not a printing character static_cast<int8_t>(n): ? ? static_cast<uint8_t>(n): ? ? ## loses the left most 16 bints leaving 0xcafe ## the max postive signed int is 0x7fff ## negative numbers start with 0x8000 = -32768 to 0xffff = -1 static_cast<int16_t>(n): cafe -13570 static_cast<uint16_t>(n): cafe 51966 ## the max postive signed int32_t is 0x7fffffff ## negative numbers start with 0x80000000 = -2147483648 to 0xffffffff = -1 static_cast<int32_t>(n): beefcafe -1091581186 static_cast<uint32_t>(n): beefcafe 3203386110 double pi = 3.14159265358979 static_cast<double>(pi): 3.14159265358979 ## float and double share the first six decimal places static_cast<float>(pi): 3.14159274101257 ## casting float/double to int returns the integer part only static_cast<int32_t>(pi): 3 static_cast<int16_t>(pi): 3 static_cast<int8_t>(pi):
c135_2_widening_cast
converts a type with fewer bits into a type with more bits
Copy/paste
// c135_2_widening_cast.cpp #include <iostream> #include <cstddef> signed char sc = 0x90; unsigned char uc = 0x90; int main() { std::cout << "----------------------------------------------" << std::endl; std::cout << "signed char sc = 0x90\n\t" << std::hex << std::showbase << sc <<'\t' << std::dec << sc << std::endl; std::cout << "unsigned char uc = 0x90\n\t" << std::hex << std::showbase << uc <<'\t' << std::dec << uc << std::endl; std::cout << "----------------------------------------------" << std::endl; std::cout << "signed to signed\tstatic_cast<int16_t>(sc):\n\t" << std::hex << std::showbase << static_cast<int16_t>(sc) << '\t' << std::dec << static_cast<int16_t>(sc) << std::endl; std::cout << "unsigned to unsigned\tstatic_cast<uint16_t>(uc):\n\t" << std::hex << std::showbase << static_cast<uint16_t>(uc) << '\t' << std::dec << static_cast<uint16_t>(uc) << std::endl; std::cout << "signed to unsigned\tstatic_cast<uint16_t>(sc):\n\t" << std::hex << std::showbase << static_cast<uint16_t>(sc) << '\t' << std::dec << static_cast<uint16_t>(sc) << std::endl; std::cout << "unsigned to signed\tstatic_cast<int16_t>(uc):\n\t" << std::hex << std::showbase << static_cast<int16_t>(uc) << '\t' << std::dec << static_cast<int16_t>(uc) << std::endl; std::cout << "----------------------------------------------" << std::endl; std::cout << "signed to signed\tstatic_cast<int32_t>(sc):\n\t" << std::hex << std::showbase << static_cast<int32_t>(sc) << '\t' << std::dec << static_cast<int32_t>(sc) << std::endl; std::cout << "unsigned to unsigned\tstatic_cast<uint32_t>(uc):\n\t" << std::hex << std::showbase << static_cast<uint32_t>(uc) << '\t' << std::dec << static_cast<uint32_t>(uc) << std::endl; std::cout << "signed to unsigned\tstatic_cast<uint32_t>(sc):\n\t" << std::hex << std::showbase << static_cast<uint32_t>(sc) << '\t' << std::dec << static_cast<uint32_t>(sc) << std::endl; std::cout << "unsigned to signed\tstatic_cast<int32_t>(uc):\n\t" << std::hex << std::showbase << static_cast<int32_t>(uc) << '\t' << std::dec << static_cast<int32_t>(uc) << std::endl; std::cout << "----------------------------------------------" << std::endl; }
Compile
# cd $HOME312/cs312/hw13/class13/ cl c135_2_widening_cast.cpp && ./a.out
Output can be dangerous when mixing signed and unsigned
---------------------------------------------------------------- signed char sc = 0x90 ? ? unsigned char uc = 0x90 ? ? ---------------------------------------------------------------- signed to signed static_cast<int16_t>(sc): 0xff90 -112 unsigned to unsigned static_cast<uint16_t>(uc): 0x90 144 signed to unsigned static_cast<uint16_t>(sc): 0xff90 65424 unsigned to signed static_cast<int16_t>(uc): 0x90 144 ---------------------------------------------------------------- signed to signed static_cast<int32_t>(sc): 0xffffff90 -112 unsigned to unsigned static_cast<uint32_t>(uc): 0x90 144 signed to unsigned static_cast<uint32_t>(sc): 0xffffff90 4294967184 unsigned to signed static_cast<int32_t>(uc): 0x90 144 ----------------------------------------------------------------
c136_1_conditional_logic.cpp \\
The three conditional logic operators: not, and, or are often used in program flow statements like if…else, do…while, and while….
C++ operator | Meaning |
---|---|
! | not |
&& | and |
| | | or |
Setup
Open the c136_logic_and_bitops folder in vsCode.
Copy/paste
// c136_conditional_logic.cpp #include <iostream> #include <string> void logic_test(std::string str, bool b) { if (b) std::cout << str << " is true" << std::endl; else std::cout << str << " is false" << std::endl; } int main() { bool t = true; bool f = false; logic_test("t", t); logic_test("f", f); logic_test("!t", !t); logic_test("!f", !f); std::cout << std::endl; logic_test("1", static_cast<bool>(1)); logic_test("1234", static_cast<bool>(1234)); logic_test("-1234", static_cast<bool>(-1234)); logic_test("0", static_cast<bool>(0)); std::cout << std::endl; logic_test("t && t", t && t); logic_test("t && f", t && f); logic_test("f && t", f && t); logic_test("f && f", f && f); std::cout << std::endl; logic_test("t || t", t || t); logic_test("t || f", t || f); logic_test("f || t", f || t); logic_test("f || f", f || f); }
Copy/paste into c136_1_conditional_logic.cpp.
Read the code before compiling. What do you expect the output to be?
Compile
# cd $HOME312/cs312/hw13/class13/ cl c136_1_conditional_logic.cpp && ./a.out
Output
t is true f is false !t is false !f is true 1 is true 1234 is true -1234 is true 0 is false t && t is true t && f is false f && t is false f && f is false t || t is true t || f is true f || t is true f || f is false
c136_2_bitops.cpp
Byte
From https://en.wikipedia.org/wiki/Byte
The byte is a unit of digital information that most commonly consists of eight bits, representing a binary number. Historically, the byte was the number of bits used to encode a single character of text in a computer...and for this reason it is the smallest addressable unit of memory in many computer architectures.
A MIDI byte is a positive (unsigned) 8 bit number.
C++ does not define a type byte but does define these 8 bit types.
C++ 8 bit Types | Range dec | Range hex | Notes |
---|---|---|---|
char | signed/unsigned | implementation defined | |
signed char + | 0 to 127 | 0x0-0x7f | signed |
signed char - | -128 to -1 | 0x80-0xFF | signed |
int8_t | same as above | same as above | typedef to signed char |
unsigned char | 0 to 255 | 0 - 0xFF | unsigned |
uint8_t | same as above | same as above | typedef to unsigned char |
Using std::cout to output a MIDI byte has the same problems as char.
You'll have to use one of the methods from c134_char2number to output a number.
- Bit And operator is &
- 0x94 & 0xF0 is 0x90
I'm using the ' to split the binary digits into the four bit hex representation.
binary 1001'0100 AND 1111'0000 = 1001'0000 - Bit OR operator is |
- 0x90 | 6 is 0x96
binary 1001'0000 OR 0000'0110 = 1001'0110 - Bit shift right operator is >> <num bits>
- 0x97 >> 4 is 9
binary 1001'0111 >> 4 = 0000'1001 - Bit shift left operator is << <num bits>
- 0x97 << 4 is 0x970
binary 1001'0111 << 4 = 1001'0111'0000
Copy/paste
// c136_2_bitops.cpp #include <iostream> void examples1to4() { std::cout << "c136_2_bitops web page examples" << std::endl; std::cout << "Bit AND 0x94 & 0xF0 = " << std::showbase << std::hex << (0x94 & 0xF0) << '\n'; std::cout << "Bit OR 0x90 | 6 = " << std::showbase << std::hex << (0x90 | 6) << '\n'; std::cout << "Bit Shift Right 0x97 >> 4 = " << std::showbase << std::hex << (0x97 >> 4) << '\n'; std::cout << "Bit Shift Left 0x97 << 8 = " << std::showbase << std::hex << (0x97 << 8) << '\n'; } void lowNibble(int8_t n) { std::cout << "lowNibble(0xAB)" << std::endl; int8_t lownib = n & 0x0F; std::cout << std::hex << static_cast<int>(lownib) << std::endl; } void highNibble(int8_t n) { std::cout << "highNibble(0xAB)" << std::endl; int8_t highnib = n >> 4; std::cout << std::hex << static_cast<int>(highnib) << std::endl; highnib &= 0x0F; std::cout << std::hex << static_cast<int>(highnib) << std::endl; } void lowByte(int16_t n) { std::cout << "lowByte(0x1234)" << std::endl; std::cout << std::hex << (n & 0x00ff) << std::endl; } void highByte(int16_t n) { std::cout << "highByte(0x1234)" << std::endl; std::cout << std::hex << (n >> 8) << std::endl; } void lowWord(int32_t n) { std::cout << "lowWord(0x12345678)" << std::endl; std::cout << std::hex << (n & 0x0000ffff) << std::endl; } void highWord(int32_t n) { std::cout << "highWord(0x12345678)" << std::endl; std::cout << std::hex << (n >> 16) << std::endl; } void changeLowNibble(const uint8_t bite, const int8_t lownib) { std::cout << "changeLowNibble(0x90, 5)" << std::endl; std::cout << "Before: " << +bite << '\n'; uint8_t n8 = (bite & 0xF0) + lownib; std::cout << "After: " << +n8 << '\n'; } int main() { examples1to4(); std::cout << '\n'; int8_t n8 = 0xAB; lowNibble(n8); highNibble(n8); std::cout << '\n'; int16_t n16 = 0x1234; lowByte(n16); highByte(n16); std::cout << '\n'; int32_t n32 = 0x12345678; lowWord(n32); highWord(n32); std::cout << '\n'; changeLowNibble(0x90, 5); return 0; }
Compile
# cd $HOME312/cs312/hw13/class13/c136_logic_and_bitops cl c136_2_bitops.cpp && ./a.out
Output
c136_2_bitops web page examples Bit AND 0x94 & 0xF0 = 0x90 Bit OR 0x90 | 6 = 0x96 Bit Shift Right 0x97 >> 4 = 0x9 Bit Shift Left 0x97 << 8 = 0x9700 lowNibble(0xAB) 0xb highNibble(0xAB) 0xfffffffa 0xa lowByte(0x1234) 0x34 highByte(0x1234) 0x12 lowWord(0x12345678) 0x5678 highWord(0x12345678) 0x1234 changeLowNibble(0x90, 5) Before: 0x90 After: 0x95
c137_function_overloads
Two or more C++ functions that have the same name but can be distinguished by the number and/or type of their parameters are called function overloads.
Setup
Open the c137_function_overloads folder in vsCode.
c137_1_overload_by_param_num.cpp
C++ functions that have a different number of parameters can be overloaded.
Copy/paste
// c137_1_overload_by_param_num.cpp #include <iostream> #include <string> const char kTAB = '\t'; void print_midi_message(unsigned int timestamp, unsigned char status, unsigned char data1) { std::cout << timestamp << kTAB << std::hex << +status << kTAB << std::dec << +data1 << std::endl; } void print_midi_message(unsigned int timestamp, unsigned char status, unsigned char data1, unsigned char data2) { std::cout << timestamp << kTAB << std::hex << +status << kTAB << std::dec << +data1 << kTAB << +data2 << std::endl; } int main() { // Patch Change message (vibraphone, channel 0) // parameters are: timestamp, status, data1 print_midi_message(0, 0xC0, 11); // NON message: at 1000ms, channel 0, middle C, max velocity // parameters are: timestamp, status, data1, data 2 print_midi_message(1000, 0x91, 60, 127); }
Compile
# cd $HOME312/cs312/hw13/class13/c137_function_overloads cl c137_1_overload_by_param_num.cpp && ./a.out
Output
0 c0 11 1000 91 60 127
c137_2_overload_by_param_type.cpp
C++ functions that have different parameter types can be overloaded.
Copy/paste
// c137_2_overload_by_param_type.cpp #include <iostream> #include <limits> // file global variables. char c = std::numeric_limits<char>::max(); int8_t i8 = std::numeric_limits<int8_t>::max(); uint8_t u8 = std::numeric_limits<uint8_t>::max(); int16_t i16 = std::numeric_limits<int16_t>::max(); uint16_t u16 = std::numeric_limits<uint16_t>::max(); int32_t i32 = std::numeric_limits<int32_t>::max(); uint32_t u32 = std::numeric_limits<uint32_t>::max(); float f = 1.1234567890; double d = 1.12345678901234567890; void print(char c) { std::cout << +c << std::endl; } void print(int8_t) { std::cout << +i8 << std::endl; } void print(uint8_t) { std::cout << +u8 << std::endl; } void print(int16_t i16) { std::cout << i16 << std::endl; } void print(uint16_t u16) { std::cout << u16 << std::endl; } void print(int32_t i32) { std::cout << i32 << std::endl; } void print(uint32_t u32) { std::cout << u32 << std::endl; } void print(float f) { std::cout << f << std::endl; } void print(double d) { std::cout << d << std::endl; } int main(int argc, char const *argv[]) { std::cout << "----------------------------------------------" << std::endl; print(c); print(i8); print(u8); std::cout << "----------------------------------------------" << std::endl; print(i16); print(u16); std::cout << "----------------------------------------------" << std::endl; print(i32); print(u32); std::cout << "----------------------------------------------" << std::endl; std::cout << "---Default std::cout.precision()---" << std::endl; print(f); print(d); std::cout << "----------------------------------------------" << std::endl; std::cout << "---Max std::cout.precision()---" << std::endl; std::cout.precision(7); // accurate to 6 decimal places print(f); std::cout.precision(15); // accurate to 15 decimal places print(d); std::cout << "----------------------------------------------" << std::endl; }
Compile
# cd $HOME312/cs312/hw13/class13/c137_function_overloads cl c137_2_overload_by_param_type.cpp && ./a.out
Output
---------------------------------------------- 127 127 255 ---------------------------------------------- 32767 65535 ---------------------------------------------- 2147483647 4294967295 ---------------------------------------------- ---Default std::cout.precision()--- 1.12346 1.12346 ---------------------------------------------- ---Max std::cout.precision()--- 1.123457 1.12345678901235 ----------------------------------------------
c137_3_template.cpp
In contrast to overloading multiple functions by parameter it simpler to use a template function.
This template function would be a one liner if we didn't have print char types as numbers. Here we're checking for char types and outputting them as numbers using the unary plus operator.
Copy/paste
// c137_3_template.cpp #include <iostream> #include <limits> // for intX_t types #include <type_traits> // for is_arithmetic() // file global variables. char c = std::numeric_limits<char>::max(); int8_t i8 = std::numeric_limits<int8_t>::max(); uint8_t u8 = std::numeric_limits<uint8_t>::max(); int16_t i16 = std::numeric_limits<int16_t>::max(); uint16_t u16 = std::numeric_limits<uint16_t>::max(); int32_t i32 = std::numeric_limits<int32_t>::max(); uint32_t u32 = std::numeric_limits<uint32_t>::max(); float f = 1.1234567890; double d = 1.12345678901234567890; // This template function would be a one liner if we were not printing char types as numbers. // Here we're checking for char types using methods in the <type_traits> library. // We're outputting them as numbers using the unary plus operator. template <typename T> void print(const T &val) { if (std::is_arithmetic<char>() || (std::is_arithmetic<signed char>()) || (std::is_arithmetic<unsigned char>())) std::cout << +val << std::endl; else std::cout << val << std::endl; } int main(int argc, char const *argv[]) { std::cout << "----------------------------------------------" << std::endl; print(c); print(i8); print(u8); std::cout << "----------------------------------------------" << std::endl; print(i16); print(u16); std::cout << "----------------------------------------------" << std::endl; print(i32); print(u32); std::cout << "----------------------------------------------" << std::endl; std::cout << "Default std::cout.precision()" << std::endl; print(f); print(d); std::cout << "----------------------------------------------" << std::endl; std::cout << "Max std::cout.precision()" << std::endl; std::cout.precision(7); // accurate to 6 decimal places print(f); std::cout.precision(15); // accurate to 15 decimal places print(d); std::cout << "----------------------------------------------" << std::endl; }
Compile
# cd $HOME312/cs312/hw13/class13/c137_function_overloads cl c137_3_template.cpp && ./a.out
Output
---------------------------------------------- 127 127 255 ---------------------------------------------- 32767 65535 ---------------------------------------------- 2147483647 4294967295 ---------------------------------------------- Default std::cout.precision() 1.12346 1.12346 ---------------------------------------------- Max std::cout.precision() 1.123457 1.12345678901235 ----------------------------------------------
c138_vector
<vector>
A vector is a container that holds items of the same type.
The difference between a vector and an array is that the vector can grow dynamically as items are added.
Items are added using the push_back() method.
The nth element in vector v can be accessed using the v.[n] bracket notation.
The nth element in vector v can be accessed using the a v.at(n) function call.
The at(n) function is preferred because it does range checking and will warns you if you access an out of range element.
The [n] notation does no range checking but is faster (on really really large vectors).
IMPORTANT:
This example is very important to study and understand. We'll be working with vectors for the next 5 weeks.
This example generates a series of NON (Note On) MIDI messages. It cannot be played in MIDIDisplay_DLS because there are no matching NOF (Note Off) messages.
For loops
This example illustrates three methods of printing every element in a vector.
Setup
Open the c138_vector folder in vsCode.
Copy/paste
c138_vector.h
// c138_vector.h #ifndef C138_VECTOR_H #define C138_VECTOR_H #include <iostream> #include <string> #include <vector> class CMyData { public: // data members int timestamp; int status; int data1; int data2; // constructor CMyData(); CMyData(int ts, int st, int d1, int d2); void init_data_vector(); void print(); }; #endif // C138_VECTOR_H
Copy/paste
c138_vector.cpp
// c138_vector.cpp #ifndef VECTOR_H #include "c138_vector.h" #endif const char kTAB = '\t'; // forward declarations // sometimes called function prototypes // C++ requires a function be declared before it's used void print_auto_for_loop(); void print_for_loop(); void print_begin_end_iterators(); // Declare a vector to hold an array of CMyData structs std::vector<CMyData> v; // constructor CMyData::CMyData() { timestamp = 0; status = 0x80; data1 = 0; data2 = 0; }; // constructor overload CMyData::CMyData(int ts, int st, int d1, int d2) { timestamp = ts; status = st; data1 = d1; data2 = d2; } // member function void CMyData::print() { std::cout << "print_auto_for_loop\n"; print_auto_for_loop(); std::cout << "\nprint_for_loop\n"; print_for_loop(); std::cout << "\nprint_begin_end_iterators\n"; print_begin_end_iterators(); } void CMyData::init_data_vector() { CMyData md; for (int ix = 0; ix < 12; ++ix) { md.timestamp = ix * 1000; md.status = 0x90; md.data1 = 60 + ix; md.data2 = 100; v.push_back(md); } } // utility functions void print_auto_for_loop() { for (auto itr : v) { std::cout << itr.timestamp << kTAB << std::hex << itr.status << kTAB << std::dec << itr.data1 << kTAB << std::dec << itr.data2 << std::endl; } } void print_for_loop() { for (int ix = 0; ix < v.size(); ++ix) { std::cout << v.at(ix).timestamp << kTAB << std::hex << v.at(ix).status << kTAB << std::dec << v.at(ix).data1 << kTAB << std::dec << v.at(ix).data2 << std::endl; } } void print_begin_end_iterators() { for (auto itr = v.begin(); itr != v.end(); ++itr) { std::cout << itr->timestamp << kTAB << std::hex << itr->status << kTAB << std::dec << itr->data1 << kTAB << std::dec << itr->data2 << std::endl; } }
Copy/paste
c138_main.cpp
// c138_main.cpp #ifndef C138_VECTOR_H #include "c138_vector.h" #endif int main() { // create a new CMyData struct std::string starry(54, '-'); std::cout << "// " << starry << std::endl; std::cout << " This example will not play in MIDIDisplay_DLS\n"; std::cout << "// " << starry << std::endl; CMyData md; md.init_data_vector(); md.print(); }
Compile
# cd $HOME312/cs312/hw13/class13/c138_vector cl c138_vector.cpp c138_main.cpp && ./a.out
Output //
// ------------------------------------------------------
This example will not play in MIDIDisplay_DLS
// ------------------------------------------------------
print_auto_for_loop
0 90 60 100
1000 90 61 100
2000 90 62 100
3000 90 63 100
4000 90 64 100
5000 90 65 100
6000 90 66 100
7000 90 67 100
8000 90 68 100
9000 90 69 100
10000 90 70 100
11000 90 71 100
print_for_loop
0 90 60 100
1000 90 61 100
2000 90 62 100
3000 90 63 100
4000 90 64 100
5000 90 65 100
6000 90 66 100
7000 90 67 100
8000 90 68 100
9000 90 69 100
10000 90 70 100
11000 90 71 100
print_begin_end_iterators
0 90 60 100
1000 90 61 100
2000 90 62 100
3000 90 63 100
4000 90 64 100
5000 90 65 100
6000 90 66 100
7000 90 67 100
8000 90 68 100
9000 90 69 100
10000 90 70 100
11000 90 71 100
Reading
We'll look at MIDI messages, C++ program structure, char types, casts, logic, function overloading, templates, and the very important <vector> library.
MIDI Messages
MIDI time
The MIDI specification does not define how to keep track of time. It's up to the programmer to mangage musical time. Methods include software timers, callback functions, interrupts, and hardware clocks. Musical time is based on a uniformly spaced beats. The number of beats per minute is called the tempo. MIDI timestamps are generally measured in integer milliseconds or microseconds and sometimes float32 in seconds.
MIDI messages
MIDI messages as defined in the official MIDI specification as consist of one status byte followed by zero or more data bytes.
Complete 1.0 MIDI Specification
MIDI bytes
Status bytes have the most significant bit (MSB) set to 1 while data bytes set the MSB to 0.
Type | Hex Min | Hex Max | Dec Min | Dec Max | Bin Min | Bin Max |
---|---|---|---|---|---|---|
STATUS | 0x80 | 0xFF | 128 | 255 | 10000000 | 11111111 |
DATA | 0 | 0x7F | 0 | 127 | 00000000 | 01111111 |
Status Bytes
We'll use hexadecimal numbers for status bytes because you can tell at a glance what type of MIDI message is being sent on which of the 16 MIDI channels. You can see that 0x93 is a NON message for channel 4 (zero based 3). What about decimal 147 which is the same NON message.
Status | Data Bytes | Function |
---|---|---|
0x8n | 2 | Note Off (NOF) message |
0x9n | 2 | Note On (NON) message |
0xAn | 2 | Key pressure or Aftertouch |
0xBn | 2 | Control message |
0xCn | 1 | Patch change message |
0xDn | 1 | Channel pressure |
0xEn | 2 | Pitch bend message |
0xFn | Varies | System, hardware, notation messages |
Status Byte Notes
- The first hex digit is the message type
- The second hex digit n represents one of the 16 available MIDI channels 0-0xF.
- The 0xFn status messages are referred to as system messages and can contain a variable number of data bytes.
- System messages will not be covered in this class. They're used for hardware identification, timimg synchronization between different MIDI devices, saving and loading hardware setups, and for providing information to music notation software.
MIDI channels in code and user documentation
- Programmers use zero based channels (0-0xF).
- User documentation uses one based channels (1-16).
Data Bytes
We'll use decimal notation for data bytes because they usually specify a range from low to high, like pitch, volume, or right to left for stereo pan.
The naming and interpretation of data bytes 1 and 2 change depending on the status byte.
- 8n and 9n
- For NOF and NON messages data1 is the MIDI note number and data2 is the velocity or loudness of the note.
- Bn
- For control messages data1 is the control type and data2 is the numerical range for that control.
- Cn
- For patch change messages data1 specifies the instrument sound and data2 is not used.
Examples
- 92 60 127
- NON message for channel 3 to turn note number 60 (middle C) on at velocity(volume) level 127 (maximum volume)
- 82 60 100
- NOF message for channel 3 to turn note number 60 (middle C) off. Data 2 is ignored.
- C1 0
- Patch Change message for channel 2 to change to General MIDI instrument 0 (piano)
- B1 7 80
- Control message for channel 2 to set the volume (7) to 80. Often used as a mixer to balance levels of individual instruments.
- 92 61 100
- NON message for channel 3 to turn note number 61 (middle C sharp) on at volume level 100.
- 92 61 0
- NOF message for channel 3 to turn note number 60 (middle C sharp) off using velocity(volume) level 0.
Note Off messages
- There are two equivalent NOF messages
- 0x80 status with any data2 value (but use zero) is always NOF.
- 0x90 status when data2 equals zero is always NOF.
MIDIDisplay Message Format
- All MIDIDisplay messages follow this format
- <timestamp> <MIDI message>
- MIDI message for status 0xCn, 0xDn
- <status> <data1>
- MIDI message for status 0x8n, 0x9n, 0xAn, 0xBn, 0xEn
- <status> <data1> <data2>
- MIDI message for status 0xFn
- <status> <data1> … <dataN>
Variable length and not implemented.
Hex byte to binary conversion
It's easy to convert a hex byte to binary.
A byte has two hex digits.
Each hex digit has a four bit binary representation.
Binary bits 1111 are read as \(2^3\) + \(2^2\) + \(2^1\) + \(2^0\) from left to right.
The leftmost bit is called the MSB (most significant bit)
The rightmost bit is called the LSB (least significant bit).
Binary 1111 is 8+4+2+1 = 0xF hex = 15 decimal.
Binary 0101 is 0+4+0+1 = 0x5 hex = 5 decimal.
Hex 0x80 is the start of MIDI status bytes and is binary 10000000.
Hex 0x7F is the maximum MIDI data byte and is binary 01111111.
The MSB is used to distinguish data bytes from status bytes.
All status bytes have MSB = 1.
All data bytes have MSB = 0.
MIDIDisplay conventions
- A timestamp precedes every MIDI message.
- Numbers are separated by tab characters.
- The status byte is always a hexadecimal number without the 0x prefix.
- All data bytes are decimal numbers.
- Timestamps are in milliseconds time based on a tempo of 60 beats per minute.
- Timestamps must maintain ascending numerical order.
- Multiple messages can occur on the same timestamp.
Example MIDI message stream
PLAY | button | clicked | Clock set to 0. | |
---|---|---|---|---|
Timestamp | Status | Data1 | Data2 | Comments |
Decimal (ms) | Hex | Decimal | Decimal | |
0 | c0 | 11 | not used | Instrument set to vibraphone (patch 11) |
500 | 90 | 60 | 85 | At clock time 500 note 60 is turned on with velocity 85 |
925 | 80 | 60 | 0 | At clock time 925 note 60 is turned off using status 0x80 (data2 ignored) |
1000 | 90 | 72 | 127 | At clock time 1000 note 72 is turned on with velocity 127 |
1900 | 90 | 72 | 0 | At clock time 19000 note 72 is turned off using status 0x90 (data2= 0) |
Interface Files And Implementation Files
It's always recommended to separate your class into an interface (.h), and an implemention (.cpp) file. All public members of the interface will availble to other clients (files) using our class. All private members and the entire implementation file are hidden from clients. The implementation file is a black box that does its magic without the client needing to know how. As long as the interface (.h) file does not change, the programmer is free to change the implementation if faster, better algorithms are discovered.
Angle brackets and Quotes
- Standard C++ include files are enclosed in angle brackets #include <iostream>.
- Include files from third party libraries and files you write are enclosed in double quotes #include "my_header_file.h".
Get in the habit of breaking up your project code into small units of related tasks. Each unit should be separated into an interface or header file (.h) and an implementation or source file (.cpp).
The header file includes all information needed by a client to use that unit. The source file should be considered private and invisible to the client. If you design wisely it should be possible to change the implementation without affecting any changes to the public interface file.
Macros
A macro begins with a # symbol. It's an instruction to the preprocessor, a program that runs before compilation begins. These common macros are used to tell the preprocessor to compile versions of the program in release mode or debug mode, test new features, comment out blocks of code, and declare sections of the code specific to Mac, Windows, or Linux.
- #define
- #undef
- #ifdef
- #ifndef
- #if
- #elif
- #else
- #endif
Header guards
Header guard macros should be included in every .h file you write. Header guards ensure the file is only included once and speed up compile times. Their format is slightly different between the .h and .cpp files.
Header guards in .h files
A common convention is to use the name of the header (.h) file in UPPER_SNAKE_CASE ending with _H_.
You can read #ifndef as "if undefined."
It does what it says: MY_HEADER_FILE_H will be defined only once by the first source file to include it.
It's good practice to include a comment after the final #endif statement to indicate the #if statement it is paired with.
#ifndef MY_HEADER_FILE_H_ #define MY_HEADER_FILE_H_ ... interface variable and function declarations #endif // MY_HEADER_FILE_H_
Header guards in .cpp files
The companion source file should have exactly the same name as the header file but end with .cpp.
#ifndef MY_HEADER_FILE_H_ #include "my_header_file.h" #endif ... implementation of all header functions ... other utility and convenience functions
ASCII
From https://en.wikipedia.org/wiki/ASCII
ASCII ... abbreviated from American Standard Code for Information Interchange, is a character encoding standard for electronic communication. ASCII codes represent text in computers, telecommunications equipment, and other devices. Most modern character-encoding schemes are based on ASCII, although they support many additional characters.
From http://www.columbia.edu/kermit/ascii.html
Codes 0 through 31 and 127 (decimal) are unprintable control characters. Code 32 (decimal) is a nonprinting spacing character. Codes 33 through 126 (decimal) are printable graphic characters.
clang-format on/off directive
These macro like comments can be used to turn clang-format on and off for sections of code.
Before
#include <iostream> #include <iostream> void write_message(unsigned int timestamp, unsigned char status, unsigned char data1, unsigned char data2) { const char kTAB = 't'; unsigned short st = status; unsigned short d1 = data1; unsigned short d2 = data2; // clang-format off std::cout << timestamp << kTAB << st << kTAB << d1 << kTAB << d2 << std::endl; // clang-format on std::cout << timestamp << kTAB << st << kTAB << d1 << kTAB << d2 << std::endl; }
After Save
#include <iostream> void write_message(unsigned int timestamp, unsigned char status, unsigned char data1, unsigned char data2) { const char kTAB = 't'; unsigned short st = status; unsigned short d1 = data1; unsigned short d2 = data2; // clang-format off std::cout << timestamp << kTAB << st << kTAB << d1 << kTAB << d2 << std::endl; // clang-format on std::cout << timestamp << kTAB << st << kTAB << d1 << kTAB << d2 << std::endl; }
Forbidden C types
These C language types are no longer allowed in CS 312 homework.
- The C array type
- Use std::vector instead.
- The C printf() function
- Use std::cout instead.
- C style casts
- Use any of the methods described this web page to display char types as numbers.
Use static_cast<new_type>(value) for all others.
1.3 Homework
Reading
The MIDI section on Week1.2 web page.
The entire Reading section on this page.
TCCP2
- Basics
- review §1.3-1.10
- Struct
- §2.1-2.2
- Modular code
- §3.1-3.2
- Namespaces
- §3.4
- Casts
- §4.2.3, p.53
- Functions
- §3.6-3.6.2
- Template
- §6.2 up to 6.2.1, §7.3.2
- Standard Library Headers and Namespaces
- §8.3-8.4
- Strings
- §9.1-9.2 up to 9.2.1
- IO
- §10.1-10.3, §10.6
- Vectors
- §11.1-11.2.2
- Iterators
- §12.1-12.2
Setup
Execute in Terminal
cd $HOME312/cs312/hw13 touch hw131_MidiPacket_sizeof.cpp touch hw131_MidiPacket.h touch hw132_MidiPacket_cout.cpp touch hw133_chromaticScale_vector.cpp code .
hw131_MidiPacket
This assignment begins the development of the CMidiPacket class that encapsulates MIDI messages. This CMidiPacket class will handle all MIDI messages with status bytes except system (0xFn) messages.
MidiPacket Data Structure
A prototype MidiPacket data structure might look like this.
struct MidiPacket { SOME_TYPE timestamp; SOME_TYPE status; SOME_TYPE data1; SOME_TYPE data2; SOME_TYPE length; };
Some considerations for choosing the optimal SOME_TYPE for each data member involve:
- How much resolution do we need for the timestamp: milliseconds, microseconds, uint32_t, uint64_t?
- How does the order of data members in a struct affect the memory footprint size
- Performance speed
- Ease of programming
- Ease of use with System and third party MIDI libraries
- Apple defines MIDI data as an an 8 bit Byte. A byte/Byte type does not exist in C++
- The C++ 8 bit type is char that outputs ASCII text instead of numbers. So type casts will be needed.
- int16_t and uint16_t output numbers but have a larger memory footprint.
We'll investigate further in upcoming classes.
hw131_MidiPacket.h contains eight different candidates for a MidiPacket structure. The assignment is to write a template function that will display the size in bytes and bits for each of the eight MidiPacket structs using the sizeof() function.
Then copy/paste this content into the two files.
hw131_MidiPacket.h Do not modify.
// boilerplate here /*** D O N O T M O D I F Y ***/ #ifndef HW131_MIDIPACKET_H_ #define HW131_MIDIPACKET_H_ #include <cstdint> // for uintN_t types struct MidiPacket1 { // size in bytes 4+1+1+1 = 7 uint32_t timestamp; uint8_t status; uint8_t data1; uint8_t data2; }; struct MidiPacket2 { // size in bytes 4+1+1+1+1 = 8 uint32_t timestamp; uint8_t status; uint8_t data1; uint8_t data2; uint8_t length; }; struct MidiPacket3 { // size in bytes 1+4+1+1 = 7 uint8_t status; uint32_t timestamp; uint8_t data1; uint8_t data2; }; struct MidiPacket4 { // size in bytes 1+1+4+1+1= 8 uint8_t status; uint32_t timestamp; uint8_t data1; uint8_t data2; uint8_t length; }; struct MidiPacket5 { // size in bytes 4+2+2+2 = 10 uint32_t timestamp; uint16_t status; uint16_t data1; uint16_t data2; }; struct MidiPacket6 { // size in bytes 4+2+2+2+2 = 12 uint32_t timestamp; uint16_t status; uint16_t data1; uint16_t data2; uint16_t length; }; struct MidiPacket7 { // size in bytes 8+1+1+1+1 = 12 uint64_t timestamp; uint8_t status; uint8_t data1; uint8_t data2; uint8_t length; }; struct MidiPacket8 { // size in bytes 8+2+2+2+2 = 16 uint64_t timestamp; uint16_t status; uint16_t data1; uint16_t data2; uint16_t length; }; struct MidiPacket9 { // size in bytes 8+4+4+4+4 = 24 uint64_t timestamp; int status; int data1; int data2; int length; }; #endif // HW132_MIDIPACKET_H_
hw131_MidiPacket_sizeof.cpp
// boilerplate here #ifndef HW131_MIDIPACKET_H_ #include "hw131_MidiPacket.h" #endif #include <iostream> #include <string> template <typename T> void print_size(const T &x, const std::string &msg) { // use the sizeof() function to find the size of any type T // Mine was two lines of code, one for sizeof(), and one for std::cout } int main() { std::cout << "Write a template function that prints the size of any type T\n"; std::cout << "print_size(MidiPacket1);" << std::endl; std::cout << "print_size(MidiPacket2);" << std::endl; std::cout << "..." << std::endl; std::cout << "print_size(MidiPacket8);" << std::endl; }
Compile and run this command
cl hw131_MidiPacket_sizeof.cpp && ./a.out
You'll see this output
Write a template function that prints the size of any type T print_size(MidiPacket1); print_size(MidiPacket2); ... print_size(MidiPacket8);
You want to see this output
MidiPacket1 bytes: 8 bits: 64 MidiPacket2 bytes: 8 bits: 64 MidiPacket3 bytes: 12 bits: 96 MidiPacket4 bytes: 12 bits: 96 MidiPacket5 bytes: 12 bits: 96 MidiPacket6 bytes: 12 bits: 96 MidiPacket7 bytes: 16 bits: 128 MidiPacket8 bytes: 16 bits: 128
Questions:
- Why do MidiPacket1 and MidiPacket2 report the same size with sizeof()?
- Why do MidiPacket3 through MidiPacket6 all report the same size with sizeof()?
- Why do MidiPacket7 and MidiPacket8 report the same size with sizeof()?
- How do you explain the discrepancy between the number of data members in each of the eight MidiPacket structs and their actual size in memory?
Append your answers in a block comment at the end hw131_MidiPacket_sizof.cpp.
hw132_MidiPacket_cout.cpp
Write a program that outputs MIDIDisplay text output for MidiPacket2 and MidiPacket6: Three or four numbers separated by tabs. Length is a struct member, but length is never displayed in the output. Length has to be set by the programmer as soon as the status byte is known. Once Length is known the programmer can use it to determine whether data2 is used. Fill in the header section and turn in as MidiPacket_cout.cpp.
Copy/paste this code into hw132_MidiPacket_cout.cpp
// boilerplate here #ifndef HW131_MIDIPACKET_H_ #include "hw131_MidiPacket.h" #endif #include <iostream> MidiPacket2 create_midipacket2(uint32_t ts, uint8_t st, uint8_t d1) { // you write // remember to return a MidiPacket2 } // function overload MidiPacket2 create_midipacket2(uint32_t ts, uint8_t st, uint8_t d1, uint8_t d2) { // you write } void print_midipacket2(const MidiPacket2 &mp) { // you write } MidiPacket6 create_midipacket6(uint32_t ts, uint16_t st, uint16_t d1) { // you write } // function overload MidiPacket6 create_midipacket6(uint32_t ts, uint16_t st, uint16_t d1, uint16_t d2) { // you write } void print_midipacket6(const MidiPacket6 &mp) { // you write } int main() { // DO NOT CHANGE MidiPacket2 mp2 = create_midipacket2(0, 0xc0, 11); print_midipacket2(mp2); mp2 = create_midipacket2(0, 0x90, 60, 100); print_midipacket2(mp2); mp2 = create_midipacket2(1000, 0x80, 60, 0); print_midipacket2(mp2); MidiPacket6 mp6 = create_midipacket6(0, 0xc0, 11); print_midipacket6(mp6); mp6 = create_midipacket6(0, 0x90, 60, 100); print_midipacket6(mp6); mp6 = create_midipacket6(1000, 0x80, 60, 0); print_midipacket6(mp6); }
First compile output produces warnings
The output shows three things that should help fix the problem.
- Line numbers where the warning occurred.
- A non-void function is a function that returns something.
- [-Wreturn-type] One of our .bash startup files added the -Wall (Warnings all) compile flag to clang++. The message is telling us to supply a return type when the function exits.
Your final correct output should look like this
MidiPacket2 - length 2 0 c0 11 MidiPacket2 - length 3 0 90 60 100 MidiPacket2 - length 3 1000 80 60 0 MidiPacket6 - length 2 0 c0 11 MidiPacket6 - length 3 0 90 60 100 MidiPacket6 - length 3 1000 80 60 0
Question
Based on homework 1.3.1 and 1.3.2 which of the eight MidiPacket struct's would you use in your CMidiPacket class?
Defend your choice based on a tradeoff between storage space, ease of output, and compatibility with Apple defining MIDI data as a Byte.
Append your answer in a block comment at the end of your hw132_MidiPacket_cout.cpp source code.
hw133_chromaticScale_vector.cpp
When you play every white key and black key on a piano in sequence, you're playing the chromatic scale. In MIDI terms, the notes of a chromatic scale increment or decrement by 1. The piano has 88 keys (black and white). MIDI allows notes (data1) from 0-127. The 88 keys on the piano are mapped to MIDI notes 21-108. Middle C is mapped to MIDI note 60.
The first five notes of the ascending chromatic scale starting on MIDI note 60 are:
60 61 62 63 64 ...
Given a starting note write a program to print MIDI messages for the notes of a two octave ascending chromatic scale in MIDIDisplay format.
Guidelines
- Use the MidiPacket you chose as your answer to Question 1.3.2.
- The scale range is 25 total notes counting beginning and end. For example if the scale started on note 60 it would end on 84, if it started on 48 it would end on 72.
- NON timestamps are uniformly spaced 250 ms apart.
- NON durations are uniformly 200 ms.
- Use status 0x90 for NON messages with data2=100;
- Use status 0x80 for NOF with data2=0;
- The NOF message is sent when the NON duration has passed.
- Do not output the 0x hex prefix
- The timestamps must occur in chronological order.
Here's the the first three notes of the chromatic starting on note number 60.
Timestamp | Status | data1 | data2 |
---|---|---|---|
0 | 90 | 60 | 100 |
200 | 80 | 60 | 0 |
250 | 90 | 61 | 100 |
450 | 80 | 61 | 0 |
500 | 90 | 62 | 100 |
700 | 80 | 62 | 0 |
etc. |
Use this code as your starting point.
hw133_chromaticScale_vector.cpp
// boilerplate here #ifndef HW131_MIDIPACKET_H_ #include "hw131_MidiPacket.h" // reuse from hw132 #endif #include <iostream> #include <string> #include <vector> std::vector<MidiPacketX> vec; void stuff_chromatic_scale_vector(int start_note, int end_note) { const int kNON_DELTA_TIME = 250; // time between NON's const int kNON_DURATION = 200; // *** pseudo code *** // create a variable tsNON to hold the value of the current NON timestamp // set tsNON = 0 // create a FOR LOOP indexed from 0 to the total number of notes minus one // create the NON message (tsNON status note velocity) // stuff into vec (std::vector<MidiPacketX> above) // calculate timestamp for the matching NOF MIDI message // stuff into vec (timestamp status note velocity) // increment tsNON // next iteration of for loop } void print_chromatic_scale() { // for every element in vec // print the MIDIDisplay message with numbers separated by a tab character std::cout << "You can do better." << std::endl; for (int ix = 1; ix < 5; ++ix) { std::cout << "note " << ix << std::endl; } } int main() { // DO NOT CHANGE stuff_chromatic_scale_vector(48, 72); print_chromatic_scale(); // play in MIDIDisplay to test // debug as needed }
Write, Compile, Run
You'll get some errors if you immediately compile the above code.
std::vector<MidiPacketX> vec; ^ hw133_chromaticScale_vector.cpp:16:13: warning: unused variable 'kNON_DURATION' [-Wunused-variable] const int kNON_DURATION = 200; ^ hw133_chromaticScale_vector.cpp:15:13: warning: unused variable 'kNON_DELTA_TIME' [-Wunused-variable] const int kNON_DELTA_TIME = 250; // time between NON's ^ 2 warnings and 1 error generated.
The error is because you need to choose a MIDIPacket from 1 to 8.
The two warnings will go away when you implement your code.
When you you're finished test it in MIDIDisplay_DLS. It should sound like this at a Tempo of 60.
Submission Format
Feel free to email jellinge@carleton.edu on any part of the homework that is unclear to you. Chances are if you have questions other students do too. I will answer those questions by email to everyone in the class. The original sender will remain anonymous. I have tried to provide clues to the homework in the web pages, reading assignments, and labs. Sometimes what seems obvious to me is not obvious to students just learning C++.
Create a folder named hwNN_LastnameFirstname1_LastnameFirstname2.
Substitute your name and your partner's name for LastnameFirstname1_LastnameFirstname2.
Remember
- Boilerplate header at top of every file.
- Make sure your program compiles before submitting.
- Programs that compile and produce partially correct output will be graded.
- You can send notes to me in your homework files by enclosing them in block comments.
- Programs that do not compile get an automatic F (59%).
- Each partner must submit identical homework folders to the course Hand-in folder.
- If only one partner submits the homework send me an email explaining why.
- Empty your build or build-debug folders using the command emptyBuildFolder.sh
- Only include .h, .cpp, Makefiles, CMakeLists.txt
- Do not include these folders/files
.git
.vscode Double check for the above two folders using this command
ls -a hwNN_LastnameFirstname1_LastnameFirstname2 # if either .git or .vscode exist in the folder you're submitting remove them with # rm -fR .git # rm -fR .vscode
Contents: hw13_LastnameFirstname_LastnameFirstname