CS 312 - Week 2.3 Class
CS 312 Audio Programming Winter 2020

Table of Contents

Class 2.3 Setup

  • 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>

Download
class23.zip

Execute in Mac Terminal

cd $HOME312/cs312
mkdir hw23
unzip ~/Downloads/class23.zip -d $HOME312/cs312/hw23/
cd $HOME312/cs312/hw23
rm -fR  __MACOSX
code .

You should have these folders and files:

$HOME312/cs312/hw23
├── class23_chrono
│   ├── c23_MidiPacketTimer
│   │   ├── C23_CTimer.h
│   │   ├── C23_timer_test.cpp
│   │   └── hw131_MidiPacket.h
│   ├── c23_chronoFeatures.cpp
│   ├── c23_playtimeLimits
│   │   ├── c23_playtimeLimits.cpp
│   │   └── c23_playtimeLimits.txt
│   └── c23_simpleDelay.cpp
├── class23_random
│   ├── rand1.cpp
│   ├── rand2.cpp
│   ├── rand3.cpp
│   ├── rand4.cpp
│   ├── rand5.cpp
│   ├── rand6.cpp
│   ├── rand7.cpp
│   ├── rand8.cpp
│   └── rand9.cpp
├── class23_vectors
│   ├── v01.cpp
│   ├── v02.cpp
│   ├── v03.cpp
│   ├── v04.cpp
│   ├── v05.cpp
│   ├── v06.cpp
│   ├── v07.cpp
│   ├── v08.cpp
│   ├── v09.cpp
│   ├── v10.cpp
│   ├── v11.cpp
│   ├── v12.cpp
│   ├── v13.cpp
│   ├── v14.cpp
│   ├── v15.cpp
│   ├── v16.cpp
│   ├── v17.cpp
│   ├── v18.cpp
│   ├── v19.cpp
│   ├── v20.cpp
│   ├── v21.cpp
│   ├── v22.cpp
│   ├── v23.cpp
│   ├── v24.cpp
│   ├── v25.cpp
│   ├── v26.cpp
│   ├── v27.cpp
│   ├── v28.cpp
│   └── v29.cpp
├── hw231_testCMP22
│   ├── CMakeLists.txt
│   ├── build
│   └── hw231_testCMP22.cpp
├── hw232_gmdrums
│   ├── hw232_drumNotes.txt
│   ├── hw232_gmdrums.cpp
│   └── hw232_main.cpp
├── hw233_playdrums
│   ├── CMakeLists.txt
│   ├── build
│   ├── hw233_main.cpp
│   ├── hw233_playdrums.cpp
│   └── hw233_playdrums.h
└── hw234_scifiSounds
    ├── CMakeLists.txt
    ├── build
    ├── hw234_main.cpp
    ├── hw234_scifiSounds.cpp
    └── hw234_scifiSounds.h

Example code

We'll look functions available in the <vector> library. It's arguably the most important library for next five weeks.

There are a lot of files to compile in this lab but they've all been written for you. The goal of this lab is to acquaint you with useful functions in <vector> library that you can use in future assignments. You do not have to memorize every function and it's parameters but you should try to remember the general features. You can always refresh your memory on the web.

General instructions for each folder/file

Open the file and read/study the code.
Compile and run the file using cl <filename> && ./a.out
Compare the output with the code until you understand it.
Go to the next example.

<vector>

A vector is a dynamically sized array that guarantees all elments will be placed in contiguous memory.

A few years ago programmers were taught to use arrays when you needed random access to elements and you had few insertions or deletions in the middle. If you had lots of insertions and deletions in the middle you were told to use a linked list. Today's on chip CPU cache lines along with vector's contiguous memory alignment make vector the fastest general use STL container. There is a short Bjarne Stroustrup video that explains this in the homework assignment.

You've already used the push_back() function to add elements to a vector and you've seen some examples using a for loop to access elements of a vector. There are 29 examples of using <vector> along with occasional uses of <cmath>, <algorithm>, <numeric>, <random>, and <chrono>. Many of these examples will be useful in current and future assignments.

initialize vector with 5 values

// v01.cpp
#include <iostream>
#include <vector>

// initialize vector with 5 values
int main()
{
    std::vector<int> v = {0, 2, 4, 6, 8};
    for ( int ix = 0; ix < v.size(); ++ix )
        std::cout << v[ ix ] << " ";
    std::cout << std::endl;
}

size and capacity

The size of a vector is the actual number of elements in the vector.
Capacity is total number of elements the vector could hold before more memory
needs to be allocated.

// v02.cpp
#include <iostream>
#include <vector>

// size and capacity
// The size of a vector is the actual number of elements in the vector.
// Capacity is total number of elements the vector could hold before more memory
// needs to be allocated.
int main()
{
    std::vector<int> v = {0, 2, 4, 6, 8};
    std::cout << "v.size(): " << v.size() << std::endl;
    std::cout << "v.capacity(): " << v.capacity() << std::endl;
}

add one element with push_back

// v03.cpp
#include <iostream>
#include <vector>

// add one element with push_back

int main()
{
    std::cout << "vector starts with 5 elements" << std::endl;
    std::vector<int> v = {0, 2, 4, 6, 8};
    for (const auto itr : v)
        std::cout << itr << "  ";
    std::cout << "\nv.size() = " << v.size() << std::endl;
    std::cout << "v.capacity() = " << v.capacity() << std::endl;
    std::cout << "add one more element " << std::endl;
    v.push_back(10);
    for (const auto itr : v)
        std::cout << itr << "  ";
    std::cout << "\nv.size() = " << v.size() << std::endl;
    std::cout << "v.capacity() = " << v.capacity() << std::endl;
    std::cout << std::endl;
}

add ten elements using push_back inside a for loop

// v04.cpp
#include <iostream>
#include <vector>

// add ten elements using push_back inside a for loop
int main()
{
    std::vector<int> v;
    for (int i = 0; i < 10; ++i)
        v.push_back(i);
    std::cout << "v.size() = " << v.size() << std::endl;
    std::cout << "v.capacity() = " << v.capacity() << std::endl;
    for (const auto itr : v)
        std::cout << itr << "  ";
    std::cout << std::endl;
}

reserve

If you know approximately how many elements you need, you could pre-allocate the memory using reserve. If you don't reserve enough your next allocation may double the size reserved. If you reserve too much you're wasting memory.

// v05.cpp
#include <iostream>
#include <vector>

// reserve
int main()
{
    std::cout << "vector with 10 elements " << std::endl;
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::cout << "Before reserve, v.size() = " << v.size()
              << ", v.capacity() = " << v.capacity() << std::endl;
    v.reserve(100);
    std::cout << "After reserve, v.size() = " << v.size()
              << ", v.capacity() = " << v.capacity() << std::endl;
}

empty

The empty() function returns true if the vector holds at least one element and false if the vector holds zero elements. It does not remove elements from the vector.

// v06.cpp
#include <iostream>
#include <vector>

// empty
// The empty() function returns true if the vector holds at least one element.
// It returns false if the vector holds zero elements.
// It does not remove elements from the vector.
int main()
{
    std::cout << "vector with 10 elements " << std::endl;
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::cout << std::boolalpha << "v.empty  = " << v.empty() << std::endl;
}

Output

v.empty  = false

clear

The clear() function is used to remove all elements from a vector.

// v07.cpp
#include <iostream>
#include <vector>

// clear
//    The clear() function is used to remove all elements from a vector.
int main()
{
    std::cout << "vector with 10 elements " << std::endl;
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (const auto itr : v)
        std::cout << itr << "  ";
    std::cout << std::endl;

    std::cout << "v.size() = " << v.size() << std::endl;
    std::cout << "v.capacity() = " << v.capacity() << std::endl;
    v.clear();
    std::cout << std::boolalpha << "after v.clear(), v.empty() = " << v.empty() << std::endl;
    std::cout << "v.size() = " << v.size() << std::endl;
    std::cout << "v.capacity() = " << v.capacity() << std::endl;
}

Accessing individual elements of a vector

