INTRO
What makes something an instrument? I’d like to borrow a concept from Rehding 2016, Three Music-Theory Lessons: ‘epistemic things,’ things that we use to practice knowledge while themselves are the knowledge in terms of how knowledge is generated. Aside from the extent to which a person can easily make acoustically pleasant sound waves out of an installation, what constitutes an instrument is that itself is an epistemic thing—having connections with and making it feasible to practice our common musical knowledge on while embodying a distinct system that generates/contributes to musical knowledge/practice.
That being said, how we divided our work is basically mapping the two sides (and the two stage of the development) to the two of us—the music and the interface, the backend and the frontend, the soft and the hard, etc. On my side, I had the Arduino and the speaker to start up a very basic synthesizer consisting of two audio oscillators and two control oscillators.
Process
In a nutshell, what I tried to realize is a simple cosine wave oscillator frequency modulated according to the four input parameters that control root frequency: intensity (the extent to which the root signal is modulated to the target frequency), vibrato speed (the rate at which the modulation is automatically carried on), and modulation ratio (the multiple to determine the target frequency from the root).
#include <Mozzi.h> #include <Oscil.h> // oscillator #include <tables/cos2048_int8.h> // table for Oscils to play #include <Smooth.h> #include <AutoMap.h> // desired carrier frequency max and min, for AutoMap const int MIN_CARRIER_FREQ = 22; const int MAX_CARRIER_FREQ = 440; // desired intensity max and min, for AutoMap, inverted for reverse dynamics const int MIN_INTENSITY = 10; const int MAX_INTENSITY = 1000; // desired modulation ratio max and min const int MIN_MODRATIO = 5; const int MAX_MODRATIO = 2; // desired mod speed max and min, for AutoMap, note they're inverted for reverse dynamics const int MIN_MOD_SPEED = 10000; const int MAX_MOD_SPEED = 1; AutoMap kMapCarrierFreq(400,700,MIN_CARRIER_FREQ,MAX_CARRIER_FREQ); AutoMap kMapIntensity(400,700,MIN_INTENSITY,MAX_INTENSITY); AutoMap kMapModRatio(400,700,MIN_MODRATIO,MAX_MODRATIO); AutoMap kMapModSpeed(400,700,MIN_MOD_SPEED,MAX_MOD_SPEED); const int FREQ_PIN = 0; const int MOD_PIN = 1; const int RATIO_PIN = 3; const int SPEED_PIN = 2; Oscil<COS2048_NUM_CELLS, MOZZI_AUDIO_RATE> aCarrier(COS2048_DATA); Oscil<COS2048_NUM_CELLS, MOZZI_CONTROL_RATE> kIntensityMod(COS2048_DATA); Oscil<COS2048_NUM_CELLS, MOZZI_AUDIO_RATE> aModulator(COS2048_DATA); int mod_ratio; // harmonics long fm_intensity; // carries control info from updateControl() to updateAudio() // smoothing for intensity to remove clicks on transitions float smoothness = 0.95f; Smooth <long> aSmoothIntensity(smoothness); void setup(){ Serial.begin(115200); // set up the Serial output for debugging startMozzi(); } void updateControl(){ // read the freq int freq_value = mozziAnalogRead<10>(FREQ_PIN); // value is 0-1023 // map the input to carrier frequency int carrier_freq = kMapCarrierFreq(freq_value); // read the ratio int ratio_value = mozziAnalogRead<10>(RATIO_PIN); // map the input to ratio mod_ratio = kMapModRatio(ratio_value); //calculate the modulation frequency to stay in ratio int mod_freq = carrier_freq * mod_ratio; // set the FM oscillator frequencies to the calculated values aCarrier.setFreq(carrier_freq); aModulator.setFreq(mod_freq); // calculate the fm_intensity int mod_level= mozziAnalogRead<10>(MOD_PIN); // value is 0-1023 fm_intensity = ((long)mod_level * (kIntensityMod.next()+128))>>8; // use a float here for low frequencies int speed_value = mozziAnalogRead<10>(SPEED_PIN); float mod_speed = (float)kMapModSpeed(speed_value)/1000; kIntensityMod.setFreq(mod_speed); } AudioOutput updateAudio(){ long modulation = aSmoothIntensity.next(fm_intensity) * aModulator.next(); return MonoOutput::from8Bit(aCarrier.phMod(modulation)); // phMod does the FM } void loop(){ audioHook(); }
THE GLOVE
Zavier here, now it’s my turn! So after testing the circuit and code (huge thanks to Xiaotian for the awesome sound control!), I began working on attaching it to the glove, which was… painful. I first just measured out and positioned things, then I started attaching them with my small transparent tape (which isn’t the ideal way (sowing it would be better), and besides my tape wasn’t strong at all). After asking a friend for help, I got the sensors attached. Ok, the easy part done. Now the troubles begin. You see, I obviously had to connect the flex sensors to the Arduino. I thought I could I just use the female-male wires, but nope! The pins on the flex sensors were too small, so it was far too lose :(. I tried thinking of a few other options, but in the end, I had to do what I was trying to avoid, soldering. To be honest, I didn’t even mind it that much before (past me talking), and thought it would take a few fun minutes, but boy oh boy. I don’t know what it is about these solders (maybe they’re dirty?), but only a very tiny bit of the tip is actually going to melt the solder, so a lot of time was spent just moving and rotating the tip. Also, I shouldn’t have attached the flex sensors already. It was a huge pain to get them soldered. Now admittedly, (probably a large) part of that is because I’ve hardly soldered before, but also (in addition to the tip issue), I was making these 3 point connections (such as connecting the ground of one the flex sensors, to the ground of the adjacent ones), so whenever I tried soldering, it would just release the other 2!
Anyways, after some work, I finally got the flex sensors wired up, and it was finally working. Great! Ok, we’re done… we’re done?…. right?. Haha, nope. I thought it would be a good idea to add neopixels (addressable LED strips) to the gloves too. After testing a strip, I didn’t repeat my mistake, and this time I soldered together the strips first, before attaching them. This went a lot smoother (also thanks to just having some experience doing it for the flex sensors), but it still took sooo long. Unfortunately, since I soldered it first, the connections weren’t the right length 🙂. Luckily, I had expected them not to be precisely correct (and besides the distance between the strips would change a bit as the hand was flexed and relaxed), and so kept it a bit longer, so that the length could be adjusted as needed. This unintentionally also ended up creating a nice pattern 😅.
While it was a lot of work, it definitely made things a LOT cooler, and also provided a way to give some info to the user visually.
Final Product
Demo (note: the sound is very distorted in the recording)
Code:
// Configuring Mozzi's options #include <MozziConfigValues.h> #define MOZZI_ANALOG_READ_RESOLUTION 10 // Not strictly necessary, as Mozzi will automatically use the default resolution of the hardware (eg. 10 for the Arduino Uno), but they recommend setting it (either here globally, or on each call) #include <Mozzi.h> #include <Oscil.h> // oscillator #include <tables/cos2048_int8.h> // table for Oscils to play #include <Smooth.h> #include <AutoMap.h> #include <FastLED.h> // Flex sensor stuff // Define flex sensor pins (these have to be analog) const int FREQ_SENSOR_PIN = A0; const int MOD_SENSOR_PIN = A1; const int SPEED_SENSOR_PIN = A2; const int RATIO_SENSOR_PIN = A3; // Smoothening for each pin (was previously using rolling averages) Smooth<unsigned int> smoothFreq(0.8f); Smooth<unsigned int> smoothMod(0.5f); Smooth<unsigned int> smoothSpeed(0.75f); Smooth<unsigned int> smoothRatio(0.5f); // Input ranges for flex sensors (will be calibrated) unsigned int freqInputMin = 1000; // Just FYI, the flex sensors in our setup roughly output in the range of ~ 200 - 650 unsigned int freqInputMax = 0; unsigned int modInputMin = 1000; unsigned int modInputMax = 0; unsigned int speedInputMin = 1000; unsigned int speedInputMax = 0; unsigned int ratioInputMin = 1000; unsigned int ratioInputMax = 0; // Neopixel (addressable LED strip) stuff // Define neopixel pins const int FREQ_NEOPIXEL_PIN = 2; const int MOD_NEOPIXEL_PIN = 3; const int SPEED_NEOPIXEL_PIN = 4; const int RATIO_NEOPIXEL_PIN = 5; // Number of LEDs in each strip const int NEOPIXEL_NUM_LEDS = 11; // Define the array of leds CRGB freqLEDs[NEOPIXEL_NUM_LEDS]; CRGB modLEDs[NEOPIXEL_NUM_LEDS]; CRGB speedLEDs[NEOPIXEL_NUM_LEDS]; CRGB ratioLEDs[NEOPIXEL_NUM_LEDS]; // Sound stuff // desired carrier frequency max and min, for AutoMap const int MIN_CARRIER_FREQ = 22; const int MAX_CARRIER_FREQ = 440; // desired intensity max and min, for AutoMap, inverted for reverse dynamics const int MIN_INTENSITY = 10; const int MAX_INTENSITY = 1000; // desired modulation ratio max and min const int MIN_MOD_RATIO = 5; const int MAX_MOD_RATIO = 2; // desired mod speed max and min, for AutoMap, note they're inverted for reverse dynamics const int MIN_MOD_SPEED = 10000; const int MAX_MOD_SPEED = 1; Oscil<COS2048_NUM_CELLS, MOZZI_AUDIO_RATE> aCarrier(COS2048_DATA); Oscil<COS2048_NUM_CELLS, MOZZI_CONTROL_RATE> kIntensityMod(COS2048_DATA); Oscil<COS2048_NUM_CELLS, MOZZI_AUDIO_RATE> aModulator(COS2048_DATA); int mod_ratio; // harmonics long fm_intensity; // carries control info from updateControl() to updateAudio() // smoothing for intensity to remove clicks on transitions float smoothness = 0.95f; Smooth<long> aSmoothIntensity(smoothness); void setup(){ Serial.begin(9600); // set up the Serial output for debugging // Set the flex sensor pins pinMode( FREQ_SENSOR_PIN, INPUT_PULLUP); pinMode( MOD_SENSOR_PIN, INPUT_PULLUP); pinMode(SPEED_SENSOR_PIN, INPUT_PULLUP); pinMode(RATIO_SENSOR_PIN, INPUT_PULLUP); // Setup the neopixels FastLED.addLeds<NEOPIXEL, FREQ_NEOPIXEL_PIN>(freqLEDs, NEOPIXEL_NUM_LEDS); FastLED.addLeds<NEOPIXEL, MOD_NEOPIXEL_PIN>(modLEDs, NEOPIXEL_NUM_LEDS); FastLED.addLeds<NEOPIXEL, SPEED_NEOPIXEL_PIN>(speedLEDs, NEOPIXEL_NUM_LEDS); FastLED.addLeds<NEOPIXEL, RATIO_NEOPIXEL_PIN>(ratioLEDs, NEOPIXEL_NUM_LEDS); FastLED.setBrightness(32); // 0 - 255 // Feed/prime/initialise the smoothing function to get a stable output from the first read (to ensure the calibration isn't messed up). A value of 1630 was chosen by trial and error (divide and conquer), and seems to work best (at least for our setup) smoothFreq.next(1630); smoothMod.next(1630); smoothSpeed.next(1630); smoothRatio.next(1630); startMozzi(); } // Basically our actual traditional loop in Mozzi (but still needs to kept reasonably lean and fast) void updateControl(){ // Read the smoothened freq int freqValue = smoothFreq.next(mozziAnalogRead(FREQ_SENSOR_PIN - 14)); // value is 0-1023, -14 since mozzi just takes a number (eg. 0 instead of A0), and the analog ones are 14 onwards // Calibrate the mapping if needed if (freqValue < freqInputMin) freqInputMin = freqValue; if (freqValue > freqInputMax) freqInputMax = freqValue; // Map the input to the carrier frequency int carrier_freq = map(freqValue, freqInputMin, freqInputMax, MIN_CARRIER_FREQ, MAX_CARRIER_FREQ); // Read the smoothened ratio int ratioValue = smoothRatio.next(mozziAnalogRead(RATIO_SENSOR_PIN - 14)); // Calibrate the mapping if needed if (ratioValue < ratioInputMin) ratioInputMin = ratioValue; if (ratioValue > ratioInputMax) ratioInputMax = ratioValue; // Map the input to the ratio mod_ratio = map(ratioValue, ratioInputMin, ratioInputMax, MIN_MOD_RATIO, MAX_MOD_RATIO); // calculate the modulation frequency to stay in ratio int mod_freq = carrier_freq * mod_ratio; // set the FM oscillator frequencies to the calculated values aCarrier.setFreq(carrier_freq); aModulator.setFreq(mod_freq); // Read the smoothened mod int modValue = smoothMod.next(mozziAnalogRead(MOD_SENSOR_PIN - 14)); // Calibrate the mapping if needed if (modValue < modInputMin) modInputMin = modValue; if (modValue > modInputMax) modInputMax = modValue; // Calculate the fm_intensity fm_intensity = ((long)modValue * (kIntensityMod.next()+128))>>8; // Read the smoothened speed int speedValue = smoothSpeed.next(mozziAnalogRead(SPEED_SENSOR_PIN - 14)); // Calibrate the mapping if needed if (speedValue < speedInputMin) speedInputMin = speedValue; if (speedValue > speedInputMax) speedInputMax = speedValue; // use a float here for low frequencies float mod_speed = (float)map(speedValue, speedInputMin, speedInputMax, MIN_MOD_SPEED, MAX_MOD_SPEED) / 1000; kIntensityMod.setFreq(mod_speed); // Set the leds FastLED.clear(); // Resets them // The frequency controls how many of the LEDs are light up (in a rainbow colour) int freqLEDAmount = map(freqValue, freqInputMin, freqInputMax, 0, NEOPIXEL_NUM_LEDS); fill_rainbow(&freqLEDs[NEOPIXEL_NUM_LEDS - freqLEDAmount], freqLEDAmount, CRGB::White, 25); // &...LEDs[i] to start lighting from there, allowing us to light them in reverse // For the mod, show a meter (blue - deep pink) showing the mix level of the 2 sounds int modLEDAmount = map(modValue, modInputMin, modInputMax, 0, NEOPIXEL_NUM_LEDS); fill_solid(modLEDs, NEOPIXEL_NUM_LEDS, CRGB::Blue); fill_solid(&modLEDs[NEOPIXEL_NUM_LEDS - modLEDAmount], modLEDAmount, CRGB::DeepPink); // The speed controls the blinking rate of its LEDs (between 1/2 to 3 seconds per blink cycle) int speedLEDBlinkRate = map(speedValue, speedInputMin, speedInputMax, 500, 3000); if (millis() % speedLEDBlinkRate < speedLEDBlinkRate/2) fill_rainbow(speedLEDs, NEOPIXEL_NUM_LEDS, CRGB::White, 25); // The ratio controls the hue of its LEDs int ratioLEDHue = map(ratioValue, ratioInputMin, ratioInputMax, 0, 360); fill_solid(ratioLEDs, NEOPIXEL_NUM_LEDS, CHSV(ratioLEDHue, 100, 50)); // We could also blend between 2 colours based on the ratio, pick the one you prefer // fract8 ratioLEDFraction = map(ratioValue, ratioInputMin, ratioInputMax, 0, 255); // fill_solid(ratioLEDs, NEOPIXEL_NUM_LEDS, blend(CRGB::Blue, CRGB::DeepPink, ratioLEDFraction)); FastLED.show(); // Shows them } // Mozzi's function for getting the sound. Must be as light and quick as possible to ensure the sound buffer is adequently filled AudioOutput updateAudio() { long modulation = aSmoothIntensity.next(fm_intensity) * aModulator.next(); return MonoOutput::from8Bit(aCarrier.phMod(modulation)); // phMod does the FM } // Since we're using Mozzi, we just call its hook void loop() { audioHook(); }