Introduction

Transcribing music is the process of notating a piece of music such that it can be documented for future reference and replicated by other musicians. The process of transcribing a piece of music is a time consuming task and can be challenging for someone without a grasp of music theory. For this reason, we elected to build a device that could take a live audio input and encode a MIDI file which could be used to produce music notation or a software instrument track. Such would ease the process of arranging and producing music for amateur musicians.
Our music transcription device takes a live audio input and produces a MIDI output. The device works in three stages: the microphone inputs is filtered and amplified, the microcontroller identifies pitches and their duration from the microphone input, and lastly, the data from the microcontroller is transmitted to a computer via serial communication and written to a MIDI file.

High Level Design

Music is an important mode of expression, but the ability to write sheet music is far from universal. A music transcription device makes the process of writing music accessible, providing people without knowledge of music theory the ability to share their ideas.
The bulk of the calculation needed to transcribe music is devoted to pitch identification. We elected to identify pitches using a filter bank, essentially a set of bandpass filters with cutoff frequencies centered about musical pitches. Within an ISR, an analog microphone input is sampled and processed by our filter bank. The maximum filter bank output is determined to be the note being played by the user. Thus, our software stores the maximum filter bank output as the note to be transcribed along with its duration, yielding a sequence of notes and times. As the data is being collected and processed by the microcontroller, it is transmitted via serial communication to a computer to be encoded as a MIDI file.
Audio filtering is performed both in hardware and software. A microphone circuit isolates and amplifies musically relevant frequencies, while our microcontroller uses the ADC input to determine pitches via the filter bank.
While patents do exist in this area, they are specific to certain methods of automatic music transcription. Most of the computerized methods used to transcribe music take a very different approach. There are a handful of different pieces of software that will take a recording and produce a MIDI file using digital signal processing methods to determine the pitch and duration. At the time of this project though, there are no dedicated devices commercially available to do this. This device explores the potential of an untapped market, and could be taken further to be independent of a computer, meaning only a small device would be needed to transcribe live music.
Due to the nature of the project it was not necessary to conform to any official standards.

Program & Hardware Design

The hardware design consists of a simple audio input and filters. The schematic can be found below. The signal is picked up by a microphone, fed through a DC blocking capacitor and is offset to a voltage of VDD/2. From there, it passes through a high pass filter with a cutoff frequency of 10 Hz, followed by a low pass filter with cutoff frequency of 4,000 Hz. In order to be certain that the signal is strong enough for the PIC32, it is then passed through an amplifier with a gain of 50. Again, the signal is fed through a DC blocking capacitor and finally to the microcontroller.
A pushbutton was connected to the PIC32 to indicate when the user began and ended the piece to be transcribed. A state machine was implemented to debounce the pushbutton input. When the button was pressed, the ISR that facilitated note extraction was enabled along with data transmission.
Our software processes the audio input from the microphone circuit using the onboard analog to digital converter and generates a record of each note played. It accomplishes this by using a filter bank to identify pitches as they are played and recording their duration. We divide this software functionality into three tasks: populate a filter bank with pitch energies, determine the maximum energy among the filter banks to determine the note being played at a given time, and determine how long a note is being played. This data can then be sent via serial communication to encode a MIDI file.
We implemented a filter bank by creating a series of butterworth filters to isolate musical pitches. In order to generate the filter coefficients, we used a MATLAB script that would produce filter coefficients for a filter bank using the native butter() function. This same script wrote a header file that could be referenced by our filter bank code. The filter coefficients included in the header file were then used to generate a filter bank that recorded the energy of each pitch in the operating region of the device. We then implement a filter bank to analyze the audio input within a timer ISR that fires at the sampling frequency. Each time the ISR executes, the audio input is read and passed through each of the bandpass filters in the filter bank. The filters use the filter coefficients for a given filter along with the current and last audio sample to generate an output. The energy of each filter output is then obtained by low passing the absolute value of the filter output.
Each time the audio input is analyzed, we determine which filter output has the greatest energy. If this energy exceeds a certain threshold, indicating that it was not produced by ambient noise, then we determine that the note corresponding to this filter is being played. In addition to determining the note being played each time the audio is sampled, we determine whether the note has changed. If the note does has not changed since that last sampling period, we increment the note’s measured duration. If a note change is detected, then we store the note’s pitch and the amount of time it has been playing and begin recording a new note. The type of note played by the user is determined by checking the length of the note in seconds against the user specified tempo. We choose to encode the type of note that most closely approximates the duration of the recorded note.
On the client side, a Python program was made to take in the serial data from the device, parse the information, and create a MIDI file. Serial communication allows the user to specify the tempo at which they want to perform their piece on the client side, while allowing information gathered about note pitch and length to be transmitted for encoding. To enable serial communication, we open a UART DMA channel on the device, write a string to the DMA buffer, then spawn the process to send the information. On the client side, the program waits for the data to become available, reads the information from the client's receive buffer, and parses the data in the format: "pitch_as_number,duration\r\n\0." The program uses the MIDIUtil Python library to create a MIDI file by reading the data one line at a time until a specified end-transmission string is received. The PySerial library is used for client-side serial communication. Additionally, at the beginning of thee communication between the client and the device, the user-specified tempo is send to the device, and read on the device by spawning the thread corresponding to the UART RX buffer, and then reading out this value from the buffer. Data transmission is initiated and terminated by pressing a button on the device, indicating that the piece has begun or ended.
The protothreads threading library and TFT display library that were made available to the students of ECE 4760 were integral to our design. In addition, we made use of the filter bank implementation provided by Professor Bruce Land to perform pitch identification.

Results

Our device achieves basic functionality. The output of the device, when supplied with this input, can be heard here and the associated score can be seen here.
Our recordings indicate that our device achieves reasonable accuracy in pitch detection but does not demonstrate the same degree of accuracy in determining note duration. With that said, one of the greatest obstacles in getting our device to function correctly was dealing with ambient noise. The device was highly sensitive to perceived fluctuation in the volume of the pitches being played, and therefore recorded notes and durations inaccurately. Most notably, longer, sustained notes were perceived by the device as being shorter than they were in reality.
One of the biggest constraints of the device is its inability to function when provided tones that contain harmonics. The tones produced by many instruments contain strong high order harmonics, and our pitch extraction method was not equipped to detect underlying fundamental frequencies. While the filter bank method functioned well for pure sine waves, it lacks the ability to identify pitches produced by other instruments. A different approach could use a Fourier analysis to better extract information about the pitch. That being said, further testing may reveal satisfactory performance with instruments having pure, or close to pure, tones.
The current implementation is not equipped to deal with more complex rhythms or time signatures and can only discern one note being played at a time. For instance, our algorithm quantizes notes only down to an eighth note and does not provide a way to encode triplets or dotted quantities and assumes a 4/4 time signature. These are things that can be changed in later iterations of our design.
The processing speed of the microcontroller constrained the range of pitches that could be extracted by our device. The computation time required by a filter bank constrains the number of pitches that can be identified. For this reason, the operational range of our music transcription device is only three octaves, ranging from C3 to C6.
An additional feature that could be added is the ability to make this a standalone device. The ability to write a MIDI file directly to a memory card or USB drive would eliminate the need for a client computer and a serial connection. This adds quite a bit of complexity since the MIDI file format is extremely powerful. This means that adding the ability to write the MIDI file directly to a usb drive of memory card would require significantly more time to develop, but would be a good end-goal if this device were to be developed into a full product.

Conclusion

