Components and supplies
1
6 DOF Sensor - MPU6050
5
NeoPixel WS2812B
1
AZDelivery ESP32 D1 Mini
1
AAA battery
1
Battery Holder, AAA
Tools and machines
1
Soldering kit
Apps and platforms
1
King Of FM
Project description
Code
SweetMaker Core
SweetMaker StrawberryString
SweetMaker BlueberryPie
MusicMallet.ino
c
1/******************************************************************************* 2 MusicMallet 3 4 Converts motion and orientation into sweet sweet music / MIDI Commands 5 6 Copyright(C) 2024 Howard James May 7 8 This file is part of the SweetMaker SDK 9 10 The SweetMaker SDK is free software: you can redistribute it and / or 11 modify it under the terms of the GNU General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 The SweetMaker SDK is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 18 GNU General Public License for more details. 19 20 You should have received a copy of the GNU General Public License 21 along with this program.If not, see <http://www.gnu.org/licenses/>. 22 23 Contact me at sweet.maker@outlook.com 24******************************************************************************** 25Release Date Change Description 26--------|-------------|--------------------------------------------------------| 27 1 18-Jan-2024 Initial release 28 2 22-Jan-2024 Renamed - various playability improvements 29*******************************************************************************/ 30 31#include "BlueberryPie.h" 32#include "ToDiscrete.h" 33#include "SigGen.h" 34#include "SigLib.h" 35 36using namespace SweetMaker; 37 38#define NUM_LIGHTS (StrawberryString::num_lights) 39 40#define Z_AXIS_QUANTIZER_REF (0) 41#define VERT_TILT_QUANTIZER_REF (1) 42#define HORIZ_DIR_QUANTIZER_REF (2) 43 44struct MusicalScale { 45 const uint8_t* midi_notes; 46 const uint8_t num_notes; 47}; 48 49struct NoteTuple { 50 uint8_t midiNote; 51 uint8_t indexInScale; 52}; 53 54uint8_t static const hue[] = { 0,160,120,200,40,230,80 }; 55 56BlueberryPie myPie; 57 58ToDiscrete zAxisRotationQuantizer(2048, 200, Z_AXIS_QUANTIZER_REF); // Used for accidental selection 59ToDiscrete verticalTiltQuantizer(2048, 200, VERT_TILT_QUANTIZER_REF); // Used for controlling playing notes 60ToDiscrete rotationAboutVerticalQuantizer(8192, 200, HORIZ_DIR_QUANTIZER_REF); // Used for selecting notes 61int16_t zAxisAboutVertical_16384; 62int16_t verticalVelocity; 63 64static const SigGen::SAMPLE PROGMEM flashLowWave[] = { 127, 0, 0, 0, 0, 127 }; 65static const SigGen::SAMPLE PROGMEM flashHighWave[] = { 127, 200, 200, 200, 200, 127 }; 66 67SigGen brightnessSigGen; 68SigGen saturationSigGen(flashLowWave, NUM_SAM(flashLowWave), 200, SigGen::DONT_FINISH_ON_ZERO); 69StaticGen myStaticGen; 70 71int8_t saturation = 255; 72ColourHSV lightStripHSV[NUM_LIGHTS]; 73 74#define MIDI_CHAN_NUM (1) 75 76boolean performPitchBend(int16_t input, uint8_t channel); 77boolean performAfterTouch(int16_t input, uint8_t channel, uint8_t note); 78boolean performModulation(int16_t input, uint8_t channel); 79 80int8_t detectAccidental(); 81struct NoteTuple selectMidiNoteFromScale(MusicalScale* scale, int16_t index); 82struct NoteTuple selectNextNote(); 83 84/* 85 * Captures events from myPie and SweetMaker framework 86 */ 87void myEventHandler(uint16_t eventId, uint8_t srcRef, uint16_t eventInfo); 88 89/* 90 * Runs once when the system starts up. 91 */ 92void setup() 93{ 94 int retVal; 95 Serial.begin(112500); // set the baud rate to 112500 on PC 96 Serial.println("Welcome to MusicMallet"); 97 98 myPie.configEventHandlerCallback(myEventHandler); 99 100 retVal = myPie.init(); 101 if (retVal != 0) { 102 Serial.println("myPie init failure"); 103 } 104 105 // Setup some starting values for the LEDs 106 lightStripHSV[0].setColour(0, 255, 127); 107 lightStripHSV[1].setColour(160, 255, 127); 108 lightStripHSV[2].setColour(80, 255, 127); 109 lightStripHSV[3].setColour(200, 255, 127); 110 lightStripHSV[4].setColour(120, 255, 127); 111 112 zAxisRotationQuantizer.start(0); 113 verticalTiltQuantizer.start(0); 114 rotationAboutVerticalQuantizer.start(0); 115} 116 117/* 118 * Called repeatedly after setup. 119 */ 120void loop() 121{ 122 // Update each light from HSV value to RGB - Brightness is set depending on mySigGen 123 uint8_t value = brightnessSigGen.isRunning() ? (uint8_t)brightnessSigGen.readValue() : 127; // Used for Lights HSV value (brightness) 124 uint8_t scaledSaturation = saturation; 125 if (saturationSigGen.isRunning()) 126 scaledSaturation = ((uint8_t)saturationSigGen.readValue() * saturation) >> 8; 127 128 for (uint8_t i = 0; i < NUM_LIGHTS; i++) { 129 ColourHSV& hsv = lightStripHSV[i]; 130 myPie.ledStrip[i] = ColourConverter::ConvertToRGB(hsv.hue, scaledSaturation, value); 131 } 132 133 // This function drives updates for LEDs and everything in the Sweetmaker framework 134 myPie.update(); 135 136 // Check for any input on the Serial Port (only relevant when connected to computer) 137 handleSerialInput(); 138} 139 140/* 141 * handleMotionSensorNewSmplRdy - called every 10ms when a new motion sensor reading 142 * is available. Interprets the rotational orientaion 143 * data and identifies when to update lights and play 144 * notes. 145 */ 146void handleMotionSensorNewSmplRdy(uint16_t eventId, uint8_t srcRef, uint16_t eventInfo) { 147 /* 148 * Start by manipulating orientation data into meaningful representation 149 */ 150 // Calculate rotation about vertical (horizontal plane) - used for modifying note selection 151 double x = myPie.motionSensor.gravity.x; 152 double y = myPie.motionSensor.gravity.y; 153 double sin_orientation = x / sqrt(x * x + y * y); 154 int16_t zAxisRotation_16384 = (int16_t)(asin(sin_orientation) * 0x8000 / M_PI); 155 zAxisRotationQuantizer.writeValue((int32_t)zAxisRotation_16384); 156 157 // Calculate angle from vertical - used for playing notes 158 Quaternion_16384 vertical = Quaternion_16384(0, 0, 0, 16384); 159 double cos_angleToVertical= (double)myPie.motionSensor.gravity.dotProduct(&vertical) / 16384; 160 int16_t angleToVertical_16384 = (int16_t)(acos(cos_angleToVertical) * 0x8000 / M_PI); 161 verticalTiltQuantizer.writeValue((int32_t)angleToVertical_16384); 162 163 // Calculate rotation of z axis about vertical - used for note selection and modulation 164 Quaternion_16384 zAxis = Quaternion_16384(0, 0, 0, 16384); 165 myPie.motionSensor.rotQuat.rotate(&zAxis); 166 x = zAxis.x; 167 y = zAxis.y; 168 sin_orientation = x / sqrt(x * x + y * y); 169 zAxisAboutVertical_16384 = (int16_t)(asin(sin_orientation) * 0x8000 / M_PI); 170 171 // Adjust value for quadrants outside of +/- 90 172 if (y < 0 && x>0) zAxisAboutVertical_16384 = 32768 - zAxisAboutVertical_16384; 173 else if (y < 0 && x < 0) zAxisAboutVertical_16384 = -32768 - zAxisAboutVertical_16384; 174 175 rotationAboutVerticalQuantizer.writeValue((int32_t)zAxisAboutVertical_16384); 176 177 // Calculate rotational velocity - WRT vertical 178 static int16_t previousAngleToVertical = 0; 179 verticalVelocity = angleToVertical_16384 - previousAngleToVertical; 180 previousAngleToVertical = angleToVertical_16384; 181 182 /* 183 * And now lets respond to what is happening in a stateful manor 184 */ 185 MALLET_handle_event_orientation_update(); 186} 187 188/* 189 * Select Midi Note from scale - this handles shifting octaves if the index is large 190 */ 191// const uint8_t c_major_notes[] = { MIDI_C4,MIDI_D4,MIDI_E4,MIDI_F4,MIDI_G4,MIDI_A4,MIDI_B4 }; 192// MusicalScale c_major_scale = { c_major_notes, 7 }; 193 194//const uint8_t e_major_notes[] = { MIDI_CS3, MIDI_DS3, MIDI_E3,MIDI_FS3,MIDI_GS3, MIDI_A3,MIDI_B3 }; 195//MusicalScale e_major_scale = { e_major_notes, 7 }; 196 197const uint8_t g_major_notes[] = {MIDI_A3,MIDI_B3,MIDI_C4,MIDI_D4, MIDI_E4,MIDI_FS4,MIDI_G4 }; 198MusicalScale g_major_scale = { g_major_notes, 7 }; 199 200MusicalScale* my_scale = &g_major_scale; 201 202/* 203 * malletStateMachine - Controls behaviour of MidiBaton 204 */ 205typedef enum { 206 MUTE = 0, 207 DAMPING = 1, 208 NOTE_SELECTION = 2, 209 COLOUR_LOCKIN = 3, 210 PRE_STRIKE = 4, 211 POST_STRIKE = 5, 212}MALLET_STATE; 213 214typedef enum { 215 ORIENTATION_UPDATE = 1, 216 HORIZONTAL_DIRECTION_CHANGE = 2, 217}MALLET_EVENT; 218 219typedef enum { 220 MUTE_ZONE = 0, // Used for muting a played note 221 DAMP_ZONE = 1, // Used for damping a played note 222 NOTE_SELECTION_ZONE = 2, // Used for selecting the next note 223 STRIKE_ZONE = 3, // Used for controlling the playing of notes 224 AFTER_ZONE = 4, // Used for after effects 225}MALLET_ZONE; 226 227/* This maps the quantized tilt to 'zones' used by MusicMallet */ 228uint8_t convertTiltToZone(int16_t tilt) { 229 static int16_t zoneMap[] = { 230 MUTE_ZONE, 231 DAMP_ZONE, 232 DAMP_ZONE, 233 DAMP_ZONE, 234 NOTE_SELECTION_ZONE, 235 NOTE_SELECTION_ZONE, 236 NOTE_SELECTION_ZONE, 237 STRIKE_ZONE, 238 STRIKE_ZONE, 239 STRIKE_ZONE, 240 AFTER_ZONE, 241 }; 242 if (tilt < 0) tilt = 0; 243 if (tilt > 10) tilt = 10; 244 return (zoneMap[tilt]); 245} 246 247MALLET_STATE mallet_state = MUTE; 248 249void MALLET_handle_event_orientation_update() { 250 /* 251 * This is the state information we use 252 */ 253 static struct NoteTuple nextNote = { 0,0 }; 254 static uint8_t currentNote = 0; 255 static int16_t lastOrientation = 0; 256 static int16_t maxVerticalVelocity = 0; 257 static int16_t pitchBendNullPosition = 0; 258 static boolean dampPedalOn = true; 259 260 int16_t tilt = verticalTiltQuantizer.current_discrete_value; 261 uint8_t zone = convertTiltToZone(tilt); 262 263 switch (mallet_state) { 264 case NOTE_SELECTION: { 265 if ((zone == STRIKE_ZONE) || (zone == AFTER_ZONE)){ 266 Serial.print("Has started strike: "); 267 Serial.println(nextNote.midiNote); 268 mallet_state = PRE_STRIKE; 269 brightnessSigGen.configSamples(flashHighWave, NUM_SAM(flashHighWave), 200, SigGen::DONT_FINISH_ON_ZERO); 270 brightnessSigGen.start(1); 271 maxVerticalVelocity = 0; 272 saturation = 0; 273 break; 274 } 275 if ((zone == DAMP_ZONE) || (zone == MUTE_ZONE)) { 276 Serial.println("Has started damping"); 277 mallet_state = DAMPING; 278 break; 279 } 280 281 nextNote = selectNextNote(); 282 } 283 break; 284 285 case PRE_STRIKE: { 286 if (verticalVelocity > maxVerticalVelocity) { 287 maxVerticalVelocity = verticalVelocity; 288 } 289 290 if (verticalVelocity < 0) { 291 Serial.print("Has struck: "); 292 uint8_t velocity = (uint8_t)((uint16_t)maxVerticalVelocity >> 3); 293 Serial.println(velocity); 294 295 int16_t pos = verticalTiltQuantizer.current_continuous_value - 14336; 296 velocity = (uint8_t)(((uint16_t)pos) >> 6); 297 Serial.println(velocity); 298 if (velocity > 127) velocity = 127; 299 300 myPie.midiBle.noteOff(MIDI_CHAN_NUM, currentNote); 301 myPie.midiBle.noteOn(MIDI_CHAN_NUM, nextNote.midiNote, velocity); 302 currentNote = nextNote.midiNote; 303 304 brightnessSigGen.configSamples(flashLowWave, NUM_SAM(flashLowWave), 200, SigGen::DONT_FINISH_ON_ZERO); 305 brightnessSigGen.start(1); 306 307 pitchBendNullPosition = zAxisAboutVertical_16384; 308 saturation = 128 + velocity; 309 mallet_state = POST_STRIKE; 310 } 311 } 312 break; 313 314 case POST_STRIKE: { 315 nextNote = selectNextNote(); 316 if (zone == NOTE_SELECTION_ZONE) { 317 Serial.println("Has started note selection"); 318 myPie.midiBle.noteOff(MIDI_CHAN_NUM, currentNote); 319 saturation = 255; 320 saturationSigGen.stop(); 321 mallet_state = NOTE_SELECTION; 322 myPie.midiBle.modulate(MIDI_CHAN_NUM, 0); 323 break; 324 } 325 if (zone == AFTER_ZONE) { 326 // Bend note 327 // performPitchBend(zAxisAboutVertical_16384 - pitchBendNullPosition, MIDI_CHAN_NUM); 328 // performAfterTouch(zAxisAboutVertical_16384 - pitchBendNullPosition, MIDI_CHAN_NUM, currentNote); 329 performModulation(zAxisAboutVertical_16384 - pitchBendNullPosition, MIDI_CHAN_NUM); 330 break; 331 } 332 } 333 break; 334 335 case DAMPING: { 336 if (zone == DAMP_ZONE) { 337 uint8_t footControlValue = (uint8_t)(((uint16_t)verticalTiltQuantizer.in_step_value) >> 5); 338 myPie.midiBle.setMidiMsg(0b10110000, 4, footControlValue); 339 nextNote = selectNextNote(); 340 } 341 else if (zone == MUTE_ZONE) { 342 if (dampPedalOn) { 343 Serial.println("Sustain pedal off"); 344 dampPedalOn = false; 345 myPie.midiBle.damperPedalOff(MIDI_CHAN_NUM); 346 lightStripHSV[0].hue = 87; 347 } 348 else { 349 Serial.println("Sustain pedal on"); 350 dampPedalOn = true; 351 myPie.midiBle.damperPedalOn(MIDI_CHAN_NUM); 352 lightStripHSV[0].hue = 0; 353 } 354 mallet_state = MUTE; 355 } 356 else { 357 Serial.println("Has started note selection"); 358 myPie.midiBle.setMidiMsg(0b10110000, 4, 127); 359 mallet_state = NOTE_SELECTION; 360 } 361 } 362 break; 363 364 case MUTE: { 365 if (zone != MUTE) { 366 Serial.println("Has stopped muting"); 367 mallet_state = DAMPING; 368 } 369 } 370 break; 371 } 372} 373 374void MALLET_handle_event_horizontal_dir_change() { 375 switch (mallet_state) { 376 case NOTE_SELECTION: { 377 saturationSigGen.start(1); 378 } 379 break; 380 381 default: 382 break; 383 384 } 385} 386 387 388/* 389* Capture and respond to SweetMaker events 390*/ 391void myEventHandler(uint16_t eventId, uint8_t srcRef, uint16_t eventInfo) 392{ 393 switch (eventId) 394 { 395 /* This functioin is the main */ 396 case MotionSensor::MOTION_SENSOR_NEW_SMPL_RDY: 397 handleMotionSensorNewSmplRdy(eventId, srcRef, eventInfo); 398 break; 399 400 401 case MotionSensor::MOTION_SENSOR_INIT_ERROR: 402 Serial.println("MotionSensor Init Error"); 403 break; 404 405 case MotionSensor::MOTION_SENSOR_READY: 406 { 407 Serial.println("MotionSensor Ready"); 408 // zAxisRotationQuantizer.start(0); 409 } 410 break; 411 412 case MotionSensor::MOTION_SENSOR_RUNTIME_ERROR: { 413 Serial.println("MotionSensor Run Time Error"); 414 } 415 break; 416 417 case MidiBle::BME_CONNECT: 418 Serial.println("BLE Connected"); 419 break; 420 421 case MidiBle::BME_DISCONNECT: 422 Serial.println("BLE Disconnected"); 423 break; 424 425 case TimerTickMngt::TIMER_TICK_S: 426 break; 427 428 case ToDiscrete::NEW_VALUE: { 429 if (srcRef == HORIZ_DIR_QUANTIZER_REF) { 430 MALLET_handle_event_horizontal_dir_change(); 431 } 432 } 433 break; 434 435 // These events are unused but handy for debug 436 case TimerTickMngt::TIMER_TICK_100MS: 437 case TimerTickMngt::TIMER_TICK_UPDATE: 438 case TimerTickMngt::TIMER_TICK_10S: 439 default: 440 break; 441 } 442} 443 444/* This supports various management functions as shown below */ 445void handleSerialInput() { 446 if (Serial.available()) { 447 char c = Serial.read(); 448 Serial.println(c); 449 450 switch (c) { 451 452 case 'c': { 453 // Calibrates the motionSensor and stores result in EEPROM 454 Serial.println("MotionSensor must be level and stationary"); 455 Serial.println("Starting to calibrate"); 456 myPie.recalibrateMotionSensor(); 457 Serial.println("Calibration complete"); 458 } 459 break; 460 461 case 'l': { 462 // Configures the motionSensor rotation offset to believe it is level 463 // Stores the configuration in EEPROM 464 Serial.println("AutoLevel"); 465 myPie.configOffsetRotation(); 466 } 467 break; 468 469 case 'z': { 470 // Removes any rotation offset from the motionSensor 471 Serial.println("Clear offset"); 472 myPie.motionSensor.clearOffsetRotation(); 473 } 474 break; 475 } 476 } 477} 478 479boolean performPitchBend(int16_t input, uint8_t channel) { 480 if (input < 2048) 481 return false; 482 483 uint16_t bendVal = (input + 2048) << 1; 484 if (bendVal > 0x3fff) 485 bendVal = 0x3fff; 486 487 myPie.midiBle.pitchBendChange(MIDI_CHAN_NUM, bendVal); 488 return true; 489} 490 491boolean performAfterTouch(int16_t input, uint8_t channel, uint8_t note) { 492 493 if (input > -2048) 494 return false; 495 uint16_t bendVal = (-input - 2048) << 1; 496 if (bendVal > 0x1fff) 497 bendVal = 0x1fff; 498 499 myPie.midiBle.polyphonicPressure(channel, note, bendVal >> 6); 500 return true; 501} 502 503boolean performModulation(int16_t input, uint8_t channel) { 504 505 if (input > -2048) 506 return false; 507 uint16_t modVal = (-input - 2048) << 1; 508 if (modVal > 0x3fff) 509 modVal = 0x3fff; 510 511 myPie.midiBle.modulate(channel, modVal); 512 return true; 513} 514 515 516int8_t detectAccidental() { 517 if (zAxisRotationQuantizer.current_discrete_value >= 2) 518 return -1; 519 else if (zAxisRotationQuantizer.current_discrete_value <= -2) 520 return +1; 521 return 0; 522} 523 524 525/* 526 * selectMidiNoteFromScale 527 */ 528struct NoteTuple selectMidiNoteFromScale(MusicalScale* scale, int16_t index) { 529 int8_t octave_shift = 0; 530 while (index < 0) { 531 index += scale->num_notes; 532 octave_shift--; 533 } 534 while (index >= scale->num_notes) { 535 index -= scale->num_notes; 536 octave_shift++; 537 } 538 int16_t midi_note = scale->midi_notes[index]; 539 return { (uint8_t)(midi_note + octave_shift * 12), (uint8_t)index }; 540} 541 542 543struct NoteTuple selectNextNote() { 544 NoteTuple note; 545 int16_t orientation = rotationAboutVerticalQuantizer.current_discrete_value; 546 note = selectMidiNoteFromScale(my_scale, orientation); 547 note.midiNote += detectAccidental(); 548 for (uint8_t i = 0; i < NUM_LIGHTS; i++) 549 lightStripHSV[i].hue = hue[note.indexInScale]; 550 return (note); 551}
Comments
Only logged in users can leave comments