SwivelSpeakers

This is an XY controlled sphere/ concentric/ “globus” loudspeaker positioning system driven by two stepper motors (vertical axis turning and horizontal axis turning) devised by Stefan Doepner. The cutting of elements was one of the first jobs for CNC2 machine. The swivel system provides movements of one to a couple of turns in each direction – therefore relatively precise positioning of loudspeaker.

MIDI control:
– midi channel defines the selection of particular system
– note with velocity above zero dictates the moving to a position defined by velocity. Velocity number is multiplied by some factor to define the maximum position
– note 1 is used for X motor position
– note 2 is used for X motor speed
– note 3 is used for X motor direction
– notes 4, pilule 5, 6 are used for the same on Y motor
– a reset button is used to define the zero position

Both arduino codes below use:
– arduino MIDI library http://arduinomidilib.fortyseveneffects.com/a00025.html
– arduino AccelStepper library http://www.schmalzhaus.com/EasyDriver/Examples/EasyDriverExamples.html

This is arduino code by Borut Savski used with a couple of old ISEL Stepper Drivers ( L297/ L298 combination; SwivelSpeaker.txt)

/*
midiInServoControl
use the serial receive (pin 0) to receive MIDI note data
check for the speed note - translate the velocity to pulse train speed
check for the dir note - translate the velocity (0 or >0)to direction pin
*/
 
/*
http://www.schmalzhaus.com/EasyDriver/Examples/EasyDriverExamples.html
*/
 
// use the AccelStepper library with L297/L298 combination
#include
 
// since we use ISEL Stepper Driver, 2 driver pins are required
// Define X stepper and the pins it will use - pin7 pulse train, pin6 dir
AccelStepper stepper1(1, 5, 4);
// Define Y stepper and the pins it will use - pin9 pulse train, pin8 dir
AccelStepper stepper2(1, 6, 7);
 
// first: 1 means DRIVER -> means that we use a driver like L297 (with Step and Direction pins)
// If an enable line is also needed, call setEnablePin() after construction.
// You may also invert the pins using setPinsInverted().
// second: Arduino digital pin number for motor pin 1. Defaults to pin 2.
// For a AccelStepper::DRIVER (interface==1), this is the Step input to the driver.
// Low to high transition means to step)
// third: Arduino digital pin number for motor pin 2. Defaults to pin 3.
// For a AccelStepper::DRIVER (interface==1), this is the Direction input the driver.
// High means forward.
 
/**
1. we receive a note for position: velocity 1-127 describes the final position
- multiply by steps by motor turns to get to the final position
2. second note is speed 1-127 - multiply to get the sensible speed
- if speed
3. third note tells about the direction: 0 or >0
-
4. do this for X and Y
 
5. we may need a zero positioning button to tell the machine the zero position
- from this zero position we must always keep in memory the current position and the both
limits - left and write
 
**/
 
/*
http://arduinomidilib.fortyseveneffects.com/a00025.html#ga414b3426cd08e148d612f94d3e462344
*/
// receive input midi data and translate to stepper control
#include
 
// we can use MIDI lib or not
const boolean usemidilib = true;
 
// use Rx Tx arduino pins 0, 1
const boolean usemidi = false;
 
const boolean printout = false;
 
#define MIDICH 1 // define the midi channel to listen to
#define LED 13   // LED pin on Arduino board
 
#define ZEROPOS 2   // zero position button - pulled up
 
#define TURNS1 1 // how many turns
#define TURNS2 1 // how many turns
 
// microstepping is the feature of stepper drivers: 1, 2, 4 for L297/298 in ISEL
#define STEPS1 1 // define the microsteps if used - 1=1, 1/2=2, 1/4=4 etc
#define STEPS2 1 // define the microsteps if used
// microsteps enlarge the resolutiuon -> multiply by steps per revolution
 
// calculate steps per revolution based on above two
const long steps1 = STEPS1 * TURNS1;
const long steps2 = STEPS2 * TURNS2;
// from here on we use steps1 and steps2 to to check the positions
 
// limits for both
const long limit1 = -steps1 * TURNS1; // 200 steps per rotation * 3 rotations = 600
long zeroposition1 = 0; // this is check against a minus and plus limits
// always remember the current position!
long currentposition1 = 0;
 
const long limit2 = steps2 * TURNS2;
long zeroposition2 = 0; // this is check against a minus and plus limits
// always remember the current position!
long currentposition2 = 0;
 
// when limits reached we need to stop nad wait for the next command
// or turn the direction
 
#define SPEEDFACTOR1 2 // multiply speed by this factor
#define SPEEDFACTOR2 2 // multiply speed by this factor
 
long speedlimit1 = 64 * SPEEDFACTOR1;
long speedlimit2 = 64 * SPEEDFACTOR2;
 
// variables
// midi read
byte midichannel = MIDICH;
byte midinote = 0;
byte midivelocity = 0;
 
// globals to check with the RxTx serial MIDI IN OUT
// may not be needed if MIDI lib works
byte commandByte;
byte noteByte;
byte velocityByte;
 
char ledstate = LOW;
long lastledtime = millis();
 
// steppers definitions
 
// adapted midi values
long midipos1 = 0;
long midispeed1 = 64 * SPEEDFACTOR1;
long mididir1 = 0;
 
long midipos2 = 0;
long midispeed2 = 64 * SPEEDFACTOR2;
long mididir2 = 0;
 
// speed and position -> these two need to be long!
// direction
long pos1 = 200;
long speed1 = 64 * SPEEDFACTOR1;
byte dir1 = 0;
 
long pos2 = 200;
long speed2 = 64 * SPEEDFACTOR2;
byte dir2 = 0;
 
// temporary globals - before conversion
byte mpos1;
byte mpos2;
byte mspeed1;
byte mspeed2;
byte mdir1;
byte mdir2;
 
// remember previous values
long prevpos1 = 0;
long prevspeed1 = 50;
byte prevdir1 = 0;
 
long prevpos2 = 0;
long prevspeed2 = 50;
byte prevdir2 = 0;
 
void setup() {
 
pinMode(LED, OUTPUT); // blink on receive
digitalWrite(LED, LOW); // blink on receive
 
pinMode(ZEROPOS, INPUT);
digitalWrite(ZEROPOS, HIGH); // turn on pull up resistor
// set the hardware interrupt for zeroposition pin
//attachInterrupt(0, setZeroPosition, LOW); // int.0 on pin2	int.1 on pin 3 // LOW FALLING RISING CHANGE HIGH
 
// output pins for stepper drivers get the output mode with the library
// AccelStepper lib
stepper1.setMaxSpeed(speedlimit1); // steps per s; 50, 127 works
stepper1.setAcceleration(100); //Calling setAcceleration() is expensive, since it requires a square root to be calculated.
stepper1.setMinPulseWidth(1000); //The minimum pulse width in microseconds - 10ms is ok
stepper1.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position.
stepper1.setSpeed(speedlimit1); //this allows to take in the new value
// first move - one round
stepper1.moveTo(200); // long absolute
//stepper1.move(100); // long relative move
 
stepper2.setMaxSpeed(speedlimit2); // steps per s; 50 works
stepper2.setAcceleration(100);
stepper2.setMinPulseWidth(1000); // 10ms
stepper2.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position.
stepper2.setSpeed(speedlimit2); //this allows to take in the new value
// first move - one round
stepper2.moveTo(200); //stepper2.moveTo(1000000);
 
if(usemidilib){
// Initiate MIDI communications
//MIDI.begin(MIDI_CHANNEL_OMNI); // listen to all channels
//MIDI.begin();     // Launch MIDI with default options - input channel set to 1
MIDI.begin(MIDICH); // input channel is set to predefined value
}
 
// if we use the Tx Rx pins with MIDI IN OUT we need to use MIDI baud rate
// Rx pin0, Tx pin1
if(usemidi)   Serial.begin(31250);  //  Set MIDI baud rate
else       Serial.begin(9600);
 
}
 
void loop() {
// timecheck to turn off led
BlinkLedOff();
 
// check if zeropisition switch pushed
if(digitalRead(ZEROPOS)==LOW){
setZeroPosition();
}
 
if(usemidilib){
// we can use the interrupt function above or this below
 
// Read messages from the serial port using the main input channel.
if (MIDI.read() && MIDI.check()) {  // Is there a MIDI message incoming?
//if(MIDI.check()) // check if a valid message is stored in the structure
// types: NoteOff NoteOn ControlChange AfterTouchPoly AfterTouchChannel PitchBend ProgramChange
switch(MIDI.getType()) {		// Get the type of the message we caught
case NoteOn:               // If it is a NoteOn
//case ProgramChange:               // If it is a Program Change
//Get the channel of the message stored in the structure.
//Channel range is 1 to 16. For non-channel messages, this will return 0
// if correct channel for this machine - read further - or ignore
midichannel = MIDI.getChannel(); //MIDI.getInputChannel();
 
if(midichannel==MIDICH){
// Get the first data byte of the last received message.
midinote = MIDI.getData1(); // get the note value = pitch
// Get the second data byte of the last received message.
midivelocity = MIDI.getData2(); // get the note velocity
 
BlinkLed();	// Blink the LED now that motor data is on their way
 
// do some action based on note received
checkMIDI(midinote, midivelocity);
 
}
break;
}
}
 
} else {
// use above or below
//if (Serial.available() > 0) {
if (Serial.available()){
// filter by note - these are hex values!
//commandByte = Serial.read();//read first byte
//midichannel==
// if command byte is channel
//if(midichannel==MIDICH){
// if command byte is noteon
//if(midichannel==MIDICH){
//      BlinkLed(); // Blink the LED
//    noteByte = Serial.read();//read next byte
//  midinote = 0; // translate note hex byte to dec
//velocityByte = Serial.read();//read final byte
//checkMIDI();
//}
//}
}
}
 
//testSteppers();
steppersPosSpeedDir();
 
}
 
void setZeroPosition() {
// redefine the global zeroposition variable for both
stepper1.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position.
stepper2.setCurrentPosition(0); //Resets the current position of the motor, so that wherever the motor happens to be right now is considered to be the new 0 position.
}
 
// check what to do with received midi
void checkMIDI(byte note, byte velocity){
 
// motor 1
// goto position
if(note==1){
// this would be: with stepper1 go to position 0-127 - we need some multiplying
if (velocity == 0) {
// stop
mpos1 = 0;
} else {
mpos1 = velocity;
}
// with speed
} else if(note==2) {
// this could be: speed for stepper1 0-127 - we need some multiplying here
if (velocity == 0) {
// stop
mspeed1 = 0;
} else {
mspeed1 = velocity;
}
// use direction
} else if(note==3) {
// this could be: direction for stepper1 0 or >0
if (velocity == 0) {
// left
mdir1 = 0;
} else {
// right
mdir1 = 1;
}
 
// motor 2
} else if(note==4) {
// this could be: go with stepper2 to position 0-127 - we need some multiplying
if (velocity == 0) {
// stop
mpos2 = 0;
} else {
mpos2 = velocity;
}
} else if(note==5) {
// this could be: speed for stepper2 0-127 - we need some multiplying here
if (velocity == 0) {
// stop
midispeed2 = 0;
} else {
midispeed2 = velocity;
}
} else if(note==6) {
// this would be: direction for stepper2 0 or >0
if (velocity == 0) {
// left
mdir2 = 0;
} else {
// right
mdir2 = 1;
}
}
// with these globals adapt the stepper values
adaptValues();
 
}
 
void adaptValues(){
long pfactor1;
long pfactor2;
long sfactor1;
long sfactor2;
 
pfactor1 = limit1 / 127;
pfactor2 = limit2 / 127;
 
// adapt position number from max 127 to max limit1
midipos1 = mpos1 * pfactor1;
midipos2 = mpos2 * pfactor2;
 
if(midipos1>limit1) midipos1=limit1;
if(midipos2>limit2) midipos2=limit2;
 
// speed * SPEEDFACTOR2
sfactor1 = speedlimit1 / 127;
sfactor2 = speedlimit2 / 127;
midispeed1 = mspeed1 * sfactor1;
midispeed2 = mspeed2 * sfactor2;
if(midispeed1>speedlimit1) midispeed1=speedlimit1;
if(midispeed2>speedlimit2) midispeed2=speedlimit2;
 
// dir is ok
 
}
 
// AccelStepper lib
// check details
void steppersPosSpeedDir() {
 
boolean change1=false;
boolean change2=false;
// here we use midi globals to adapt stepper globals
// stepper receives moveTo position command
// and runs
// setting setSpeed(0) allows to take in the new value for moveTo()!
// if some action occurs - like new note
//stepper1.setSpeed(0);
//stepper2.setSpeed(0);
// we have globals: midipos1, midispeed1, mididir1
// ADAPT HERE THE VALUES
// check for previous values - allow to change on the fly
if(midipos1!=prevpos1){
prevpos1 = midipos1; // remember anew
change1 = true;
}
if(midispeed1!=prevspeed1){
prevspeed1 = midispeed1; // remember anew
change1 = true;
}
if(mididir1!=prevdir1){
prevdir1 = mididir1; // remember anew
change1 = true;
}
 
if(change1 == true) {
stepper1.setSpeed(0); // this allows to change the moveTo value
stepper1.moveTo(midipos1);
stepper1.setSpeed(midispeed1);
}
 
if(midipos2!=prevpos2){
prevpos2 = midipos2; // remember anew
change2 = true;
}
if(midispeed2!=prevspeed2){
prevspeed2 = midispeed2; // remember anew
change2 = true;
}
if(mididir2!=prevdir2){
prevdir2 = mididir2; // remember anew
change2 = true;
}
 
if(change2 == true) {
stepper2.setSpeed(0); // this allows to change the moveTo value
stepper2.moveTo(midipos2);
stepper2.setSpeed(midispeed2);
}
 
//stepper1.currentPosition()
 
// Change direction at the limits
if (stepper1.distanceToGo() == 0) stepper1.moveTo(-stepper1.currentPosition());
if (stepper2.distanceToGo() == 0) stepper2.moveTo(-stepper2.currentPosition());
 
// stepper1.distanceToGo() is long
// this would mean: if distance is reached - turn direction
if (stepper1.distanceToGo() == 0)  {
pos1 = -pos1;
stepper1.moveTo(pos1);
}
 
if (stepper2.distanceToGo() == 0)  {
pos2 = -pos2;
stepper2.moveTo(pos2);
}
 
// moveTo argument should be limited to what we can handle
// currentPosition() -> the current step
// distanceToGo() -> how far still to go
// setMaxSpeed(300); // steps per ?
// setAcceleration(100);
//moveTo(600)
 
// this must hppen as often as possible - in the  main loop
// - one step per loop
// run steppers
stepper1.run();
stepper2.run();
 
if(printout) {
Serial.print(" current position: ");
Serial.print(stepper1.currentPosition());
Serial.print(", speed: ");
Serial.print(stepper1.speed());
Serial.print(", speed limit: ");
Serial.println(midispeed1);
// some delay
delay(10);
}
 
}
 
void testSteppers() {
// this must be in the loop
// stepper receives motoTo position command
// and runs
// setting this allows to take in the new value for moveTo()!
// if some action occurs - like new note
//stepper1.setSpeed(0);
//stepper2.setSpeed(0);
 
// moveTo argument should be limited to what we can handle
// currentPosition() -> the current step
// distanceToGo() -> how far still to go
// setMaxSpeed(300); // steps per ?
// setAcceleration(100);
//moveTo(600)
 
//stepper1.moveTo(600); // one round = 200
//stepper2.moveTo(600); // one round = 200
 
// Change direction at the limits and go back
if (stepper1.distanceToGo() == 0){
// change speed?
//stepper1.setMaxSpeed(200); // steps per sec?
//stepper1.setAcceleration(50);
stepper1.moveTo(-stepper1.currentPosition());
}
if (stepper2.distanceToGo() == 0){
stepper2.moveTo(-stepper2.currentPosition());
}
 
// must be in the loop each loop cycle!
stepper1.run();
stepper2.run();
 
/**
Serial.print(" values: ");
Serial.print(stepper1.currentPosition());
Serial.print(", ");
Serial.println(stepper2.currentPosition());
// some delay
delay(10);
**/
}
 
void checkMIDI2(){
do{
if (Serial.available()){
commandByte = Serial.read();//read first byte
noteByte = Serial.read();//read next byte
velocityByte = Serial.read();//read final byte
BlinkLed(); // Blink the LED a number of times
}
}
while (Serial.available() > 2); //when at least three bytes available
}
 