Our device demonstrates satisfactory performance given the complexity of the problem and the amount of time devoted to the project. As described previously, there are many aspects of our device that could be improved upon, given more time and a faster processor. One of the goals we aimed to meet was to transcribe the output of a real instrument. Unfortunately, our pitch extraction method provided poor performance for impure tones. Given an instrument with a purer tone, such as a clarinet, or a different pitch extraction method, we may achieve different results. That being said, our device provides a useful proof of concept for a more robust transcription device.
A high performance music transcription device could alter the landscape of the music industry. At the same time, it can make it easier to infringe upon copyrights by providing an easy means to copy others’ music. For this reason, a music transcription device must be used responsibly and with respect for the work of other musicians.
As mentioned above, the code used was either written by us, the ECE 4760 course staff, or was licensed for public use. Additionally, we did not need to consider restrictions from the FCC, IEEE code of ethics, or any patents or trademarks. All patents found related to transcribing music perform this using a different method. There is more work to be done though before this project could be pattended. At the moment the device is still somewhat buggy, and quite a handful of changes would need to be made before turning this into a finished product, which would qualify as a different patent.

Appendix

Code

Music_transcription.c:

/*
 * File:        Music_transcription.c
 * Author:      Mikayla Diesch (mld243), Shubha Sekar (ss2694), Maayan Kline (mok8)
 * Adapted from:
 *              TFT_keypad_BRL4.c
 * Author:      Bruce Land
 * Target PIC:  PIC32MX250F128B
 */

// graphics libraries
#include "config.h"
#include "tft_master.h"
#include "tft_gfx.h"
// need for rand function
#include <stdlib.h>

// threading library
#include <<plib.h>
// config.h sets 40 MHz
#define SYS_FREQ 40000000
#include "pt_cornell_1_2_1.h"

#include "Filter_bandpass_14bit.h"

#define RELEASE 0
#define MAYBE_PRESSED 1
#define STILL_PRESSED 2
#define MAYBE_RELEASED 3

#define NOT_TRANSMITTING 0
#define TRANSMITTING 1
#define TERMINATION_STR "end\r\n"
#define NEWLINE_CHARACTER "\r\n"

// PORT B
#define EnablePullDownB(bits) CNPUBCLR=bits; CNPDBSET=bits;
#define DisablePullDownB(bits) CNPDBCLR=bits;
#define EnablePullUpB(bits) CNPDBCLR=bits; CNPUBSET=bits;
#define DisablePullUpB(bits) CNPUBCLR=bits;

//===============================================================

typedef signed int fix16 ;
#define multfix16(a,b) ((fix16)(((( signed long long)(a))*(( signed long long)(b)))>>16)) //multiply two fixed 16:16
#define float2fix16(a) ((fix16)((a)*65536.0)) // 2^16
#define fix2float16(a) ((float)(a)/65536.0)
#define fix2int16(a)    ((int)((a)>>16))
#define int2fix16(a)    ((fix16)((a)<<16))
#define divfix16(a,b) ((fix16)((((signed long long)(a)<<16)/(b)))) 
#define sqrtfix16(a) (float2fix16(sqrt(fix2float16(a)))) 
#define absfix16(a) abs(a)

// lowpass coeff 
volatile fix16 alpha = float2fix16(0.005) ; // time constant ~ 200/8000 sec = 0.25 sec

// == bit fixed point 2.14 format ===============================
// resolution 2^-14 =  6.1035e-5
// dynamic range is +1.9999/-2.0
typedef signed short fix14 ;
#define multfix14(a,b) ((fix14)((((long)(a))*((long)(b)))>>14)) //multiply two fixed 2.14
#define float2fix14(a) ((fix14)((a)*16384.0)) // 2^14
#define fix2float14(a) ((float)(a)/16384.0)
#define absfix14(a) abs(a)
// Filter_bank_size  from Filter_bank_coeff.h
volatile fix14 history_1[Filter_bank_size], history_2[Filter_bank_size] ; //output history
volatile fix14 history_in1[Filter_bank_size], history_in2[Filter_bank_size];
volatile fix14 filter_out[Filter_bank_size], thresh;
volatile float filter_out_float[Filter_bank_size];
volatile int ISR_time;
volatile int max, init_index, tempo;
volatile fix16 Q, Q3, Q1_5, Q0_75, Q0_25;

static fix16 cycle_period = float2fix16(0.00041545);

static char note[2] = {'N', ' '};

//================================================================


/***************************************************
  Code for the ECE 4760 final project of Mikayla Diesch (mld243),
  Shubha Sekar (ss2694), Maayan Kline (mok8), for a device to transcribe
  live music to a MIDI file. This is done by sampling the input from
  the microphone circuit, running it through a filter bank, and
  checking which bank has the greatest energy. The filter banks are
  tuned to the frequencies corresponding to the pitches within a
  given range. This data is then sent over UART using DMA to a
  client which cxollects this data and formats it as a MIDI file.
 ****************************************************/

// string buffer
char buffer[100];

// === thread structures ============================================
// thread control structs
// note that UART input and output are threads
static struct pt pt_timer, pt_filter_bank, pt_key, pt_DMA_output, pt_button, pt_init, pt_input;
volatile int time[1000];
volatile fix16 time_secs[1000];
volatile int notes[1000];
volatile int previous_max, idx, last_idx;
volatile int note_time; 


// system 1 second interval tick
int sys_time_seconds;

// === Timer Thread =================================================
// update a 1 second tick counter
static PT_THREAD (protothread_timer(struct pt *pt))
{
    PT_BEGIN(pt);
     tft_setCursor(0, 0);
     tft_setTextColor(ILI9340_WHITE);  tft_setTextSize(1);
     tft_writeString("Time in seconds since boot\n");
      while(1) {
        // yield time 1 second
        PT_YIELD_TIME_msec(1000) ;
        sys_time_seconds++ ;
        
        //draw sys_time
        tft_fillRoundRect(0,10, 100, 14, 1, ILI9340_BLACK);// x,y,w,h,radius,color
        tft_setCursor(0, 10);
        tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2);
        sprintf(buffer,"%d", sys_time_seconds);
        tft_writeString(buffer);
        // NEVER exit while
      } // END WHILE(1)
  PT_END(pt);
} // timer thread



void displayState(char s[]) {
    // draw key number
    tft_fillRoundRect(10, 290, 200, 28, 1, ILI9340_BLACK); // x,y,w,h,radius,color
    tft_setCursor(10, 290);
    tft_setTextColor(ILI9340_YELLOW);
    tft_setTextSize(4);
    tft_writeString(s);
}

void displayKey(int key) {

    // draw key number
    tft_fillRoundRect(30, 260, 200, 28, 1, ILI9340_BLACK); // x,y,w,h,radius,color
    tft_setCursor(30, 260);
    tft_setTextColor(ILI9340_YELLOW);
    tft_setTextSize(4);
    sprintf(buffer, "%d", key);
    tft_writeString(buffer);
}

volatile int UART_STATE = NOT_TRANSMITTING;

// === Filter Banks Thread =================================================
volatile int i, k;
static char cmd[16]; 
static int value;

