Tag Archives: midi

Strudel and Hydra

(Live real time using text (code) to make music and visuals)

Last week, I discovered Strudel.
I was playing around with it and thought: “This would be perfect for Tyrone”.
He is/was the main musical artist in our Group.

Strudel REPL is a web-based, live coding environment for creating music, based on the Tidal Cycles pattern language. It allows users to write and edit code in a browser to instantly hear the music it produces, with features like visual feedback and an interactive tutorial to help beginners and experts. The “REPL” stands for Read-Eval-Print Loop, a common term for interactive programming environments where you can type code, and the system immediately evaluates it and prints the result. 

Check it out on : https://strudel.cc/

Below a strudel example

I can make some simple patterns, but I discovered that it can be used multiuser with realtime synthesizer named Hydra.
I’m better doing visuals, than creating realtime digital music.
Check out https://www.henriaanstoot.nl/?s=shaders – for my previous shader programming posts.
(I love playing real instruments, let’s keep it that way.)

Hydra video synth is a free, open-source, live-coding environment for creating real-time visuals that runs in a web browser. It uses JavaScript and WebGL to allow users to “patch” together different visual sources and transformations, similar to an analog modular synthesizer, to generate effects like video feedback, generative patterns, and audio-reactive visuals. 

It is JavaScript being compiled to WebGL

So using https://flok.cc we can start a multiuser session with realtime live coding.

So the first test session, learning to use Hydra and Strudel together over the internet. Plan is to do a live session for others using a screen projector.

Left Tyrone (Strudel) right me with Hydra.

In the last few days, I made some examples using hydra.

These examples use Microphone for sound reactive, and Webcam for embedded effects.
NOTE: These are only in your browser session, nothing is being recorded/stored.

You can check some examples here :

https://media.henriaanstoot.nl/hydra/

UPDATE : Controlling Hydra input using my DIY Midi Controller (see other post) (Music is NOT related, no microphone input)

Strudel midi input with lpf volume and room controls.

let cc = await midin(1)
note("c a f e").lpf(cc(7).range(0, 1000)).room(cc(3).range(0, 10)).sound("sawtooth")

Midi control YM2203 and Analog AMP stuff

While watching TV, I drew some schematics for the YM2203 player.

For another project, I was researching the best way to get the voltage levels perfect for below analogue input.

(See the little red breadboard in the right picture above! )

Damn, I couldn’t do this stuff in school. Tried to forget it all … now I need it!

Top of this image shows what I want to accomplish. Audio or other input which has negative values also, convert to another voltage range to be read by analog read on an Wemos. These can only read 0-3v3

Mini midi monitor

Two versions of a mini monitor

A version using a Arduino and a Midi shield (Yellow wires are for display)
D0 (RX) is used for the Midi IN signal.

10K pullups SDA/CLK

Above a Teensy 4.0 version. This one uses MIDI over USB.

Next to add: Rotary encoders, to select a CC Channel and display values graphically

CODE for Teensy version

#include <U8g2lib.h>
#include <Wire.h>
#include <MIDIUSB.h>   // Teensy's built-in USB MIDI

// SH1106 128x64 I2C constructor
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

void setup() {
  u8g2.begin();
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x12_tf);
  u8g2.drawStr(0,12,"MIDI Monitor Ready");
  u8g2.sendBuffer();
}

void loop() {
  // Check for incoming MIDI

  while (usbMIDI.read()) {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x12_tf);
  u8g2.drawStr(0,12,"MIDI Monitor Ready");
  u8g2.sendBuffer();
    byte type = usbMIDI.getType();
    byte channel = usbMIDI.getChannel();
    byte data1 = usbMIDI.getData1();
    byte data2 = usbMIDI.getData2();

    int y = 24;

    if (type == usbMIDI.NoteOn && data2 > 0) {
      u8g2.setCursor(0, y);      u8g2.print("Note ON     "); // pad
      u8g2.setCursor(0, y+12);   u8g2.printf("Ch:%-3d", channel);  // pad width 3
      u8g2.setCursor(0, y+24);   u8g2.printf("Note:%-3d", data1);
      u8g2.setCursor(0, y+36);   u8g2.printf("Vel:%-3d", data2);
    } 
    else if (type == usbMIDI.NoteOff || (type == usbMIDI.NoteOn && data2 == 0)) {
      u8g2.setCursor(0, y);      u8g2.print("Note OFF    "); // pad
      u8g2.setCursor(0, y+12);   u8g2.printf("Ch:%-3d", channel);
      u8g2.setCursor(0, y+24);   u8g2.printf("Note:%-3d", data1);
    } 
    else if (type == usbMIDI.ControlChange) {
      u8g2.setCursor(0, y);      u8g2.print("Control Chg ");
      u8g2.setCursor(0, y+12);   u8g2.printf("Ch:%-3d", channel);
      u8g2.setCursor(0, y+24);   u8g2.printf("CC#:%-3d", data1);
      u8g2.setCursor(0, y+36);   u8g2.printf("Val:%-3d", data2);
    } 
    else {
      u8g2.setCursor(0, y);      u8g2.print("Other MIDI  ");
      u8g2.setCursor(0, y+12);   u8g2.printf("Type:%-3d", type);
    }

    u8g2.sendBuffer();
  }
}