// Basic blink function
void BlinkLed() {
digitalWrite(LED, HIGH);
lastledtime = millis();
ledstate = HIGH;
}
void BlinkLedOff() {
if(ledstate == HIGH) {
if(millis()-lastledtime > 10){ //ms
digitalWrite(LED, LOW);
lastledtime = millis();
ledstate = LOW;
}
}
}

Boštjan ?adež made a much more streamlined program for the second SwivelSpeaker with another stepper driver – GECKO with abundant microstepping (MidiXYStepper.txt)

#include
#include
#include
#include
#include
#include
 
struct MySettings : public midi::DefaultSettings
{
// static const bool UseRunningStatus = false; // Messes with my old equipment!
};
MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial1, MIDI, MySettings);
 
#define USBMIDI 1  //for Teensy with usbMIDI; comment out if not
#define MICROSTEP 1
 
#ifdef MICROSTEP
#define MS1 14
#define MS2 15
#define MS3 16
#endif
 
#define MOT1_STEP 19
#define MOT1_DIR 22
#define MOT2_STEP 20
#define MOT2_DIR 23
//#define MOT_DISABLE 23
 
#define STEPS_PER_REVOLUTION 3200
#define MOVEMENT_RANGE 1000  //movement range - negative and positive axis together (1.5 round in each direction)
#define MAX_SPEED 183050
#define ACCELERATION 57850
 
#define MIDI_CHANNEL 2
 
#define LED 13
 
long lastCommandX=0; //last received coordinates
long lastCommandY=0;
boolean haveNewCommandX=false;
boolean haveNewCommandY=false;
 
// Define a stepper and the pins it will use
AccelStepper stepper1(1,MOT1_STEP,MOT1_DIR);
AccelStepper stepper2(1,MOT2_STEP,MOT2_DIR);
 
#ifdef USBMIDI
IntervalTimer ledTimer;
#endif
 
void setup()
{
#ifdef MICROSTEP
pinMode(MS1,OUTPUT);
pinMode(MS2,OUTPUT);
pinMode(MS3,OUTPUT);
 
digitalWrite(MS1,HIGH);
digitalWrite(MS2,HIGH);
digitalWrite(MS3,HIGH);
#endif
 
#ifdef USBMIDI
usbMIDI.setHandleNoteOn(OnNoteOn) ;
usbMIDI.setHandleControlChange(OnNoteOn) ;
#endif
MIDI.setHandleNoteOn(OnNoteOn);
MIDI.setHandleControlChange(OnNoteOn);
MIDI.begin(MIDI_CHANNEL);
stepper1.setMaxSpeed(MAX_SPEED);
stepper1.setAcceleration(ACCELERATION);
stepper2.setMaxSpeed(MAX_SPEED);
stepper2.setAcceleration(ACCELERATION);
 
// pinMode(MOT_DISABLE,OUTPUT);
// digitalWrite(MOT_DISABLE,LOW);
 
pinMode(LED,OUTPUT);
 
}
 
void loop()
{
#ifdef USBMIDI
usbMIDI.read(MIDI_CHANNEL);
#endif
MIDI.read(MIDI_CHANNEL);
stepper1.run();
stepper2.run();
 
}
 
void OnNoteOn(byte channel, byte note, byte velocity)
{
 
#ifdef USBMIDI
digitalWrite(LED,HIGH);
ledTimer.begin(turnOffLed,100000);
#endif
switch(note)
{
case 0:// x os
{
//haveNewCommandX=true;
lastCommandX=((float)velocity/127.f-.5f)*MOVEMENT_RANGE;
stepper1.moveTo(lastCommandX);
break;
}
case 1:// y axis
{
//haveNewCommandY=true;
lastCommandY=((float)velocity/127.f-.5f)*MOVEMENT_RANGE;
stepper2.moveTo(lastCommandY);
break;
}
case 2:// speed
{
stepper1.setMaxSpeed(((float)velocity/127.f)*MAX_SPEED);
stepper2.setMaxSpeed(((float)velocity/127.f)*MAX_SPEED);
break;
}
}
//usbMIDI.sendNoteOn(note,velocity,channel);
}
 
void turnOffLed()
{
digitalWrite(LED,LOW);
ledTimer.end();
}