MIDI
Was ist MIDI?
Spectral Canon for Conlon Nancarrow (James Tenney, 1974) Digitale Reproduktion
Midi ist ein Standard zum Austausch von Daten zwischen musikalischen Klangerzeugern, Keyboards und Sequenzern. Midi wurde 1982 eingeführt und später von DAWs (Digital Audio Workstations) wie Cubase, Logic, Fruity Loops, Ableton Live, etc. übernommen. Es lassen sich damit sowohl digitale Instrumente im Laptop-Sequenzer (VST-Plugins) sowie nach wie vor auch externe Geräte ansteuern und automatisieren.
MIDI-Geräte aus Processing ansteuern
MidiBus-Library
Derzeit ist in den Core-Libraries von Processing keine Midi-Funktionalität implementiert. Möchte man Midi-Signale an externe Klangerzeuger senden, so sollte man daher die Library The MidiBus über den Contribution Manager von Processing hinzufügen.
TheMIDIBus ist die Standard-Bibliothek für MIDI-Kommunikation in Processing und ermöglicht das Senden und Empfangen von MIDI-Nachrichten zwischen Processing-Sketches und MIDI-fähigen Geräten wie Keyboards, Controllern oder Software. Die Library bietet eine einfache API zum Verarbeiten von Noten, Control Changes, Program Changes und anderen MIDI-Events in Echtzeit, wodurch interaktive Musik- und Medieninstallationen realisiert werden können.
Beispiel 3-8
import themidibus.*;
MidiBus gaiaBus;
int channel = 0;
int pitch = 64;
int velocity = 64;
void setup() {
size(800, 600);
gaiaBus = new MidiBus(this, -1, 1);
}
void draw() {
}
void mouseMoved(){
pitch = int(map(mouseX, 0, width, 50, 60));
float c = map(mouseX, 0, width, 0, 255);
background(c);
gaiaBus.sendNoteOn(channel, pitch, velocity);
gaiaBus.sendNoteOff(channel, pitch, velocity);
}
MIDI-Controller zur Eingabe verwenden
MIDI-Controller können umgekehrt als Interface dienen um Processing anzusteuern. So könnte man beispielsweise Parameter von Live-Visuals nicht nur durch den Sound, sondern auch durch manuelle Eingriffe steuern.
Der Controller AKAI MIDIMIX bietet beispielsweise ein Interface aus Schiebereglern und Drehknöpfen, das an die Haptik eines Mischpultes angelehnt ist. Indem man seinen Sketch entsprechend programmiert können die Regler und Knöpfe jedoch ganz nach Belieben für andere Funktionen verwendet werden.
Der folgende Sketch verwendet die drei Drehregler von Kanalzug 1 des AKAI MIDIMIX, um die RGB-Werte des Hintergrunds einzustellen. Mit den Schiebereglern der ersten drei Kanalzüge kann die Größe und Position des Objekts gesteuert werden.
Beispiel 3-9
import themidibus.*;
MidiBus midiMixBus;
final byte hueKnob = 16;
final byte satKnob = 17;
final byte brightKnob = 18;
final byte xSlider = 19;
final byte ySlider = 23;
final byte zSlider = 27;
final byte xRotSlider = 31;
final byte yRotSlider = 49;
final byte zRotSlider = 53;
float hue = 0;
float sat = 100;
float bright = 255;
float boxX = width/2;
float boxY = height/2;
float boxZ = 0;
float rotX = 0;
float rotY = 0;
float rotZ = 0;
void setup() {
size(800, 600, P3D);
midiMixBus = new MidiBus(this, "MIDI Mix", 1);
colorMode(HSB);
}
void draw() {
background(hue, sat, bright);
translate(boxX, boxY, boxZ);
rotateX(rotX);
rotateY(rotY);
rotateZ(rotZ);
box(200);
}
void rawMidi(byte[] data) {
switch(data[1]){
case hueKnob:
hue = map(data[2], 0, 127, 0, 255);
break;
case satKnob:
sat = map(data[2], 0, 127, 0, 255);
break;
case brightKnob:
bright = map(data[2], 0, 127, 0, 255);
break;
case xSlider:
boxX = map(data[2], 0, 127, 0, width);
break;
case ySlider:
boxY = map(data[2], 0, 127, 0, height);
break;
case zSlider:
boxZ = map(data[2], 0, 127, -400, 0);
break;
case xRotSlider:
rotX = map(data[2], 0, 127, 0, 2);
break;
case yRotSlider:
rotY = map(data[2], 0, 127, 0, 2);
break;
case zRotSlider:
rotZ = map(data[2], 0, 127, 0, 2);
break;
}
}
SimpleSynth & Midi
Ein einfacher Synthesizer kann nun so programmiert werden, dass er von einem externen Midi-Keyboard gespielt werden kann. Hierzu müssen die eingehenden Midi-Noten (dabei handelt es sich um Werte zwischen 0 und 127) in Frequenzen für die Oszillatoren umgerechnet werden:
Midi-Note in Hertz:
frequenz = pow((midinote-69)/12) * 440;
Beispiel 3-10: simpleSynth mit MIDI-Steuerung
import controlP5.*; // GUI library for sliders, buttons, etc.
import processing.sound.*; // Sound synthesis and processing
import themidibus.*; // MIDI input/output
ControlP5 cp5; // GUI controller object
MidiBus midi; // MIDI communication object
int count; // Timer for note duration
int Duration = 1000; // Note length in milliseconds
// Oscillator states
boolean sinoPlay = false; // Is sine wave playing?
boolean sawoPlay = false; // Is sawtooth playing?
boolean squaroPlay = false; // Is square wave playing?
boolean delayDo = false; // Delay effect on/off
boolean envelopeDo = false; // Envelope on/off
// Frequencies for each oscillator
float FreqSin, FreqSaw, FreqRect;
// ADSR envelope parameters
float attack, sustain, susLevel, release;
// Variables linked to GUI toggles
float Sinus, Saegezahn, Rechteck;
// Sound objects
SinOsc sino; // Sine wave oscillator
SawOsc sawo; // Sawtooth oscillator
SqrOsc squaro; // Square wave oscillator
WhiteNoise noise; // White noise generator
BandPass bandPass; // Band-pass filter
Env envelope; // ADSR envelope
Reverb reverb; // Reverb effect
void setup() {
size(500,200);
background(0);
// Initialize sound objects
sino = new SinOsc(this);
sawo = new SawOsc(this);
squaro = new SqrOsc(this);
noise = new WhiteNoise(this);
bandPass = new BandPass(this);
envelope = new Env(this);
reverb = new Reverb(this);
// Initialize GUI and MIDI
cp5 = new ControlP5(this);
midi = new MidiBus(this, "Plug 1", -1); // "Plug 1" = MIDI input device
// Play button
cp5.addBang("bang")
.setPosition(10, 10)
.setSize(40, 20)
.setTriggerEvent(Bang.RELEASE)
.setLabel("Play")
;
// Duration slider
cp5.addSlider("Duration")
.setPosition(100,10)
.setSize(100, 20)
.setRange(0, 2000)
.setValue(1000)
.setNumberOfTickMarks(5)
.setSliderMode(Slider.FLEXIBLE)
;
// Oscillator toggles
cp5.addToggle("Sinus") // Sine wave on/off
.setPosition(310,10)
.setSize(40,20)
.setValue(true)
;
cp5.addToggle("Saegezahn") // Sawtooth on/off
.setPosition(360,10)
.setSize(40,20)
.setValue(false)
;
cp5.addToggle("Rechteck") // Square wave on/off
.setPosition(410,10)
.setSize(40,20)
.setValue(false)
;
// Frequency sliders for each oscillator
cp5.addSlider("FreqSin")
.setPosition(310,50)
.setSize(40, 20)
.setRange(16, 2000) // 16Hz to 2000Hz
.setValue(440) // A4 note
;
cp5.getController("FreqSin").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
cp5.addSlider("FreqSaw")
.setPosition(360,50)
.setSize(40, 20)
.setRange(16, 2000)
.setValue(440)
.setSliderMode(Slider.FLEXIBLE)
;
cp5.getController("FreqSaw").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
cp5.addSlider("FreqRect")
.setPosition(410,50)
.setSize(40, 20)
.setRange(16, 2000)
.setValue(440)
.setSliderMode(Slider.FLEXIBLE)
;
cp5.getController("FreqRect").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
// Envelope controls
cp5.addToggle("envelopeOn")
.setPosition(10,160)
.setSize(40,20)
.setValue(false)
.setLabel("Env on");
;
// ADSR parameters
cp5.addSlider("attack") // Time to reach peak volume
.setPosition(60,160)
.setSize(40, 20)
.setRange(0.01, 0.5)
.setValue(0.1)
.setSliderMode(Slider.FLEXIBLE)
;
cp5.getController("attack").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
cp5.addSlider("sustain") // Time at peak before decay
.setPosition(110,160)
.setSize(40, 20)
.setRange(0.01, 0.5)
.setValue(0.2)
.setSliderMode(Slider.FLEXIBLE)
;
cp5.getController("sustain").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
cp5.addSlider("susLevel") // Volume during sustain phase
.setPosition(160,160)
.setSize(40, 20)
.setRange(0, 1)
.setValue(0.5)
.setSliderMode(Slider.FLEXIBLE)
;
cp5.getController("susLevel").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
cp5.addSlider("release") // Time to fade out
.setPosition(210,160)
.setSize(40, 20)
.setRange(0.01, 0.5)
.setValue(0.1)
.setSliderMode(Slider.FLEXIBLE)
;
cp5.getController("release").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
}
void draw() {
fill(20,20,50);
noStroke();
// Draw envelope visualization background
rect(10,50,280,100);
stroke(255);
// Draw ADSR envelope shape
float sp = 150-susLevel*100; // Sustain level Y position
line(10,150, attack*200+10, sp); // Attack line
line(attack*200+10,sp, attack*200+sustain*200+10,sp); // Sustain line
line(attack*200+sustain*200+10, sp, attack*200+sustain*200+release*200+10,149); // Release line
// Stop sound after duration (when envelope is off)
if (millis() > count+Duration && envelopeDo == false){
if (envelopeDo == false){
if (sinoPlay){ sino.stop(); }
if (sawoPlay){ sawo.stop(); }
if (squaroPlay){ squaro.stop(); }
}
}
}
void bang() { // Called when Play button pressed
if (sinoPlay){
if (envelopeDo){
sino.play(FreqSin, 0.5); // Play with frequency and amplitude
envelope.play(sino, attack, sustain, susLevel, release); // Apply envelope
}else{
sino.stop(); // Stop and restart for clean sound
sino.play(FreqSin, 0.5);
}
}
if(sawoPlay){
if (envelopeDo){
sawo.play(FreqSaw, 0.5);
envelope.play(sawo, attack, sustain, susLevel, release);
}else{
sawo.stop();
sawo.play(FreqSaw, 0.5);
}
}
if(squaroPlay){
if (envelopeDo){
squaro.play(FreqRect, 0.5);
envelope.play(squaro, attack, sustain, susLevel, release);
} else{
squaro.stop();
squaro.play(FreqRect, 0.5);
}
}
count = millis(); // Record start time
}
void rawMidi(byte[] data) { // Called when MIDI data received
int midinote = data[1]; // MIDI note number (0-127)
float envDuration = (attack+sustain+release)*1000;
if(data[2] == 127){ // Note ON (velocity 127), not Note OFF (64)
// Convert MIDI note to frequency
float frequenz = 440 * pow(2.0,(midinote-69.0)/12.0); // A4 = MIDI note 69
// Update all frequency sliders
cp5.getController("FreqSin").setValue(frequenz);
cp5.getController("FreqSaw").setValue(frequenz);
cp5.getController("FreqRect").setValue(frequenz);
Sinus = frequenz;
Saegezahn = frequenz;
Rechteck = frequenz;
bang(); // Play the note
}
}
// Toggle functions - automatically called by controlP5
void envelopeOn(boolean flag){
if(flag){
envelopeDo = true;
}else{
envelopeDo = false;
}
}
void delayOn(boolean flag){ // Not implemented yet
if(flag){
delayDo = true;
}else{
delayDo = false;
}
}
void Sinus(boolean flag){ // Sine toggle callback
if(flag){
sinoPlay = true;
}else{
sinoPlay = false;
}
}
void Saegezahn(boolean flag){ // Sawtooth toggle callback
if(flag){
sawoPlay = true;
}else{
sawoPlay = false;
}
}
void Rechteck(boolean flag){ // Square wave toggle callback
if(flag){
squaroPlay = true;
}else{
squaroPlay = false;
}
}