CODE for Arduino plus shield

#include <U8g2lib.h>
#include <Wire.h>
#include <MIDI.h>   // FortySevenEffects MIDI library

// SH1106 128x64 I2C (page buffer, low RAM)
U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

// MIDI on hardware Serial (RX=D0)
MIDI_CREATE_INSTANCE(HardwareSerial, Serial, MIDI);

char line[12];  // small buffer for formatting

void setup() {
  u8g2.begin();
  u8g2.setFont(u8g2_font_6x12_tf); // proportional font, small size

  // Initial message
  u8g2.firstPage();
  do {
    u8g2.setCursor(0, 12);
    u8g2.print("MIDI Monitor Ready");
  } while (u8g2.nextPage());

  MIDI.begin(MIDI_CHANNEL_OMNI);  // listen to all channels
}

void loop() {
  if (MIDI.read()) {
    byte type    = MIDI.getType();
    byte channel = MIDI.getChannel();
    byte data1   = MIDI.getData1();
    byte data2   = MIDI.getData2();

    // Page buffer redraw
    u8g2.firstPage();
    do {
      // Title
      u8g2.setCursor(0, 12);
      u8g2.print("MIDI Monitor");

      int y = 24; // start lower down

      if (type == midi::NoteOn && data2 > 0) {
        u8g2.setCursor(0, y);      
        u8g2.print("Note ON   ");

        snprintf(line, sizeof(line), "Ch:%-3d", channel);
        u8g2.setCursor(0, y+12);   u8g2.print(line);32

        snprintf(line, sizeof(line), "Note:%-3d", data1);
        u8g2.setCursor(0, y+24);   u8g2.print(line);

        snprintf(line, sizeof(line), "Vel:%-3d", data2);
        u8g2.setCursor(0, y+36);   u8g2.print(line);
      } 
      else if (type == midi::NoteOff || (type == midi::NoteOn && data2 == 0)) {
        u8g2.setCursor(0, y);      
        u8g2.print("Note OFF  ");

        snprintf(line, sizeof(line), "Ch:%-3d", channel);
        u8g2.setCursor(0, y+12);   u8g2.print(line);

        snprintf(line, sizeof(line), "Note:%-3d", data1);
        u8g2.setCursor(0, y+24);   u8g2.print(line);
      } 
      else if (type == midi::ControlChange) {
        u8g2.setCursor(0, y);      
        u8g2.print("Control Chg");

        snprintf(line, sizeof(line), "Ch:%-3d", channel);
        u8g2.setCursor(0, y+12);   u8g2.print(line);

        snprintf(line, sizeof(line), "CC#:%-3d", data1);
        u8g2.setCursor(0, y+24);   u8g2.print(line);

        snprintf(line, sizeof(line), "Val:%-3d", data2);
        u8g2.setCursor(0, y+36);   u8g2.print(line);
      } 
      else {
        u8g2.setCursor(0, y);      
        u8g2.print("Other MIDI");

        snprintf(line, sizeof(line), "Type:%-3d", type);
        u8g2.setCursor(0, y+12);   u8g2.print(line);
      }
    } while (u8g2.nextPage());
  }
}

Cynthcart (c64) midi control

I bought a teensyrom a while ago.

UPDATE 20250712 : Display
UPDATE 20250904 : Added PitchBend and stabilized potmeters.

Tyrone and I wanted to control the settings using potmeters.
So I grabbed a Teensy 4.1 controller and some 10K potmeters, and worte some code

Code for 12 pots, pitch bend and display, don’t forget to set your USB mode to midi!
Schematic soon, also all tweaks and note sending.

Todo: Nice 3D printed Pitch Bend wheel, rest of display code and extra buttons!

Let’s use an old box to hold the pots!

3D printed wooden knobs (Yes wood filament)
Unstable release pot due too bad wires. Cynthcart has no decay and hold (hold is how long you are pressing key)

Knob 1 = volume now, and left of box has centering Pitch Bend.


CODE

#include <MIDIUSB.h>
#include <ResponsiveAnalogRead.h>

const int pitchbend = A14;
const int filterresonance = A0;
const int filtercutoff = A1;
const int voicemode = A2;
const int modulationmode = A3;
const int attack = A4;
const int release = A5;
const int pulsewidth = A6;
const int tremolodepth = A7;
const int tremolospeed = A8;
const int oscwave = A9;
const int oscvoice2 = A10;
const int oscvoice3 = A11;
const int volume = A12;

