Jump to content
43oh

Launchpad Simple MIDI Synth


Recommended Posts

I made a square wave synth that has a standard MIDI interface with launchpad. I think I should make the midi stuff into a library so the code isn't so hard to follow, but clean and polished has never been my thing. :)

 

I seem to always have trouble using the USI with serial that doesn't have a clock signal. [E]rr from IRC told me I should use a state machine. I just had sort of a general idea of what that was when I coded it, so I'm not sure "state machine" is the proper term for what I'm using, but I think it's something like that.

 

Here's the BOM:

1x TI Launchpad or another MSP430

1x 280 ohm resistor (can use internal pull-up instead)

1x 220 ohm resistor

1x Sharp PC900V optoisolator

1x Speaker (I just bought one from RadioShack)

1x MIDI female jack

A midi cable and some kind of midi controller

 

...and the code.

http://naturetm.com/wp-content/uploads/2011/02/main.c

 

There's a little more info and some videos of a Launchpad quintet on my blog.

http://naturetm.com/?p=111

Link to post
Share on other sites
  • 4 weeks later...
  • 1 month later...

Update:

This is still a work in progress, but I feel like showing some changes I've made. I wanted to make the synth polyphonic and try using a DAC, while keeping the code small enough to still fit on a G2231.

 

For the DAC, I chose the MCP4725. I picked this one because it was the cheapest DAC-on-a-breakout from SparkFun. http://www.sparkfun.com/products/8736 I used some TI I2C example code to communicate with it.

 

Since the I2C communications are using the USI, I needed a different way to get the serial MIDI data from the MIDI controller. I set up TimerA in capture mode, and triggered it on both rising and falling edges in the midi data. From that I could see how long the MIDI line was in a given state and thus how many 0's or 1's I got. (TimerA ticks in state / TimerA ticks per bit = number of bits.) My previous code using the USI was a mess, so I was happy to try something different.

 

For the synth part, I have a function that takes the frequency and position within the wave, and returns an amplitude. To mix for polyphony, I just average the amplitudes of several of the waves and send the average to the DAC. The synth sounds pretty cruddy. There's several reasons for this, but it mostly boils down to a need for more free CPU time. A faster chip or more efficient code would help. I have ideas for the latter, but I may just take a completely different approach for the synth altogether. I think I'll start concentrating more on the actual sound, and less on seeing what I can squeeze from the chip. There are functions for square, triangle, and sawtooth waves, however all but square are too computationally expensive for polyphony.

 

Here's the code -- you might want to copy/paste it into a bigger window (I really need to learn how to make a library):

#include "msp430g2231.h"
#include 

#define MCLK_FREQUENCY				16000000
#define MIDI_FREQUENCY				31250
#define PIN_MIDI_DATA				BIT2
#define PIN_SPEAKER					BIT4
#define MIDI_CHANNEL				0	// MIDI is 1-indexed so setting this to 0 is midi channel 1
#define MAX_POLYPHONY				7
#define WDT_DIVIDER					64
#define	SCALE_SIZE					12
#define number_of_bytes 			2

const unsigned long WDT_FREQUENCY = MCLK_FREQUENCY / WDT_DIVIDER;
const unsigned int synthNotes[sCALE_SIZE] = {9956, 10548, 11175, 11840, 12544, 13290, 14080, 14917, 7902, 8372, 8869, 9397};
const unsigned int TICKS_PER_BIT = MCLK_FREQUENCY / MIDI_FREQUENCY;
unsigned int wdtCounter = 0;

char MST_Data[number_of_bytes] = {0x00, 0x00};
char SLV_Addr = 0xC0;                       
char  byteCount, Transmit = 0;
volatile char I2C_State = 0;

char newVelocity;
char newNote;
int noteIndex = 0;
unsigned int wdtPeriodNotes[MAX_POLYPHONY];
unsigned int tNotes[MAX_POLYPHONY] = {0};
char notes[MAX_POLYPHONY] = {0};
char velocities[MAX_POLYPHONY] = {0};

bool midiRXBitState = true;
char partialMidiData = 0xFF;
bool haveNewMidiByte = false;
char newMidiByte;

void waitForMidiLoop();
void handleNoteOn();
void handleNoteOff();
void updateWaveTimes();
void synthProcess();
void mixToOutputArray();
void handleNoteEvent();
void shiftLeft(char index);
void updateSynth(bool on);
char getNoteIndex(char note);
void addBits(unsigned int stateCount, bool state);
void updateADC(void);
void Setup_USI_Master_TX (void);

