CS 312 - Week 1.3
CS 312 Audio Programming Winter 2020

Table of Contents

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.

c13_tree.png

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

http://www.cplusplus.com/reference/climits/

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:

  1. Why do MidiPacket1 and MidiPacket2 report the same size with sizeof()?
  2. Why do MidiPacket3 through MidiPacket6 all report the same size with sizeof()?
  3. Why do MidiPacket7 and MidiPacket8 report the same size with sizeof()?
  4. 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.

c13203.png

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.

c13301.png

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.

chromatic scale (mp3)

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

c13303.png

Author: John Ellinger

Created: 2020-01-10 Fri 16:20

Validate