ResponsiveAnalogRead analogpitchbend(pitchbend, true);

ResponsiveAnalogRead analogfilterresonance(filterresonance, true);
ResponsiveAnalogRead analogfiltercutoff(filtercutoff, true);
ResponsiveAnalogRead analogvoicemode(voicemode, true);
ResponsiveAnalogRead analogmodulationmode(modulationmode, true);
ResponsiveAnalogRead analogattack(attack, true);
ResponsiveAnalogRead analogrelease(release, true);
ResponsiveAnalogRead analogpulsewidth(pulsewidth, true);
ResponsiveAnalogRead analogtremolodepth(tremolodepth, true);
ResponsiveAnalogRead analogtremolospeed(tremolospeed, true);
ResponsiveAnalogRead analogoscwave(oscwave, true);
ResponsiveAnalogRead analogoscvoice2(oscvoice2, true);
ResponsiveAnalogRead analogoscvoice3(oscvoice3, true);
ResponsiveAnalogRead analogvolume(volume, true);


// A0 -> A3, A6 -> A13
const int numPots = 12;


int lastfilterresonance = -1;
int lastfiltercutoff = -1;
int lastvoicemode = -1;
int lastmodulationmode = -1;
int lastattack = -1;
int lastrelease = -1;
int lastpulsewidth = -1;
int lasttremolodepth = -1;
int lasttremolospeed = -1;
int lastoscwave = -1;
int lastoscvoice2 = -1;
int lastoscvoice3 = -1;
int lastvolume = -1;


// CC 7 ff niet

// Custom mappings:
const int potPins[numPots]     = {A0, A1, A2, A3, A6, A7, A8, A9, A10, A11, A12, A13};  // Analog pins
const int ccNumbers[numPots]   = {0,1,2,3,4,5,6,8,9,13,14,15};             // CC numbers
const int midiChannels[numPots]= {1,1,1,1,1,1,1,1,1,1,1,1};                // MIDI channels (1–16)

int lastValues[numPots];  // Store last values to reduce redundant MIDI messages


void setup() {
  Serial.begin(9600);
  for (int i = 0; i < numPots; i++) {
    pinMode(potPins[i], INPUT);
    lastValues[i] = -1;
  }
  pinMode(pitchbend, INPUT);
}

void loop() {
  analogfilterresonance.update();
  if(analogfilterresonance.hasChanged()) {
    int analogValue = analogfilterresonance.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastfilterresonance) > 0){
    usbMIDI.sendControlChange(7, midiValue, 1);
    lastfilterresonance = midiValue;
    }
  }

  analogfiltercutoff.update();
  if(analogfiltercutoff.hasChanged()) {
    int analogValue = analogfiltercutoff.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastvoicemode) > 0){
    usbMIDI.sendControlChange(1, midiValue, 1);
    lastvoicemode = midiValue;
    }  
  }

  analogvoicemode.update();
  if(analogvoicemode.hasChanged()) {
    int analogValue = analogvoicemode.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastvoicemode) > 0){
    usbMIDI.sendControlChange(2, midiValue, 1);
    lastvoicemode = midiValue;
    }
  }

  analogmodulationmode.update();
  if(analogmodulationmode.hasChanged()) {
    int analogValue = analogmodulationmode.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastmodulationmode) > 0){
    usbMIDI.sendControlChange(3, midiValue, 1);
    lastmodulationmode = midiValue;
    }
  }


  analogattack.update();
  if(analogattack.hasChanged()) {
    int analogValue = analogattack.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastattack) > 0){
    usbMIDI.sendControlChange(4, midiValue, 1);
    lastattack = midiValue;
    }
  }


  analogrelease.update();
  if(analogrelease.hasChanged()) {
    int analogValue = analogrelease.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastrelease) > 0){
    usbMIDI.sendControlChange(5, midiValue, 1);
    lastrelease = midiValue;
    }
  }


  analogpulsewidth.update();
  if(analogpulsewidth.hasChanged()) {
    int analogValue = analogpulsewidth.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastpulsewidth) > 0){
    usbMIDI.sendControlChange(6, midiValue, 1);
    lastpulsewidth = midiValue;
    }
  }


  analogtremolodepth.update();
  if(analogtremolodepth.hasChanged()) {
    int analogValue = analogtremolodepth.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lasttremolodepth) > 0){
    usbMIDI.sendControlChange(8, midiValue, 1);
    lasttremolodepth = midiValue;
    }
  }


  analogtremolospeed.update();
  if(analogtremolospeed.hasChanged()) {
    int analogValue = analogtremolospeed.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lasttremolospeed) > 0){
    usbMIDI.sendControlChange(9, midiValue, 1);
    lasttremolospeed = midiValue;
    }
  }


  analogoscwave.update();
  if(analogoscwave.hasChanged()) {
    int analogValue = analogoscwave.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastoscwave) > 0){
    usbMIDI.sendControlChange(13, midiValue, 1);
    lastoscwave = midiValue;
    }
  }


  analogoscvoice2.update();
  if(analogoscvoice2.hasChanged()) {
    int analogValue = analogoscvoice2.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastoscvoice2) > 0){
    usbMIDI.sendControlChange(14, midiValue, 1);
    lastoscvoice2 = midiValue;
    }
  }


  analogoscvoice3.update();
  if(analogoscvoice3.hasChanged()) {
    int analogValue = analogoscvoice3.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastoscvoice3) > 0){
    usbMIDI.sendControlChange(15, midiValue, 1);
    lastoscvoice3 = midiValue;
    }
  }