Two methods are available to access a single vector element.

  • brackets [n]
  • the at(n) member function.

If you use brackets the compiler will not warn you about out of range access and will happily return garbage.
The at() function is range checked and will throw a compile time error giving you a chance to correct the problem.

accessing a single element using [ ]

// v08.cpp
#include <iostream>
#include <vector>

// Accessing individual elements of a vector
// There are two methods available to access a single vector element.
//     brackets
//     the at( ) member function.
// The difference is that at() is range checked and will throw an error
// if you try to access a position that is outside of the vector's size.
// accessing a single element using [ ]
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::cout << "v[4] = " << v[4] << std::endl;
}

accessing an out of range element using [ ]

No range checking performed

// v09.cpp
#include <iostream>
#include <vector>

// accessing an out of range element using [ ]

//no range checking performed//
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::cout << "v[11] = " << v[ 11 ] << std::endl;
}

accessing a single element using .at()

// v10.cpp
#include <iostream>
#include <vector>

// accessing a single element using .at()
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::cout << "v.at(4) = " << v.at( 4 ) << std::endl;
}

accessing an out of range element using .at()

Out of range errors caught at compile time

// v11.cpp
#include <iostream>
#include <vector>

// accessing an out of range element using .at()
//range errors caught at compile time//
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // The error is caught by the compiler, not by the user.
    std::cout << "v.at(11) = " << v.at( 10 ) << std::endl;
}

accessing an out of range element using .at() in a try catch block

Exception is caught, and the program keeps running.
If unnoticed or not fixed it will eventually lead to hard to track down bugs.

Test 1
return is commented - program continues.
Test 2
return is not commented - program ends.
Shortcut for comments - Command-/.
// v12.cpp
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int x = 0;
    try
    {
        std::cout << "v.size() = " << v.size() << std::endl;
        x = v.at(25);
    }
    catch (const std::exception &e)
    {
        std::cout << "caught error: " << e.what() << std::endl;
        // return 0;
    }
    std::cout << "We got here\n";
}

accessing every element of the vector in sequence

The "range for" loop is the C++11 preferred method when you need to access every member of the vector in order. Within the for loop the itr variable takes on the element value at that position. The C++17 keyword auto tries to figure out the varible type for you.

// v13.cpp
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    for ( auto itr : v )
        std::cout << itr << " ";
    std::cout << std::endl;
}

The name itr is often the used because it iterates over the vector elements. It could have been called x and it would work the same way.

// v14.cpp
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    for ( auto x : v )
        std::cout << x << " ";
    std::cout << std::endl;
}

print a subset of elements using a "range-for" for loop

// v15.cpp
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // Method 1
    int count = 0;
    for (auto i : v)
    {
        if ((count > 4) && (count < 8))
            std::cout << i << " ";
        ++count;
    }
    std::cout << std::endl;

    // Method 2
    for (auto i : {5, 6, 7})
    {
        std::cout << i << " ";
    }
    std::cout << std::endl;
}

print elements using an iterator for loop

v.begin() is a pointer to the first element in the vector
v.end() is a pointer to one past the last element of the vector
When the terminating condition itr != v.end() is reached the last element has already been processed

// v16.cpp
#include <iostream>
#include <vector>

// print elements using an iterator for loop
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for ( auto itr = v.begin(); itr != v.end(); ++itr )
        std::cout << *itr << " ";
    std::cout << std::endl;
}

print elements using an iterator for loop (alternative syntax)

An iterator alternative to v.begin() is begin(v)

// v17.cpp
#include <iostream>
#include <vector>

// print elements using an alternate begin() end() syntax
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for ( auto itr = begin( v ); itr != end( v ); ++itr )
        std::cout << *itr << " ";
    std::cout << std::endl;
}

print the even numbered elements using an initializer list and a standard for loop

% is the mod or modulus operator. x % y is the remainder of x divided by y.

// v18.cpp
#include <iostream>
#include <vector>

// print even numbered indices
int main()
{
    // method 1
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for ( auto n : {0, 2, 4, 6, 8} )
        std::cout << v[ n ] << " ";
    std::cout << std::endl;

    // method 2
    int k{0};
    for ( int i = 0; i < v.size(); ++i )
    {
        if ( i % 2 == 0 )
            std::cout << v[ i ] << " ";
    }
    std::cout << std::endl;
}

print the odd numbered elements using an initializer list and a range for loop

// v19.cpp
#include <iostream>
#include <vector>

// print the odd numbered indices
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (auto n : {1, 3, 5, 7, 9})
        std::cout << v[n] << " ";
    std::cout << std::endl;

    // method 2
    int count{0};
    for (auto itr : v)
    {
        if (count % 2 == 1)
            std::cout << itr << " ";
        ++count;
    }
    std::cout << std::endl;
}

print element cubed using a range for loop

// v20.cpp
#include <cmath>
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // method 1
    // print element cubed using a range for loop
    for (auto x : v)
        std::cout << x * x * x << " ";
    std::cout << std::endl;

    // method 2
    // using <cmath> pow()
    for (auto x : v)
        std::cout << std::pow(x, 3) << " ";
    std::cout << std::endl;
}

print element squared using for_each

  • First with a function object
  • Second with a lambda function
// v21.cpp
#include <algorithm> // for_each()
#include <iostream>
#include <vector>

// a C++ function object
struct Squared
{
    int sqr;
    // constructor
    Squared() : sqr{0} {}
    // overload function operator ()
    void operator()(int n)
    {
        sqr = n * n;
        std::cout << sqr << " ";
    }
};

// print element cubed using for_each and a lambda function
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // method 1
    // print element squared using a function object
    std::for_each(v.begin(), v.end(), Squared());
    std::cout << std::endl;

    // method 2
    // print element squared using for_each and a lambda function
    std::for_each(v.begin(), v.end(),
                  [](int x) { std::cout << x * x << " "; });
    std::cout << std::endl;
}

sum the elements in a vector using std::accumulate

// v22.cpp
#include <iostream>
#include <numeric> // accumulate()
#include <vector>

// print the sum of a vector of ints using std::accumulate
int main()
{
    std::vector<int> v;
    for (int i = 1; i <= 100; ++i)
        v.push_back(i);
    int sum = std::accumulate(v.begin(), v.end(), 0);
    std::cout << "sum of 1..100 = " << sum << std::endl;
}

print elements backwards using a reverse iterator in the for loop

begin() and end() are forward iterators
rbegin() and rend() are reverse iterators.

// v23.cpp
#include <iostream>
#include <vector>

int main()
{
    // print vector backwards
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (auto itr = v.rbegin(); itr != v.rend(); ++itr)
        std::cout << *itr << " ";
    std::cout << std::endl;
}

erase a single element

// v24.cpp
#include <iostream>
#include <vector>
// erase a single element
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    auto itr_begin = v.begin();
    v.erase( itr_begin + 5 );
    for ( const auto itr : v )
        std::cout << itr << " ";
    std::cout << "(erased 5)" << std::endl;
}

erase a range of elements

// v25.cpp
#include <iostream>
#include <vector>

// erase a range of elements
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for ( const auto itr : v )
        std::cout << itr << " ";
    std::cout << std::endl;
    v.erase( v.begin() + 2, v.begin() + 5 );
    for ( const auto itr : v )
        std::cout << itr << " ";
    std::cout << "\nafter erase [2:5) v.size() = " << v.size() << std::endl;
}

resize

// v26.cpp
#include <iostream>
#include <vector>

// erase resize
int main()
{
    // from v25.cpp
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for ( const auto itr : v )
        std::cout << itr << " ";
    v.erase( v.begin() + 2, v.begin() + 5 );
    std::cout << "\nafter erase [2:5)" << std::endl;
    for ( const auto itr : v )
        std::cout << itr << " ";
    std::cout << "\nv.size() = " << v.size() << std::endl;

    v.resize( 10 );
    std::cout << "after resize" << std::endl;
    for ( const auto itr : v )
        std::cout << itr << " ";
    std::cout << "\nv.size() = " << v.size() << std::endl;
}

insert

// v27.cpp
#include <iostream>
#include <vector>

// insert
int main()
{
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (const auto itr : v)
        std::cout << itr << " ";
    std::cout << std::endl;
    std::cout << "v.size = " << v.size() << std::endl;
    for (int count = 5; count < 10; ++count)
    {
        v.insert(v.begin() + count, 0x90);
    }

    int count{0};
    for (const auto itr : v)
    {
        if ((count >= 5) && (count < 10))
            std::cout << std::hex << std::showbase << itr << " ";
        else
            std::cout << std::dec << itr << " ";
        ++count;
    }
    std::cout << "\nv.size after insert =  " << v.size() << std::endl;
    std::cout << std::endl;
}

duplicate elements of a vector

// v28.cpp
#include <iostream>
#include <vector>

// duplicate elements of a vector
int main()
{
    const std::vector<int> v = {0, 1, 2, 3, 4};
    std::vector<int> outvec;
    for (auto ix = 0; ix < 7; ++ix)
    {
        std::copy(v.begin(), v.end(), back_inserter(outvec));
    }
    for (auto itr : outvec)
        std::cout << itr << " ";
    std::cout << std::endl;
}

Sort

// v29.cpp
#include <iostream>
#include <vector>

int main()
{

    const std::vector<int> v = {1, 2, 3, 4, 5};
    std::vector<int> outvec;
    for ( auto ix = 0; ix < 7; ++ix )
        std::copy( v.begin(), v.end(), back_inserter( outvec ) );

    for ( auto itr : outvec )
        std::cout << itr << " ";
    std::cout << std::endl;

    // Sort
    std::sort( outvec.begin(), outvec.end() );
    for ( auto itr : outvec )
        std::cout << itr << " ";
    std::cout << std::endl;
}

<random>

https://stackoverflow.com/questions/38367976/do-stdrandom-device-and-stdmt19937-follow-an-uniform-distribution

The different classes from the C++ random number library roughly work as follows:

 * std::random_device is a uniformly-distributed random number generator that may access a
hardware device in your system, or something like /dev/random on Linux. It is usually just
used to seed a pseudo-random generator, since the underlying device wil usually run out of
entropy quickly.

 * std::mt19937 is a fast pseudo-random number generator using the Mersenne
Twister engine which, according to the original authors' paper title, is also uniform.
This generates fully random 32-bit or 64-bit unsigned integers. Since std::random_device
is only used to seed this generator, it does not have to be uniform itself (e.g., you
often seed the generator using a current time stamp, which is definitely not uniformly
distributed).

 * Typically, you use a generator such as std::mt19937 to feed a particular
distribution, e.g. a std::uniform_int_distribution or std::normal_distribution which then
take the desired distribution shape.

 * std::shuffle, according to the documentation,
  Reorders the elements in the given range [first, last) such that each possible
  permutation of those elements has equal probability of appearance.

You'll use some of these examples in hw234_scifiSounds.

default_random_engine

This code shows two example functions
One that returns the same random sequence every time it is called.
One that returns a different sequence every time it is called.
// rand1.cpp

#include <iostream>
#include <random>

void print10randomNumbers_1()
{
  // All sequences will be the same every time
  std::default_random_engine dre{}; // no std::random_device
  for (int ix = 0; ix < 10; ++ix)
  {
    std::cout << dre() << " ";
  }
  std::cout << std::endl;
}

void print10randomNumbers_2()
{
  // Different sequences every time
  std::random_device rd;
  std::default_random_engine dre{rd()}; // uses std::random_device
  for (int ix = 0; ix < 10; ++ix)
  {
    std::cout << dre() << " ";
  }
  std::cout << std::endl;
}

int main()
{
  const std::string kDivider(72, '-');
  std::cout << kDivider << '\n';
  print10randomNumbers_1();
  print10randomNumbers_2();
  std::cout << kDivider << '\n';
}

Build

Run ./a.out several times in a row and observe the results.

std::mt19937 Mersenne Twister generator

// rand2.cpp
#include <iostream>
#include <random>

// Mersenne Twister generator
// std:: mt19937
int main()
{
  std::random_device rd;
  std::mt19937 mt_gen{rd()};
  for (int ix = 0; ix < 10; ++ix)
  {
    std::cout << mt_gen() << " ";
  }
  std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

More Info on Mersenne prime numbers

https://en.wikipedia.org/wiki/Mersenne_Twister

The Mersenne Twister is a pseudorandom number generator (PRNG). It is by far the most
widely used general-purpose PRNG.[1] Its name derives from the fact that its period
length is chosen to be a Mersenne prime.

The Mersenne Twister was developed in 1997 by Makoto Matsumoto...and Takuji
Nishimura ... It was designed specifically to rectify most of the flaws found
in older PRNGs.

The most commonly used version of the Mersenne Twister algorithm is based on the Mersenne
prime 219937−1. The standard implementation of that, MT19937, uses a 32-bit word length.
There is another implementation (with five variants[3]) that uses a 64-bit word length,
MT19937-64; it generates a different sequence.

Adoption in software systems
The Mersenne Twister is the default PRNG for the following software systems: Microsoft
Excel,[4] GAUSS,[5] GLib,[6] GNU Multiple Precision Arithmetic Library,[7] GNU Octave,[8]
GNU Scientific Library,[9] gretl,[10] IDL,[11] Julia,[12] CMU Common Lisp,[13] Embeddable
Common Lisp,[14] Steel Bank Common Lisp,[15] Maple,[16] MATLAB,[17] Free Pascal,[18]
PHP,[19] Python,[20][21] R,[22] Ruby,[23] SageMath,[24] Scilab,[25] Stata.[26]

It is also available in Apache Commons,[27] in standard C++ (since C++11),[28][29] and in
Mathematica.[30] Add-on implementations are provided in many program libraries, including
the Boost C++ Libraries,[31] the CUDA Library,[32] and the NAG Numerical Library.[33]

The Mersenne Twister is one of two PRNGs in SPSS: the other generator is kept only for
compatibility with older programs, and the Mersenne Twister is stated to be "more
reliable".[34] The Mersenne Twister is similarly one of the PRNGs in SAS: the other
generators are older and deprecated.[35]

https://blogs.mathworks.com/cleve/2015/04/17/random-number-generator-mersenne-twister/

Mersenne primes Mersenne primes are primes of the form 2^p - 1 where p itself is prime.
They are named after a French friar who studied them in the early 17th century. We learn
from Wikipedia that the largest known prime number is the Mersenne prime with p equal to
57,885,161. The Mersenne Twister has p equal to 19937. This is tiny as far as Mersenne
primes go, but huge as far as random number generators are concerned.

I calculated 2^19937 - 1 on the following web site. It had 2001 digits.

https://defuse.ca/big-number-calculator.htm

uniform_int_distribution

This code produces a sequence of random integers in a given range. Because the specified range was no larger than char it was necessary to cast results to display numbers.

// rand3.cpp
#include <iostream>
#include <random>
#include <cstdint>

int main()
{
  std::random_device rd;
  std::default_random_engine dre{rd()};
  std::uniform_int_distribution<uint8_t> dist{48, 72};
  for (int ix = 0; ix < 20; ++ix)
  {
    std::cout << dist(dre) << " ";
  }
  std::cout << std::endl;

  for (int ix = 0; ix < 20; ++ix)
  {
    std::cout << +dist(dre) << " "; // convert int8_t to int16_t for printing
  }
  std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

uniform_int_distribution 1

This code stuffs a sequence of random integers within a given range into a vector.
After that it sorts the vector and normalizes the values to start from zero.

// rand4.cpp
#include <iostream>
#include <random>
#include <cstdint>
#include <vector>
#include <algorithm>

int main()
{
  std::random_device rd;
  std::default_random_engine dre{rd()};
  std::uniform_int_distribution<uint32_t> dist{0, 1000};

  std::vector<uint32_t> v;
  for (int ix = 0; ix < 20; ++ix)
  {
    v.push_back(dist(dre));
  }

  // sorting is optional
  sort(v.begin(), v.end());

  // offset starting time to zero is optional
  for (auto itr : v)
  {
    std::cout << itr - v[0] << " ";
  }
  std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

uniform_int_distribution 2

This code generates random indices into a vector.
After that it accesses the vector using those indices and adds an offset.

// rand5.cpp
#include <iostream>
#include <random>
#include <cstdint>
#include <vector>
#include <algorithm>

int main()
{
  std::vector vscale{0, 2, 4, 7, 9, 12};
  std::vector<uint32_t> vnotes;
  const unsigned int v_size = vscale.size() - 1;

  std::random_device rd;
  std::default_random_engine dre{rd()};
  std::uniform_int_distribution<uint32_t> dist{0, v_size};

  for (int ix = 0; ix < 20; ++ix)
  {
    int n = dist(dre);
    int note = vscale.at(n) + 60;
    vnotes.push_back(note);
    std::cout << vnotes.at(ix) << " ";
  }
  std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

uniform_int_distribution 3

This code is taken from TCPP2 §14.5 to simulate the roll of a dice.

There are two methods
One directly from the book.
The other with minor modifications I made to get it to work correctly.
// rand6.cpp
#include <iostream>
#include <random>

// from TCPP2 section 14.5
int rollDice()
{
    using my_engine = std::default_random_engine;
    using my_distribution = std::uniform_int_distribution<>;

    my_engine re{};
    my_distribution one_to_six{1, 6};
    // note &, error in TCPP2
    auto die = [&]() { return one_to_six(re); };
    int x = die();
    return x;
}

int rollDice2()
{
    using my_device = std::random_device;
    using my_engine = std::default_random_engine;
    using my_distribution = std::uniform_int_distribution<>;

    // necessary for different random seed
    my_device rd;
    my_engine re{rd()};
    my_distribution one_to_six{1, 6};
    // note &, error in TCPP2
    auto die = [&]() { return one_to_six(re); };
    int x = die();
    return x;
}

int main()
{
    for (int ix = 0; ix < 10; ++ix)
    {
        std::cout << rollDice() << " ";
    }
    std::cout << std::endl;

    for (int ix = 0; ix < 10; ++ix)
    {
        std::cout << rollDice2() << " ";
    }
    std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

Shuffle 1

This code makes 7 copies of a small vector using a for loop, std::copy(), and std::back_inserter() and then shuffles the vector.

no seed
this code produces the same series every time it is run.
// rand7.cpp
#include <iostream>
#include <random>
#include <vector>

// with no seed, this code produces the same series every time it is run
int main()
{
    const std::vector<int> v = {1, 2, 3, 4, 5};
    std::vector<int> vout;
    // make 7 copies of v.
    for (auto ix = 0; ix < 7; ++ix)
        std::copy(v.begin(), v.end(), back_inserter(vout));
    std::cout << "before shuffle\n";
    for (auto itr : vout)
        std::cout << itr << " ";
    std::cout << std::endl;

    std::shuffle(vout.begin(), vout.end(), std::default_random_engine(0));
    std::cout << "after shuffle\n";
    for (auto itr : vout)
        std::cout << itr << " ";
    std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

Shuffle 2

with seed
Same code as rand7.cpp adding a random seed function.
This code produces a different series every time it is run.
// rand8.cpp
#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//     - with seed ::
//                    this code produces a different series every time
int main()
{

    const std::vector<int> v = {1, 2, 3, 4, 5};
    std::vector<int> outvec;
    for (auto ix = 0; ix < 7; ++ix)
        std::copy(v.begin(), v.end(), back_inserter(outvec));

    std::cout << "before shuffle\n";
    for (auto itr : outvec)
        std::cout << itr << " ";
    std::cout << std::endl;

    // obtain a time-based seed
    unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
    std::shuffle(outvec.begin(), outvec.end(), std::default_random_engine(seed));

    std::cout << "after shuffle\n";
    for (auto itr : outvec)
        std::cout << itr << " ";
    std::cout << std::endl;
}

Random seed function in rand8.cpp

// obtain a time-based seed
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
std::shuffle( outvec.begin(), outvec.end(), std::default_random_engine( seed ) );

Build

Run ./a.out several times in a row and observe the results.

Shuffle 3

using std::random_device and std::default_random_engine

Similar to rand8.cpp but differs in how the seed is generated.
Implicit random seed function in rand9.cpp

// use std::random_device for seed
std::random_device rd;
std::default_random_engine dre( rd() );
std::shuffle( outvec.begin(), outvec.end(), dre );
// rand9.cpp
#include <iostream>
#include <random>
#include <vector>

//     - using std::random_device and std::default_random_engine ::
//          this code produces a different series every time
int main()
{

    const std::vector<int> v = {1, 2, 3, 4, 5};
    std::vector<int> outvec;
    for (auto ix = 0; ix < 7; ++ix)
        std::copy(v.begin(), v.end(), back_inserter(outvec));

    std::cout << "before shuffle\n";
    for (auto itr : outvec)
        std::cout << itr << " ";
    std::cout << std::endl;

    // use std::random_device for seed
    std::random_device rd;
    std::default_random_engine dre(rd());
    std::shuffle(outvec.begin(), outvec.end(), dre);

    std::cout << "after shuffle\n";
    for (auto itr : outvec)
        std::cout << itr << " ";
    std::cout << std::endl;
}

Build

Run ./a.out several times in a row and observe the results.

<chrono>

The <chrono> library contains routines for manipulating dates and time. It includes three timers that you can use to measure time in the millisecond to nanosecond range. All functions in the <chrono> library are declared in the std::chrono namespace.

C++ timers on Mac OS X

Here's a short program that lists the features of the three timers: system_clock, high_resolution_clock, and steady_clock. These clocks are available on all C++11 and later compilers although their capabilities may differ according to CPU hardware. Modern Windows and Linux systems should be similar to the Mac.

c23_chronoFeatures.cpp

//  c23_chronoFeatures.cpp
#include <iostream>
#include <chrono>

// From: CPL4 Section 35.2
void print_chrono_features()
{
  // namespace alias
  // sc means std::chrono
  namespace sc = std::chrono;
  std::cout << std::endl
            << "STD::CHRONO CLOCK FEATURES" << std::endl;
  std::cout << "===> system_clock" << std::endl;
  std::cout << "period numerator = " << sc::system_clock::period::num << std::endl;
  std::cout << "period denominator = " << sc::system_clock::period::den << std::endl;
  std::cout << "measures in microseconds" << std::endl;
  std::cout << "steady = " << std::boolalpha << sc::system_clock::is_steady << std::endl;

  std::cout << "\n===> high_resolution_clock" << std::endl;
  std::cout << "period numerator = " << sc::high_resolution_clock::period::num << std::endl;
  std::cout << "period denominator = " << sc::high_resolution_clock::period::den << std::endl;
  std::cout << "measures in nanoseconds" << std::endl;
  std::cout << "steady = " << std::boolalpha << sc::high_resolution_clock::is_steady << std::endl;

  std::cout << "\n===> steady_clock" << std::endl;
  std::cout << "period numerator = " << sc::steady_clock::period::num << std::endl;
  std::cout << "period denominator = " << sc::steady_clock::period::den << std::endl;
  std::cout << "measures in nanoseconds" << std::endl;
  std::cout << "steady = " << std::boolalpha << sc::steady_clock::is_steady << std::endl;
}

int main()
{
  print_chrono_features();
}

Output
The system_clock counts in microseconds. The high_resolution_clock and steady_clock count in nanoseconds.

STD::CHRONO CLOCK FEATURES
===> system_clock
period numerator = 1
period denominator = 1000000
measures in microseconds
steady = false

===> high_resolution_clock
period numerator = 1
period denominator = 1000000000
measures in nanoseconds
steady = true

===> steady_clock
period numerator = 1
period denominator = 1000000000
measures in nanoseconds
steady = true

c23_simple_delay.cpp

Here's an example program that uses the code from TCPP2 §15.6.

// c23_simpleDelay.cpp
#include <iostream>
#include <mutex>
#include <chrono>
#include <thread>

// From: A Tour of C++ 2nd Edition Section 15.6, page 200.
using namespace std::chrono;

void simple_delay(int delay)
{
  auto t0 = high_resolution_clock::now();
  std::this_thread::sleep_for(milliseconds(delay));
  auto t1 = high_resolution_clock::now();

  // std::cout << duration_cast<nanoseconds>(t1 - t0).count() << " nanoseconds passed\n";
  std::cout << duration_cast<microseconds>(t1 - t0).count() << " microseconds passed\n";
  // std::cout << duration_cast<milliseconds>(t1 - t0).count() << " milliseconds passed\n";
}

int main()
{
  std::cout << "==== 1000 ms" << std::endl;
  for (int ix = 0; ix < 10; ++ix)
    simple_delay(1000); // 1 second

  std::cout << "==== 500 ms" << std::endl;
  for (int ix = 0; ix < 10; ++ix)
    simple_delay(500); // 500 ms

  std::cout << "==== 100 ms" << std::endl;
  for (int ix = 0; ix < 10; ++ix)
    simple_delay(100); // 100 ms

  std::cout << "==== 1 ms" << std::endl;
  for (int ix = 0; ix < 10; ++ix)
    simple_delay(1); // 1 ms
}

Build and run

Try different time display settings. One line active, two lines commented.

// std::cout << duration_cast<nanoseconds>(t1-t0).count() << " nanoseconds passed\n";
// std::cout << duration_cast<microseconds>(t1 - t0).count() << " microseconds passed\n";
std::cout << duration_cast<milliseconds>(t1-t0).count() << " milliseconds passed\n";

My output
From a 2012 Mac Pro, 3.33GHz 6 Core Xeon was generally accurate to 4 ms.

CMidiPacket considerations

There are tradeoffs to be made in terms of size, ease of programming, and runtime speed.

Choosing a timestamp

This code displays the available playtime in milliseconds based on the maximum value of these unsigned types: char (uint8_t), short (uint16_t), int (uint32_t), and long (uint64_t)..

c23_playtimeLimits.cpp

// c23_playtimeLimits.cpp
#include <iostream>
#include <limits>
#include <iomanip>

// Order dependent consts
const float millis_per_second = 1000.0;
const float millis_per_minute = millis_per_second * 60;
const float millis_per_hour = millis_per_minute * 60;
const float millis_per_day = millis_per_hour * 24;
const float millis_per_year = millis_per_day * 365;

template <typename T>
void print_time(T x)
{
  std::cout  << std::fixed << std::setprecision(2)
            << std::setw(12) << std::setfill(' ') << "size:" << std::setw(24)
            << std::setfill('.') << sizeof(T) << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "max:" << std::setw(24)
            << std::setfill('.') << std::numeric_limits<T>::max() << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "seconds:" << std::setw(24)
            << std::setfill('.') << x / millis_per_second << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "minutes:" << std::setw(24)
            << std::setfill('.') << x / millis_per_minute << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "hours:" << std::setw(24)
            << std::setfill('.') << x / millis_per_hour << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "years:" << std::setw(24)
            << std::setfill('.') << x / millis_per_year << std::endl;
  std::cout << std::endl;
}

int main()
{
// unsigned char does not display correctly using print_time()
  unsigned char x = 255;
  std::cout  << std::fixed << std::setprecision(2)
            << std::setw(12) << std::setfill(' ') << "size:" << std::setw(24)
            << std::setfill('.') << sizeof(x) << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "max:" << std::setw(24)
            << std::setfill('.') << +x << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "seconds:" << std::setw(24)
            << std::setfill('.') << x / millis_per_second << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "minutes:" << std::setw(24)
            << std::setfill('.') << x / millis_per_minute << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "hours:" << std::setw(24)
            << std::setfill('.') << x / millis_per_hour << std::endl;
  std::cout << std::setw(12) << std::setfill(' ') << "years:" << std::setw(24)
            << std::setfill('.') << x / millis_per_year << std::endl;
  std::cout << std::endl;

//   std::cout << "unsigned char max is 255 (displayed as ASCII)" << std::endl;
//   print_time(std::numeric_limits<unsigned short>::max());

  std::cout << "unsigned short\t" << std::endl;
  print_time(std::numeric_limits<unsigned short>::max());

  std::cout << "unsigned int\t" << std::endl;
  print_time(std::numeric_limits<unsigned int>::max());

  std::cout << "unsigned long\t" << std::endl;
  print_time(std::numeric_limits<unsigned long>::max());
}

Output

       size:.......................1
        max:.....................255
    seconds:....................0.25
    minutes:....................0.00
      hours:....................0.00
      years:....................0.00

unsigned short
       size:.......................2
        max:...................65535
    seconds:...................65.54
    minutes:....................1.09
      hours:....................0.02
      years:....................0.00

unsigned int
       size:.......................4
        max:..............4294967295
    seconds:..............4294967.50
    minutes:................71582.79
      hours:.................1193.05
      years:....................0.14

unsigned long
       size:.......................8
        max:....18446744073709551615
    seconds:....18446744949882880.00
    minutes:......307445737979904.00
      hours:........5124095737856.00
      years:............584942400.00

I ruled out unsigned char and unsigned short because they did not have enough playtime.
I ruled out unsigned long because it had excessive playtime.
I chose unsigned int.

timestamp
unsigned int for time in milliseconds.
pros
Unsigned because time is never negative.
MIDI transmit rate is 31250 buad (bits/second) that's about 3 bits/ms. A single MIDI byte is actually 10 bits (startBit-Byte-stopBit) so it takes about 30 ms per MIDI byte or approximately 100 ms for a status-data1-data2 message or about 10 of these per second which is a musical tempo of 600 beats/minute.
cons
Audio rate uses timestamps in microseconds.
Apple and most third party MIDI/Audio libraries report time in microseconds.

A 2.5 GHz processor provides 2500 processing cycles per microsecond. Most of the time the CPU is idle no matter how fast the music sounds playing either MIDI or digital audio.

Choosing MIDI Byte type

status, data1, data2, length

We'll use unsigned char (uint8_t) for data bytes.

pros
These four types will never be be negative or exceed 0xFF (255).
They use memory efficiently.
Apple and most 3rd party MIDI libraries use 8 bit types for MIDI data. The unaray plus operator makes it easy to display an unsigned char as a number.
Just prefix the it with a + sign.
cons
Unsigned chars were designed to display text as ascii characters.
They require a type cast to display as a number.
uint8_t c = 100;
std::cout << c << std::endl;                        // displays 'x'
std::cout << +c << std::endl;                       // displays 100

Memory footprint

Let's calculate the amount of memory needed for a large MIDIDisplay file.

Facts
There are 16 performers each playing 4 notes a second for the entire song.
Song duration is 5 minutes.
(16 * 4) * (5 * 60) = 19200 notes
19200 NON messages and 19200 NOF messages
MidiPacket2 (8 bytes)
Each NON (note on) is one 8 byte MIDIPacket (uint32_t, uint8_t, uint8_t, uint8_t, uint8_t).
Each NOF (note off) is one 8 byte MIDIPacket (uint32_t, uint8_t, uint8_t, uint8_t, uint8_t).
Each NON/NOF pair takes 16 bytes.
19200 notes * 16 bytes/note = 307200 bytes = 300k (207200/1024)
MidiPacket6 (12 bytes)
Each NON (note on) is one 12 byte MIDIPacket (uint32_t, uint16_t, uint16_t, uint16_t, uint16_t).
Each NOF (note off) is one 12 byte MIDIPacket (uint32_t, uint16_t, uint16_t, uint16_t, uint16_t).
If we use MidiPacket6 each NON/NOF pair takes 24 bytes.
19200 notes * 24 bytes/note = 460800 bytes = 450k (460800/1024)
Compare that with 5 minutes of stereo audio
CD audio rate is 44100 samples per second.
Stereo is 88200 samples per second.
One sample is 16 or 24 bits. We'll use 16 bits = 2 bytes.
88200 samples/second * (60 * 5) seconds * 2 bytes/sample = 52,920,000 bytes = 50.468 Mb (52920000/(1024*1024))
A rule of thumb for audio is about 5 Mb per minute of stereo audio at CD sampling rate.

Performance

I created this class to test how long it takes to stuff one million MidiPacket structs from hw131_MidiPacket.h.

C23_CTimer.h

// C23_Timer.h

#ifndef C23_CTIMER_H_
#define C23_CTIMER_H_

#include <iostream>
#include <chrono>

namespace CS312
{
class CTimer
{
private:
  bool print_;
  // start time t0
  std::chrono::time_point<std::chrono::high_resolution_clock> t0;
  // stop time t1;
  std::chrono::time_point<std::chrono::high_resolution_clock> t1;

public:
  CTimer(bool print) : print_{print}
  {
    t0 = std::chrono::high_resolution_clock::now();
  }

  virtual ~CTimer()
  {
    t1 = std::chrono::high_resolution_clock::now();
    if (print_)
      milli_time();
  }

  void reset()
  {
    t0 = std::chrono::high_resolution_clock::now();
  }

  void milli_time()
  {
    t1 = std::chrono::high_resolution_clock::now();
    std::cout << "#Time ms:\t";
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count() << '\n';
  }
};
} // namespace CS312

#endif // c23_CTIMER_H_

C23_timer_test.cpp

// C23_timer_test.cpp

#include <iostream>
#include <vector>

#ifndef C23_CTIMER_H_
#include "C23_CTimer.h"
#endif

#ifndef HW131_MIDIPACKET_H_
#include "hw131_MidiPacket.h"
#endif

using namespace CS312; // CTimer

using MidiPacket = MidiPacket2;

void do_reset_print()
{
  // add 1,000,000 MidiPackets to a vector
  std::cout << "No reserve()\n";
  CTimer tm(false);
  std::vector<MidiPacket> v1;
  MidiPacket mp{1000, 0x90, 60, 100};
  for (int ix = 0; ix < 1'000'000; ++ix)
    v1.push_back(mp);
  tm.milli_time();

  // add 1,000,000 MidiPackets to a vector
  // call reserve(oneMillion*sizeof(MidiPacket))
  std::cout << "tm.reset() -> v1.clear() -> reserve()\n";
  tm.reset();
  v1.clear();
  v1.reserve(1'000'000 * sizeof(MidiPacket));
  for (int ix = 0; ix < 1'000'000; ++ix)
    v1.push_back(mp);
  tm.milli_time();
  tm.reset();

  // add 1,000,000 MidiPackets to a vector
  // call reserve(oneMillion*sizeof(MidiPacket))
  std::cout << "v1.clear() -> tm.reset() -> reserve()\n";
  v1.clear();
  tm.reset();
  v1.reserve(1'000'000 * sizeof(MidiPacket));
  for (int ix = 0; ix < 1'000'000; ++ix)
    v1.push_back(mp);
  tm.milli_time();
  tm.reset();

  // add 1,000,000 MidiPackets to a vector
  // call reserve(oneMillion*sizeof(MidiPacket))

  std::cout << "v1.clear() -> reserve() -> tm.reset()\n";
  v1.clear();
  v1.reserve(1'000'000 * sizeof(MidiPacket));
  tm.reset();
  for (int ix = 0; ix < 1'000'000; ++ix)
    v1.push_back(mp);
  tm.milli_time();
  tm.reset();
}

void do_scope_print()
{
  // add 1,000,000 MidiPackets to a vector
  std::cout << "No reserve\n";
  {
    CTimer tm(true);
    std::vector<MidiPacket> v1;

    MidiPacket mp{1000, 0x90, 60, 100};
    for (int ix = 0; ix < 1'000'000; ++ix)
      v1.push_back(mp);
  } //  out of scope, prints when destructor called

  // add 1,000,000 MidiPackets to a vector
  // call reserve(oneMillion*sizeof(MidiPacket))
  std::cout << "CTimer tm -> reserve()\n";
  {
    CTimer tm(true);
    std::vector<MidiPacket> v1;
    v1.reserve(1'000'000 * sizeof(MidiPacket));
    MidiPacket mp{1000, 0x90, 60, 100};
    for (int ix = 0; ix < 1'000'000; ++ix)
      v1.push_back(mp);
  } //  out of scope, prints when destructor called

  // declare before timer created
  // add 1,000,000 MidiPackets to a vector
  // call reserve(oneMillion*sizeof(MidiPacket))
  std::cout << "reserve() -> Create tm\n";
  {
    std::vector<MidiPacket> v1;
    v1.reserve(1'000'000 * sizeof(MidiPacket));
    CTimer tm(true);
    MidiPacket mp{1000, 0x90, 60, 100};
    for (int ix = 0; ix < 1'000'000; ++ix)
      v1.push_back(mp);
  } //  out of scope, prints when destructor called
}

int main()
{
  std::cout << "Time in ms\n";
  std::cout << "==== do_scope_print() ====\n";
  do_scope_print();
  std::cout << "==== do_reset_print() ====\n";
  do_reset_print();
}

The starting time_point is set in the constructor and in the reset() function. The Timer is destroyed in the destructor. Time can be printed by calling milli_time(). If the print_ variable is true the time will be printed when the timer goes out of scope. If the print_ variable is false you must call milli_time() to print. Here's an example of its use.

Build and run

Change this line to test a different MidiPacket.
using MidiPacket = MidiPacket2 to using MidiPacket = MidiPacket6.

My Results
I saw very little difference between all nine packets Using vector.reserve() improved the times.

Compatibility

I felt compatibility with OS and 3rd party MIDI frameworks was a significant factor in deciding the MidiPacket stuct to use.

Apple Core MIDI
/System/Library/Frameworks/CoreMIDI.framework/Versions/A/Headers/MIDIServices.h

Apple Core MIDI
Mac OS defines MidiPacket types as Byte and defines Byte as unsigned char.

typedef unsigned char           UInt8;
typedef UInt8                   Byte;

Mac OS also defines a MidiPacket type and a MidiPacketList type.

struct MIDIPacket
{
  MIDITimeStamp timeStamp; // UInt64 type
  UInt16    length;
  Byte    data[256];
};
typedef struct MIDIPacket MIDIPacket;

struct MIDIPacketList
{
  UInt32    numPackets;
  MIDIPacket    packet[1];
};

RtMidi (also for Windows and Linux)
We'll be using RtMidi in a few weeks.
https://github.com/thestk/rtmidi/blob/master/RtMidi.cpp

Mac OS

unsigned char status; // UInt8
unsigned short nBytes, iByte, size; // UInt16
unsigned long long time; // UInt64

# On Mac it's the same as Apples MIDIPacket
const MIDIPacket *packet = &list->packet[0];

PortMidi (also for Windows and Linux)
https://sourceforge.net/p/portmedia/wiki/portmidi/

PortMidi stuffs status-data1-data2 into a 24 bit long.

 #define Pm_Message(status, data1, data2) \
00455          ((((data2) << 16) & 0xFF0000) | \
00456           (((data1) << 8) & 0xFF00) | \
00457           ((status) & 0xFF))
00458 #define Pm_MessageStatus(msg) ((msg) & 0xFF)
00459 #define Pm_MessageData1(msg) (((msg) >> 8) & 0xFF)
00460 #define Pm_MessageData2(msg) (((msg) >> 16) & 0xFF)

My choice
I chose MidiPacket2.

Reading/Study/Reference

TCCP2

Some of these have been listed before. It won't hurt to read them again.

general
§1.7-1.9.2
§2.1-2.3
§3.2-3.4
§4.1-4.2.3
Chapter 5 all
Chapter 8 all
§11.6, 11.7[1-10]
§12.6
§14.1, 14.2, 14.3, 14.8[1-3, 5-7]
vector
Note: The Vector class developed in is a lite version of <vector>
§3.5.1
§11.1-11.2.2
array
§13.4.1
sort
§12.1
string
§9.1-9.2.1
chrono
§13.7
§15.6
templates
§6.1, 6.2.2,
function objects
§6.3.1, 6.3.2, 6.3.3
iterators
§9.4.3, 9.5[1-10]
§12.2
exceptions and errors
§3.5-3.5.3
sstream
§10.8
fstream
§10.7
random
§14.5
File IO for hw 2.3.2
http://www.cplusplus.com/doc/tutorial/files/
http://www.learncpp.com/cpp-tutorial/136-basic-file-io/
Random
https://en.cppreference.com/w/cpp/numeric/random
http://www.dreamincode.net/forums/topic/379290-generating-random-numbers-the-c-way/ https://stackoverflow.com/questions/38367976/do-stdrandom-device-and-stdmt19937-follow-an-uniform-distribution

Homework 2.3

The appropriate folders and files were downloaded at the beginning of this web page.
Homework assignments hw231_CMidiPacket/, hw233_playDrums and hw234_scifiSounds all use the CMidiPacket class.

Setup homework 2.3

Add files to the common folder
Copy your hw222_CMidiPacket.h and hw222_CMidiPacket.cpp files into the common folder.

cd $HOME312/cs312/common
cp $HOME312/cs312/hw22/hw222_CMidiPacket/hw222_CMidiPacket.h ./
cp $HOME312/cs312/hw22/hw222_CMidiPacket/hw222_CMidiPacket.cpp ./

hw231_CMidiPacket

In this assignment we'll begin using the hw222_CMidiPacket class to create musical examples that can be played in MIDIDisplay. Because hw222_CMidiPacket will be used in several future assignments we're going copy it to the $HOME312/cs312/common folder.

The common folder necessitates changes to the build process. The easiest method for dealing with this is to adopt CMake as our build system.

We will be making changes and adding improvements to hw222_CMidiPacket in the future. When we make changes/additions to the code we'll change the namespace directive.

This assignment takes you through the process of

  • copying files into the $HOME312/cs312/common folder.
  • modifying the vsCode search path to search the common folder for #include files
  • Modifying the CMakeLists.txt file to search the common folder for #include
  • compiling a small test program to verify everything works.

Setup

Open the hw231_testCMP22 folder in vsCode.

Assignment 2.3.1

hw231_testCMP22.cpp
Copy/paste

// hw231_testCMP22.cpp
#include "hw222_CMidiPacket.h"

using namespace CMP22;

int main(int argc, char const *argv[])
{
  CMidiPacket mp(1000, 0x90, 60, 100);
  mp.print();
}

Notes:

using namespace CMP22 directive
By using namespace CMP22; directive you won't have to prefix every function with CMP22:: as shown below.
CMP22::CMidiPacket mp(1000, 0x90, 60, 100);

Problems right away
Open the Terminal panel in vsCode (shortcut: Ctrl-backtick).
Select the PROBLEMS tab.

vsc23100.png

Cannot open hw222_CMidiPacket.h
Problems/Errors in vsCode are underlined with "squiggles".

vsc23101.png

Click the squiggle and a light bulb icon should appear.
Click the light bulb.

vsc23102.png

A popup menu will appear.
Select Edit "includePath" setting.

vsc23103.png

In the C/C++ Configurations window that appears scroll down to the Include path section.
Enter $HOME312/cs312/common under the workspaceFolder line.
Click outside of the text entry box and an error box will appear.
vsCode does not recognize our shell variable $HOME312.

vsc23104.png

You'll need to enter the full path name.
It's probably something like this:
/Volumes/cs312-00-w20/StuWork/<your_email_name>/cs312/common

If your course folder is mounted on the desktop navigate to your $HOME312/cs312 folder and drag the common folder icon into a terminal window (Mac or vsCode) and the full pathname will appear there, ready to copy/paste.

Paste that full pathname into the Include path section.
Click outside of the text entry box and the error box should go away.
If it doesn't ask for help.

Close the C/C++ Configurations window.

The PROBLEMS tab should show no problems.

CMakeLists.txt
Copy/paste this code into CMakeLists.txt.

cmake_minimum_required(VERSION 2.8)
set(APPNAME "testCMP22")
project(${APPNAME})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall ")
set(SOURCE_FILES
  hw231_testCMP22.cpp)
add_executable(${APPNAME} ${SOURCE_FILES})

cmake

cd build
cmake ..

vsc23106.png

make

make

Houston, we've got a problem!
hw222_CMidiPacket.h not found

vsc23107.png

It's not surprising there's an error. CMake doesn't know about the common folder.

CMakeLists.txt
The set(VARIABLE name) in CMake lets you to refer to name with this syntax: ${VARIABLE}

Defining these variables should help us write a more general purpose CMakeLists.txt file.

HOME
set(HOME "/Volumes/cs312-00-w20/StuWork/<your_email_name>")
This should be portable between WCC and LAB as long as your courses folder is mounted.
COMMON
set(COMMON "${HOME}/cs312/common")
pathname to common folder

Replace your current CMakeLists.txt with this new content.
The CMakeLists.txt file below can serve as a template for future assignments.

Edit HOME as needed.

cmake_minimum_required(VERSION 2.8)
set(APPNAME "testCMP22")
project(${APPNAME})

# edit next line
set(HOME "/Volumes/cs312-00-w20/StuWork/<your_email_name>")
set(COMMON "${HOME}/cs312/common")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall ")
set(SOURCE_FILES
    ${COMMON}/hw222_CMidiPacket.cpp
     hw231_testCMP22.cpp
   )

include_directories(${COMMON})
add_executable(${APPNAME} ${SOURCE_FILES})

Build steps

# cd build
emptyBuildFolder.sh
cmake ..
make
./testCMP22

Notes

emptyBuildFolder.sh
We created this script in class22
cd build
cmake should be run from the build folder
cmake ..
.. the two dots tell cmake that CMakeLists.txt is in the parent folder
(no term)
make cmake created the Makefile inside the build folder
(no term)
./testCMP22 Execute the program ${APPNAME} that was created in the build folder

You may receive warning during the make process.
It depends on how much of hw221_CMidiPacket.cpp you've completed.
If you receive Errors you'll have to fix them.

A successful make should report this.

vsc23108.png

A successful run should report this.

vsc23109.png

hw232_gmdrums

File IO
This assignment introduces you to opening, reading, and displaying a text file in C++. You'll create a command line tool named gmdrums that will display the MIDI note numbers for each of the General MIDI drum sounds.

As defined in General MIDI (GM) the drum channel is channel 10. All drum sounds are mapped to MIDI note numbers 35-81 on the drum channel. User documentation says the drum channel is channel 10. MIDI programmers know that MIDI channels are zero based and the drum channel in code is really channel 9.

Setup Open hw232_gmdrums folder in vsCode.

Assignment 2.3.2

Step 1
Open, read, and display the hw232_drumNotes.txt file.

hw232_gmdrums.cpp
Copy/paste

// boilerplate here

#include <iostream>
// add more includes as needed

void print_header()
{
  std::cout << "Match Header line by line from web page.\n";
  std::cout << std::endl;
}

int main()
{
  // Use either full pathname or relative pathname to the working directory
  const std::string file_name = "xxx";

  // Open the drumNotes.txt file

  // Report error if file could not be opened
  if (!file.is_open())
  {
    std::cout << file_name << " could not be opened" << std::endl;
    return 0;
  }

  print_header();

  // use std::getline to read file line by line
  // display each line using std::cout

  return 0;
}

Build and run
You'll get errors.

hw232_main.cpp:21:8: error: use of undeclared identifier 'file'; did you mean 'false'?
  if (!file.is_open())
       ^~~~
       false
hw232_main.cpp:21:12: error: member reference base type 'bool' is not a structure or union
  if (!file.is_open())
       ~~~~^~~~~~~~
2 errors generated.

Fix the errors until you can open and read the hw232_drumNotes.txt file and produce this output. Output

======================================================
General MIDI drum kit note assignments on channel 10
   Use status 0x99 for NON
   Use status 0x99 and velocity of 0 for NOF
======================================================

Bass Drum 2
Bass Drum 1
Side Stick
Snare Drum 1
Hand Clap
Snare Drum 2
Low Tom 2
Closed Hi-hat
Low Tom 1
Pedal Hi-hat
Mid Tom 2
Open Hi-hat
Mid Tom 1
High Tom 2
Crash Cymbal 1
High Tom 1
Ride Cymbal 1
Chinese Cymbal
Ride Bell
Tambourine
Splash Cymbal
Cowbell
Crash Cymbal 2
Vibra Slap
Ride Cymbal 2
High Bongo
Low Bongo
Mute High Conga
Open High Conga
Low Conga
High Timbale
Low Timbale
High Agogo
Low Agogo
Cabasa
Maracas
Short Whistle
Long Whistle
Short Guiro
Long Guiro
Claves
High Wood Block
Low Wood Block
Mute Cuica
Open Cuica
Mute Triangle
Open Triangle

Step 2
Prefix every drum sound name with its MIDI note number.

Build and run

Output

======================================================
General MIDI drum kit note assignments on channel 10
   Use status 0x99 for NON
   Use status 0x99 and velocity of 0 for NOF
======================================================

35 Bass Drum 2
36 Bass Drum 1
...
80 Mute Triangle
81 Open Triangle

Step 3
Move hw232_drumNotes.txt to a different folder or rename it and run the program again.
It won't work.

cd $HOME312/cs312/hw23/hw232_gmdrums
# rename it
mv hw232_drumNotes.txt hw232_drumNotesOFF.txt
#or move it
# mv hw232_drumNotes.txt ../
./a.out

Step 4
Fix the program so it does not require the presence of drumNotes.txt.

The idea is to modify Step 2 output and format it as a const std::vector<std::string> similar to that shown below. You can do this in two ways:

  • Copy the output into a new file and add the variable declaration, double quotes, and commas by hand.
  • Modify the the code to output the format shown below directly.

kDRUM_NAMES_VEC

const std::vector<std::string> kDRUM_NAMES_VEC = {
"35 Bass Drum 2",
"36 Bass Drum 1",
...
"80 Mute Triangle",
"81 Open Triangle"
};

Paste the kDRUM_NAMES_VEC vector directly into your code.
Remove the File IO routines and output kDRUM_NAMES_VEC using an auto for loop.

This is the main() function I used.

int main() {
  print_header();
  for (auto itr : kVEC_DRUM_NAMES)
    std::cout << itr << std::endl;
}

Compile and run

cl hw232_gmdrums.cpp
./a.out

Step 5

Convert a.out into a command line tool
Rename a.out to gmdrums and move it to your $HOME312/bin directory

mv a.out $HOME312/bin/gmdrums

gmdrums usage
Use the shell pipe symbol | with grep -i to search for specific sounds.
The -i flag means case insensitive.
Try these examples.

gmdrums | grep -i snare
# 38 Snare Drum 1
# 40 Snare Drum 2

gmdrums | grep -i tom
# 41 Low Tom 2
# 43 Low Tom 1
# 45 Mid Tom 2
# 47 Mid Tom 1
# 48 High Tom 2
# 50 High Tom 1

gmdrums | grep -i cymbal
# 49 Crash Cymbal 1
# 51 Ride Cymbal 1
# 52 Chinese Cymbal
# 55 Splash Cymbal
# 57 Crash Cymbal 2
# 59 Ride Cymbal 2

hw233_playDrums

Setup

Open hw233_playdrums folder in vsCode.

hw233_playdrums.h
Copy/paste this code.

// Add boilerplate here

// DO NOT MODIFY BELOW

#ifndef HW233_PLAYDRUMS_H_
#define HW233_PLAYDRUMS_H_

#ifndef HW222_CMIDIPACKET_H_
#include "hw222_CMidiPacket.h"
#endif

void create_gmdrums_vector(int start_note, int end_note);
void print_drums();

#endif // HW233_PLAYDRUMS_H_

Fix the vsCode error squiggle for missing #includes for hw222_CMidiPacket.h the same way you did in hw231_testCMP22.

hw233_main.cpp
Copy/paste this code.

// Add boilerplate here

#ifndef HW233_PLAYDRUMS_H_
#include "hw233_playdrums.h"
#endif

int main()
{
  const int kFIRST_DRUM = 35;
  const int kLAST_DRUM = 81;
  create_gmdrums_vector(kFIRST_DRUM, kLAST_DRUM);
  print_drums();
}

hw233_playdrums.cpp

// boilerplate here

#ifndef HW233_PLAYDRUMS_H_
#include "hw233_playdrums.h"
#endif

#include <iostream>
#include <vector>

using namespace CMP22;

std::vector<CMidiPacket> drum_vec;

void create_gmdrums_vector(int start_note, int end_note)
{
#ifdef USE_THESE_CONSTS
  const int kNON_DELTA_TIME = 500; // time between NON's
  const int kNON_DURATION = 100;   // time NOF appears after NON
#endif
  std::cout << "create_gmdrums_vector(...) needs to be implemented.\n";
}

void print_drums()
{
  std::cout << "print_drums() needs to be implemented.\n";
}

CMakeLists.txt
Copy the CMakeLists.txt file from hw231_testCMP22.
Edit as needed to produce ${APPNAME} = playDrums.

Assignment 2.3.3

  • Create a CMidiPacket message for every drum sound in sequence.
  • Use MIDI note numbers 35-81 on the drum channel.
  • Every NON (Note On) message must be matched with a NOF (Note Off) message.
  • Store the NON/NOF messages in a std::vector<CMidiPacket>.
  • Use an auto for loop and the CMidiPacket::print() function to output the std::vector<CMidiPacket>.
  • Build, make, and run using cmake.
  • Copy/paste the output into MIDIDisplay_DLS and play.

Rules

  • Use (#include) your hw221_CMidiPacket.h and hw221_CMidiPacket.cpp files in the cs312/common folder.
  • NON messages start every 500 ms.
  • NON duration is 100 ms; i.e. NOF messages happen 100 ms after their corresponding NON
  • NOF messages must use status 0x8n with data2 = 0;

Build and run using cmake

Partial Output
data2 in NON and NOF messages is called velocity.
Do not reproduce my comments.

# NON note 35 velocity 100
0 99  35  100
# matching NOF note 35 velocity 0
100 89  35  0
500 99  36  100
600 89  36  0
1000  99  37  100
1100  89  37  0
...
22500 99  80  100
22600 89  80  0
23000 99  81  100
23100 89  81  0

When you've got it working execute this command.

./playdrums | pbcopy

Notes:
pbcopy is a built-in Mac command that copies to the "pasteboard" pb.

Go directly to MIDIDisplay_DLS and paste
Play in MIDIDisplay_DLS

hw234_scifiSounds

Setup
Open hw234_sfiSounds folder in vsCode.

Assignment 2.3.4

Write a program to generate 100 random NON/NOF MIDI message pairs that can be played in MIDIDisplay_DLS.

Randomize two or more of these parameters

  • Notes chosen from within chromatic scale of {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}.
  • Scale note offset,
  • Scale range from low note to high note
  • Instrument patch
  • Timestamps and NON duration
    • Durations of notes, could be random
    • Randomized timestamps must be sorted in ascending order starting from zero
    • Matching NOF must come before next NON
    • Note on velocity (data2)

Rules

  • Use of the hw221_CMidiPacket.h and hw221_CMidiPacket.cpp files in the cs312/common folder.
  • Write CMakeLists.txt
  • Write hw234_scifiSounds.h
  • Write hw234_scifiSounds.cpp
  • Write hw234_main.cpp
  • Output 100 notes
  • Use MIDI channel zero
  • Build using cmake

Borrow ideas from this page and previous assignments.

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: hw23_LastnameFirstname_LastnameFirstname

vsc23submit.png

Author: John Ellinger

Created: 2020-01-17 Fri 13:22

Validate