Turntable Sequencer

This project has been on my mind since the very first time I attended a concert from ‘Institut fuer Feinmotorik’, a Germany based ‘Band’ which is using turntables in a pretty uncommon way in order to generate sound and noises. What struck me most about their show (besides the fascinating sound) was that you could actually follow the objects they were laying out on the turntable platter, and visually experience the sound generation.

I would describe this as visual (or mechanical) sequencing, and in a live performance very much self explaining. In fact you can correlate the produced sound to the actual object generating it. Compared to the average laptop-act, here you get an insight on the performer’s work and it is quite fun to watch and follow the soundmaking object moving.

From there, I came up with the plan to build a sequencer in a similar fashion, using a turntable with a white slipmat, on which one would layout a sequence in form of some black dots as input to an optical scanner, which would then generate midi events each time a black dot would pass that scanner.

Since then, some time has passed but I eventually came up with something.

Turntable Sequencer

Opto Scanner and Black Felt Dots

Lately, I tried out the Microchip HITEC C compiler (in Lite mode) for PIC microcontrollers, in order to prepare something for a larger project and I needed something easy to code in order to get used to the compiler and that’s how the TurntableSequencer got born.

See the turntable sequencer in action: (skip to half the video immediately if you’re not interested in the setup part)

Demo video

Project Outline:

I am using a PIC16F88 uC which has 7 AD converter inputs and integrated USART, which makes it rather appropriate for this type of application.

As optical input devices I chose the LPT80 photo-transistors connected over a voltage divider to the AD-inputs. In order to light up the scanned surface, I used some white Led’s, one per used phototransistor and lined up in parallel.

You can download the schematics, C-Code and compiled hex file from here.

Turntable Sequencer Prototype

Photo Transistors and Led's

The microcontroller is setup to scan the individual analog  inputs at a regular interval and compares the values on the analog inputs to a user-presettable threshold value for white respectively black background. Based on this evaluation, the uC the outputs a Midi Note On/Off message, depending on which change just occurred.

Since there are 7 inputs, the system will also output 7 different Note values, one for each input. The analog input scan rate is controlled by the uC Timer0 which generates an interrupt at each timer elapsed, where the ISR routine performs the AD conversion and processing. This way a constant sampling rate is guaranteed.

PIC16F88 and Power Supply

The Timer 0 prescaler bits are all set to 000 which means that (from the 16F88 datasheet) we are using a prescaler value 1:2. The timer 0 is incrementing on each second instruction cycle and counting up to 255 where it generates an interrupt.

At a clock rate of 20MHz, we have an instruction cycle time of:
20MHz Clock => 0.05us/cycle = 0.2us/instruction, which means for the AD conversion rate:

Timer0 count to 0xFF (255) with prescaler 1:2 => 510cycles x 0.2us = 102us/timer_interrupt = 9803Hz

Since we multiplex between the 7 inputs of the AD-converter we will end up with a sampling rate of 9803Hz/7=1400Hz per channel.

Furthermore, the interrupt interval of 102us will allow us to execute 510 instructions inbetween, so we have to take care that the code  executed inbetween 2 interrupts is not longer than that.

Now we need to see if at a sampling rate of 1400Hz we are able to detect a moving object with some 10mm of size. (Remember, we want to detect black felt dots on a white surface turning on a turntable platter).

First, the max speed of our object:

Turntable speed at 33rpm, outer diameter of a vinyl d = 305mm:

f=33rpm/60s = 0.55Hz

vmax= smax/t => 2π*r*f = 6.28*150.25mm*0.55 1/s = 518.96 mm/s =0.58m/s (which is = 31km/h)

So, an object with a length of 10mm cruising at 0.58m/s will remain in the field of our scanner for:

t=s/v (where s = distance and v= speed) 10mm/518.96mm/s = 0.0193s

This means for our 1400Hz sampling rate that we will get following number of measurement points:

sampling points =  f*t  => 1400 1/s * 0.0193s = 27.02 measurements

27 points is not a lot but will be sufficient for a good detection.