// PITCHBEND!
  analogpitchbend.update();
  if(analogpitchbend.hasChanged()) {
    int pitchBend = map(analogpitchbend.getValue(), 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range
    sendPitchBend(pitchBend, 0);
  }
  /*
  int potValue = analogRead(A14);         // Read pot (0–1023)
  int pitchBend = map(potValue, 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range
  //pitchBend = pitchBend + 8192;


  if (abs(pitchBend - lastPitch) > 250) { // Send only on significant change
    sendPitchBend(pitchBend, 0); 
        Serial.println(pitchBend);

    lastPitch = pitchBend;
  }
  */


  delay(5);  // CPU-friendly update rate
}

void sendPitchBend(int value, byte channel) {
  byte lsb = value & 0x7F;
  byte msb = (value >> 7) & 0x7F;

  midiEventPacket_t pitchBendPacket = {0x0E, 0xE0 | (channel & 0x0F), lsb, msb};
  MidiUSB.sendMIDI(pitchBendPacket);
  // Needed? 
  MidiUSB.flush();
}

CODE for display

#include <MIDIUSB.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET 4
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// A0 -> A3, A6 -> A13
const int numPots = 12;

// A14 - Pitch bend!
int lastPitch = -1;

//A4 A5 I2C display


// Custom mappings:
const int potPins[numPots]     = {A0, A1, A2, A3, A6, A7, A8, A9, A10, A11, A12, A13};  // Analog pins
const int ccNumbers[numPots]   = {0,1,2,3,4,5,19,7,8,9,13,14};             // CC numbers
const int midiChannels[numPots]= {1,1,1,1,1,1,1,1,1,1,1,1};                // MIDI channels (1–16)

int lastValues[numPots];  // Store last values to reduce redundant MIDI messages

void setup() {
  for (int i = 0; i < numPots; i++) {
    pinMode(potPins[i], INPUT);
    lastValues[i] = -1;
  }
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.clearDisplay();
  display.display();
}

void loop() {
  for (int i = 0; i < numPots; i++) {
    int analogValue = analogRead(potPins[i]);
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127

    if (abs(midiValue - lastValues[i]) > 1) {
      usbMIDI.sendControlChange(ccNumbers[i], midiValue, midiChannels[i]);
      lastValues[i] = midiValue;
    }
  }
  int potValue = analogRead(A12);         // Read pot (0–1023)
  int pitchBend = map(potValue, 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range

  if (abs(pitchBend - lastPitch) > 5) { // Send only on significant change
    sendPitchBend(pitchBend, 0); // Channel 1
    lastPitch = pitchBend;
  }

  displayInfo();


  delay(5);  // CPU-friendly update rate
}

void sendPitchBend(int value, byte channel) {
  byte lsb = value & 0x7F;
  byte msb = (value >> 7) & 0x7F;

  midiEventPacket_t pitchBendPacket = {0x0E, 0xE0 | (channel & 0x0F), lsb, msb};
  MidiUSB.sendMIDI(pitchBendPacket);
  // Needed? 
  MidiUSB.flush();
}

void displayInfo(){
 byte x0, y0, x1, y1;     // start/end coordinates for drawing lines on OLED
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print("Attack / Release");

  // draw attack line
  x0 = 0;
  y0 = 63;
  x1 = map(attackParam, 0, 127, 0, ((SCREEN_WIDTH / 4) - 1));
  y1 = 20;
  display.drawLine(x0, y0,  x1,  y1, SH110X_WHITE);

   // draw release line
  x0 = x1;  // start line from previous line's final x,y location
  y0 = y1;
  x1 = x0 + map(releaseParam, 0, 127, 0, ((SCREEN_WIDTH / 4) - 1));
  y1 = 63;
  display.drawLine(x0, y0,  x1,  y1, SH110X_WHITE);

  display.display();

}