static PT_THREAD (protothread_filter_bank(struct pt *pt))
{
    PT_BEGIN(pt);  
      while(1) {
        // yield time 1 second
        PT_YIELD_TIME_msec(10) ;

        tft_fillRoundRect(100,10, 100, 14, 1, ILI9340_BLACK); // x,y,w,h,radius,color
        tft_setCursor(100, 10);
        tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2);
        sprintf(buffer,"%d", ReadADC10(0));
        tft_writeString(buffer);

        if (max == -1) {
            note[0] = 'N';
            note[1] = ' ';
        }
        if (max == 0 || max == 12 || max == 24) note[0] = 'C'; 
        else if (max == 1 || max == 13 || max ==  25) {
            note[0] ='C';
            note[1] = '#';
        }
        else if (max == 2 || max == 14 || max ==  26) note[0] = 'D';
        else if (max == 3 || max == 15 || max ==  27) {
            note[0] ='D';
            note[1] = '#';
        }
        else if (max == 4 || max == 16 || max ==  28) note[0] = 'E'; 
        else if (max == 5 || max == 17 || max ==  29) note[0] = 'F'; 
        else if (max == 6 || max == 18 || max ==  30) {
            note[0] ='F';
            note[1] = '#';
        }
        else if (max ==7 || max == 19 || max ==  31) note[0] = 'G'; 
        else if (max ==8 || max == 20 || max ==  32) {
            note[0] ='G';
            note[1] = '#';
        }
        else if (max ==9 || max == 21 || max ==  33) note[0] = 'A'; 
        else if (max ==10 || max == 22 || max ==  34) {
            note[0] ='A';
            note[1] = '#';
        }
        else if (max == 11 || max == 23 || max ==  35) note[0] = 'B'; 
        
        // Print the note being played
        tft_fillRoundRect(0,100, 100, 14, 1, ILI9340_BLACK); // x,y,w,h,radius,color
        tft_setCursor(0, 100);
        tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2);
        sprintf(buffer,"%c %c", note[0], note[1]);
        tft_writeString(buffer);
        
        // Print the first note recorded and its duration
        tft_fillRoundRect(0,150, 100, 14, 1, ILI9340_BLACK); // x,y,w,h,radius,color
        tft_setCursor(0, 150);
        tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(2);
        sprintf(buffer,"tempo = %d", tempo);
        tft_writeString(buffer);
        
        // Print the threshold and the max filter bank value
        tft_fillRoundRect(0,200, 100, 50, 1, ILI9340_BLACK); // x,y,w,h,radius,color
        tft_setCursor(0, 200);
        tft_setTextColor(ILI9340_YELLOW); tft_setTextSize(1);
        sprintf(buffer,"note time= %f \n8thresh = %f", fix2float16(time_secs[idx-1]), fix2float16(Q0_25));
        tft_writeString(buffer);
        
        // send the data via DMA to serial
        if (last_idx != idx && (UART_STATE == TRANSMITTING)) {
            sprintf(PT_send_buffer, "%d,%d%s", notes[idx-1], time[idx-1], NEWLINE_CHARACTER);
            tft_writeString(PT_send_buffer);
            //sprintf(PT_send_buffer, "index = %d\n",idx);
            last_idx = idx; 
            //displayState("transmitting");
            // by spawning a print thread
            PT_SPAWN(pt, &pt_DMA_output, PT_DMA_PutSerialBuffer(&pt_DMA_output)); 
        }

        // NEVER exit while
      } // END WHILE(1)
  PT_END(pt);
} // filter banks thread

// === Init Thread =================================================

static PT_THREAD (protothread_init(struct pt *pt))
{
    PT_BEGIN(pt);  
    static int j = 0;
    static float cmd_num = 0;
        //spawn a thread to handle terminal input
        // the input thread waits for input
        // -- BUT does NOT block other threads
        // string is returned in "PT_term_buffer"
        PT_SPAWN(pt, &pt_input, PT_GetSerialBuffer(&pt_input) );
        // returns when the thread dies
        // in this case, when < enter > is pushed
        // now parse the string
        sscanf(PT_term_buffer, "%s %d", cmd, &value);

        while (cmd[j] != '\0') {
                cmd_num *= 10;
                cmd_num += cmd[j] - '0';
            j++;
        }
        
        tempo = cmd_num;
        Q = float2fix16(60.0/tempo);
        Q3 = multfix16(int2fix16(3), Q);
        Q1_5 = multfix16(float2fix16(1.5), Q);
        Q0_75 = multfix16(float2fix16(0.75), Q);
        Q0_25 = multfix16(float2fix16(0.5), Q);
  
  PT_END(pt);
}
 // init thread

//== Timer 2 interrupt handler ===================================
// ipl2 means "interrupt priority level 2"
// rate is 8000/sec
void __ISR(_TIMER_2_VECTOR, ipl2) Timer2ISR(void)
{
    fix14 output;
    static int i, input, j;
    thresh = float2fix14(0.004);
    // timing
    mPORTBSetPinsDigitalOut(BIT_13);
    mPORTBSetBits(BIT_13);
    // read the ADC
    input = (ReadADC10(0)-512)<<4;
    // read the first buffer position
    //channel0 = ReadADC10(0);   // read the result of channel 4 conversion from the idle buffer
 
    for (i=0; i< Filter_bank_size; i++) {
     
        // second order butterworth computed by matlab butter(1,[lower upper])
        output = multfix14(b1[i], input-history_in2[i]) + multfix14(history_1[i], -a2[i]) +
                 multfix14(history_2[i], -a3[i]) ;
        //output = input; 
        history_2[i] = history_1[i];
        history_1[i] = output;
        history_in2[i] = history_in1[i];
        history_in1[i] = input;
         
        // now lowpass the absolute value of the filter output
        // new_output = alpha*input + (1-alpha)*last_output
        // time constant about 64 samples
        filter_out[i] = (filter_out[i] + ((absfix14(output) - filter_out[i])>>6));
        //pow[i] = filter_out[i]>>3;
        
        if (filter_out[i] > filter_out[max]) { 
            max = i;
        }
        // if no entry in the filter bank is greater than thresh, register silence
        if (filter_out[max] < thresh) {
            max = -1;
        }
    }
    
    time_secs[idx] = multfix16(cycle_period, int2fix16(note_time));
    
    // if this note has been playing since the last sample, increment time
    if (previous_max == max || time_secs[idx] < Q0_25) {
        note_time++;
    }
    
    if (time_secs[idx] >= Q3) {
        notes[idx] = previous_max;
        time[idx] = 1;
        note_time = 0;
        idx++;
        previous_max = max;
    }

    // else a new note is playing
    // store note info
    if (previous_max != max && time_secs[idx] > Q0_25) {
        notes[idx] = previous_max;
        //time[idx] = multfix16(int2fix16(note_time), cycle_period);
        //time[idx] = note_time;
        if (time_secs[idx] >= Q3) {
            time[idx] = 1;
        }
        if (time_secs[idx] >= Q1_5 && time_secs[idx] < Q3) {
            time[idx] = 2;
        }
        if (time_secs[idx] >= Q0_75 && time_secs[idx] < Q1_5) {
            time[idx] = 4;
        }
        if (time_secs[idx] < Q0_75) {
            time[idx] = 8;
        }
        note_time = 0;
        idx++;
        previous_max = max;
    }
    
    mPORTBClearBits(BIT_13);
    
    // clear the interrupt flag
    ISR_time = ReadTimer2();
    mT2ClearIntFlag();
    
}
// end interrupt