Since the system should be able to be used in live-performances or workshops, some convenience features had to be added:

  • Midi Channel Select, changes the midi channel the note values are broadcasted on.
  • Note Transpose, midi notes can be transposed up to 2 Octaves up (via Pushbutton).
  • Input Level Calibration, this feature is used to set the threshold levels for analog input level change detection, which is useful when working under different lighting conditions or if the phototransistors have a large difference in collector current.
Schematic
Schematic Opto Sequencer

Turntable Sequencer Schematic

Code details

For those who are interested, here some parts of the code for a quick lookup. The complete code can be downloaded below.

All code has been written in HitechC.

Microcontoller Setup:

__CONFIG(UNPROTECT & DEBUGEN & UNPROTECT & UNPROTECT & LVPDIS & BORDIS & MCLREN & WDTDIS & HS);

#define _XTAL_FREQ 20000000  // 20MHz crystal

// Macros
#define LoByte(i)    ( (char) i )  // get LSB
#define HiByte(i)    ( (char) ( ((int) i) >>8) ) // get MSB

// Parameters
volatile unsigned char rxData;
volatile char adChannel = 0; // Pointer for currently active AD channel
volatile unsigned int adData; // AD Conversion result
volatile int adDataArray[7];  // Data array for AD data of all 7 inputs
char thrFlagArrayH[7];
char thrFlagArrayL[7];
unsigned int minThresVar[7];   // White 2.18V = 0x01C0
unsigned int maxThresVar[7];   // Black 3.75V = 0x0300
volatile char midiNoteOn = 0x90; // MSB Nibble = On , LSB Nibble = Midi Channel
volatile char midiNoteOff = 0x80; // MSB Nibble = Off , LSB Nibble = Midi Channel
volatile char midiChannel = 0x00; // LSB Nibble = Midi Channel, default = ch 1 = 0x00
char rootNote = 48; // C3 as root note
char velocity = 127;
char transpose = 0; // Value for Note transposition
const char midiChEepromAddr = 0x20;

// Controller Setup
void init(void)
{
	// port directions: 1=input, 0=output
	TRISA = 0b00011111;  // RA0-4 Input
	TRISB = 0b11111100;  // 2 and 5 as Inputs for USART operation,RB3 MidiCh & Transpose,RB4 Calibration Button in, RB6-7 Input for analog
	RB0 = 1; // Set PortB0 High LED indicator

	// AD setup
	ANSEL = 0b01111111;  // All Analog inputs on
	CMCON = 0;  // Disable comparators
	ADCON0 = 0b10000001; // div32,An Channel 0, AD ON
	ADCON1 = 0b10000000; // Right Justified, Clock div disables, Vref = Vdd

	// USART setup
	TXSTA = 0; // Clear TXSTA Register
	RCSTA = 0; // Clear RCSTA Register
	SPBRG = 39;	//42 = 28800 Baud //  Midi Bitrate = 31.25Bd, SPBRG = 39 (Baud Rate = FOSC/(16(X + 1)))
	BRGH = 1; // High Speed USART
	SYNC = 0; // Async Mode

	RCIE = 0; // USART Receive Interrupt disable
	TXIE = 0; //
	SPEN = 1; // Serial Port Enable
	CREN = 1; // Continous receive Enable Reception
	TXEN = 1; // Enable Transmission

	// Timer0 setup (Sampling rate clock)
	OPTION = 0b00000000; //Option Register: Enable Timer0 as timer, set Prescaler 1:2
	// Interrupts
	GIE = 0; // Global Interrupt disable
	PEIE = 0; // Peripheral Interrupt Enable bit disabled
	TMR0IE = 0; // Enable Timer0 interrupt disabled
	TMR0 = 0; // Clear Timer0

	// Midi Channel from EEprom
	adChannel = 0;  // Set AD pointer to channel 0
	midiChannel = eeprom_read(midiChEepromAddr); // Read default Midi Channel
	midiChannel&= 0b00001111;  // Set MSB to 0
	midiNoteOn|= midiChannel;  // Generate Note On message (depending on midi channel)
	midiNoteOff|= midiChannel; // Generate Note On message (depending on midi channel)
}

Main Loop:

// The main routine
void main(void)
{
	init();	// Setup uC
	strobeLed(10, 5, 5); // Show it's running
	// Test for midi select button pressed
	if(RB3 == 0) // MIDI Channel Select (Button hold at power on)
	{
		__delay_ms(10); // Debounce
		RB0 = 0; // Turn Indicator Led off
		midiChannel = midiChSelect(); // Get the channel equivalent to Analog Input High
		midiChannel&=0b00001111; // Truncate MSB
		eeprom_write(midiChEepromAddr,midiChannel); // Store value for next boot
		midiNoteOn&= 0b11110000; // Clear MSB
		midiNoteOff&= 0b11110000; // Clear LSB
		midiNoteOn|= midiChannel;  // Generate Note On message (depending on midi channel)
		midiNoteOff|= midiChannel; // Generate Note On message (depending on midi channel)
		RB0 = 1; // Turn Indicator Led on
	}

	applyCalibration();	// Load Threshold values from EEprom
	del_20us();
	del_20us();

	// Interrupts
	TMR0 = 0; // Reset Timer0
	GIE = 1; // Global Interrupt enable
	PEIE = 0; // Peripheral Interrupt Enable bit
	TMR0IE = 1; // Enable Timer0 interrupt
	for (;;)
	{	// Stay here until either interrupt occurs or one of the button gets pressed
		if(RB4 == 0)
		{	// Calibration mode
			del_20us();
			calibration(); // Calibrate inputs
			del_20us();
		}
		del_20us();
		if(RB3 == 0)
		{	// Transpose Mode
			GIE = 0; //Disable interrupts
			del_x10ms(50); // Debounce
			transposeMidi(); // Transpose 1 Octave up
			__delay_ms(10); // Debounce Switch
			GIE = 1; //Enable Interrupts
		}
	}
}

Interrupt Routine:

// Interrupt Routine, each time we get here, we sample the analog input of the specified channel
static void interrupt isr()
{
	GIE = 0; // Interrupt disable for the moment
	PEIE = 0;
	// Test interrupt Type
	if (TMR0IF == 1) // Timer0 Interrupt
	{
		TMR0IE = 0;  // Disable Timer Interrupt

		adDataArray[adChannel] = adcRead(adChannel);  	// Sample Analog Input
		processAD_Data(adDataArray[adChannel],adChannel);  // Compare Data and send Midi message
		adChannel = getDataPointer(adChannel); // reposition Pointer
		changeADChannel(adChannel);  // Select new AD channel

		TMR0 = 0; // Reset Timer0
		TMR0IF = 0; // Clear interrupt
		TMR0IE = 1; // Enable Timer Interrupt
	}
	GIE = 1; // Re-enable interrupts
	PEIE = 1;
}

AD Sampling Routine:

// Get analog input value from specified channel
unsigned int adcRead(char channel)
{
	unsigned int result;
	GODONE = 1;
	while(GODONE); // wait for conversion complete
	result=(ADRESH <<8) + ADRESL; // 10 bit result, shift MSB result into MSB part of register
	return result;
}

Moving the AD multiplexer pointer and changing the AD input:

// Change Analog Input channel (Ch0-6 are available on PIC16F88)
void changeADChannel(char channelPointer)
{
	channelPointer&=0x07; // Truncate channel to 7 bits
	ADCON0&=0b11000111;  //0b11000111 Reset AD channel bits
	ADCON0|=(channelPointer<<3); // shift Channel pointer 3 positions left and then OR with oldAdcon value
}

// Move multiplexer Input pointer to next channel
char getDataPointer(char pointer)
{	// Channel Multiplexer pointer is 3LSB bits,
	if (pointer == 0b00000110)
	{
		pointer = 0;  // Set AD pointer to channel 0
	}
	else
	{
		pointer++; // increment ad channel
	}
	return pointer;
}

Sample Data Processing and threshold comparison:

