CS 312 - Week 5.1
CS 312 Audio Programming Winter 2020

Table of Contents

5.1 Class

Project 1 - due week 7.1

Reading

12 bar blues

Blues Form

  • The standard blues form is 12 measures long.
  • A measure is four beats, just like the drum machine.
  • A beat is 1000 ms.
  • The beat can be subdivided in threes or fours but must add up to 1000 ms.
  • The musical form for a 12 bar (measure) blues usually shown in Roman numerals representing the chords used.

    12 bar blues in the key of C
    The I chord is built on C (startNoteOffset + 0)
    the IV chord is built on F (startNoteOffset + 5
    the V chord is built on G (startNoteOffset + 7)

    I I I I
    IV IV I I
    V IV I V

Blues piano chords
You can play a piano chord with three or more notes by sending multiple NON messages with the same timestamp.
Remember to turn the chord off using multiple NOF messages.
The notes of these chords are for a blues in the key of C. Sometimes called "Blues in C".

Chord Notes Numbers Offset from I chord
I (C7) C E G Bb 0 4 7 10 0
IV (F7) F A C Eb 5 9 (7+5) (10+5) +5
V (G7) G B D F 7 (4+7) (7+7) (10+7) +7

Octave shifts
You can add or subtract 12 from any MIDI note number to get the same note one octave higher or lower.

Blues scales We'll use the

Rock blues scale
{0, 3, 4, 7, 9, 10, 12}

Blues scale note offsets
MIDI note numbers can never be negative or greater than 127.
The piano uses MIDI note numbers 24-108 so subracting 5 or 7 will work in that range.

Scale notes Scale Offset Starting Note Offset 60
With I chord 0 60, 63, 64, 67, 69, 70, 72
With IV chord +5 or -7 65, 68, 69, 72, 74, 75, 77 (+5)
With V chord +7 or -5 55, 59, 61, 62, 64, 65, 67 (-5)
Rock blues scale notes for the I chord (C = 0)

kROCK_BLUES_SCALE = {0, 3, 4, 7, 9, 10, 12};
Use in measures 1, 2, 3, 4, 7, 8, 11

I I I I
    I I
    I  
Rock blues scale notes for the IV chord ( add 5)

kROCK_BLUES_SCALE = {5, 8, 9, 12, 15, 17};
Use in measures 5, 6, 10

       
IV IV    
  IV    
Rock blues scale notes for the V chord ( add 7)

kROCK_BLUES_SCALE = {7, 10, 11, 14, 17, 19};
Use in measures 9, 12

       
       
V     V

Measure start times These are the millisecond start times for each of the 12 measures.

0 4000 8000 12000
16000 20000 24000 28000
32000 36000 40000 44000

Next repeat (2nd time through) would start at 48000 Next repeat (3rd time through) would start at 96000 etc.

Markov chains

From: https://en.wikipedia.org/wiki/Markov_chain

A Markov chain is a stochastic [random process] model describing a sequence
of possible events in which the probability of each event depends only on the
state attained in the previous event.

From: https://brilliant.org/wiki/markov-chains/

 A Markov chain is a mathematical system that experiences transitions from
one state to another according to certain probabilistic rules. The defining
characteristic of a Markov chain is that no matter how the process arrived a
t its present state, the possible future states are fixed. In other words, the
probability of transitioning to any particular state is dependent solely on the
current state and time elapsed. The state space, or set of all possible states,
can be anything: letters, numbers, weather conditions, baseball scores, or stock
performances.

Markov chains are usually set up as a probability matrix. Large scale Markov chains are derived by analyzing large amounts of existing data.

Here is a completely arbitrary table for the 7 notes of the rock blues scale. I made it up to discourage large leaps between the notes of the rock blues scale (RB). Each row sums to 10. If the current note is 3, then note 4 has a 5/10 chance of being chosen next.

RB 0 1 2 3 4 5 6 7 8 9 10 11 12
0 5 . . 2 3 . . . . . . . .
3 . . . 2 4 . . 4 . . . . .
4 . . . 2 3 . . 5 . . . . .
7 . . . . 3 . . 4 . . 3 . .
9 . . . . . . . 4 . 1 5 . .
10 . . . .   . . 4 . . 2 . 4
12 . . . . . . . 4 . . 3 . 3

The first column represents the notes of the rock blues scale. The first row represents all 12 notes of the chromatic scale with the rock blues scale notes in bold. Each row adds up to ten. A cell in that row represents the probability that that note will follow the column one note. Dots are a probability of 0. The row for note 4 says that only 3, 4, or 7 can follow a 4. Note 7 is most likely to follow with a 5/10 chance. I've made the integer in each row add up to 10. That's not a strict requirement. C++ will sum the values in each row and figure out the the probability of each cell divided by the sum of that row. You you can also use floats instead of integers.

discrete_distribution
The C++ <random> library includes a discrete_distribution function that produces integer values based on probabilities. I've slightly modified example code from en.cppreference.com to to illustrate the discrete_distribution for the note that follows 3. I've reduced the probability of note 3 following itself from 1 to 0.5.

Run this code in https://www.onlinegdb.com/online_c++_compiler

#include <iostream>
#include <map>
#include <random>

int main()
{
    std::random_device rd;
    std::default_random_engine engine(rd());
    // the array represents the 12 note os the chromatic scale 0-12
    std::discrete_distribution<> d({2 , 0 , 0 , 0.5 , 5 , 0 , 0 , 2 , 0 , 0 ,  0 ,  0 ,  0});
    std::map<int, int> m;
    for(int n=0; n<10000; ++n) {
        ++m[d(engine)];
    }
    for(auto p : m) {
        std::cout << p.first << " generated " << p.second << " times\n";
    }
}

Output
It might be different but in general quite close to these values.

0 generated 2088 times
3 generated 531 times
4 generated 5223 times
7 generated 2158 times

c511 c512 c513 Setup

cd $HOME312/cs312
mkdir hw51
mkdir hw51/c511_CInstrument
cd hw51/c511_CInstrument
touch hw511_CInstrument.cpp
touch hw511_CInstrument.h

cd $HOME312/cs312/hw51
mkdir c512_pianoChords
cd c512_pianoChords
mkdir build
touch c512_main.cpp
touch hw511_CBluesPianoTrack.cpp
touch hw511_CBluesPianoTrack.h
touch CMakeLists.txt
touch c512_main.cpp

cd $HOME312/cs312/hw51
mkdir c513_markovChain
cd c513_markovChain
mkdir build
touch c513_CMarkovBlues.cpp
touch c513_CMarkovBlues.h
touch c513_main.cpp
touch CMakeLists.txt

c511_CInstrument

The CScalesTrack class from homework hw411 was a sublcass of CMidiTrack, now defined in hw423_CMidiTrack. Homework hw511_blues uses four separate insturments, melody, piano rhythm, bass, and drums.

This is a new CInstrument class that is also a sublcass of CMidiTrack but implements functions to deal with among other things, counting measures, and adjusting the scale note offsets based on the what measure of the blues form we're in.

The c511_CInstrument folder is just a temporary placeholder. You'll copy the code below into the files and then move them to your common folder.

This class is fully written and you should not have to modify it. The hw511_ prefix indicates it will be used in hw511_blues.

hw511_CInstrument.h

// hw511_CInstrument.h
#ifndef HW511_INSTRUMENT_H_
#define HW511_INSTRUMENT_H_

#ifndef HW423_CMIDITRACK_H_
#include "hw423_CMidiTrack.h"
#endif

// DO NOT MODIFY

// Blues form
const int kNUM_MEASURES = 12;
const int kNUM_REPEATS = 1;
const int kTOTAL_MEASURES = kNUM_REPEATS * kNUM_MEASURES;
const int kSONG_END_TIME = kTOTAL_MEASURES * 4000;

class CInstrument : public CMidiTrack
{
private:
  int start_note; // MIDI note offset for pitch 0 in kBLUES_SCALE;
  int meas_num;   // updates measure number as song is playing

public:
  // additional constructor
  CInstrument(uint32_t beginTm, uint32_t endTm, int noteOffset, int chan,
              int patch, int vol, int pan, int startNote, int measNum);

  // getters - defined inline
  int get_start_note() const { return start_note; }
  int get_meas_num() const { return meas_num; }

  // setters - defined inline
  void set_start_note(const int sn) { start_note = sn; };
  void set_meas_num(const int mn) { meas_num = mn; };

  // Utility functions used by melody, piano, and bass classes.
  int get_note_offset(int meas, int note_offset);
  uint32_t get_measure_start_time(int meas);

  // This is a pure virtual function in CMidiTrack
  // and we need to declare it here and define it in CInstrument.cpp
  void write_track() override;

  // write_one_measure is a pure virtual function in CInstrument
  // The sax, piano, and bass subclasses must implement their own versions
  virtual void write_one_measure(int meas, int chan) = 0;
};
#endif // HW511_INSTRUMENT_H_

hw511_CInstrument.cpp

// hw511_CInstrument.cpp
#ifndef HW511_CINSTRUMENT_H_
#include "hw511_CInstrument.h"
#endif

// additional constructor
CInstrument::CInstrument(uint32_t beginTm, uint32_t endTm, int noteOffset,
                         int chan, int patch, int vol, int pan, int startNote,
                         int measNum)
    : CMidiTrack(beginTm, endTm, noteOffset, chan, patch, vol, pan),
      start_note{startNote},
      meas_num{measNum}
{
}

// This is a pure virtual function in CInstrument and we need to implement it
// here. This CInstrument function does not implement write_one_measure() and
// instead calls the functions implemented in sax, piano, and bass CInstrument
// subclasses.
void CInstrument::write_track()
{
  vtrk.clear();
  push_patch(0, get_channel(), get_patch());
  push_volume(0, get_channel(), get_volume());
  push_pan(0, get_channel(), get_pan());

  // Calls write_one_measure(), however because it is a pure virtual function
  // it will be implemented in each of the Sax, Piano, and Bass subclasses
  // according to the needs of that subclass
  for (int ix = 0; ix < kTOTAL_MEASURES; ++ix)
    write_one_measure(ix, get_channel());
}

// changes the note offset based on which measure
// of the blues form is being written
// measure numbers are zero based; measure 1 == 0, measure 12 == 11
// Return the scale note offset for meas_num
// A 12 bar blues with 4 repeats means you have to account for measures 13-48
int CInstrument::get_note_offset(int meas, int note_offset)
{
  uint16_t offset;
  switch (meas % 12)
  {
  case 0:
  case 1:
  case 2:
  case 3:
  case 6:
  case 7:
  case 10:
    offset = note_offset;
    break;
  case 4:
  case 5:
  case 9:
    offset = note_offset + 5;
    break;
  case 8:
    offset = note_offset + 7;
    break;
  case 11:
    offset = note_offset - 5;
    break;
  default:
    offset = note_offset;
    break;
  }
  return offset;
}

// returns the timestamp at the start of the measure
// measure numbers are zero based
// there are 4 beats in a measure, each beat == 1000ms
uint32_t CInstrument::get_measure_start_time(int meas)
{
  return meas * 4000;
}

Then copy to your common folder.

cp $HOME312/cs312/hw51/c511_CInstrument/hw511_CInstrument.h $HOME312/common
cp $HOME312/cs312/hw51/c511_CInstrument/hw511_CInstrument.cpp $HOME312/common

The CInstrument class will be used in all remaining projects.

c512_pianoChords

This project is also complete and demonstrates how you can write piano chords that follow the chord and scale changes of the blues form.

The files prefixed hw511_ will be used in homework hw511_blues.

hw511_CBluesPianoTrack.cpp

// hw511_CBluesPianoTrack.cpp
#ifndef HW511_CBLUESPIANOTRACK_H_
#include "hw511_CBluesPianoTrack.h"
#endif

struct ChordTm
{
  int note1;
  int note2;
  int note3;
  int note4;
  int dur;
};

// rock blues scale
ChordTm chord1{3, 7, 10, 12, 1500};
ChordTm chord2{7, 10, 12, 15, 2000}; // {7, 10, 12, 3+12}
ChordTm chord3{3, 7, 10, 12, 2500};

const std::vector<ChordTm> one_meas_chord_vec = {chord1, chord2, chord3};

// additional constructor
CBluesPianoTrack::CBluesPianoTrack(uint32_t beginTm, uint32_t endTm,
                                   int scaleOffset, int chan, int patch,
                                   int vol, int pan, int startNote, int meas)
    : CInstrument(beginTm, endTm, scaleOffset, chan, patch, vol, pan, startNote,
                  meas) {}

void CBluesPianoTrack::write_one_measure(int meas, int chn)
{
  CMidiPacket mp;

  uint16_t offset = get_note_offset(meas, get_start_note());
  uint32_t meas_tm = get_measure_start_time(meas);
  for (auto itr : one_meas_chord_vec)
  {
    push_non(meas_tm + itr.dur, chn, itr.note1 + offset, 127);
    push_non(meas_tm + itr.dur, chn, itr.note2 + offset, 127);
    push_non(meas_tm + itr.dur, chn, itr.note3 + offset, 127);
    push_non(meas_tm + itr.dur, chn, itr.note4 + offset, 127);

    push_nof(meas_tm + 300 + itr.dur, chn, itr.note1 + offset);
    push_nof(meas_tm + 300 + itr.dur, chn, itr.note2 + offset);
    push_nof(meas_tm + 300 + itr.dur, chn, itr.note3 + offset);
    push_nof(meas_tm + 300 + itr.dur, chn, itr.note4 + offset);
  }
  set_meas_num(get_meas_num() + 1);
}

hw511_CBluesPianoTrack.h

// hw511_CBluesPianoTrack.h
#ifndef HW511_CBLUESPIANOTRACK_H_
#define HW511_CBLUESPIANOTRACK_H_

#ifndef HW511_CINSTRUMENT_H_
#include "hw511_CInstrument.h"
#endif

// DO NOT MODIFY

class CBluesPianoTrack : public CInstrument
{
public:
  // constructor
  CBluesPianoTrack(uint32_t beginTm, uint32_t endTm, int scaleOffset, int chan,
                   int patch, int vol, int pan, int startNote, int meas);

  // This is the only function needed
  void write_one_measure(int meas, int chan) override;
};
#endif // HW511_CBLUESPIANOTRACK_H_

c512_main.cpp

// c512_main.cpp
#ifndef HW511_CINSTRUMENT_H_
#include "hw511_CInstrument.h"
#endif

#ifndef HW511_CBLUESPIANOTRACK_H_
#include "hw511_CBluesPianoTrack.h"
#endif

#ifndef HW421_CDELAYMS_H_
#include "hw421_CDelayMs.h"
#endif

#ifndef HW422_CAPPLEMIDISYNTH_H_
#include "hw422_CAppleMidiSynth.h"
#endif

#include <vector>

using namespace CMP33;

std::vector<CMidiPacket> vplay;

void write_piano()
{
  uint32_t startTm = 0;
  int scaleOffset = 0;
  int chan = 1;
  int patch = 0; // piano
  int vol = 100;
  int pan = 64; // pan center
  int startNote = 48;
  int meas = 0;

  CBluesPianoTrack pno_trk(startTm, kSONG_END_TIME, scaleOffset, chan, patch,
                           vol, pan, startNote, meas);
  pno_trk.write_track();
  // append to vplay
  std::copy(pno_trk.vtrk.begin(), pno_trk.vtrk.end(),
            std::back_inserter(vplay));
}

int main(int argc, char const *argv[])
{
  write_piano();

  CDelayMs::s_tempo = 135;
  CAppleMidiSynth ams;
  ams.send(vplay);

  return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 2.8)
set(APPNAME "pianoChords")
project(${APPNAME})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall ")

# JE sets FOR_CLASS to 0
# YOU set FOR_CLASS to 1
set(FOR_CLASS 0)
if(${FOR_CLASS})
  set( HOME "/Volumes/cs312-00-w20/StuWork/<your_email_name>" )
elseif(NOT ${FOR_CLASS})
  set( HOME "/Users/je/cs312/_cs312")
endif(${FOR_CLASS})
set(COMMON "${HOME}/common")

set(SOURCE_FILES
  ${COMMON}/hw332_CMidiPacket.cpp
  ${COMMON}/hw411_rand_int.cpp
  ${COMMON}/hw421_CDelayMs.cpp
  ${COMMON}/hw422_CAppleMidiSynth.cpp
  ${COMMON}/hw423_CMidiTrack.cpp
  ${COMMON}/hw511_CInstrument.cpp
  hw511_CBluesPianoTrack.cpp
  c512_main.cpp
)

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

target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AudioToolbox")
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreMIDI")
target_link_libraries(${PROJECT_NAME} PRIVATE  "-framework CoreAudio")

Build/run/listen
Study the code.
The hw511_CBluesPianoTrack files will be copied and used in hw511_blues.

c513_markovChain

While testing hw511_blues I wrote this code to test adding Markov chain logic for the melody line. To incorporate it into hw511_blues I used the hw411_randUtils function get_rand_num(…) to choose the starting note of each measure. The remaining melody notes for the measure called the chooseMarkovNote(…) function from the CMarkov class.

The idea is to avoid large melodic leaps and opt for a smoother sequence of notes.

All code is complete and should compile.

c513_CMarkovBlues.cpp
The markov weights were set up to allow only the three notes closest to the starting note.
All weights add up to ten.
Each number times ten is the percentage probability for the next note that follows.

// c513_CMarkovBlues.cpp
#ifndef C513_CMARKOVBLUES_H_
#include "c513_CMarkovBlues.h"
#endif

#include <iostream>
#include <vector>
#include <random>

int CMarkovBlues::chooseMarkovNote(int note)
{
  // rock blues
  // const std::vector<int> kweigh{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
  const std::vector<int> k0weights{5, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0};
  const std::vector<int> k3weights{0, 0, 0, 2, 4, 0, 0, 4, 0, 0, 0, 0, 0};
  const std::vector<int> k4weights{0, 0, 0, 2, 3, 0, 0, 5, 0, 0, 0, 0, 0};
  const std::vector<int> k7weights{0, 0, 0, 0, 3, 0, 0, 4, 0, 0, 3, 0, 0};
  const std::vector<int> k9weights{0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 5, 0, 0};
  const std::vector<int> k10weights{0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 2, 0, 4};
  const std::vector<int> k12weights{0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 3, 0, 3};

  std::random_device rd;
  std::default_random_engine engine(rd());

  std::discrete_distribution<int> distribution_k0(k0weights.begin(), k0weights.end());
  std::discrete_distribution<int> distribution_k3(k3weights.begin(), k3weights.end());
  std::discrete_distribution<int> distribution_k4(k4weights.begin(), k4weights.end());
  std::discrete_distribution<int> distribution_k7(k7weights.begin(), k7weights.end());
  std::discrete_distribution<int> distribution_k9(k9weights.begin(), k9weights.end());
  std::discrete_distribution<int> distribution_k10(k10weights.begin(), k10weights.end());
  std::discrete_distribution<int> distribution_k12(k12weights.begin(), k12weights.end());

  int n;
  switch (note)
  {
  case 0:
    n = distribution_k0(engine);
    break;

  case 3:
    n = distribution_k3(engine);
    break;

  case 4:
    n = distribution_k4(engine);
    break;

  case 7:
    n = distribution_k7(engine);
    break;

  case 9:
    n = distribution_k9(engine);
    break;

  case 10:
    n = distribution_k10(engine);
    break;

  case 12:
    n = distribution_k12(engine);
    break;

  default:
    n = distribution_k0(engine);
    break;
  }
  return n;
}

c513_CMarkovBlues.h

// c513_CMarkovBlues.h
#ifndef C513_CMARKOVBLUES_H_
#define C513_CMARKOVBLUES_H_

#include <iostream>
#include <vector>
#include <random>

extern std::random_device rd;
extern std::default_random_engine engine;

class CMarkovBlues
{
public:
  std::vector<int> vRBscale{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

  int chooseMarkovNote(int note);
};

#endif // CMARKOVBLUES_H_

c513_main.cpp

// c513_main.cpp
#ifndef CMARKOVBLUES_H_
#include "CMarkovBlues.h"
#endif

#ifndef HW411_RAND_INT_H_
#include "hw411_rand_int.h"
#endif

#ifndef HW421_CDELAYMS_H_
#include "hw421_CDelayMs.h"
#endif

#ifndef HW422_CAPPLE_MIDISYNTH_H_
#include "hw422_CAppleMidiSynth.h"
#endif

#include <random>

int startNote{0};
bool isStartSet = false;

const std::vector<int> vscale{0, 3, 4, 7, 9, 10, 12}; // rock blues

void play_melody(std::vector<int> v)
{
  CDelayMs::s_tempo = 74;
  CMidiPacket mp;
  std::vector<CMidiPacket> vplay;
  uint32_t tm{0};
  for (int ix = 0; ix < v.size(); ++ix)
  {
    // NON
    uint8_t note = v.at(ix);
    {
      mp = {tm, 0x90, note, 100};
      if (ix % 2 == 0)
      {
        tm += 667;
        mp.set_data2(120);
      }
      else
      {
        tm += 333;
        mp.set_data2(85);
      }
      vplay.push_back(mp);

      // NOF
      mp = {tm - 1, 0x80, note, 0};
      vplay.push_back(mp);
    }
  }

  CAppleMidiSynth ams;
  ams.send(vplay);
  CDelayMs d(1000, false);
}

void gen_random_melody(const int numNotes)
{
  std::vector<int> vec;
  std::vector<int> vi;

  // vec.clear();
  // vi.clear();

  for (auto n = 0; n < numNotes; ++n)
  {
    // reset note for the next time through the inner loop
    int ni = get_rand_int(0, vscale.size() - 1);
    if (!isStartSet)
    {
      startNote = vscale.at(ni);
      isStartSet = true;
    }
    int note = vscale.at(ni) + 60;
    vec.push_back(note);
    vi.push_back(ni);
  }
  isStartSet = false;

  std::cout << "==== random_melody ====\n";
  for (auto itr : vi)
  {
    std::cout << vscale.at(itr) << " ";
  }
  std::cout << std::endl;
  play_melody(vec);
}

void gen_markov_melody(const int numNotes)
{
  // std::cout << "==== gen_markov_melody ====\n";
  CMarkovBlues mb;
  std::vector<int> vec;
  std::vector<int> vi;

  vec.clear();
  vi.clear();
  int note = startNote;
  // Inner for loop
  for (auto n = 0; n < numNotes; ++n)
  {
    // reset note for the next time through the inner loop
    if (n == 0)
      note = startNote;
    else
    {
      // This statement chooses the next note based on the previous one
      // note = mb.chooseMarkovNote(note);

      // This statement chooses all notes based on the starting note
      // that was set as the first starting note of the random melody
      note = mb.chooseMarkovNote(startNote);
    }
    vec.push_back(note + 60);
  }

  std::cout << "==== markov_melody ====\n";
  for (auto itr : vec)
  {
    std::cout << itr - 60 << " ";
  }
  std::cout << std::endl;
  play_melody(vec);
}

int main(int argc, char const *argv[])
{
  const int kNUM_NOTES = 8; // number of notes to generate
  const int kNUM_TESTS = 4; // number of experiments

  for (int i = 0; i < kNUM_TESTS; ++i)
  {
    gen_random_melody(kNUM_NOTES);
    // std::cout << std::endl;

    gen_markov_melody(kNUM_NOTES);
    std::cout << std::endl;
  }
  return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 2.8)
set(APPNAME "markovTest")
project(${APPNAME})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall ")

# JE sets FOR_CLASS to 0
# YOU set FOR_CLASS to 1
set(FOR_CLASS 0)
if(${FOR_CLASS})
  set( HOME "/Volumes/cs312-00-w20/StuWork/<your_email_name>" )
elseif(NOT ${FOR_CLASS})
  set( HOME "/Users/je/cs312/_cs312")
endif(${FOR_CLASS})
set(COMMON "${HOME}/common")

set(SOURCE_FILES
  ${COMMON}/hw332_CMidiPacket.cpp
  ${COMMON}/hw411_rand_int.cpp
  ${COMMON}/hw421_CDelayMs.cpp
  ${COMMON}/hw422_CAppleMidiSynth.cpp
  c513_CMarkovBlues.cpp
  c513_main.cpp
)

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

target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AudioToolbox")
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreMIDI")
target_link_libraries(${PROJECT_NAME} PRIVATE  "-framework CoreAudio")

Build/run/listen
Study the code.

First you'll hear a random melody.
Then you'll hear a markov melody starting from the first note chosen in the random melody.
If you look at the code you'll see this. Try them both.

// This statement chooses the next note based on the previous one
// note = mb.chooseMarkovNote(note);

// This statement chooses all notes based on the starting note
// that was set as the first starting note of the random melody
note = mb.chooseMarkovNote(startNote);

Homework 5.1

Setup hw511

cd $HOME312/cs312/hw51
mkdir hw511_blues
cd hw511_blues
# files assigned previously
cp $HOME312/cs312/hw43/hw431_drumMachine/hw431_CDrumTrack.cpp .
cp $HOME312/cs312/hw43/hw431_drumMachine/hw431_CDrumTrack.h .
cp $HOME312/cs312/hw43/hw431_drumMachine/hw431_CDrumMachine.cpp .
cp $HOME312/cs312/hw43/hw431_drumMachine/hw431_CDrumMachine.h .
cp $HOME312/cs312/hw51/c512_pianochords/hw511_CBluesPianoTrack.cpp .
cp $HOME312/cs312/hw51/c512_pianochords/hw511_CBluesPianoTrack.h .
# end files assigned previously

mkdir build
touch CMakeLists.txt
touch hw511_CBluesMelodyTrack.cpp
touch hw511_CBluesMelodyTrack.h
touch hw511_CBluesBassTrack.cpp
touch hw511_CBluesBassTrack.h
touch hw511_main.cpp

hw511_blues

In this homework you'll write a twelve bar blues using four instruments: Melody, Piano, Acoustic Bass, and Drums. We'll use the parts of hw411_CMidiTrack, hw422_CMidiTrack_dls and hw431_drumMachine.

The CDrumTrack and CDrumMachine classes are from hw431_drumMachine which should be completed before continuing with hw511_blues.

Files from previous assignments.

hw431_CDrumTrack.h
Copy your latest version here.

hw431_CDrumTrack.cpp
Copy your latest version here.

hw431_CDrumMachine.h
Copy your latest version here.

hw431_CDrumMachihne.cpp
Copy your latest version here.

CInstrument class

This was given in c511_CInstrument earlier and should be in your common folder. hw511_CInstrument.h
Should be in your common folder.

hw511_CInstrument.cpp
Should be in your common folder.

Notes:

  • Subclass of CMidiTrack.
  • New constructor overload to initialize variables startNote and measNum
  • One override method to implement CMidiTrack's pure virtual function write_track()
  • The keyword override is optional but helps indicate that it is overriding a function in the parent class
  • One pure virtual function virtual write_one_measure( int meas, int startNote, int chan ) = 0;
    This function will be implemented differently in each of the Melody, Piano, and Bass subclasses
  • CBluesMelodyTrack, CBluesPianoTrack, and CBluesBassTrack are subclasses of CInstrument which is in turn a sublcass of CMidiTrack.

CBluesMelodyTrack, CBluesPianoTrack, CBluesBassTrack classes

  • Are all subclasses of CInstrument.
  • All have a similar .h interface file.
  • You'll need to implement two functions:
    the constructor and the pure virtual function
    virtual void write_one_measure(int meas, int chan) = 0;

CBluesMelodyTrack class

hw511_CBluesMelodyTrack.h

// hw511_CBluesMelodyTrack.h
#ifndef HW511_CBLUESMELODYTRACK_H_
#define HW511_CBLUESMELODYTRACK_H_

#ifndef HW511_CINSTRUMENT_H_
#include "hw511_CInstrument.h"
#endif

// DO NOT MODIFY

class CBluesMelodyTrack : public CInstrument
{
public:
  // constructor
  CBluesMelodyTrack(uint32_t beginTm, uint32_t endTm, int scaleOffset, int chan,
                    int patch, int vol, int pan, int startNote, int meas);

  // This is the only function needed
  void write_one_measure(int meas, int chan) override;
};
#endif // HW511_CBLUESMELODYTRACK_H_

hw511_CBluesMelodyTrack.cpp

// hw511_CBluesMelodyTrack
#ifndef HW511_CBLUESMELODYTRACK_H_
#include "hw511_CBluesMelodyTrack.h"
#endif

#ifndef HW411_RAND_INT_H_
#include "hw411_rand_int.h"
#endif

#include <cstdint>
#include <vector>

const std::vector<int> vBluesScale = {0, 3, 4, 7, 9, 10, 12}; // rock blues scale
const std::vector<uint32_t> vTime = {500, 1000, 1500, 2000, 2500};

// additional constructor
CBluesMelodyTrack::CBluesMelodyTrack(uint32_t beginTm, uint32_t endTm,
                                     int scaleOffset, int chan, int patch, int vol,
                                     int pan, int startNote, int meas)
    : CInstrument(beginTm, endTm, scaleOffset, chan, patch, vol, pan, startNote,
                  meas) {}

void CBluesMelodyTrack::write_one_measure(int meas, int chan)
{
  // kNOTE_DURATION has to be smaller than smallest delta time difference in vTime
  const int kNOTE_DURATION = 300;
  CMidiPacket mp;

  // use get_note_offset(meas, get_start_note()) to get the pitch offset based on the chord used in meas
  // use get_measure_start_time(meas) to get the starting time for the measure we're writing

  for (auto itr : vTime)
  {
    // get a random index into vBluesScale
    // set note to vBluesScale.at(index);

    // Use push_non() to write the Note On message
    // Use push_nof() to write the Note Off  message
  }

  set_meas_num(get_meas_num() + 1);
  std::cout << "Write: CBluesMelodyTrack::write_one_measure(int meas, int chan)\n";
}

CBluesPianoTrack class

hw511_CBluesPianoTrack.h
Already copied from c512_pianoChords

hw511_CBluesPianoTrack.cpp
Already copied from c512_pianoChords

Note:
The CBluesPianoTrack code is complete and should not need changes.

CBluesBassTrack class

hw511_CBluesBassTrack.h

// hw511_CBluesBassTrack.h
#ifndef HW511_CBLUESBASSTRACK_H_
#define HW511_CBLUESBASSTRACK_H_

#ifndef HW511_CINSTRUMENT_H_
#include "hw511_CInstrument.h"
#endif

// DO NOT MODIFY

class CBluesBassTrack : public CInstrument
{
public:
  // constructor
  CBluesBassTrack(uint32_t beginTm, uint32_t endTm, int scaleOffset, int chan,
                  int patch, int vol, int pan, int startNote, int meas);

  // This is the only function needed
  void write_one_measure(int meas, int chan) override;
};
#endif // HW511_CBLUESBASSTRACK_H_

hw511_CBluesBassTrack.cpp

// hw511_CBluesBassTrack.cpp
#ifndef HW511_CBLUESBASSTRACK_H_
#include "hw511_CBluesBassTrack.h"
#endif

// Store note and time together
struct NoteTm
{
  int note;
  int tm;
};

// clang-format off
const std::vector<NoteTm> one_meas_vec = {
                                          {7, 0},
                                          {0, 500},
                                          {9, 1000},
                                          {0, 1500},
                                          {10, 2000},
                                          {0, 2500},
                                          {9, 3000},
                                          {0, 3500}
                                          };
// Alternate choice or write your own
// const std::vector<NoteTm> one_meas_vec = {
//                                           {0, 0},
//                                           {4, 500},
//                                           {7, 1000},
//                                           {9, 1500},
//                                           {10, 2000},
//                                           {9, 2500},
//                                           {7, 3000},
//                                           {4, 3500}
//                                           };
// clang-format on

// additional constructor
CBluesBassTrack::CBluesBassTrack(uint32_t beginTm, uint32_t endTm,
                                 int scaleOffset, int chan, int patch, int vol,
                                 int pan, int startNote, int meas)
    : CInstrument(beginTm, endTm, scaleOffset, chan, patch, vol, pan, startNote,
                  meas) {}

void CBluesBassTrack::write_one_measure(int meas, int chan)
{
  CMidiPacket mp;


  // use get_note_offset(meas, get_start_note()) to get the pitch offset based on the chord used in meas
  // use get_measure_start_time(meas) to get the starting time for the measure we're writing

  for (auto itr : one_meas_vec)
  {
    // get a random index into vBluesScale
    // set note to vBluesScale.at(index);

    // Use push_non() to write the Note On message
    // Use push_nof() to write the Note Off  message
  }

  set_meas_num(get_meas_num() + 1);
}

main()

hw511_main.cpp
This is the complete code I used.

// hw511_main.cpp
#ifndef HW511_CINSTRUMENT_H_
#include "hw511_CInstrument.h"
#endif

#ifndef HW511_CBLUEMELODYTRACk_H
#include "hw511_CBluesMelodyTrack.h"
#endif

#ifndef HW511_CBLUESPIANOTRACK_H
#include "hw511_CBluesPianoTrack.h"
#endif

#ifndef HW511_CBLUESBASSTRACK_H
#include "hw511_CBluesBassTrack.h"
#endif

#ifndef hw431_CDRUMMACHINE_H_
#include "hw431_CDrumMachine.h"
#endif

#ifndef HW421_CDELAYMS_H_
#include "hw421_CDelayMs.h"
#endif

#ifndef HW422_CAPPLEMIDISYNTH_H_
#include "hw422_CAppleMidiSynth.h"
#endif

#include <vector>

using namespace CMP33;

std::vector<CMidiPacket> vplay;

void write_melody()
{
  // S A X
  uint32_t startTm = 0;
  int scaleOffset = 0;
  int chan = 0;
  int patch = 56; // Trumpet
  // Other atches I tried
  // Tenor Sax
  // Electric Guitar (jazz)
  // Drawbar organ
  // Lead 2 (sawtooth)
  // Clarinet

  int vol = 127;
  int pan = 32; // pan left -32 from center 64
  int startNote = 60;
  int meas = 0;

  // create the saxophone track
  CBluesMelodyTrack sax_trk(startTm, kSONG_END_TIME, scaleOffset, chan, patch, vol,
                         pan, startNote, meas);
  sax_trk.write_track();
  // append to vplay
  std::copy(sax_trk.vtrk.begin(), sax_trk.vtrk.end(),
            std::back_inserter(vplay));
}

void write_piano()
{
  // P I A N O
  uint32_t startTm = 0;
  int scaleOffset = 0;
  int chan = 1;
  int patch = 0; // piano
  int vol = 127;
  int pan = 64; // pan center
  int startNote = 48;
  int meas = 0;

  CBluesPianoTrack pno_trk(startTm, kSONG_END_TIME, scaleOffset, chan, patch,
                           vol, pan, startNote, meas);
  pno_trk.write_track();
  // append to vplay
  std::copy(pno_trk.vtrk.begin(), pno_trk.vtrk.end(),
            std::back_inserter(vplay));
}

void write_bass()
{
  // B A S S
  uint32_t startTm = 0;
  int scaleOffset = 0;
  int chan = 2;
  int patch = 32; // acoustic bass
  int vol = 127;
  int pan = 92; // pan right +32 from center 64
  int startNote = 36;
  int meas = 0;

  CBluesBassTrack bass_trk(startTm, kSONG_END_TIME, scaleOffset, chan, patch,
                           vol, pan, startNote, meas);
  bass_trk.write_track();
  // append to vplay
  std::copy(bass_trk.vtrk.begin(), bass_trk.vtrk.end(),
            std::back_inserter(vplay));
}

void write_drums()
{
  const uint8_t kRIDE_CYMBAL = 51;

  // create the drum machine
  // You do not have to use every row
  // This is the pattern I made up
  // You can make up your own.
  CDrumMachine dm;
  dm.make_track(kBASS_DRUM, {"1000", "1000", "1000", "1000"});
  dm.make_track(kSNARE_DRUM, {"0001", "0010", "0001", "0010"});
  // dm.make_track(kCLAP, {"0000", "0000", "0000", "0010"});
  dm.make_track(kRIDE_CYMBAL, {"1000", "1000", "1000", "1000"});
  dm.make_track(kCLOSED_HIHAT, {"0010", "0010", "0010", "0011"});
  // dm.make_track(kLOW_TOM, {"0010", "0000", "0000", "0000"});
  dm.make_track(kHIGH_TOM, {"0001", "0001", "0010", "0010"});
  // dm.make_track(kWOOD_BLOCK, {"0010", "0100", "0100", "0101"});

  int loops = kSONG_END_TIME / 4000;
  for (auto n = 0; n < dm.vDrumTracks.size(); ++n)
  {
    dm.vDrumTracks[n].write_track(loops);
    // append each drum track to vplay
    std::copy(dm.vDrumTracks[n].vtrk.begin(),
              dm.vDrumTracks[n].vtrk.end(), std::back_inserter(vplay));
  }
}

int main()
{
  // write tracks
  write_melody();
  write_piano();
  write_bass();
  write_drums();

  // sort the playback vector using CMidiPacket43::operator<
  std::sort(vplay.begin(), vplay.end());

  // output for MIDIDisplay
  for (auto itr : vplay)
    std::cout << itr;

  CDelayMs::s_tempo = 125;
  CAppleMidiSynth ams;
  ams.send(vplay);
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
set(APP_NAME "blues51")
project(${APP_NAME})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -Wall ")

# change this to  your $HOME directory
set(HOME "/Users/je/cs312/_cs312")
set(COMMON "${HOME}/common")
set(SOURCE_FILES
    ${COMMON}/hw332_CMidiPacket.cpp
    ${COMMON}/hw423_CMidiTrack.cpp
    ${COMMON}/hw411_rand_int.cpp
    ${COMMON}/hw421_CDelayMs.cpp
    ${COMMON}/hw422_CAppleMidiSynth.cpp
    hw511_CInstrument.cpp
    hw511_CBluesMelodyTrack.cpp
    hw511_CBluesBassTrack.cpp
    hw511_CBluesPianoTrack.cpp
    hw431_CDrumMachine.cpp
    hw431_CDrumTrack.cpp
    hw511_main.cpp
)

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

target_link_libraries(${PROJECT_NAME} PRIVATE "-framework AudioToolbox")
target_link_libraries(${PROJECT_NAME} PRIVATE "-framework CoreMIDI")
target_link_libraries(${PROJECT_NAME} PRIVATE  "-framework CoreAudio")

Build and Run
You'll get three warnings from the write_one_measure functions in the Melody and Bass tracks. You'll get output telling you to write those functions.

On the plus side you'll get to hear the drums and piano duet following the blues form.

Assignment hw411_blues

  • Implement the two missing write_one_measure functions in the Melody and Bass tracks.
  • Feel free to change notes, durations, patterns to make it more interesting, but stay within the blues scale and form.
  • You can simplify the drum machine by commenting out any combination of the eight drum tracks in void CDrumMachine::setup_patterns().

hw511_blues continued

This assignments adds Markov chain rules for changing the melody notes. The hard work has already been done in c513_markovChain.

Setup

cd  $HOME312/cs312/hw51/hw511_blues

cp $HOME312/cs312/hw51/c513_markovchain/c513_CMarkovBlues.cpp .
cp $HOME312/cs312/hw51/c513_markovchain/c513_CMarkovBlues.h .

Changes

CMakeLists.txt
Add c513_CMarkovBlues to the SOURCE_FILES section

hw511_CBluesMelodyTrack.cpp

Add these defines above the constructor

// set to 1 to use markov
// wet to 0 to use random
#define USE_MARKOV 1
#if USE_MARKOV
#ifndef C513_CMARKOVBLUES_H_
#include "c513_CMarkovBlues.h"
#endif
#endif

// additional constructor
CBluesMelodyTrack::CBluesMelodyTrack(uint32_t beginTm, uint32_t endTm,
...

Add these defines to the write_one_measure function

void CBluesMelodyTrack::write_one_measure(int meas, int chan)
{
  // kNOTE_DURATION has to be smaller than smallest delta time difference in vTime
  const int kNOTE_DURATION = 300;
  CMidiPacket mp;

#if USE_MARKOV
  CMarkovBlues mb;
  // choose starting notes only on 0,4,7 of the scale
  int n = get_rand_int(0, 2);
  int note{0};
  if (meas % 12 == 0)
    note = vBluesScale.at(0);
  else if (n == 1)
    note = vBluesScale.at(4);
  else if (n == 3)
    note = vBluesScale.at(7);
  else
    note = vBluesScale.at(0);
#endif

Also inside the for loop of write_one_measure add this

  for (auto itr : vTime)
  {

#if USE_MARKOV
    note = mb.chooseMarkovNote(note);
#else
    int ni = get_rand_int(0, vBluesScale.size() - 1);
    int note = vBluesScale.at(ni);
#endif

    push_non(meas_tm + itr, chan, note + offset, 100);
    push_nof(meas_tm + kNOTE_DURATION + itr, chan, note + offset);
  }

Build and run

cmake .. && make && ./blues51

Change the #define USE_MARKOV from 1 to 0 to hear the difference between random notes and Markov rule smoothing. Use whichever one you like better. Or rewrite the Markov rules.

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
    

Hand-in folder contents

$HOME312/common
├── hw332_CMidiPacket.cpp
├── hw332_CMidiPacket.h
├── hw411_rand_int.cpp
├── hw411_rand_int.h
├── hw421_CDelayMs.cpp
├── hw421_CDelayMs.h
├── hw422_CAppleMidiSynth.cpp
├── hw422_CAppleMidiSynth.h
├── hw423_CMidiTrack.cpp
├── hw423_CMidiTrack.h
├── hw511_CInstrument.cpp
├── hw511_CInstrument.h
└── hw511_CInstrument.h

$HOME312/cs312/hw51/hw511_blues
├── CMakeLists.txt
├── build
├── c513_CMarkovBlues.cpp
├── c513_CMarkovBlues.h
├── hw431_CDrumMachine.cpp
├── hw431_CDrumMachine.h
├── hw431_CDrumTrack.cpp
├── hw431_CDrumTrack.h
├── hw511_CBluesBassTrack.cpp
├── hw511_CBluesBassTrack.h
├── hw511_CBluesMelodyTrack.cpp
├── hw511_CBluesMelodyTrack.h
├── hw511_CBluesPianoTrack.cpp
├── hw511_CBluesPianoTrack.h
└── hw511_main.cpp

Author: John Ellinger

Created: 2020-02-03 Mon 12:51

Validate