Sunday, 21 February 2010

Stylophone MIDI controller

A few months ago I used an Arduino clone board to send MIDI messages out of a Stylophone. I always intended to take it to the next level and get another Stylophone (preferably a broken one) and rip out the guts to fit all the electronics inside, and also add a few buttons and pots for perfomance controllers.

Well, I finally got round to it. This time I am using a PIC16F688 microcontroller.. this little monkey only has 14 pins and costs a mere £1 yet it has a built in clock, serial port and ADC, which means its pretty much the *only* component needed in this project (with the exception of a couple of resistors and switches).

I added a pitchbend pot, a modwheel and a pot to control the note velocity. And pushbuttons to shift octaves and "hold" a MIDI note (basically force the code to forget to send note-off message so the last note rings on after lifting the stylus). This allows a kind of polyphonic drone out of the usually strictly monophonic stylophone.



I will include the code below. I wont bother with a schematic, but the wiring to the PIC16F688 is as follows

pin 1 - 5 volt supply
2 - octave UP momentary switch (other side of switch connected to ground)
3 - octave DOWN momentary switch (other side of switch connected to ground)
6 - to pin 5 of MIDI out socket via a 220R resistor. Pin 4 of the socket is pulled up to 5V via another 220R resitor
7 - wiper of PITCHBEND pot (100k). Pot terminal between from ground/+5V
8 - wiper of VELOCITY pot (100k). Pot terminal between from ground/+5V
9 - to the stylus. Also pulled up to +5v via 470k resistor
10 - activity LED via 1k resistor
11 - wiper of MOD WHEEL pot (100k). Pot terminal between from ground/+5V
13 - HOLD NOTE momentary switch (other side of switch connected to ground)
14 - to ground

If you want to run it from a PP3 you'll need a 5V voltage regulator. You also need to connect the stylophone keyboard/resistor ladder between 0V and 5V and you will need to set up the scale[] array based on the ADC values you get from each pad on *your* stylophone keyboard (which are almost certainly different to mine)

A few photos






// MIDI STYLOPHONE.. PIC16F688.. (c) 2010 hotchk155

// SourceBoost C



// Header files

#include <system.h>

#include <memory.h>



#pragma DATA _CONFIG, _MCLRE_OFF & _WDT_OFF & _INTRC_OSC_NOCLKOUT

#pragma CLOCK_FREQ 8000000

typedef unsigned char byte;



#define MIDI_A 45 // default root note

#define NO_NOTE 0x7f // means stylus "off keyboard"

#define NUM_PADS 20 // number of stylophone pads



#define BUTTON_DEBOUNCE 10 // debounce octave buttons

#define ADC_AQUISITION_DELAY 10 // settling time for ADC

#define PBD_TOL 16 // tolerance applied to pitchbend ADC

#define MOD_TOL 5 // tolerance applied to modulation ADC



// Digital pins

#define P_HEARTBEAT portc.0 // activity LED

#define P_UP porta.5 // octave UP button

#define P_DN porta.4 // octave DOWN button

#define P_HOLD porta.0 // note hold button



// Analog pin mappings

#define ANA_MOD 0b00001000 // AN2 - MOD WHEEL

#define ANA_KBD 0b00010100 // AN5 - KEYBOARD STYLUS

#define ANA_VEL 0b00011100 // AN6 - VELOCITY

#define ANA_PBD 0b00011000 // AN7 - PITCHBEND



// define the four analog inputs

enum {

ADC_KBD,

ADC_VEL,

ADC_MOD,

ADC_PBD,

ADC_MAX

};



// for the state machine which read analog inputs

enum {

ADC_CONNECT,

ADC_ACQUIRE,

ADC_CONVERT

};



// define the ADC readings for each stylophone key pad. This

// is likely to be different if you make your own circuit, so

// you will need to work out your own ADC values

int scale[NUM_PADS+1] = {

0x000,

0x043,

0x081,

0x0b9,

0x0ed,

0x110,

0x149,

0x172,

0x199,

0x1bd,

0x1df,

0x1ff,

0x21c,

0x238,

0x252,

0x26b,

0x281,

0x298,

0x2ab,

0x2c0,

0x3ff

};



// midi note at bottom of scale (can be shifted

// up and down by an octave at a time)

char baseNote = MIDI_A;



// data used by doADC function

byte adcInput[ADC_MAX] = {ANA_KBD, ANA_VEL, ANA_MOD, ANA_PBD};

byte adcInitComplete = 0;