// Test if value is in the threshold range, if yes send midi data
void processAD_Data(unsigned int data, char channel)
{
		if(data  maxThresVar[channel]) // Test if value above threshold value
		{
			if(thrFlagArrayH[channel] == 0) // Has already been set previously?
			{
				RB0 = 0; // Turn Led off
				sendUsart(midiNoteOn);  // Send Midi Note Off Message
				sendUsart(rootNote + transpose + channel);// Send Midi Note Data
				sendUsart(velocity);
				thrFlagArrayH[channel] = 1;
				thrFlagArrayL[channel] = 0;
				RB0 = 1; // Turn Led on
			}
		}
		else if (data > maxThresVar[channel]) // Test if value above threshold value
		{
			if(thrFlagArrayH[channel] == 0) // Has already been set previously?
			{
				RB0 = 0; // Turn Led off
				sendUsart(midiNoteOn);  // Send Midi Note Off Message
				sendUsart(rootNote + transpose + channel);// Send Midi Note Data
				sendUsart(velocity);
				thrFlagArrayH[channel] = 1;
				thrFlagArrayL[channel] = 0;
				RB0 = 1; // Turn Led on
			}
		}
}

Sending Data over the USART:

// Transmit incoming data over Usart
void sendUsart(unsigned char data)
{
	while (TXIF == 0)  // Wait until previous transmission is finished
	{
	}
	TXREG = data;
}

Analog input calibration function:

// Performs a scan of the white (low Level) and the black surface (High Level)
// in order to set the threshold values to the current setup.
void calibration(void)
{
	// Disable Interrupts
	GIE = 0; // Interrupt disable for the moment
	PEIE = 0;
	TMR0IE = 0; // Disable Timer Interrupt

	strobeLed(2,20,20); // Indicate White mode
	RB0 = 0; // Turn Led Off
	for (char i = 0; i < 7; i++)  // Cycle through channels
	{
		// WHITE Calibration
		for (int n = 0; n < 5000; n++)
		{
			adDataArray[i] = ((adDataArray[i] + adcRead(i))/2) ;  // Take measurement and average
			del_20us();	// Wait time for next sample
		}
		minThresVar[i] = (adDataArray[i] + 0x1A); // Set Threshold value + safety factor
		changeADChannel(i+1);  // Select new AD channel
		del_20us();	// Wait time for next sample
	}

	while(RB4 == 1) // Wait for Button Push to move on
	{
		strobeLed(2,20,20); // Indicate black mode
	}
	RB0 = 0; // Turn Led Off
	for (char i = 0; i < 7; i++) // Cycle through channels
	{
		// BLACK Calibration
		for (int n = 0; n < 5000; n++)
		{
			adDataArray[i] = ((adDataArray[i] + adcRead(i))/2) ;  // Take measurement and average
			del_20us();	// Wait time for next sample
		}
		maxThresVar[i] = (adDataArray[i] - 0x1A); // Set Threshold value + safety factor
		changeADChannel(i+1);  // Select new AD channel
		del_20us();	// Wait time for next sample
	}

	strobeLed(8,10,10); // show it's finished
	storeCalibration();  // Save calibration Data in EEprom
	//Re-enable Interrupts
	TMR0 = 0; // Reset Timer0
	GIE = 1; // Global Interrupt enable
	PEIE = 0; // Peripheral Interrupt Enable bit
	TMR0IE = 1; // Enable Timer0 interrupt
}

Midi Channel Selection:

// Midi Channel select mode
char midiChSelect(void)
{	// Short button presses < 1s increment midi channel, long button press exits selection mode
	while(RB3 == 0)
	{	// Wait for Button release
		__delay_ms(20);
	}
	del_x10ms(50);
	int i = 0;
	char channelSelect = 0;
	char setFlag = 0;
	RB0 = 1;
	// Button is now released from power on - hold, now count the button presses = midiCh
	for(;;)
	{
		// Press long for exit
		if(RB3 == 0)
		{
			__delay_ms(10);
			i = 10;
			while(RB3 == 0)
			{
				__delay_ms(1);
				i++;
			}
			setFlag = 1;
			__delay_ms(5);
		} 

		if (RB3 == 1 && (i  1000) && setFlag == 1) // Long Button press
		{
			del_x10ms(30);
			break;
		}
	}
	strobeLed(10, 5, 5); // Show that is is done
	return channelSelect;
}
Code and Schematics Download

You can download the schematics, C-Code and compiled hex file from here.




%d bloggers like this: