Drum Master
Ye Kuang (yk749)
Haodong Ping(hp394)
This project is a rhythm game consists of software preprocessing and playing MIDI file and running game logic, and hardware which are two homemade drum controllers and a piece of signal conditioning circuit. We proposed and implemented this project completely out of our love for video games and music.
Our design is mostly based on all kinds of popular rhythm game on the market but there is no direct copy of visual design or code. Also the MIDI files we use in this project are all free online samples without copyright concerns. Overall this project is implemented for the purpose of the course and also for fun.
The software part of this project consists of direct digital synthesis, game logic, GUI and MIDI file preprocessing. Direct digital synthesis is used for music notes play back, gamle logic controls the behavior of our rhythm game, GUI is the visual representation of our game and MIDI file preprocessing is a python code piece used for extracting certain channel of notes we want to use for the game.
The hardware part is two drum controllers connected to a piece of signal conditioning circuit. We attached two flex piezo sensors to two plastic bowls in order to use them as drum controller, and we put a piece of signal conditioning circuit between the controllers and PIC32’s ADC to increase the signal to noise ratio and also to enable us to distinguish hard beats from soft beats.
We make use of an online open source python program to parse our midi files, with some tweaking we can easily parse a midi file and extracting instrument channels from the files. In our parsing program, we care about the note on information and the note lasting time. The note number go from 0 to 127, so we generate the corresponding frequency table and put it into music.h in PIC32. We print the drum note time information as an integer array from channel 10 and melody info from first nine channels.
In our program, each song has the following structure. The guitar pointer point to the song melody note number array. The guiter_time pointer point to the melody note lasting time array.
Every time we play the melody of a song, we just traverse guitar array to get the note number and use this number to get the sound frequency from frequency table mentioned above. We also need to traverse the guitar time array to know how long the note should last. After we know all those music information, we use Direct Digital Synthesis (DDS) to create the guitar sound for the game. To achieve this, we have a phase accumulator and DDS phase accumulator and both of them are increased by one every time an Interrupt Service Routine is called. We set up Timer 2 to trigger this ISR at 50kHz. Within the ISR, we are able to produce the sound. On each call of the ISR, we set the DAC data output to be the sine table indexed at the phase accumulator values shifted by 24. We shift by 24 so that the phase accumulator value is on the scale of 0 to 256. After this, we add 2048 to this value so that the values we output to the DAC range from 0 to 4096, then OR with the DAC control bits. Finally, we send this out over the SPI to the DAC.
We read the ADC channel within an Interrupt Service Routine, every time when the ADC reading value is greater than the threshold, we start to find the maximum value in next 500 timer interrupt. We use Timer 3 to trigger this ISR at 10kHz. Every time we get the maximum value, we may get the left or right beat value and according to the beat value to initialize the beat to soft or hard. The beat structure is shown below.
Beat value can be SOFT or HARD. The used indicate if the beat has been used in previous frames of the game.
The game logic is pretty straightforward. The player needs to beat the correct side of drum with correct strength at the correct time. The correct time is when any part of the falling ball is within the correct area at the bottom of the screen. The bigger ball means we need to beat hard. In our program, we have a circular buffer whose size is 25 to store the current appearing balls on the screen. So we traverse the circular buffer to see if we beat correctly within the correct area. If the player beats correctly, the score on the top of the screen increases by one. Figure 1 is the picture of screen when playing the game.
Figure 1. Game Play
Our game runs on 30 frames per second. We get the start time and end time of our game play thread. With those two values, we have a fixed frame value of our game.
We also have another thread to generate the note balls. Below is the structure of the beat ball. X and y is the position of the beat ball. Appeared flag indicates if the beat ball should be displayed on the screen. Hard indicate the size of the ball and color is the ball color.
We use a circular buffer to store those balls appearing on the screen. We traverse the drum time array to get the thread sleeping time, after that, we create a ball and put it into the circular buffer. The beat ball disappear when player earns one point or it goes off the screen.
We also have a menu to help the player choose a song and change the difficulty setting. The control law is very simple, beating the left side drum softly move the cursor down and beating right side drum move the cursor up. Beat random side of the drum hard can confirm or exit from the current menu. Figure 2 shows our main menu. The red part indicates the cursor position.
Figure 2. Main Menu
Hardware Design
Our bongo set contains two plastic bowls each contains a flex sensor, we have tried different materials (paper bowl, plastic cookie can lid, wooden board, ect) but at the end we chose plastic bowl as our bongo due to it has the best bouncing behavior among all the materials and can generate easy to process signal for later use. Figure 2 shows materials we tried to use as bongos.
Figure 3. Materials we tried as bongo
We built a signal conditioning circuit to preprocess signals generated from flex sensors, the circuit serves as a signal amplifier and low-pass filter, we made use of the non-inverting voltage amplifier, the voltage gain we used is 5 so even the slightest beat to the bongo can generate voltage around 100mv. Figure x show typical output of the conditioning circuit.
Figure 4. Non-Inverting voltage amplifier
Figure 5. Our signal conditioning circuit
Figure 6. Typical output the conditioning circuit
After weeks of hard work, our game looks great at the end. Our game controllers are very sensitive (it takes 5ms to process each beat, so the maximum bpm is 200) and barely miss a beat, after some parameters tuning, each bongo controller can easily distinguish soft beats from hard beats. The game software features good looking visual presentation with stable 15 frames per second, also the control logic is very intuitive, players can easily navigate the game menu using our bongo controllers. Also with MIDI files paring and direct digital synthesis we can generate songs sound exactly like the original midi files. Our game provides three different difficulties, with each difficulty setting we assigned a probability to each note, for example, under easy difficulty, each note has only 30% chance of being chosen as a playable note, and under hard difficulty the chance is 80%, which will make the game very intense. The link below is our demo video, enjoy it !
https://www.youtube.com/watch?v=6C86kxek2Oo&feature=youtu.be
Figure 7. Our project’s final form
Overall our project meets our proposal specifications, we made full use of PIC32’s hardware to build our game, we used on chip ADC for signal sampling at 10k sample rates, and DAC for music playback using direct digital synthesis technique. Also we made full use of Pthread library to coordinate our threads for different tasks, and it’s very important for our project since we need to provide 15 frames of game graphics per second while keeping a ratherly high sample rate. We optimized our code so each thread will do its job with minimum computation.
There are a few aspects we’d like to improve if we can redo this project. We think it’s better to make use of a higher level platform for music playback, since PIC32 only has two DAC channels, meaning we can only generate two different instruments sound at a time, it’s not enough to reconstruct many modern music pieces since there are always more than two instruments involved in a song. We think it’s a good idea to make use of Raspberry Pi for the music playback, we just need to write an easy communication protocol for PIC32 and Raspberry Pi and each time we play the game, PIC32 will send a command to Raspberry Pi to play the song, and with Raspberry Pi’s well built sound driver, the job can be done perfectly. Also by doing so we don’t need to store any music data in PIC32, which will definitely increase our music database capacity since Raspberry Pi has gigabytes of free storage.
Also since our game involves drawing circles on the TFT screen, sometimes the frame rate will drop if there are too many circles on the screen, the problem can be alleviated by writing our own efficient drawing library, since the provided graphic library is not very efficient according to what Bruce said from earlier Labs.
The music files we used in our game, while are all based on commercially written titles, are synthesized from royalty-free online sample MIDI files, and we do not intend to make any profit from our project.
We did not reuse anyone’s code nor did we use any code from the public domain. The game is built from scratch with some of our own code from earlier labs, the visual presentation of our game is also original and we believe our project is safe from any copyright issues.
The group approves this report for inclusion on the course website.
The group approves the video for inclusion on the course Youtube channel.
Part | Vendor | Quantity | Price |
Two plastic bowls | Ourself | 2 | free |
PIC32MX 250F128B | Lab stock | 1 | $5 |
Resistors | Lab stock | 6 | $0.3 |
Breadboard | Lab stock | 1 | $6 |
MCP6242 | Lab stock | 1 | $0.36 |
Flex sensor | Lab stock | 2 | $10 |
Speakers | Lab stock | 1 | $20 |
Total price : $41.66
Ye Kuang : Circuit construction, Controller Construction, Game logic design
Haodong Ping: Game logic coding, MiDi file parsing
from mido import MidiFile
songName = 'song.mid'
mid = MidiFile(songName)
id = 0
note_list = []
note_time_list = []
note_time_list.append(0)
for entries in mid.tracks[3]:
if not entries.is_meta:
ent = str(entries).split(' ')
print(str(entries))
if len(ent) == 5:
if(ent[0] == 'note_on' and int(ent[2].split('=')[1]) >= 55):
note = ent[2].split('=')[1]
time = ent[4].split('=')[1]
note_time_list[id] = int((note_time_list[id] + int(time)) / 2)
if note_time_list[id] < 200:
note_time_list[id] = 200
n = int(note)
note_list.append(n)
id = id + 1
note_time_list.append(0)
else:
time = ent[3].split('=')[1]
note_time_list[id] = note_time_list[id] + int(time)
note_time_list.remove(note_time_list[len(note_time_list) - 1])
print('timeLen:'+str(len(note_time_list)))
print('noteLen:'+str(len(note_list)))
print(note_time_list)
print(note_list)
/*
* File: Drum Master
* Author: Haodong Ping, Ye Kuang
* Target PIC: PIC32MX250F128B
* NOTE: Based on code written by Bruce Land.
*/
////////////////////////////////////
// clock AND protoThreads configure!
#include "config_1_3_2.h"
// threading library
#include "pt_cornell_1_3_2.h"
// the expander
#include "port_expander_brl4.h"
#include "music.h"
////////////////////////////////////
// graphics libraries
// SPI channel 1 connections to TFT
#include "tft_master.h"
#include "tft_gfx.h"
// need for rand function
#include <stdlib.h>
// need for sine function
#include <math.h>
// The fixed point types
#include <stdfix.h>
#include <stdio.h>
////////////////////////////////////
// lock out timer interrupt during spi comm to port expander
// This is necessary if you use the SPI2 channel in an ISR
#define start_spi2_critical_section INTEnable(INT_T2, 0);
#define end_spi2_critical_section INTEnable(INT_T2, 1);
////////////////////////////////////
// some precise, fixed, short delays
// to use for extending pulse durations on the keypad
// if behavior is erratic
#define NOP asm("nop");
// 20 cycles
#define wait20 NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;
// 40 cycles
#define wait40 wait20;wait20;
#define LEFTTHRESHOLD 30
#define RIGHTTHRESHOLD 30
#define LEFTSOFT 180
#define RIGHTSOFT 180
////////////////////////////////////
#define MAIN_MENU 0
#define DIFFICULTY 1
#define SONGSELECTION 2
#define SCORE 3
#define PLAY 4
#define NO_BEAT 0
#define SOFT_BEAT 1
#define HARD_BEAT 2
#define DIFFICULTY_HARD 800
#define DIFFICULTY_MEDIUM 500
#define DIFFICULTY_EASY 300
// string buffer
char buffer[60];
char score_buffer[60];
////////////////////////////////////
// Audio DAC ISR
// A-channel, 1x, active
#define DAC_config_chan_A 0b0011000000000000
// B-channel, 1x, active
#define DAC_config_chan_B 0b1011000000000000
// audio sample frequency
#define Fs 44000.0
// need this constant for setting DDS frequency
#define two32 4294967296.0 // 2^32
// sine lookup table for DDS
#define sine_table_size 256
#define LEFT 0
#define RIGHT 1
#define ON 1
#define OFF 0
#define LEFT_POS 183
#define RIGHT_POS 58
#define FALL_SPEED 6
#define BALL_SIZE 50
#define BALL_RADIUS 7
#define BIGBALL_RADIUS 10
volatile _Accum sine_table[sine_table_size] ;
//static int Fouts[8] = {262, 294, 330, 349, 392, 440, 494, 523};
static int soundFlag = 0;
// phase accumulator for DDS
volatile unsigned int DDS_phase ;
volatile unsigned int FM_DDS_phase ;
// phase increment to set the frequency DDS_increment = Fout*two32/Fs
// For A above middle C DDS_increment = = 42949673 = 440.0*two32/Fs
volatile unsigned int DDS_increment = 0 ; //42949673 ;
volatile unsigned int FM_DDS_increment = 0 ; //42949673 ;
// waveform amplitude
volatile _Accum max_amplitude=2000;
// FM waveform amplitude
volatile _Accum FM_max_amplitude=2000;
// waveform amplitude envelope parameters
// rise/fall time envelope 44 kHz samples
volatile unsigned int attack_time=200, decay_time=35000, sustain_time=1000;
// FM waveform amplitude envelope parameters
volatile unsigned int FM_attack_time=200, FM_decay_time=5000, FM_sustain_time=1000 ;
// 0<= current_amplitude < 2048
volatile _Accum current_amplitude ;
volatile _Accum FM_current_amplitude = 0;
// amplitude change per sample during attack and decay
// no change during sustain
volatile _Accum attack_inc, decay_inc ;
volatile _Accum FM_attack_inc, FM_decay_inc ;
//== Timer 2 interrupt handler ===========================================
volatile unsigned int DAC_data_A, DAC_data_B ;// output values
volatile SpiChannel spiChn = SPI_CHANNEL2 ; // the SPI channel to use
volatile int spiClkDiv = 4 ; // 10 MHz max speed for port expander!!
// interrupt ticks since beginning of song or note
volatile unsigned int song_time, note_time ;
volatile unsigned int drum_index = 0;
volatile unsigned int guitar_index = 0;
volatile unsigned int max_drum_id, max_guitar_id;
volatile unsigned int drum_side = LEFT;
volatile unsigned int start_button = OFF;
volatile int left_sample_state;
volatile int right_sample_state;
volatile int menu_state;
volatile int menu_cursor;
volatile Music *current_song;
volatile int difficulty;
typedef struct ball {
int x;
int y;
int appear;
int color;
int hard;
} Ball;
typedef struct drumBeat {
int beat;
int used;
} Beat;
volatile Beat left_beat, right_beat;
volatile Ball balls[BALL_SIZE];
volatile int cur_ballsize = 0;
volatile int circular_buffer[50];
volatile int left_id, left_max_val, left_temp_val;
volatile int right_id, right_max_val, right_temp_val;
volatile int score;
volatile int left_display_frame, right_display_frame;
void __ISR(_TIMER_3_VECTOR, ipl2) Timer3Handler(void){
mT3ClearIntFlag();
left_temp_val = ReadADC10(0);
right_temp_val = ReadADC10(1);
if(left_temp_val > LEFTTHRESHOLD && left_sample_state == 0) {
left_sample_state = 1;
left_id = 0;
}
if(right_temp_val > RIGHTTHRESHOLD && right_sample_state == 0) {
right_sample_state = 1;
right_id = 0;
}
// circular_buffer[id] = temp_val;
if(left_sample_state == 1) {
left_max_val = left_temp_val > left_max_val ? left_temp_val : left_max_val;
left_id++;
if(left_id == 500) {
if(left_max_val < LEFTTHRESHOLD)
left_beat.beat = NO_BEAT;
else if(left_max_val >= LEFTTHRESHOLD && left_max_val < LEFTSOFT)
left_beat.beat = SOFT_BEAT;
else
left_beat.beat = HARD_BEAT;
//left_beat.beat = left_max_val;
left_beat.used = 0;
left_max_val = 0;
left_id = 0;
left_sample_state = 0;
}
}
if(right_sample_state == 1) {
right_max_val = right_temp_val > right_max_val ? right_temp_val : right_max_val;
right_id++;
if(right_id == 500) {
if(right_max_val < LEFTTHRESHOLD)
right_beat.beat = NO_BEAT;
else if(right_max_val >= LEFTTHRESHOLD && right_max_val < LEFTSOFT)
right_beat.beat = SOFT_BEAT;
else
right_beat.beat = HARD_BEAT;
// right_beat.beat = right_max_val;
right_beat.used = 0;
right_max_val = 0;
right_id = 0;
right_sample_state = 0;
}
}
}
void __ISR(_TIMER_2_VECTOR, ipl2) Timer2Handler(void)
{
int junk;
mT2ClearIntFlag();
// generate sinewave
// advance the phase
DDS_phase += DDS_increment ;
DAC_data_B = (int) max_amplitude ;
if (note_time < (attack_time + decay_time + sustain_time) && DDS_increment != 0){
//envelope control
current_amplitude = (note_time <= attack_time)?
current_amplitude + attack_inc :
(note_time <= attack_time + sustain_time)? current_amplitude:
current_amplitude - decay_inc ;
//FM envelope control
FM_current_amplitude = (note_time <= FM_attack_time)?
FM_current_amplitude + FM_attack_inc :
(note_time <= FM_attack_time + FM_sustain_time)? FM_current_amplitude:
FM_current_amplitude - FM_decay_inc ;
if(note_time >= FM_attack_time + FM_sustain_time + FM_decay_time) {
FM_DDS_increment = 0;
FM_current_amplitude = 0;
}
//FM synthesis
FM_DDS_phase += FM_DDS_increment;
int FM_out = (int)(FM_current_amplitude*sine_table[FM_DDS_phase>>24]);
DDS_phase += (FM_out << 16);
DAC_data_A = (int)(current_amplitude*sine_table[DDS_phase>>24]) + 2048 ;
DAC_data_B = (int)(FM_current_amplitude*sine_table[FM_DDS_phase>>24])/5 + 2048 ;
} else {
DAC_data_A = 2048;
DDS_increment = 0;
}
//
//
// test for ready
while (TxBufFullSPI2());
// reset spi mode to avoid conflict with expander
SPI_Mode16();
// DAC-A CS low to start transaction
mPORTBClearBits(BIT_4); // start transaction
// write to spi2
WriteSPI2(DAC_config_chan_A | (DAC_data_A & 0xfff) );
// fold a couple of timer updates into the transmit time
note_time++ ;
// test for done
while (SPI2STATbits.SPIBUSY); // wait for end of transaction
// MUST read to clear buffer for port expander elsewhere in code
junk = ReadSPI2();
// CS high
mPORTBSetBits(BIT_4); // end transaction
}
void tft_initGame(){
tft_drawLine(120,60,120,320,ILI9340_WHITE);
}
void redrawCircle(int left_hard, int right_hard){
int left_ball_r = left_hard ? BIGBALL_RADIUS : BALL_RADIUS;
int right_ball_r = right_hard ? BIGBALL_RADIUS : BALL_RADIUS;
if(left_display_frame >= 3) {
tft_fillCircle(LEFT_POS, 300, BIGBALL_RADIUS, ILI9340_BLACK);
tft_drawCircle(LEFT_POS, 300, left_ball_r, ILI9340_WHITE);
} else {
tft_fillCircle(LEFT_POS, 300, left_ball_r, ILI9340_YELLOW);
}
if(right_display_frame >= 3) {
tft_fillCircle(RIGHT_POS, 300, BIGBALL_RADIUS, ILI9340_BLACK);
tft_drawCircle(RIGHT_POS, 300, right_ball_r, ILI9340_WHITE);
} else {
tft_fillCircle(RIGHT_POS, 300, right_ball_r, ILI9340_YELLOW);
}
}
void updatePos(int x, int y, int color, int hard) {
int r = hard ? BIGBALL_RADIUS : BALL_RADIUS;
tft_fillCircle(x, y, r, ILI9340_BLACK); //x, y, radius, color
switch (color) {
case 0: tft_fillCircle(x, y + FALL_SPEED, r, ILI9340_RED); break;
case 1: tft_fillCircle(x, y + FALL_SPEED, r, ILI9340_GREEN); break;
}
}
void printLine(int line_number, char* print_buffer, short text_color, short back_color){
// line number 0 to 31
/// !!! assumes tft_setRotation(0);
// print_buffer is the string to print
int v_pos;
v_pos = line_number * 10 ;
// erase the pixels
tft_fillRoundRect(0, v_pos, 239, 8, 1, back_color);// x,y,w,h,radius,color
tft_setTextColor(text_color);
tft_setCursor(0, v_pos);
tft_setTextSize(1);
tft_writeString(print_buffer);
}
static struct pt pt_drum, pt_display, pt_guitar, pt_adc, pt_main_menu, pt_score_menu, pt_difficulty_menu, pt_song_menu;
static PT_THREAD (protothread_display(struct pt *pt)) {
PT_BEGIN(pt);
while(1) {
if(menu_state != PLAY) {
PT_YIELD_TIME_msec(60);
continue;
}
tft_initGame();
int i;
int left_min_dis, right_min_dis;
int left_min_id, right_min_id;
left_min_dis = 10000;
right_min_dis = 10000;
left_min_id = -1;
right_min_id = -1;
for(i = 0; i < cur_ballsize; i++) {
// if(balls[i].y < 0) balls[i].appear = 0;
if(balls[i].appear == 1) {
updatePos(balls[i].x, balls[i].y, balls[i].color, balls[i].hard);
balls[i].y += FALL_SPEED;
if(balls[i].x == LEFT_POS) {
int left_dis = abs(balls[i].y - 300);
if(left_dis <= left_min_dis) {
left_min_dis = left_dis;
left_min_id = i;
}
}
else if(balls[i].x == RIGHT_POS) {
int right_dis = abs(balls[i].y - 300);
if(right_dis <= right_min_dis) {
right_min_dis = right_dis ;
right_min_id = i;
}
}
int r = balls[i].hard ? BIGBALL_RADIUS : BALL_RADIUS;
if(balls[i].y <= 0) {
tft_fillCircle(balls[i].x, balls[i].y, r, ILI9340_BLACK);
balls[i].appear = 0;
}
}
}
int left_hard = left_min_id == -1 ? 0 : balls[left_min_id].hard;
int right_hard = right_min_id == -1 ? 0 : balls[right_min_id].hard;
redrawCircle(left_hard, right_hard);
if(left_beat.used == 0) {
left_display_frame = 0;
int r = left_hard ? BIGBALL_RADIUS : BALL_RADIUS;
tft_fillCircle(LEFT_POS, 300, r, ILI9340_YELLOW);
left_beat.used = 1;
if(left_min_id != -1){
if(left_min_dis <= (2 * r + 5) && left_hard == (left_beat.beat - 1)) {
score++;
}
}
} else {
left_display_frame = left_display_frame >= 3 ? 3 : left_display_frame + 1;
}
if(right_beat.used == 0) {
right_display_frame = 0;
int r = right_hard ? BIGBALL_RADIUS : BALL_RADIUS;
tft_fillCircle(RIGHT_POS, 300, r, ILI9340_YELLOW);
right_beat.used = 1;
if(right_min_id != -1){
if(right_min_dis <= (2 * r + 5)&& right_hard == (right_beat.beat - 1)) {
score++;
}
}
} else {
right_display_frame = right_display_frame >= 3 ? 3 : right_display_frame + 1;
}
// if(left_beat.beat == SOFT_BEAT)
// sprintf(buffer,"left:soft:%d left_dis: %d score:%d", left_beat.beat, left_min_dis, score);
// else if(left_beat.beat == HARD_BEAT)
// sprintf(buffer,"left:hard:%d left_dis: %d score:%d",left_beat.beat, left_min_dis, score);
tft_fillRect(145, 25, 60, 30, ILI9340_BLACK);
tft_setCursor(80, 30);
tft_setTextColor(ILI9340_WHITE);
tft_setTextSize(2);
sprintf(buffer,"Score: %d", score);
tft_writeString(buffer);
PT_YIELD_TIME_msec(15);
}
PT_END(pt);
}
static PT_THREAD (protothread_drum(struct pt *pt)) {
PT_BEGIN(pt);
static Ball temp;
while(1) {
if(menu_state != PLAY) {
PT_YIELD_TIME_msec(60);
continue;
}
if(drum_index >= current_song->drum_len) {
menu_state = SCORE;
tft_fillScreen(ILI9340_BLACK);
current_song->latest_score = score;
current_song->highest_score = score > current_song->highest_score ?
score : current_song->highest_score;
continue;
}
if(current_song->drum_time[drum_index] != 0)
PT_YIELD_TIME_msec(current_song->drum_time[drum_index]);
if(current_song->drum_time[drum_index] < 300) {
drum_side = drum_side ? LEFT : RIGHT;
}
//if()
temp.x = drum_side ? RIGHT_POS : LEFT_POS;
temp.y = 70;
temp.appear = rand() % 1000 <= difficulty ? 1 : 0;
temp.color = drum_side ? 1 : 0;
temp.hard = rand() % 1000 <= 200 ? 1 : 0;
balls[drum_index % BALL_SIZE] = temp;
cur_ballsize = cur_ballsize >= BALL_SIZE ? BALL_SIZE : cur_ballsize + 1;
drum_index++;
}
PT_END(pt);
}
static PT_THREAD (protothread_guitar(struct pt *pt)) {
PT_BEGIN(pt);
static Ball temp;
while(1) {
if(menu_state != PLAY) {
PT_YIELD_TIME_msec(60);
continue;
}
if(guitar_index >= current_song->guitar_len) {
guitar_index = current_song->guitar_len;
menu_state = SCORE;
tft_fillScreen(ILI9340_BLACK);
current_song->latest_score = score;
current_song->highest_score = score > current_song->highest_score ?
score : current_song->highest_score;
continue;
}
int sleep_time = current_song->guitar_time[guitar_index] ;//>= 500 ?
// current_song->guitar_time[guitar_index] : 500;
// if(current_song->guitar_time[guitar_index] >= 500)
PT_YIELD_TIME_msec(sleep_time);
note_time = 0;
current_amplitude = 0;
FM_current_amplitude = 0;
int freq = Fouts[current_song->guitar[guitar_index]];
DDS_increment = freq * two32/Fs;
FM_DDS_increment = 3 * freq * two32/Fs ;
guitar_index++;
}
PT_END(pt);
}
static PT_THREAD (protothread_main_menu(struct pt *pt)) {
PT_BEGIN(pt);
while(1) {
if(menu_state != MAIN_MENU) {
PT_YIELD_TIME_msec(60);
continue;
}
// sprintf(buffer,"left:%d right:%d", left_beat.beat, right_beat.beat);
printLine(0, buffer, ILI9340_WHITE, ILI9340_BLACK);
// tft_fillScreen(ILI9340_BLACK);
/*1. draw UI
2. track the cursor */
if(left_beat.used == 0 && left_beat.beat == SOFT_BEAT) {
menu_cursor = (menu_cursor + 1) % 4;
}
else if(right_beat.used == 0 && right_beat.beat == SOFT_BEAT) {
menu_cursor = menu_cursor == 0 ? 3 : menu_cursor - 1;
}
int pressed = (left_beat.used == 0 && left_beat.beat == HARD_BEAT) ||
(right_beat.used == 0 && right_beat.beat == HARD_BEAT);
left_beat.used = 1;
right_beat.used = 1;
switch (menu_cursor) {
case 0:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_RED);
tft_writeString("Start Game");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Choose Songs");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Difficulty Setting");
tft_setCursor(100, 160);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Score");
if(pressed) {
menu_state = PLAY;
guitar_index = 0;
drum_index = 0;
cur_ballsize = 0;
score = 0;
tft_fillScreen(ILI9340_BLACK);
}
break;
case 1:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Start Game");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_RED);
tft_writeString("Choose Songs");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Difficulty Setting");
tft_setCursor(100, 160);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Score");
if(pressed) {
menu_state = SONGSELECTION;
tft_fillScreen(ILI9340_BLACK);
menu_cursor = 0;
}
break;
case 2:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Start Game");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Choose Songs");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_RED);
tft_writeString("Difficulty Setting");
tft_setCursor(100, 160);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Score");
if(pressed) {
menu_state = DIFFICULTY;
tft_fillScreen(ILI9340_BLACK);
menu_cursor = 0;
}
break;
case 3:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Start Game");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Choose Songs");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Difficulty Setting");
tft_setCursor(100, 160);
tft_setTextColor(ILI9340_RED);
tft_writeString("Score");
if(pressed) {
menu_state = SCORE;
tft_fillScreen(ILI9340_BLACK);
}
break;
}
PT_YIELD_TIME_msec(30);
}
PT_END(pt);
}
static PT_THREAD (protothread_score_menu(struct pt *pt)) {
PT_BEGIN(pt);
while(1) {
if(menu_state != SCORE) {
PT_YIELD_TIME_msec(60);
continue;
}
//sprintf(buffer,"left:%d right:%d", left_beat.beat, right_beat.beat);
printLine(0, buffer, ILI9340_WHITE, ILI9340_BLACK);
// tft_fillScreen(ILI9340_BLACK);
/*1. draw UI
2. track the cursor */
int pressed = (left_beat.used == 0 && left_beat.beat == HARD_BEAT) ||
(right_beat.used == 0 && right_beat.beat == HARD_BEAT);
left_beat.used = 1;
right_beat.used = 1;
if(pressed) {
tft_fillScreen(ILI9340_BLACK);
menu_state = MAIN_MENU;
continue;
}
tft_setCursor(25, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Song Name");
tft_setCursor(125, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Highest");
tft_setCursor(175, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Latest");
tft_setCursor(25, 140);
tft_setTextColor(ILI9340_WHITE);
sprintf(score_buffer,"%s", song_1.name);
tft_writeString(score_buffer);
tft_setCursor(125, 140);
tft_setTextColor(ILI9340_WHITE);
sprintf(score_buffer,"%d", song_1.highest_score);
tft_writeString(score_buffer);
tft_setCursor(175, 140);
tft_setTextColor(ILI9340_WHITE);
sprintf(score_buffer,"%d", song_1.latest_score);
tft_writeString(score_buffer);
tft_setCursor(25, 160);
tft_setTextColor(ILI9340_WHITE);
sprintf(score_buffer,"%s", song_2.name);
tft_writeString(score_buffer);
tft_setCursor(125, 160);
tft_setTextColor(ILI9340_WHITE);
sprintf(score_buffer,"%d", song_2.highest_score);
tft_writeString(score_buffer);
tft_setCursor(175, 160);
tft_setTextColor(ILI9340_WHITE);
sprintf(score_buffer,"%d", song_2.latest_score);
tft_writeString(score_buffer);
PT_YIELD_TIME_msec(30);
}
PT_END(pt);
}
static PT_THREAD (protothread_difficulty_menu(struct pt *pt)) {
PT_BEGIN(pt);
while(1) {
if(menu_state != DIFFICULTY) {
PT_YIELD_TIME_msec(60);
continue;
}
sprintf(buffer,"");
printLine(0, buffer, ILI9340_WHITE, ILI9340_BLACK);
if(left_beat.used == 0 && left_beat.beat == SOFT_BEAT) {
menu_cursor = (menu_cursor + 1) % 3;
}
else if(right_beat.used == 0 && right_beat.beat == SOFT_BEAT) {
menu_cursor = menu_cursor == 0 ? 2 : menu_cursor - 1;
}
/*1. draw UI
2. track the cursor */
int pressed = (left_beat.used == 0 && left_beat.beat == HARD_BEAT) ||
(right_beat.used == 0 && right_beat.beat == HARD_BEAT);
left_beat.used = 1;
right_beat.used = 1;
switch (menu_cursor) {
case 0:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_RED);
tft_writeString("Easy");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Medium");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Hard");
if(pressed) {
menu_state = MAIN_MENU;
difficulty = DIFFICULTY_EASY;
menu_cursor = 2;
tft_fillScreen(ILI9340_BLACK);
}
break;
case 1:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Easy");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_RED);
tft_writeString("Medium");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Hard");
if(pressed) {
menu_state = MAIN_MENU;
difficulty = DIFFICULTY_MEDIUM;
menu_cursor = 2;
tft_fillScreen(ILI9340_BLACK);
}
break;
case 2:
tft_setCursor(100, 100);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Easy");
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_WHITE);
tft_writeString("Medium");
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_RED);
tft_writeString("Hard");
if(pressed) {
menu_state = MAIN_MENU;
difficulty = DIFFICULTY_HARD;
menu_cursor = 2;
tft_fillScreen(ILI9340_BLACK);
}
break;
PT_YIELD_TIME_msec(30);
}
PT_END(pt);
}
}
static PT_THREAD (protothread_song_menu(struct pt *pt)) {
PT_BEGIN(pt);
while(1) {
if(menu_state != SONGSELECTION) {
PT_YIELD_TIME_msec(60);
continue;
}
sprintf(buffer,"");
printLine(0, buffer, ILI9340_WHITE, ILI9340_BLACK);
if(left_beat.used == 0 && left_beat.beat == SOFT_BEAT) {
menu_cursor = (menu_cursor + 1) % 2;
}
else if(right_beat.used == 0 && right_beat.beat == SOFT_BEAT) {
menu_cursor = menu_cursor == 0 ? 1 : menu_cursor - 1;
}
/*1. draw UI
2. track the cursor */
int pressed = (left_beat.used == 0 && left_beat.beat == HARD_BEAT) ||
(right_beat.used == 0 && right_beat.beat == HARD_BEAT);
left_beat.used = 1;
right_beat.used = 1;
switch (menu_cursor) {
case 0:
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_RED);
tft_writeString(song_1.name);
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_WHITE);
tft_writeString(song_2.name);
if(pressed) {
menu_state = MAIN_MENU;
current_song = &song_1;
menu_cursor = 1;
tft_fillScreen(ILI9340_BLACK);
}
break;
case 1:
tft_setCursor(100, 120);
tft_setTextColor(ILI9340_WHITE);
tft_writeString(song_1.name);
tft_setCursor(100, 140);
tft_setTextColor(ILI9340_RED);
tft_writeString(song_2.name);
if(pressed) {
menu_state = MAIN_MENU;
current_song = &song_2;
menu_cursor = 1;
tft_fillScreen(ILI9340_BLACK);
}
break;
PT_YIELD_TIME_msec(30);
}
PT_END(pt);
}
}
void adc_init() {
CloseADC10(); // ensure the ADC is off before setting the configuration
#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_ON | ADC_SAMPLES_PER_INT_2 | ADC_ALT_BUF_OFF | ADC_ALT_INPUT_OFF
#define PARAM3 ADC_CONV_CLK_PB | ADC_SAMPLE_TIME_15 | ADC_CONV_CLK_Tcy
// define setup parameters for OpenADC10
// set AN11 and as analog inputs
#define PARAM4 ENABLE_AN11_ANA | ENABLE_AN5_ANA //
// define setup parameters for OpenADC10
// do not assign channels to scan
#define PARAM5 SKIP_SCAN_AN0 | SKIP_SCAN_AN1 | SKIP_SCAN_AN2 | SKIP_SCAN_AN3 | SKIP_SCAN_AN4 | SKIP_SCAN_AN6 | SKIP_SCAN_AN7 | SKIP_SCAN_AN8 | SKIP_SCAN_AN9 | SKIP_SCAN_AN10 | SKIP_SCAN_AN12 | SKIP_SCAN_AN13 | SKIP_SCAN_AN14 | SKIP_SCAN_AN15
// use ground as neg ref for A | use AN11 for input A
// configure to sample AN11
SetChanADC10( ADC_CH0_NEG_SAMPLEA_NVREF); //
OpenADC10( PARAM1, PARAM2, PARAM3, PARAM4, PARAM5 ); // configure ADC using the parameters defined above
EnableADC10(); // Enable the ADC
}
// === Main ======================================================
void main(void) {
//SYSTEMConfigPerformance(PBCLK);
ANSELA = 0; ANSELB = 0;
// set up DAC on big board
// timer interrupt //////////////////////////
// Set up timer2 on, interrupts, internal clock, prescalar 1, toggle rate
// at 40 MHz PB clock
// 40,000,000/Fs = 909 : since timer is zero-based, set to 908
OpenTimer2(T2_ON | T2_SOURCE_INT | T2_PS_1_1, 908);
// set up the timer interrupt with a priority of 2
ConfigIntTimer2(T2_INT_ON | T2_INT_PRIOR_2);
mT2ClearIntFlag(); // and clear the interrupt flag
OpenTimer3(T3_ON | T3_SOURCE_INT | T3_PS_1_1, 4000);
ConfigIntTimer3(T3_INT_ON | T3_INT_PRIOR_2);
mT3ClearIntFlag(); // and clear the interrupt flag
// OpenTimer4(T4_ON | T4_SOURCE_INT | T4_PS_1_1, 4000);
// ConfigIntTimer4(T4_INT_ON | T4_INT_PRIOR_2);
// mT4ClearIntFlag(); // and clear the interrupt flag
// SCK2 is pin 26
// SDO2 (MOSI) is in PPS output group 2, could be connected to RB5 which is pin 14
PPSOutput(2, RPB5, SDO2);
// control CS for DAC
mPORTBSetPinsDigitalOut(BIT_4);
mPORTBSetBits(BIT_4);
// divide Fpb by 2, configure the I/O ports. Not using SS in this example
// 16 bit transfer CKP=1 CKE=1
// possibles SPI_OPEN_CKP_HIGH; SPI_OPEN_SMP_END; SPI_OPEN_CKE_REV
// For any given peripherial, you will need to match these
// clk divider set to 4 for 10 MHz
SpiChnOpen(SPI_CHANNEL2, SPI_OPEN_ON | SPI_OPEN_MODE16 | SPI_OPEN_MSTEN | SPI_OPEN_CKE_REV , 4);
// end DAC setup
adc_init();
//
// build the sine lookup table
// scaled to produce values between 0 and 4096
int i;
for (i = 0; i < sine_table_size; i++){
sine_table[i] = (_Accum)(sin((float)i*6.283/(float)sine_table_size));
}
// set up increments for calculating bow envelope
attack_inc = max_amplitude/(_Accum)attack_time ;
decay_inc = max_amplitude/(_Accum)decay_time ;
// set up increments for calculating FM bow envelope
FM_attack_inc = FM_max_amplitude/(_Accum)FM_attack_time ;
FM_decay_inc = FM_max_amplitude/(_Accum)FM_decay_time ;
// init the display
// NOTE that this init assumes SPI channel 1 connections
tft_init_hw();
tft_begin();
tft_fillScreen(ILI9340_BLACK);
init_song();
current_song = &song_1;
left_sample_state = 0;
right_sample_state = 0;
score = 0;
//240x320 vertical display
tft_setRotation(0); // Use tft_setRotation(1) for 320x240
// === setup system wide interrupts ========
INTEnableSystemMultiVectoredInt();
// === config threads ==========
// turns OFF UART support and debugger pin, unless defines are set
PT_setup();
// init the threads
PT_INIT(&pt_drum);
PT_INIT(&pt_guitar);
PT_INIT(&pt_display);
PT_INIT(&pt_main_menu);
PT_INIT(&pt_score_menu);
PT_INIT(&pt_difficulty_menu);
PT_INIT(&pt_song_menu);
start_button = ON;
menu_state = MAIN_MENU;
//menu_state = PLAY;
menu_cursor = 0;
left_beat.beat = 0;
right_beat.beat = 0;
left_beat.used = 0;
right_beat.used = 0;
difficulty = DIFFICULTY_MEDIUM;
// round-robin scheduler for threads
while (1){
PT_SCHEDULE(protothread_drum(&pt_drum));
PT_SCHEDULE(protothread_guitar(&pt_guitar));
PT_SCHEDULE(protothread_display(&pt_display));
PT_SCHEDULE(protothread_main_menu(&pt_main_menu));
PT_SCHEDULE(protothread_score_menu(&pt_score_menu));
PT_SCHEDULE(protothread_difficulty_menu(&pt_difficulty_menu));
PT_SCHEDULE(protothread_song_menu(&pt_song_menu));
}
} // main
// === end ======================================================