//== Push Button FSM Thread ===================================
static PT_THREAD (protothread_button(struct pt *pt))
{
    PT_BEGIN(pt); 
    //displayState("In thread");
    static int STATE = RELEASE;
    static int PRESSED = 0;
    
    // PortB as inputs
    mPORTBSetPinsDigitalIn(BIT_7); //Set port as input
    // Enable 10K pulldown resistors on B port pins
    EnablePullDownB(BIT_7);
    while(1)
    {
        PRESSED = mPORTBReadBits(BIT_7);
        //displayKey(PRESSED);
        switch(STATE) {
            case RELEASE: 
                //displayState("RELEASE");
                if ( PRESSED ) {
                    STATE = MAYBE_PRESSED;
                }
                break;
            
            case MAYBE_PRESSED: 
                //displayState("MAYBE_PRESSED");
                if( !PRESSED )
                    STATE = RELEASE;
                else {
                    STATE = STILL_PRESSED;
                    if ( UART_STATE == TRANSMITTING )
                    {
                        displayState("NOT_TRASMITTING");
                        sprintf(PT_send_buffer, TERMINATION_STR);// by spawning a print thread
                        PT_SPAWN(pt, &pt_DMA_output, PT_DMA_PutSerialBuffer(&pt_DMA_output));
                        UART_STATE = NOT_TRANSMITTING;                       
                        //Disable time interrupt 
                        DisableIntT2; 
                    }
                    else {
                        displayState("TRANSMITTING");
                        UART_STATE = TRANSMITTING;                       
                        //Enable time interrupt 
                        EnableIntT2; 
                    }
                }
                break;
            
            case STILL_PRESSED: 
                //displayState("STILL_PRESSED");
                if ( !PRESSED ) 
                    STATE = MAYBE_RELEASED;
                break;
            
            case MAYBE_RELEASED:
                //displayState("MAYBE_RELEASED");
                if ( !PRESSED )
                    STATE = RELEASE;
                else
                    STATE = STILL_PRESSED;
                break;
                
            default: 
                STATE = RELEASE;
                break;
        }

        // yield time
        PT_YIELD_TIME_msec(30);
    } // END WHILE(1)
  PT_END(pt);
} // Push button FSM thread

// === Main  ======================================================
void main(void) {
    
    static char cmd[16]; 
    static int value;
    static int j = 0;
    static float cmd_num = 0;
    static int is_float = 0;
    static int mag = -1;
    SYSTEMConfigPerformance(PBCLK);

    ANSELA = 0; ANSELB = 0; CM1CON = 0; CM2CON = 0;

    // the ADC ///////////////////////////////////////
    // configure and enable the ADC
    CloseADC10();   // ensure the ADC is off before setting the configuration

    // define  setup parameters for OpenADC10
    // Turn module on | ouput in integer | trigger mode auto | enable autosample
    // ADC_CLK_AUTO -- Internal counter ends sampling and starts conversion (Auto convert)
    // ADC_AUTO_SAMPLING_ON -- Sampling begins immediately after last conversion completes; SAMP bit is automatically set
    // ADC_AUTO_SAMPLING_OFF -- Sampling begins with AcquireADC10();
    #define PARAM1  ADC_FORMAT_INTG16 | ADC_CLK_AUTO | ADC_AUTO_SAMPLING_ON//

    // define setup parameters for OpenADC10
    // ADC ref external  | disable offset test | disable scan mode | do 1 sample | use single buf | alternate mode off
    #define PARAM2  ADC_VREF_AVDD_AVSS | ADC_OFFSET_CAL_DISABLE | ADC_SCAN_OFF | ADC_SAMPLES_PER_INT_1 | ADC_ALT_BUF_OFF | ADC_ALT_INPUT_OFF

    // Define setup parameters for OpenADC10
    // use peripheral bus clock | set sample time | set ADC clock divider
    // ADC_CONV_CLK_Tcy2 means divide CLK_PB by 2 (max speed)
    // ADC_SAMPLE_TIME_5 seems to work with a source resistance < 1kohm
    // At PB clock 30 MHz, divide by two for ADC_CONV_CLK gives 66 nSec
    #define PARAM3 ADC_CONV_CLK_PB | ADC_SAMPLE_TIME_5 | ADC_CONV_CLK_Tcy2 //ADC_SAMPLE_TIME_15| ADC_CONV_CLK_Tcy2

    // define setup parameters for OpenADC10
    // set AN9 and  as analog inputs
    #define PARAM4  ENABLE_AN9_ANA

    // define setup parameters for OpenADC10
    // do not assign channels to scan
    #define PARAM5  SKIP_SCAN_ALL

    // use ground as neg ref for A | use AN9 for input A
    // configure to sample AN9 pin26
    SetChanADC10( ADC_CH0_NEG_SAMPLEA_NVREF | ADC_CH0_POS_SAMPLEA_AN9 ); // configure to sample AN9
    OpenADC10( PARAM1, PARAM2, PARAM3, PARAM4, PARAM5 ); // configure ADC using the parameters defined above

    EnableADC10(); // Enable the ADC

    // timer interrupt //////////////////////////
    // Set up timer2 on,  interrupts, internal clock, prescalar 1, toggle rate
    // at 40 MHz PB clock 
    // 5000 is 8 ksamples/sec
    OpenTimer2(T2_ON | T2_SOURCE_INT | T2_PS_1_1, 16618);

    // set up the timer interrupt with a priority of 2
    ConfigIntTimer2(T2_INT_ON | T2_INT_PRIOR_2);
    mT2ClearIntFlag(); // and clear the interrupt flag
    //Disable time interrupt 
    DisableIntT2;

    // === config threads ==========
    // turns OFF UART support and debugger pin
    PT_setup();

    // === setup system wide interrupts  ========
    INTEnableSystemMultiVectoredInt();

    // init the threads
    PT_INIT(&pt_timer);
    PT_INIT(&pt_filter_bank);
    PT_INIT(&pt_button);
    PT_INIT(&pt_init);

    // init the display
    tft_init_hw();
    PPSOutput(2, RPA1, SDO1);
    tft_begin();
    tft_fillScreen(ILI9340_BLACK);
    //240x320 vertical display
    tft_setRotation(0); // Use tft_setRotation(1) for 320x240

    // seed random color
    srand(1);

    note_time = 0;
    last_idx = 0;
    idx = 0;
    max = -1;
    previous_max = -1;
    init_index = 0;
    thresh = 0;
    
    // round-robin scheduler for threads
    while (1){
        PT_SCHEDULE(protothread_init(&pt_init));
        PT_SCHEDULE(protothread_timer(&pt_timer));
        PT_SCHEDULE(protothread_button(&pt_button));
        PT_SCHEDULE(protothread_filter_bank(&pt_filter_bank));
    }
} // main

// === end  ======================================================


							
config.h:
                                /*
 * File:   config.h
 * Author: Syed Tahmid Mahbub, Bruce Land
 * Modifed by: Mikayla Diesch (mld243), Shubha Sekar (ss2694), Maayan Kline (mok8)
 */

#ifndef CONFIG_H
#define CONFIG_H
#define _SUPPRESS_PLIB_WARNING 1
#include <plib.h>
// serial stuff
#include <stdio.h>

//=============================================================
// 40 MHz
#pragma config FNOSC = FRCPLL, POSCMOD = OFF
#pragma config FPLLIDIV = DIV_2, FPLLMUL = MUL_20 //40 MHz
#pragma config FPBDIV = DIV_1, FPLLODIV = DIV_2 // PB 40 MHz
#pragma config FWDTEN = OFF,  FSOSCEN = OFF, JTAGEN = OFF

//==============================================================
// Protothreads configure

// IF use_vref_debug IS defined, pin 25 is Vref output
//#define use_vref_debug

// IF use_uart_serial IS defined, pin 21 and pin 22 are used by the uart
#define use_uart_serial
#define BAUDRATE 9600 // must match PC terminal emulator setting

/////////////////////////////////
// set up clock parameters
// system cpu clock
#define sys_clock 40000000

// sys_clock/FPBDIV
#define pb_clock sys_clock // divide by one in this case

#endif  /* CONFIG_H */


                            
Filter_bandpass_14bit.h:
                                #define Filter_bank_size 37