int adcResult[ADC_MAX] = {-1,-1,-1,-1};

int adcIndex = 0;

int adcState = ADC_CONNECT;



////////////////////////////////////////////////////////////////

//

// init_usart

//

// Initialise the PIC16F688 USART (serial port) according to the

// requirements of sending MIDI traffic

//

void init_usart()

{

pir1.1 = 1; //TXIF transmit enable

pie1.1 = 0; //TXIE no interrupts



baudctl.4 = 0; // synchronous bit polarity

baudctl.3 = 1; // enable 16 bit brg

baudctl.1 = 0; // wake up enable off

baudctl.0 = 0; // disable auto baud detect



txsta.6 = 0; // 8 bit transmission

txsta.5 = 1; // transmit enable

txsta.4 = 0; // async mode

txsta.2 = 0; // high baudrate BRGH



rcsta.7 = 1; // serial port enable

rcsta.6 = 0; // 8 bit operation

rcsta.4 = 0; // enable receiver



spbrgh = 0; // brg high byte

spbrg = 15; // brg low byte (31250 baud)

}



////////////////////////////////////////////////////////////////

//

// send

//

// Send a single byte out on the serial port

//

void send(byte c)

{

txreg = c;

while(!txsta.1);

}



////////////////////////////////////////////////////////////////

//

// sendController

//

// Send a MIDI continous controller message

//

void sendController(byte channel, byte controller, byte value)