void main(){
DCOCTL = CALDCO_16MHZ;
BCSCTL1 = CALBC1_16MHZ;

WDTCTL = WDTPW + WDTTMSEL + WDTIS1 + WDTIS0;
IE1 |= WDTIE;

TACTL |= TASSEL_2;						// TACLK = SMCLK
P1SEL |= PIN_MIDI_DATA;					// Timer A compare input
TACCTL1 |= CCIE + CAP + CM_3;			// enable capture and interrupt on rising and falling edge
TACCTL0 |= CCIE;						// enable timeout interrupt
TACCR0 = 9 * TICKS_PER_BIT;				// should never have more than 8 bits

P1OUT |= 0xC0;
P1REN |= 0xC0;
P1DIR = BIT0 + PIN_SPEAKER + BIT6 + BIT7;

I2C_State = 0;							// wasn't init'ing properly???

TACTL |= MC_1;							// start Timer A

_EINT();								// enable interrupts

while(1){
	waitForMidiLoop();
  	if(newMidiByte == (0x90 | MIDI_CHANNEL)){
  		haveNewMidiByte = false;
		handleNoteOn();
		P1OUT ^= BIT0;
  	}
  	else if(newMidiByte == (0x80 | MIDI_CHANNEL)){
  		haveNewMidiByte = false;
		handleNoteOff();
  	}
  	else
		haveNewMidiByte = false;

}
}

void waitForMidiLoop(){
while(!haveNewMidiByte){
	synthProcess();
}
}


// ##################################################
// ################# Synth Stuff ####################

void synthProcess(){
if(noteIndex){
	updateWaveTimes();
	if(haveNewMidiByte)
		return;
	mixToOutputArray();
	if(haveNewMidiByte)
		return;
//		if(!I2C_State){
	updateADC();
//		}
}
}

// basicly tracks our position within the waveform
void updateWaveTimes(){
static unsigned int lastWdt = 0;
char iNote;

if(lastWdt != wdtCounter){
	unsigned int wdtDelta = wdtCounter - lastWdt;
	lastWdt = wdtCounter;

	for(iNote = 0; iNote < noteIndex; iNote++){
		tNotes[iNote] += wdtDelta;

		if(tNotes[iNote] >= wdtPeriodNotes[iNote]){
			tNotes[iNote] = tNotes[iNote] - wdtPeriodNotes[iNote];
		}
	}
}
}

unsigned int triangle(unsigned int position, unsigned int wavelength){
unsigned int halfWavelength = wavelength / 2;
if(position <= halfWavelength)
	return 0x0FFF * (unsigned long)position / halfWavelength;
else
	return 0x0FFF - (0x0FFF * (unsigned long)(position - halfWavelength) / halfWavelength);
}

unsigned int sawtooth(unsigned int position, unsigned int wavelength){
return (unsigned long)position * 0x0FFF / wavelength;
}

// use this one for velocity sensitivity
//unsigned int square(unsigned int position, unsigned int wavelength, char velocity){
//	if(position > wavelength >> 1)
//		return 0x001F * velocity;
//	else
//		return 0;
//}

unsigned int square(unsigned int position, unsigned int wavelength){
if(position > wavelength >> 1)
	return 0x0FFF;
else
	return 0;
}

// A fast but lo-fi digital mixer.
void mixToOutputArray(){
char iSum;
unsigned int sum = 0;

for(iSum = 0; iSum < noteIndex; iSum++)
	sum += square(tNotes[iSum], wdtPeriodNotes[iSum]);
	// using waveforms other than square takes extra cpu time and can lead to unexpected results,
	// but they're there to mess with.  (you may miss midi events and get crudd(y/ier) sound)
//		sum += triangle(tNotes[iSum], wdtPeriodNotes[iSum]);
//		sum += sawtooth(tNotes[iSum], wdtPeriodNotes[iSum]);

	// use this one for velocity sensitivity
//		sum += square(tNotes[iSum], wdtPeriodNotes[iSum], velocities[iSum]);

// Hack to keep the volume more level
if(noteIndex == 1)
	sum /= 2;
else
	sum /= noteIndex;

MST_Data[0] = 0x000F & (sum >> 8);
 	MST_Data[1] = 0x00FF & sum;
}

unsigned int midiNoteToWdtPeriod(char midiNote){
return (WDT_FREQUENCY / (synthNotes[midiNote % 12] >> ((127 - midiNote) / 12)));
}

#pragma vector=WDT_VECTOR
__interrupt void watchdog_timer(void){
wdtCounter++;
}


// ###############################################
// ################# MIDI stuff ##################

void handleNoteOn(){
do{
	waitForMidiLoop();
	if(newMidiByte & 0x80)
		break;
	newNote = newMidiByte;
	haveNewMidiByte = false;

	waitForMidiLoop();
	if(newMidiByte & 0x80)
		break;
	newVelocity = newMidiByte;
	haveNewMidiByte = false;

	handleNoteEvent();
}while(1);
}