const int b1[37]={
     138,
     146,
     155,
     164,
     174,
     184,
     195,
     206,
     219,
     231,
     245,
     259,
     275,
     291,
     308,
     326,
     345,
     365,
     386,
     408,
     432,
     457,
     483,
     511,
     541,
     572,
     605,
     639,
     676,
     714,
     755,
     798,
     843,
     890,
     940,
     993,
    1048};
const int a2[37]={
  -30616,
  -30373,
  -30103,
  -29802,
  -29467,
  -29093,
  -28677,
  -28214,
  -27698,
  -27125,
  -26488,
  -25781,
  -24997,
  -24127,
  -23164,
  -22099,
  -20924,
  -19629,
  -18204,
  -16641,
  -14929,
  -13061,
  -11030,
   -8829,
   -6455,
   -3908,
   -1193,
    1680,
    4697,
    7830,
   11043,
   14289,
   17503,
   20608,
   23504,
   26078,
   28193};
const int a3[37]={
   16106,
   16090,
   16073,
   16054,
   16035,
   16014,
   15993,
   15970,
   15945,
   15920,
   15893,
   15864,
   15833,
   15801,
   15767,
   15731,
   15693,
   15653,
   15611,
   15566,
   15519,
   15469,
   15416,
   15360,
   15301,
   15239,
   15173,
   15104,
   15031,
   14954,
   14873,
   14787,
   14697,
   14602,
   14502,
   14397,
   14286};

                            
pt_cornell_1_2_1_custom.h:
                                /*
 * File:   pt_cornell_1_2_1.h
 * Author: brl4
 * Modified by: Mikayla Diesch (mld243), Shubha Sekar (ss2694), Maayan Kline (mok8)
 * Created on Sept 22, 2015
 */

/*
 * Copyright (c) 2004-2005, Swedish Institute of Computer Science.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * This file is part of the Contiki operating system.
 *
 * Author: Adam Dunkels <adam@sics.se>
 *
 * $Id: pt.h,v 1.7 2006/10/02 07:52:56 adam Exp $
 */
#include <plib.h>
/**
 * \addtogroup pt
 * @{
 */

/**
 * \file
 * Protothreads implementation.
 * \author
 * Adam Dunkels <adam@sics.se>
 *
 */

#ifndef __PT_H__
#define __PT_H__

////////////////////////
//#include "lc.h"
////////////////////////
/**
 * \file lc.h
 * Local continuations
 * \author
 * Adam Dunkels <adam@sics.se>
 *
 */

#ifdef DOXYGEN
/**
 * Initialize a local continuation.
 *
 * This operation initializes the local continuation, thereby
 * unsetting any previously set continuation state.
 *
 * \hideinitializer
 */
#define LC_INIT(lc)

/**
 * Set a local continuation.
 *
 * The set operation saves the state of the function at the point
 * where the operation is executed. As far as the set operation is
 * concerned, the state of the function does not include the
 * call-stack or local (automatic) variables, but only the program
 * counter and such CPU registers that needs to be saved.
 *
 * \hideinitializer
 */
#define LC_SET(lc)

/**
 * Resume a local continuation.
 *
 * The resume operation resumes a previously set local continuation, thus
 * restoring the state in which the function was when the local
 * continuation was set. If the local continuation has not been
 * previously set, the resume operation does nothing.
 *
 * \hideinitializer
 */
#define LC_RESUME(lc)

/**
 * Mark the end of local continuation usage.
 *
 * The end operation signifies that local continuations should not be
 * used any more in the function. This operation is not needed for
 * most implementations of local continuation, but is required by a
 * few implementations.
 *
 * \hideinitializer
 */
#define LC_END(lc)

/**
 * \var typedef lc_t;
 *
 * The local continuation type.
 *
 * \hideinitializer
 */
#endif /* DOXYGEN */

//#ifndef __LC_H__
//#define __LC_H__


//#ifdef LC_INCLUDE
//#include LC_INCLUDE
//#else

/////////////////////////////
//#include "lc-switch.h"
/////////////////////////////

//#ifndef __LC_SWITCH_H__
//#define __LC_SWITCH_H__

/* WARNING! lc implementation using switch() does not work if an
 LC_SET() is done within another switch() statement! */

/** \hideinitializer */
/*
 typedef unsigned short lc_t;
 
 #define LC_INIT(s) s = 0;
 
 #define LC_RESUME(s) switch(s) { case 0:
 
 #define LC_SET(s) s = __LINE__; case __LINE__:
 
 #define LC_END(s) }
 
 #endif /* __LC_SWITCH_H__ */

/** @} */

//#endif /* LC_INCLUDE */

//#endif /* __LC_H__ */

/** @} */
/** @} */

/////////////////////////////
//#include "lc-addrlabels.h"
/////////////////////////////

#ifndef __LC_ADDRLABELS_H__
#define __LC_ADDRLABELS_H__

/** \hideinitializer */
typedef void * lc_t;

#define LC_INIT(s) s = NULL

#define LC_RESUME(s)                \
do {                        \
if(s != NULL) {             \
goto *s;                    \
}                       \
} while(0)

#define LC_CONCAT2(s1, s2) s1##s2
#define LC_CONCAT(s1, s2) LC_CONCAT2(s1, s2)

#define LC_SET(s)               \
do {                        \
LC_CONCAT(LC_LABEL, __LINE__):              \
(s) = &&LC_CONCAT(LC_LABEL, __LINE__);  \
} while(0)

#define LC_END(s)

#endif /* __LC_ADDRLABELS_H__ */



//////////////////////////////////////////
struct pt {
    lc_t lc;
    int pri;
};

#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED  2
#define PT_ENDED   3

/**
 * \name Initialization
 * @{
 */

/**
 * Initialize a protothread.
 *
 * Initializes a protothread. Initialization must be done prior to
 * starting to execute the protothread.
 *
 * \param pt A pointer to the protothread control structure.
 *
 * \sa PT_SPAWN()
 *
 * \hideinitializer
 */
#define PT_INIT(pt)   LC_INIT((pt)->lc)

/** @} */

/**
 * \name Declaration and definition
 * @{
 */

/**
 * Declaration of a protothread.
 *
 * This macro is used to declare a protothread. All protothreads must
 * be declared with this macro.
 *
 * \param name_args The name and arguments of the C function
 * implementing the protothread.
 *
 * \hideinitializer
 */
#define PT_THREAD(name_args) char name_args

/**
 * Declare the start of a protothread inside the C function
 * implementing the protothread.
 *
 * This macro is used to declare the starting point of a
 * protothread. It should be placed at the start of the function in
 * which the protothread runs. All C statements above the PT_BEGIN()
 * invokation will be executed each time the protothread is scheduled.
 *
 * \param pt A pointer to the protothread control structure.
 *
 * \hideinitializer
 */
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)

/**
 * Declare the end of a protothread.
 *
 * This macro is used for declaring that a protothread ends. It must
 * always be used together with a matching PT_BEGIN() macro.
 *
 * \param pt A pointer to the protothread control structure.
 *
 * \hideinitializer
 */
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \
PT_INIT(pt); return PT_ENDED; }

/** @} */

/**
 * \name Blocked wait
 * @{
 */

/**
 * Block and wait until condition is true.
 *
 * This macro blocks the protothread until the specified condition is
 * true.
 *
 * \param pt A pointer to the protothread control structure.
 * \param condition The condition.
 *
 * \hideinitializer
 */
#define PT_WAIT_UNTIL(pt, condition)            \
do {                        \
LC_SET((pt)->lc);               \
if(!(condition)) {              \
return PT_WAITING;          \
}                       \
} while(0)