{

P_HEARTBEAT = 1;

send(0xb0 | channel);

send(controller&0x7f);

send(value&0x7f);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////////

//

// startNote

//

// Send a MIDI note on message (or note off if 0 velocity)

//

void startNote(byte channel, byte note, byte velocity)

{

P_HEARTBEAT = 1;

send(0x90 | channel);

send(note&0x7f);

send(velocity&0x7f);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////////

//

// pitchBend

//

// Send a MIDI pitchbend message (14 data bits)

//

void pitchBend(byte channel, int value) {

P_HEARTBEAT = 1;

byte msb = (value>>7)&0x7f;

byte lsb = value&0x7f;

send(0xE0 | channel);

send(lsb);

send(msb);

P_HEARTBEAT = 0;

}



////////////////////////////////////////////////////////////////

//

// doADC

//

// State machine for running the ADC and updating the adcResult

// array with the result from each analog input. This function is

// called periodically and keeps the adcResult[] array updated so

// other code can just check the array rather than making direct

// calls to the ADC

//

void doADC()

{

switch(adcState)

{

// Connect ADC to the correct analog input

case ADC_CONNECT:

adcon0=0b10000001 | adcInput[adcIndex];

tmr0 = 0;

adcState = ADC_ACQUIRE;

// fall through



// Waiting for a delay while the ADC input settles

// - this is neededs or you can get garbage readings

// as the ADC transitions between one input voltage

// and another. The TMR0 (timer 0) register is used for

// timings this

case ADC_ACQUIRE:

if(tmr0 > ADC_AQUISITION_DELAY)

{

// Start the conversion

adcon0.1=1;

adcState = ADC_CONVERT;

}

break;



// Waiting for the conversion to complete

case ADC_CONVERT:

if(!adcon0.1)

{

// store the result. Note that the PIC16F688 has

// a 10 bit ADC so we need to form a 10 bit value

// from ADRESH and ADRESL

adcResult[adcIndex] = (((int)adresh)<<8)|adresl;



// and prepare for the next ADC

if(++adcIndex>=ADC_MAX)

{

adcIndex = 0;



// flag that each ADC has been read at least

// one time, so adcResult now contains valid

// information

adcInitComplete = 1;

}

adcState = ADC_CONNECT;

}

break;

}

}



////////////////////////////////////////////////////////////////

//

// getNote

//

// Map ADC values from the stylus to MIDI note values by

// looking for the scale[] entry which lies closest to the

// input value

//

char getNote(int input)

{

for(int i = 0; i < NUM_PADS; ++i)

{

int lo = 0;

int hi = 0x3ff;

if(i>0)

{

lo = (scale[i-1] + scale[i]) / 2;

}

if(i<NUM_PADS)

{

hi = (scale[i] + scale[i+1]) / 2;

}

if(input >=lo && input <=hi)

{

if(i==NUM_PADS)

return NO_NOTE;

return baseNote+i;

}

}

return NO_NOTE;

}



////////////////////////////////////////////////////////////////

//

// main

//

// Where program starts running!

//

void main()

{

// osc control / 8MHz / internal

osccon = 0b01110001;



// timer0... configure source and prescaler

// port A weak pull ups enabled

option_reg = 0b00000011;



// enable pull ups on each button

wpua = 0b00110001;



// turn off the comparator to allow digital IO on CIO pins

cmcon0 = 7;



// set data direction on each pin

trisa = 0b00110101;

trisc = 0b00001110;



// set up the analog input pins

ansel = 0b11100100;



// turn on the ADC

adcon1=0b00100000; //fOSC/32

adcon0=0b10000001; // Right justify / Vdd / AD on



// start up the serial port

init_usart();



// ensure that the initial aquisition is completed

// for all analog inputs that we're using

adcInitComplete = 0;

while(!adcInitComplete)

doADC();





//char buttons = 0;

char debounce = 0;

char lastNote = NO_NOTE;

int lastPitchBend = -1;

int lastModWheel = -1;

int value;

int diff;

for(;;)

{

// the debounce variable makes sure that user

// has release buttons for a period of time before

// a new press on the button can be registered.

// handles possibility of "switch bounce"



// Buttons are pulled up and touch ground when

// pressed, so the pin reads low when the button

// is being pressed

if(debounce > 0)

{

if(P_UP&&P_DN)

--debounce;



}

else

{

if(!P_UP)

{

// octave shift UP

if(baseNote < 103)

baseNote+=12;

debounce = BUTTON_DEBOUNCE;

}

else if(!P_DN)

{

// octave shift DOWN

if(baseNote > 12)

baseNote-=12;

debounce = BUTTON_DEBOUNCE;

}

}



// poll the ADCs

doADC();



// check for a new note being played

char note = getNote(adcResult[ADC_KBD]);

if(note != lastNote)

{

// do we need to kill the previous note?

if(lastNote != NO_NOTE && P_HOLD)

{

// make it so!

startNote(0,lastNote,0);

}

// is a new note playing (rather than stylus

// removed from keyboard?)

if(note != NO_NOTE)

{

// play a note with appropriate velocity

char velocity = (adcResult[ADC_VEL]>>3)&0x7f;

startNote(0,note,velocity);

}

lastNote = note;

}



// check for change in pitchbend which is

// outside the "noise" tolerance

value = adcResult[ADC_PBD];

diff = value - lastPitchBend;

if(diff*diff > (PBD_TOL*PBD_TOL))

{

// Send MIDI pitchbend.. this has a 14-bit

// data value

pitchBend(0, value<<4);

lastPitchBend = value;

}



// check for change in modwheel which is

// outside the "noise" tolerance

value = adcResult[ADC_MOD]>>3;

diff = value - lastModWheel;

if(diff*diff > (MOD_TOL*MOD_TOL))

{

// Send MIDI continuous controller message

// for controller #1 (mod wheel) which has

// 7 bit data value

sendController(0, 1, value);

lastModWheel = value;

}

}

}

3 comments:

  1. Hi, Great stuff! I really like this and thought I'd have a go.. I've got a modern Stylophone but they don't have the old resistor ladder arrangement so I wanted to make my own using resistors of the same value. I experimented with a pre-set pot and changed the hex values in the code which helps a bit, but all the notes are crammed together at one end - kind of exponentially. Any idea what I'm doing wrong? Thanks and keep up the good work!

    ReplyDelete
  2. Hey, yeah I believe you will need to change the values in the scale[] array.. this array depends on the voltage the code expects to read from each pad on the stylophone and depends on the resistors you use. The easiest way to work out the values to use is to print out the values from the ADC input of the PIC depending on which pad you connect to with the stylus.. these come back from the expression adcResult[ADC_KBD] in the main loop of the code. You will need some way to print them to a serial port etc to see them (I wrote them to MIDI pitchbend messages and used MIDIOX to look at the MIDI trace to read the values - just remember each byte of MIDI only has 7 bits). The alternative way is trial and error... if you can work out the right ADC values for first and last pad you should be able to get the others by division - assuming all your resistors are the same value (and you may need still need to tweak the values a bit if a pad is flicking between 2 different notes). Hope that helps!

    ReplyDelete
  3. Could you make a simple instruction video for you-tube to show exactly how to achieve this. Perhaps you could start by showing how to install the midi to the stylophone and also how and where to put your program on a pc. It would help if you showed us using one of the new modern versions of stylophone as well.
    I think what you have achieved is fantastic but I'm not as skilled as you so I need lots of tuition. Thankyou.

    ReplyDelete