void handleNoteOff(){
do{
	waitForMidiLoop();
	if(newMidiByte & 0x80)
		break;
	newNote = newMidiByte;
	haveNewMidiByte = false;
	newVelocity = 0;

	handleNoteEvent();
}while(1);
}

void handleNoteEvent(){
if(newVelocity != 0){
	// add a new note when the poly cache is full, replacing the oldest
	if(MAX_POLYPHONY == noteIndex){
		shiftLeft(0);
		notes[MAX_POLYPHONY - 1] = newNote;
		velocities[MAX_POLYPHONY - 1] = newVelocity;
		wdtPeriodNotes[MAX_POLYPHONY - 1] = midiNoteToWdtPeriod(newNote);
	}
	// add a new note
	else{
		notes[noteIndex] = newNote;
		velocities[noteIndex] = newVelocity;
		wdtPeriodNotes[noteIndex] = midiNoteToWdtPeriod(newNote);
		noteIndex++;
	}
}
else if(getNoteIndex(newNote) < MAX_POLYPHONY){
	shiftLeft(getNoteIndex(newNote));
	noteIndex -= 2;
	if(noteIndex >= 0){
		noteIndex++;
	}
	else{
		noteIndex = 0;
		// set DC offset to zero when no notes
		MST_Data[0] = 0;
 			MST_Data[1] = 0;
 			updateADC();
	}
}
}
// Shift all notes to the right of index left by one, 
// overwriting index and freeing a spot at the end of
// the array
void shiftLeft(char index){
int i;
for(i = index; i < MAX_POLYPHONY - 1; i++){
	notes[i] = notes[i + 1];
	velocities[i] = velocities[i + 1];
	wdtPeriodNotes[i] = wdtPeriodNotes[i + 1];
}
}

char getNoteIndex(char note){
int i;
for(i = 0; i < MAX_POLYPHONY; i++)
	if(notes[i] == note)
		return i;
return MAX_POLYPHONY + 1;
}

// add a new bit to the incomplete midi data variable
void shiftInMSB(bool state){
if(state){
	partialMidiData >>= 1;
	partialMidiData |= BIT7;
}
else{
	partialMidiData >>= 1;
}
}

// add the new midi data to the incomplete midi data variable
void addBits(unsigned int stateCount, bool state){
while(stateCount > TICKS_PER_BIT / 2){
	if(!(partialMidiData & BIT0)){ // are we shifting out the start bit?
//			if(haveNewMidiByte)
//				while(1);	// catch when we miss midi data (for debugging)
		shiftInMSB(state);
		newMidiByte = partialMidiData;
//			if((newMidiByte != 0xFE) && (newMidiByte != 0xFF))
		if(newMidiByte != 0xFE)
			haveNewMidiByte = true;
		partialMidiData = 0xFF;
	}
	else	
		shiftInMSB(state);

	stateCount -= TICKS_PER_BIT;
}
}

// CCR1 capture interrupt.  Triggers on both edges, adds fresh midi data
#pragma vector=TIMERA1_VECTOR
__interrupt void CCR1_interrupt(void){
TAR = 0;
TACCTL0 &= ~CCIFG;
unsigned int tEdge = TACCR1;
midiRXBitState = PIN_MIDI_DATA & P1IN;
TAIV = 0;

addBits(tEdge, !midiRXBitState);
}

// Like a timeout to add midi data if there's new data, but no edge in too long
#pragma vector=TIMERA0_VECTOR
__interrupt void CCR0_interrupt(void){
// don't add data if no partial data received, unless the new is all 0's
if((partialMidiData != 0xFF) || !midiRXBitState)
	addBits(9 * TICKS_PER_BIT, midiRXBitState);

// in case we captured an edge during this ISR, don't re-add these bits
// in the CCR1 ISR
TACCR1 = 0;
}


// ############################################################
// ###################### I2C ADC Stuff #######################

void updateADC(void){
Setup_USI_Master_TX();
   USICTL1 |= USIIFG;                      // Set flag and start communication
}

void Data_TX (void){
     USISRL = MST_Data[byteCount];          // Load data byte
     USICNT |=  0x08;              // Bit counter = 8, start TX
     I2C_State = 10;               // next state: receive data (N)Ack
     byteCount++;
}

void Setup_USI_Master_TX (void)
{ 
 _DINT();
 byteCount = 0;
 Transmit = 1;
 USICTL0 = USIPE6+USIPE7+USIMST+USISWRST;  // Port & USI mode setup
 USICTL1 = USII2C+USIIE;                   // Enable I2C mode & USI interrupt
 USICKCTL = USIDIV_3+USISSEL_2+USICKPL;    // USI clk: SCL = SMCLK/128
 USICNT |= USIIFGCC;                       // Disable automatic clear control
 USICTL0 &= ~USISWRST;                     // Enable USI
 USICTL1 &= ~USIIFG;                       // Clear pending flag
 _EINT();
}

// I2C handler for ADC adapted from TI example code.  This runs pretty quick,
// so hopefully it doen't clash with Midi input interrupts.
#pragma vector = USI_VECTOR
__interrupt void USI_TXRX (void)
{
 switch(__even_in_range(I2C_State,14))
   {
     case 0: // Generate Start Condition & send address to slave
             byteCount = 0;
             USISRL = 0x00;                // Generate Start Condition...
             USICTL0 |= USIGE+USIOE;
             USICTL0 &= ~USIGE;
             USISRL = SLV_Addr;
             USICNT = (USICNT & 0xE0) + 0x08; // Bit counter = 8, TX Address
             I2C_State = 2;                // next state: rcv address (N)Ack
             break;

     case 2: // Receive Address Ack/Nack bit
             USICTL0 &= ~USIOE;            // SDA = input
             USICNT |= 0x01;               // Bit counter=1, receive (N)Ack bit
             I2C_State = 4;                // Go to next state: check (N)Ack
             break;

     case 4: // Process Address Ack/Nack & handle data TX
             USICTL0 |= USIOE;             // SDA = output
             if (USISRL & 0x01)            // If Nack received...
             { // Send stop...
               USISRL = 0x00;
               USICNT |=  0x01;            // Bit counter=1, SCL high, SDA low
               I2C_State = 14;             // Go to next state: generate Stop
             }
             else
             { // Ack received, TX data to slave... 
             USISRL = MST_Data[byteCount];  // Load data byte
             USICNT |=  0x08;              // Bit counter = 8, start TX
             I2C_State = 10;               // next state: receive data (N)Ack
             byteCount++;
             }
             break;

     case 10: // Receive Data Ack/Nack bit
             USICTL0 &= ~USIOE;            // SDA = input
             USICNT |= 0x01;               // Bit counter = 1, receive (N)Ack bit
             I2C_State = 12;               // Go to next state: check (N)Ack
             break;

     case 12: // Process Data Ack/Nack & send Stop
             USICTL0 |= USIOE;
             if (byteCount == number_of_bytes){// If last byte
			USISRL = 0x00;
      		I2C_State = 14;               // Go to next state: generate Stop
      		USICNT |=  0x01; 

          }        // set count=1 to trigger next state
             else{
               Data_TX();                  // TX byte
             }
             break;

     case 14:// Generate Stop Condition
             USISRL = 0x0FF;               // USISRL = 1 to release SDA
             USICTL0 |= USIGE;             // Transparent latch enabled
             USICTL0 &= ~(USIGE+USIOE);    // Latch/SDA output disabled
             I2C_State = 0;                // Reset state machine for next xmt
             break;
   }

 USICTL1 &= ~USIIFG;                       // Clear pending flag
}

 

 

I was planning on entering this in the POTM, but it sounds so bad! My cats start meowing and scratching at the door when I play it! I think I will enter the original version, however.

 

dac.jpg

NOTE: There should be about a 190K resistor between the DAC and the amp in the schematic. Not entirely necessary, but will scale the DAC output down to something more reasonable for the LM386

 

dac_amp_top.jpg

 

dac_amp_bottom.jpg

 

optoisolator.jpg

 

optoisolator_top.jpg

 

optoisolator_Bottom.jpg

Link to post
Share on other sites
I was planning on entering this in the POTM, but it sounds so bad! My cats start meowing and scratching at the door when I play it! I think I will enter the original version, however.

Its not so bad. well written. About CPU time... I bet the averaging is taking the most time. I normally toggle an LED to view times on a scope. On one of our DSPs, adding 100 samples together in a array was very expensive.

Link to post
Share on other sites

MystBoy had this comment in the April 2011 POTM thread, but I didn't feel like it would be appropriate for me to talk about my project in that thread during voting.

 

i tried something simmilar but i discontinued it...

 

Feel free to work off of what I have! The midi receiver code by itself is fairly small and could lead to some interesting projects with physical instruments (solonoids plucking strings, hitting bells), stage-lighting, using midi patterns to control some other system, etc. I don't really have the tools, fabrication skills, or experience to do most of this stuff. For someone with a little experience on the physical side of things, it might not be too difficult to go way beyond what I've done with my little synth.

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...