/**
 * Block and wait while condition is true.
 *
 * This function blocks and waits while condition is true. See
 * PT_WAIT_UNTIL().
 *
 * \param pt A pointer to the protothread control structure.
 * \param cond The condition.
 *
 * \hideinitializer
 */
#define PT_WAIT_WHILE(pt, cond)  PT_WAIT_UNTIL((pt), !(cond))

/** @} */

/**
 * \name Hierarchical protothreads
 * @{
 */

/**
 * Block and wait until a child protothread completes.
 *
 * This macro schedules a child protothread. The current protothread
 * will block until the child protothread completes.
 *
 * \note The child protothread must be manually initialized with the
 * PT_INIT() function before this function is used.
 *
 * \param pt A pointer to the protothread control structure.
 * \param thread The child protothread with arguments
 *
 * \sa PT_SPAWN()
 *
 * \hideinitializer
 */
#define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread))

/**
 * Spawn a child protothread and wait until it exits.
 *
 * This macro spawns a child protothread and waits until it exits. The
 * macro can only be used within a protothread.
 *
 * \param pt A pointer to the protothread control structure.
 * \param child A pointer to the child protothread's control structure.
 * \param thread The child protothread with arguments
 *
 * \hideinitializer
 */
#define PT_SPAWN(pt, child, thread)     \
do {                        \
PT_INIT((child));               \
PT_WAIT_THREAD((pt), (thread));     \
} while(0)

/** @} */

/**
 * \name Exiting and restarting
 * @{
 */

/**
 * Restart the protothread.
 *
 * This macro will block and cause the running protothread to restart
 * its execution at the place of the PT_BEGIN() call.
 *
 * \param pt A pointer to the protothread control structure.
 *
 * \hideinitializer
 */
#define PT_RESTART(pt)              \
do {                        \
PT_INIT(pt);                \
return PT_WAITING;          \
} while(0)

/**
 * Exit the protothread.
 *
 * This macro causes the protothread to exit. If the protothread was
 * spawned by another protothread, the parent protothread will become
 * unblocked and can continue to run.
 *
 * \param pt A pointer to the protothread control structure.
 *
 * \hideinitializer
 */
#define PT_EXIT(pt)             \
do {                        \
PT_INIT(pt);                \
return PT_EXITED;           \
} while(0)

/** @} */

/**
 * \name Calling a protothread
 * @{
 */

/**
 * Schedule a protothread.
 *
 * This function shedules a protothread. The return value of the
 * function is non-zero if the protothread is running or zero if the
 * protothread has exited.
 *
 * \param f The call to the C function implementing the protothread to
 * be scheduled
 *
 * \hideinitializer
 */
#define PT_SCHEDULE(f) ((f) < PT_EXITED)
//#define PT_SCHEDULE(f) ((f))

/** @} */

/**
 * \name Yielding from a protothread
 * @{
 */

/**
 * Yield from the current protothread.
 *
 * This function will yield the protothread, thereby allowing other
 * processing to take place in the system.
 *
 * \param pt A pointer to the protothread control structure.
 *
 * \hideinitializer
 */
#define PT_YIELD(pt)                \
do {                        \
PT_YIELD_FLAG = 0;              \
LC_SET((pt)->lc);               \
if(PT_YIELD_FLAG == 0) {            \
return PT_YIELDED;          \
}                       \
} while(0)

/**
 * \brief      Yield from the protothread until a condition occurs.
 * \param pt   A pointer to the protothread control structure.
 * \param cond The condition.
 *
 *             This function will yield the protothread, until the
 *             specified condition evaluates to true.
 *
 *
 * \hideinitializer
 */
#define PT_YIELD_UNTIL(pt, cond)        \
do {                        \
PT_YIELD_FLAG = 0;              \
LC_SET((pt)->lc);               \
if((PT_YIELD_FLAG == 0) || !(cond)) {   \
return PT_YIELDED;                        \
}                       \
} while(0)

/** @} */

#endif /* __PT_H__ */

#ifndef __PT_SEM_H__
#define __PT_SEM_H__

//#include "pt.h"

struct pt_sem {
    unsigned int count;
};

/**
 * Initialize a semaphore
 *
 * This macro initializes a semaphore with a value for the
 * counter. Internally, the semaphores use an "unsigned int" to
 * represent the counter, and therefore the "count" argument should be
 * within range of an unsigned int.
 *
 * \param s (struct pt_sem *) A pointer to the pt_sem struct
 * representing the semaphore
 *
 * \param c (unsigned int) The initial count of the semaphore.
 * \hide initializer
 */
#define PT_SEM_INIT(s, c) (s)->count = c

/**
 * Wait for a semaphore
 *
 * This macro carries out the "wait" operation on the semaphore. The
 * wait operation causes the protothread to block while the counter is
 * zero. When the counter reaches a value larger than zero, the
 * protothread will continue.
 *
 * \param pt (struct pt *) A pointer to the protothread (struct pt) in
 * which the operation is executed.
 *
 * \param s (struct pt_sem *) A pointer to the pt_sem struct
 * representing the semaphore
 *
 * \hideinitializer
 */
#define PT_SEM_WAIT(pt, s)  \
do {                        \
PT_WAIT_UNTIL(pt, (s)->count > 0);      \
--(s)->count;               \
} while(0)

/**
 * Signal a semaphore
 *
 * This macro carries out the "signal" operation on the semaphore. The
 * signal operation increments the counter inside the semaphore, which
 * eventually will cause waiting protothreads to continue executing.
 *
 * \param pt (struct pt *) A pointer to the protothread (struct pt) in
 * which the operation is executed.
 *
 * \param s (struct pt_sem *) A pointer to the pt_sem struct
 * representing the semaphore
 *
 * \hideinitializer
 */
#define PT_SEM_SIGNAL(pt, s) ++(s)->count

#endif /* __PT_SEM_H__ */

//=====================================================================
//=== BRL4 additions for PIC 32 =======================================
//=====================================================================

// macro to time a thread execution interveal in millisec
// max time 4000 sec
//#include <plib.h>
//#include <limits.h>
//#include "config.h"

#define PT_YIELD_TIME_msec(delay_time)  \
do { static unsigned int time_thread ;\
time_thread = time_tick_millsec + (unsigned int)delay_time ; \
PT_YIELD_UNTIL(pt, (time_tick_millsec >= time_thread)); \
} while(0);

// macro to return system time
#define PT_GET_TIME() (time_tick_millsec)

// init rate sehcduler
//#define PT_INIT(pt, priority)   LC_INIT((pt)->lc ; (pt)->pri = priority)
//PT_PRIORITY_INIT
#define PT_RATE_INIT() int pt_pri_count = 0;
// maitain proority frame count
//PT_PRIORITY_LOOP maitains a counter used to control execution
#define PT_RATE_LOOP() pt_pri_count = (pt_pri_count+1) & 0xf ;
// schecedule priority thread
//PT_PRIORITY_SCHEDULE
// 5 levels
// rate 0 is highest -- every time thru loop
// priority 1 -- every 2 times thru loop
// priority 2 -- every 4 times thru loop
//  3 is  -- every 8 times thru loop
#define PT_RATE_SCHEDULE(f,rate) \
if((rate==0) | \
(rate==1 && ((pt_pri_count & 0b1)==0) ) | \
(rate==2 && ((pt_pri_count & 0b11)==0) ) | \
(rate==3 && ((pt_pri_count & 0b111)==0)) | \
(rate==4 && ((pt_pri_count & 0b1111)==0))) \
PT_SCHEDULE(f);

// macro to use 4 bit DAC as debugger output
// level range 0-15; duration in microseconds
// -- with zero meaning HOLD it on forever
//while((signed int)ReadTimer45() <= time_hold){};
// time_hold = duration + ReadTimer45() ;
#define PT_DEBUG_VALUE(level, duration) \
do { static int i ; \
CVRCON = CVRCON_setup | (level & 0xf); \
if (duration>0){                   \
for (i=0; i < duration*7; i++){};\
CVRCON = CVRCON_setup; \
} \
} while(0);

// macros to manipulate a semaphore without blocking
#define PT_SEM_SET(s) (s)->count=1
#define PT_SEM_CLEAR(s) (s)->count=0
#define PT_SEM_READ(s) (s)->count
#define PT_SEM_ACCEPT(s) \
s->count; \
if (s->count) s->count-- ; \

//====================================================================
//=== serial setup ===================================================
//#ifdef use_uart_serial
///////////////////////////
// UART parameters

#define PB_DIVISOR (1 << OSCCONbits.PBDIV) // read the peripheral bus divider, FPBDIV
#define PB_FREQ sys_clock/PB_DIVISOR // periperhal bus frequency
#define clrscr() printf( "\x1b[2J")
#define home()   printf( "\x1b[H")
#define pcr()    printf( '\r')
#define crlf     putchar(0x0a); putchar(0x0d);
#define backspace 0x7f // make sure your backspace matches this!
#define max_chars 64 // for input/output buffer
//====================================================================
// build a string from the UART2 /////////////
//////////////////////////////////////////////
char PT_term_buffer[max_chars];
int num_char;
int PT_GetSerialBuffer(struct pt *pt)
{
    static char character;
    // mark the beginnning of the input thread
    PT_BEGIN(pt);
    
    num_char = 0;
    //memset(term_buffer, 0, max_chars);
    
    while(num_char < max_chars)
    {
        // get the character
        // yield until there is a valid character so that other
        // threads can execute
        PT_YIELD_UNTIL(pt, UARTReceivedDataIsAvailable(UART2));
        // while(!UARTReceivedDataIsAvailable(UART2)){};
        character = UARTGetDataByte(UART2);
        //PT_YIELD_UNTIL(pt, UARTTransmitterIsReady(UART2));
        //UARTSendDataByte(UART2, character);
        
        // unomment to check backspace character!!!
        //printf("--%x--",character );
        
        // end line
        if(character == '\r'){
            PT_term_buffer[num_char] = 0; // zero terminate the string
            //crlf; // send a new line
            //PT_YIELD_UNTIL(pt, UARTTransmitterIsReady(UART2));
            //UARTSendDataByte(UART2, '\n');
            break;
        }
        // backspace
        /*else if (character == backspace){
            PT_YIELD_UNTIL(pt, UARTTransmitterIsReady(UART2));
            UARTSendDataByte(UART2, ' ');
            PT_YIELD_UNTIL(pt, UARTTransmitterIsReady(UART2));
            UARTSendDataByte(UART2, backspace);
            num_char--;
            // check for buffer underflow
            if (num_char<0) {num_char = 0 ;}
        }*/
        else  {PT_term_buffer[num_char++] = character ;}
        //if (character == backspace)
        
    } //end while(num_char < max_size)
    
    // kill this input thread, to allow spawning thread to execute
    PT_EXIT(pt);
    // and indicate the end of the thread
    PT_END(pt);
}

//====================================================================
// === send a string to the UART2 ====================================
char PT_send_buffer[max_chars];
int num_send_chars ;
int PutSerialBuffer(struct pt *pt)
{
    PT_BEGIN(pt);
    num_send_chars = 0;
    while (PT_send_buffer[num_send_chars] != 0){
        PT_YIELD_UNTIL(pt, UARTTransmitterIsReady(UART2));
        UARTSendDataByte(UART2, PT_send_buffer[num_send_chars]);
        num_send_chars++;
    }
    // kill this output thread, to allow spawning thread to execute
    PT_EXIT(pt);
    // and indicate the end of the thread
    PT_END(pt);
}

//====================================================================
// === DMA send string to the UART2 ==================================
int PT_DMA_PutSerialBuffer(struct pt *pt)
{
    PT_BEGIN(pt);
    //mPORTBSetBits(BIT_0);
    // check for null string
    if (PT_send_buffer[0]==0)PT_EXIT(pt);
    // sent the first character
    PT_YIELD_UNTIL(pt, UARTTransmitterIsReady(UART2));
    UARTSendDataByte(UART2, PT_send_buffer[0]);
    //DmaChnStartTxfer(DMA_CHANNEL1, DMA_WAIT_NOT, 0);
    // start the DMA
    DmaChnEnable(DMA_CHANNEL1);
    // wait for DMA done
    //mPORTBClearBits(BIT_0);
    PT_YIELD_UNTIL(pt, DmaChnGetEvFlags(DMA_CHANNEL1) & DMA_EV_BLOCK_DONE);
    //wait until the transmit buffer is empty
    PT_YIELD_UNTIL(pt, U2STA&0x100);
    
    // kill this output thread, to allow spawning thread to execute
    PT_EXIT(pt);
    // and indicate the end of the thread
    PT_END(pt);
}
//#endif //#ifdef use_uart_serial

//======================================================================
// vref confing (if used)
int CVRCON_setup ;

// system time
volatile unsigned int time_tick_millsec ;

// Timer 5 interrupt handler ///////
// ipl2 means "interrupt priority level 2"
void __ISR(_TIMER_5_VECTOR, IPL2AUTO) Timer5Handler(void) //_TIMER_5_VECTOR
{
    // clear the interrupt flag
    mT5ClearIntFlag();
    //count milliseconds
    time_tick_millsec++ ;
}

void PT_setup (void)
{
    // Configure the device for maximum performance but do not change the PBDIV
    // Given the options, this function will change the flash wait states, RAM
    // wait state and enable prefetch cache but will not change the PBDIV.
    // The PBDIV value is already set via the pragma FPBDIV option above..
    SYSTEMConfig(sys_clock, SYS_CFG_WAIT_STATES | SYS_CFG_PCACHE);
    
    ANSELA =0; //make sure analog is cleared
    ANSELB =0;
    
#ifdef use_uart_serial
    // === init the uart2 ===================
    PPSInput (2, U2RX, RPB11); //Assign U2RX to pin RPB11 -- Physical pin 22 on 28 PDIP
    PPSOutput(4, RPB10, U2TX); //Assign U2TX to pin RPB10 -- Physical pin 21 on 28 PDIP
    UARTConfigure(UART2, UART_ENABLE_PINS_TX_RX_ONLY);
    UARTSetLineControl(UART2, UART_DATA_SIZE_8_BITS | UART_PARITY_NONE | UART_STOP_BITS_1);
    UARTSetDataRate(UART2, pb_clock, BAUDRATE);
    UARTEnable(UART2, UART_ENABLE_FLAGS(UART_PERIPHERAL | UART_RX | UART_TX));
    //printf("\n\r..protothreads start..\n\r");
    // === set up DMA for UART output =========
    // configure the channel and enable end-on-match
    DmaChnOpen(DMA_CHANNEL1, DMA_CHN_PRI2, DMA_OPEN_MATCH);
    // trigger a byte everytime the UART is empty
    DmaChnSetEventControl(DMA_CHANNEL1, DMA_EV_START_IRQ_EN|DMA_EV_MATCH_EN|DMA_EV_START_IRQ(_UART2_TX_IRQ));
    // source and destination
    DmaChnSetTxfer(DMA_CHANNEL1, PT_send_buffer+1, (void*)&U2TXREG, max_chars, 1, 1);
    // signal when done
    DmaChnSetEvEnableFlags(DMA_CHANNEL1, DMA_EV_BLOCK_DONE);
    // set null as ending character (of a string)
    DmaChnSetMatchPattern(DMA_CHANNEL1, 0x00);
#endif //#ifdef use_uart_serial
    
    // ===Set up timer5 ======================
    // timer 5: on,  interrupts, internal clock,
    // set up to count millsec
    OpenTimer5(T5_ON  | T5_SOURCE_INT | T5_PS_1_1 , pb_clock/1000);
    // set up the timer interrupt with a priority of 2
    ConfigIntTimer5(T5_INT_ON | T5_INT_PRIOR_2);
    mT5ClearIntFlag(); // and clear the interrupt flag
    // zero the system time tick
    time_tick_millsec = 0;
    
    //=== Set up VREF as a debugger output =======
#ifdef use_vref_debug
    // set up the Vref pin and use as a DAC
    // enable module| eanble output | use low range output | use internal reference | desired step
    CVREFOpen( CVREF_ENABLE | CVREF_OUTPUT_ENABLE | CVREF_RANGE_LOW | CVREF_SOURCE_AVDD | CVREF_STEP_0 );
    // And read back setup from CVRCON for speed later
    // 0x8060 is enabled with output enabled, Vdd ref, and 0-0.6(Vdd) range
    CVRCON_setup = CVRCON; //CVRCON = 0x8060 from Tahmid http://tahmidmc.blogspot.com/
    
#endif //#ifdef use_vref_debug
    
}
                            
Client-side python application:
import sys
import os
import logging
import serial
import serial.threaded
from serial.threaded import ReaderThread
from serial.threaded import LineReader
from midiutil.MidiFile import MIDIFile

TERMINATION_CHARACTER = 'end\r\n'
NEWLINE_CHARACTER = '\r\n'

class InvalidSerialDataException(Exception):
    pass

def parseData( data ):
    if ( data[-2:] != NEWLINE_CHARACTER ):
        raise InvalidSerialDataException('Invalid endline character: [%s]'.format(data[-2:]))
    data = data.replace('\0', '')

    pitch    = int( data[0:data.find(',')] )
    duration = int( data[data.find(',') + 1 : data.find(NEWLINE_CHARACTER)] )
    if ( pitch != -1 ):
        pitch = pitch + 48

    return { 'pitch': pitch, 'duration': duration }

if __name__ == '__main__':

    import argparse

    parser = argparse.ArgumentParser(
        description="ECE 4760 Final Project python serial-to-file writer")

    parser.add_argument('SERIALPORT')
    parser.add_argument(
            'TEMPO',
            type=int,
            help='the tempo at which the song is being played, default: %(default)s',
            default=60)

    parser.add_argument(
            '-p', '--filepath',
            type=str,
            help='the filepath and filename relative to the current directory, default: %(default)s',
            metavar='FILEPATH',
            default='music.mid'
            )

    parser.add_argument(
            '-v', '--verbose',
            dest='verbosity',
            action='count',
            help='print more diagnostic messages (option can be given multiple times)',
            default=0
            )

    parser.add_argument(
            '-b', '--baud',
            dest='BAUD',
            help='BAUD rate to use for transmission, default: %(default)s',
            default=9600
            )

    parser.add_argument(
            '-d', '--debug',
            dest='debug',
            action='store_true',
            help='print diagnostic messages on debug level'
            )

    args = parser.parse_args()

    if args.verbosity > 3:
        args.verbosity = 3
    level = (
            logging.WARNING,
            logging.INFO,
            logging.DEBUG,
            logging.NOTSET,
            )[args.verbosity]
    logging.basicConfig(level=logging.DEBUG, filename='serial.log')
    if args.debug:
        level = logging.DEBUG
    logging.getLogger('initialization').setLevel(level)
   
    device_path = args.SERIALPORT
    logging.debug('device path set to %s', device_path)

    # Note, filepath is relative to current location
    output_filepath = args.filepath

    # Open file, append _{number} if file exists 
    filepath_append = ''
    output_file_extension = output_filepath[output_filepath.rfind('.'):]
    output_filepath = output_filepath[0:output_filepath.rfind('.')]
    i = 0
    while os.path.exists(output_filepath + filepath_append + output_file_extension):
        i = i + 1
        filepath_append = '_{}'.format(i)
    output_filepath = output_filepath + filepath_append + output_file_extension
    logging.debug('output filepath set to be %s', output_filepath)

    ser = serial.Serial(
        port=args.SERIALPORT,
        baudrate=args.BAUD,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE,
        bytesize=serial.EIGHTBITS,
        rtscts = False,
        dsrdtr = False
    )
    ser.write(str(args.TEMPO) + NEWLINE_CHARACTER);
    
    logging.info('port opened')
    midiFile = MIDIFile(1)
    track = 0
    time = 0

    midiFile.addTempo(track, time, int(args.TEMPO))

    channel = 0
    volume = 100

    sys.stdout.write('A connection has been made with the device. Collecting information...\n')
    done = False

    while( not done ):
        data = ser.readline()
        logging.debug('line received: {}'.format(repr(data)))
        if ( data.replace('\0', '') == TERMINATION_CHARACTER ):
            done = True
        else:
            data = parseData( data )
            if ( data['pitch'] != -1 ):
                logging.debug('Adding note: pitch=%d duration=%d, time=%f', data['pitch'], data['duration'], time)
                midiFile.addNote(track, channel, data['pitch'], time, 1 / data['duration'], volume)
            time = time + ( 4.0 / data['duration'] * 60 / args.TEMPO )
            logging.debug('time: %f', time)

    sys.stdout.write('Looks like we\'re done here. Go checkout the result in {}\n'
            .format(output_filepath))
    logging.info('connection closed')
    logging.info('writing information')
    mfile = open(output_filepath, 'wb')
    midiFile.writeFile(mfile)
    mfile.close()
    logging.info('closed file {}'.format(output_filepath))
    sys.exit(0)

                            

Schematics and other Figures

The schematic describing the hardware behind our device

A diagram of the finite state machine for the push button debouncing mechanism

An image of the finished device

Cost

Part Name Part # Vendor Price Use
PIC32MX250F128B PIC32MX250F128B Microstick $5 Microcontroller for signal processing
MicrostickII DM330013-2 Microchip $10 Program MCU and connect MCU to white board
White board N/A N/A 2x$6 Connects components
TFT LCD Screen Arduino TFT Arduino $10 Provides display for tempo and pitch
Microphone In lab To process audio input
Op-amp MCP6242 Microchip In lab Allows for filtering and amplification
Wire and passive elements In lab Used in constructing mic circuit

Tasks

Maayan Kline:
  • Researched how to interface with serial communication on client-side
  • Researched how to produce MIDI files on client-side
  • Wrote code for client to read serial data from the MCU
  • Wrote code for client to interface with MIDI library and write collected data to MIDI files
  • Wrote/modified UART code to send data from MCU to client
  • Implemented debounce FSM for push button
Mikayla Diesch:
  • Provided group with project idea
  • Designed, implemented and tested mic circuitry
  • Worked to implement filter bank
  • Tested and debugged pitch identification
  • Helped to implement pitch duration algorithm
Shubha Sekar:
  • Worked to design, implement and debug mic circuit
  • Helped to implement, test, and refine filter bank
  • Developed pitch identification and duration analysis algorithm
  • Worked on UART code to enable user defined tempo
  • Implemented note quantization based on song tempo
  • Performed testing to develop demo-ready product

References

Python Libraries:
  • Library for serial communication in Python: PySerial
  • Library for MIDI file handling in Python: MIDIUtil
Pic32 Libraries: Webpage Template: Documentation: