Fallstudie – Aufbau von Technitone.com

Sean Middleditch
Sean Middleditch
Technitone – ein Web-Audio-Erlebnis.

Technitone.com ist eine Kombination aus WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash und der neuen Web Audio API in Chrome.

Dieser Artikel befasst sich mit allen Aspekten der Produktion: dem Plan, dem Server, den Sounds, den visuellen Elementen und einem Teil des Workflows, den wir für interaktive Designs nutzen. Die meisten Abschnitte enthalten Code-Snippets, eine Demo und einen Download. Am Ende des Artikels finden Sie einen Download-Link, über den Sie alle Dateien als ZIP-Datei herunterladen können.

Das gskinner.com-Produktionsteam.

Gig

Wir sind keineswegs Audioingenieure bei gskinner.com, aber wenn wir uns einer Herausforderung stellen, erarbeiten wir einen Plan:

  • Nutzer zeichnen die Farbtöne auf ein Raster, „inspiriert“ von Andres ToneMatrix.
  • Die Töne sind mit gesampelten Instrumenten, Schlagzeugen oder sogar eigenen Aufnahmen verknüpft.
  • Mehrere verbundene Nutzer spielen gleichzeitig im selben Raster
  • ...oder auf eigene Faust auf Entdeckungsreise gehen,
  • Im Rahmen von Einladungssitzungen können Nutzer eine Band organisieren und eine spontane Jamsession veranstalten

Wir bieten Nutzern die Möglichkeit, die Web Audio API mithilfe eines Tools zu erkunden, in dem Audiofilter und Effekte auf ihre Töne angewendet werden.

Technitone von gskinner.com

Außerdem:

  • Kompositionen und Effekte von Nutzern als Daten speichern und clientübergreifend synchronisieren
  • Biete verschiedene Farboptionen an, damit sie coole Songs zeichnen können.
  • Bieten Sie eine Galerie an, in der Nutzer die Werke anderer anhören, lieben oder sogar bearbeiten können

Wir blieben bei der bekannten Raster-Metapher fest, schwebten sie im 3D-Raum, fügten einige Licht-, Textur- und Partikeleffekte hinzu und platzierten sie in einer flexiblen (oder Vollbild-) CSS- und JS-gesteuerten Oberfläche.

Road trip

Instrument-, Effekt- und Rasterdaten werden auf dem Client konsolidiert und serialisiert und dann an unser benutzerdefiniertes Node.js-Back-End gesendet, um sie entsprechend Socket.io für mehrere Nutzer aufzulösen. Diese Daten werden mit den Beiträgen der einzelnen Spieler an den Client zurückgesendet, bevor sie auf die relativen CSS-, WebGL- und WebAudio-Ebenen verteilt werden, die für das Rendern der Benutzeroberfläche, der Samples und der Effekte bei der Wiedergabe mehrerer Nutzer zuständig sind.

Durch die Echtzeitkommunikation mit Sockets werden JavaScript auf dem Client und JavaScript auf dem Server eingespeist.

Technitone-Serverdiagramm

Wir verwenden Node für jeden Aspekt des Servers. Er ist ein statischer Webserver und unser Socket-Server in einem. Wir haben dann Express verwendet. Es ist ein vollständiger Webserver, der vollständig auf Node Node basiert. Er ist äußerst skalierbar, hochgradig anpassbar und verarbeitet die Low-Level-Serveraspekte für Sie (genau wie Apache oder Windows Server). Dann müssen Sie sich als Entwickler auf die Erstellung Ihrer Anwendung konzentrieren.

Demo für mehrere Nutzer (ok, es ist eigentlich nur ein Screenshot)

Diese Demo muss über einen Knotenserver ausgeführt werden. Da es sich bei diesem Artikel nicht um einen Artikel handelt, haben wir einen Screenshot beigefügt, wie die Demo aussieht, nachdem Sie Node.js installiert, Ihren Webserver konfiguriert und ihn lokal ausgeführt haben. Jedes Mal, wenn ein neuer Nutzer deine Demoinstallation besucht, wird ein neues Raster hinzugefügt, sodass die Arbeit aller für die anderen sichtbar ist.

Screenshot der Demo zu Node.js

Node.js ist einfach. Mit einer Kombination aus Socket.io und benutzerdefinierten POST-Anfragen mussten wir keine komplexen Routinen für die Synchronisierung erstellen. Socket.io verarbeitet dies transparent und JSON wird weitergeleitet.

Wie einfach? Sieh dir das hier an.

Mit drei Zeilen JavaScript haben wir einen Webserver mit Express eingerichtet.

//Tell  our Javascript file we want to use express.
var express = require('express');

//Create our web-server
var server = express.createServer();

//Tell express where to look for our static files.
server.use(express.static(__dirname + '/static/'));

Es fehlen noch einige Punkte, um socket.io für die Kommunikation in Echtzeit einzubinden.

var io = require('socket.io').listen(server);
//Start listening for socket commands
io.sockets.on('connection', function (socket) {
    //User is connected, start listening for commands.
    socket.on('someEventFromClient', handleEvent);

});

Jetzt warten wir nur noch auf eingehende Verbindungen von der HTML-Seite.

<!-- Socket-io will serve it-self when requested from this url. -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>

 <!-- Create our socket and connect to the server -->
 var sock = io.connect('http://localhost:8888');
 sock.on("connect", handleConnect);

 function handleConnect() {
    //Send a event to the server.
    sock.emit('someEventFromClient', 'someData');
 }
 ```

## Sound check

A big unknown was the effort entailed with using the Web Audio API. Our initial findings confirmed that [Digital Signal Processing](http://en.wikipedia.org/wiki/Digital_Signal_Processing) (DSP) is very complex, and we were likely in way over our heads. Second realization: [Chris Rogers](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html) has already done the heavy lifting in the API.
Technitone isn't using any really complex math or audioholicism; this functionality is easily accessible to interested developers. We really just needed to brush up on some terminology and [read the docs](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). Our advice? Don't skim them. Read them. Start at the top and end at the bottom. They are peppered with diagrams and photos, and it's really cool stuff.

If this is the first you've heard of the Web Audio API, or don't know what it can do, hit up Chris Rogers' [demos](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html). Looking for inspiration? You'll definitely find it there.

### Web Audio API Demo

Load in a sample (sound file)…

```js
/**
 * The XMLHttpRequest allows you to get the load
 * progress of your file download and has a responseType
 * of "arraybuffer" that the Web Audio API uses to
 * create its own AudioBufferNode.
 * Note: the 'true' parameter of request.open makes the
 * request asynchronous - this is required!
 */
var request = new XMLHttpRequest();
request.open("GET", "mySample.mp3", true);
request.responseType = "arraybuffer";
request.onprogress = onRequestProgress; // Progress callback.
request.onload = onRequestLoad; // Complete callback.
request.onerror = onRequestError; // Error callback.
request.onabort = onRequestError; // Abort callback.
request.send();

// Use this context to create nodes, route everything together, etc.
var context = new webkitAudioContext();

// Feed this AudioBuffer into your AudioBufferSourceNode:
var audioBuffer = null;

function onRequestProgress (event) {
    var progress = event.loaded / event.total;
}

function onRequestLoad (event) {
    // The 'true' parameter specifies if you want to mix the sample to mono.
    audioBuffer = context.createBuffer(request.response, true);
}

function onRequestError (event) {
    // An error occurred when trying to load the sound file.
}

... modulares Routing einrichten ...

/**
 * Generally you'll want to set up your routing like this:
 * AudioBufferSourceNode > [effect nodes] > CompressorNode > AudioContext.destination
 * Note: nodes are designed to be able to connect to multiple nodes.
 */

// The DynamicsCompressorNode makes the loud parts
// of the sound quieter and quiet parts louder.
var compressorNode = context.createDynamicsCompressor();
compressorNode.connect(context.destination);

// [other effect nodes]

// Create and route the AudioBufferSourceNode when you want to play the sample.

...einen Laufzeiteffekt anwenden (Faltung mithilfe einer Impulsantwort)...

/**
 * Your routing now looks like this:
 * AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
 */

var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;

...einen weiteren Laufzeiteffekt anwenden (Verzögerung)

/**
 * The delay effect needs some special routing.
 * Unlike most effects, this one takes the sound data out
 * of the flow, reinserts it after a specified time (while
 * looping it back into itself for another iteration).
 * You should add an AudioGainNode to quieten the
 * delayed sound...just so things don't get crazy :)
 *
 * Your routing now looks like this:
 * AudioBufferSourceNode -> ConvolverNode > CompressorNode > AudioContext.destination
 *                       |  ^
 *                       |  |___________________________
 *                       |  v                          |
 *                       -> DelayNode > AudioGainNode _|
 */

var delayGainNode = context.createGainNode();
delayGainNode.gain.value = 0.7; // Quieten the feedback a bit.
delayGainNode.connect(convolverNode);

var delayNode = context.createDelayNode();
delayNode.delayTime = 0.5; // Re-sound every 0.5 seconds.
delayNode.connect(delayGainNode);

delayGainNode.connect(delayNode); // make the loop

...und dann hörbar machen.

/**
 * Once your routing is set up properly, playing a sound
 * is easy-shmeezy. All you need to do is create an
 * AudioSourceBufferNode, route it, and tell it what time
 * (in seconds relative to the currentTime attribute of
 * the AudioContext) it needs to play the sound.
 *
 * 0 == now!
 * 1 == one second from now.
 * etc...
 */

var sourceNode = context.createBufferSource();
sourceNode.connect(convolverNode);
sourceNode.connect(delayNode);
sourceNode.buffer = audioBuffer;
sourceNode.noteOn(0); // play now!

Bei unserem Wiedergabeansatz in Technitone dreht sich alles um die Planung. Anstatt ein Timer-Intervall festzulegen, das unserem Tempo entspricht, um Töne taktweise zu verarbeiten, richten wir ein kleineres Intervall ein, das die Töne in einer Warteschlange verwaltet und geplant. Auf diese Weise kann die API die Vorarbeit erledigen, um Audiodaten aufzulösen sowie Filter und Effekte zu verarbeiten, bevor die CPU die Daten tatsächlich hörbar machen muss. Wenn der Takt endlich ankommt, verfügt er bereits über alle Informationen, die er für die Präsentation des Endergebnisses an die Sprecher benötigt.

Insgesamt musste alles optimiert werden. Als wir unsere CPUs zu stark beanspruchten, wurden Prozesse übersprungen (knacken, klicken, kratzen), um mit dem Zeitplan Schritt zu halten. Wir haben alles daran gesetzt, den Wahnsinn aufzuhalten, wenn du in Chrome zu einem anderen Tab wechselst.

Lichtshow

Vorn in der Mitte befinden sich unser Raster und der Partikeltunnel. Dies ist die WebGL-Schicht von Technitone.

WebGL bietet eine erheblich bessere Leistung als die meisten anderen Ansätze zum Rendern von visuellen Elementen im Web, da die GPU angewiesen wird, in Verbindung mit dem Prozessor zu arbeiten. Dieser Leistungszuwachs geht mit einem erheblich höheren Entwicklungsaufwand und einer deutlich steileren Lernkurve einher. Wenn Sie jedoch leidenschaftlich leidenschaftlich im Web aktiv sind und möglichst wenige Leistungsbeschränkungen nutzen möchten, bietet WebGL eine Lösung, die mit Flash vergleichbar ist.

WebGL-Demo

WebGL-Inhalte werden auf einem Canvas, dem HTML5-Canvas, gerendert und bestehen aus folgenden Grundbausteinen:

  • Objekteckpunkte (Geometrie)
  • Positionsmatrizen (3D-Koordinaten)
    • Shader (eine Beschreibung der Geometriedarstellung, die direkt mit der GPU verknüpft ist)
    • den Kontext („Verknüpfungen“ zu den Elementen, auf die die GPU verweist)
    • Puffer (Pipelines zum Übertragen von Kontextdaten an die GPU)
    • den Hauptcode (die für die gewünschte interaktive Interaktion spezifische Geschäftslogik)
    • Die"draw"-Methode (aktiviert die Shader und zeichnet Pixel auf dem Canvas)

Der grundlegende Vorgang zum Rendern von WebGL-Inhalten auf dem Bildschirm sieht so aus:

  1. Legen Sie die Perspektivenmatrix fest. Dadurch werden die Einstellungen für die Kamera angepasst, die in den 3D-Raum gerichtet ist und so die Bildebene definiert.
  2. Legen Sie die Positionsmatrix fest (deklarieren Sie in den 3D-Koordinaten einen Ursprung, zu dem Positionen relativ zu gemessen werden).
  3. Füllen Sie die Puffer mit Daten (Scheitelpunktposition, Farbe, Texturen usw.), um sie über die Shader an den Kontext zu übergeben.
  4. Extrahieren und organisieren Sie Daten aus den Puffern mit den Shadern und übergeben Sie sie an die GPU.
  5. Rufen Sie die „draw“-Methode auf, um dem Kontext mitzuteilen, dass Shader aktiviert, mit den Daten ausgeführt und das Canvas aktualisiert werden soll.

Dies sieht in Aktion so aus:

Perspektivenmatrix festlegen...

// Aspect ratio (usually based off the viewport,
// as it can differ from the canvas dimensions).
var aspectRatio = canvas.width / canvas.height;

// Set up the camera view with this matrix.
mat4.perspective(45, aspectRatio, 0.1, 1000.0, pMatrix);

// Adds the camera to the shader. [context = canvas.context]
// This will give it a point to start rendering from.
context.uniformMatrix4fv(shader.pMatrixUniform, 0, pMatrix);

...Positionsmatrix festlegen...

// This resets the mvMatrix. This will create the origin in world space.
mat4.identity(mvMatrix);

// The mvMatrix will be moved 20 units away from the camera (z-axis).
mat4.translate(mvMatrix, [0,0,-20]);

// Sets the mvMatrix in the shader like we did with the camera matrix.
context.uniformMatrix4fv(shader.mvMatrixUniform, 0, mvMatrix);

...Geometrie und Darstellung definieren...

// Creates a square with a gradient going from top to bottom.
// The first 3 values are the XYZ position; the last 4 are RGBA.
this.vertices = new Float32Array(28);
this.vertices.set([-2,-2, 0,    0.0, 0.0, 0.7, 1.0,
                   -2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2,-2, 0,    0.0, 0.0, 0.7, 1.0
                  ]);

// Set the order of which the vertices are drawn. Repeating values allows you
// to draw to the same vertex again, saving buffer space and connecting shapes.
this.indices = new Uint16Array(6);
this.indices.set([0,1,2, 0,2,3]);

... die Puffer mit Daten füllen und sie an den Kontext übergeben ...

// Create a new storage space for the buffer and assign the data in.
context.bindBuffer(context.ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ARRAY_BUFFER, this.vertices, context.STATIC_DRAW);

// Separate the buffer data into its respective attributes per vertex.
context.vertexAttribPointer(shader.vertexPositionAttribute,3,context.FLOAT,0,28,0);
context.vertexAttribPointer(shader.vertexColorAttribute,4,context.FLOAT,0,28,12);

// Create element array buffer for the index order.
context.bindBuffer(context.ELEMENT_ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ELEMENT_ARRAY_BUFFER, this.indices, context.STATIC_DRAW);

...und rufen Sie die "draw"-Methode auf.

// Draw the triangles based off the order: [0,1,2, 0,2,3].
// Draws two triangles with two shared points (a square).
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0);

Denken Sie bei jedem Frame daran, den Canvas zu leeren, wenn Alpha-basierte visuelle Elemente nicht übereinander gestapelt werden sollen.

Der Veranstaltungsort

Abgesehen vom Raster und dem Partikeltunnel wurden alle anderen UI-Elemente in HTML / CSS und interaktive Logik in JavaScript erstellt.

Wir haben uns von Anfang an das Ziel gesetzt, dass die Nutzenden so schnell wie möglich mit dem Raster interagieren können sollten. Kein Ladebildschirm, keine Anleitung, keine Tutorials, sondern direkt loslegen. Wenn die Benutzeroberfläche geladen ist, sollte sie durch nichts aufgehalten werden.

Deshalb mussten wir uns genau überlegen, wie wir Erstnutzer durch ihre Interaktionen führen können. Wir haben subtile Hinweise eingefügt, wie die Änderung der CSS-Cursor-Eigenschaft basierend auf der Mausposition des Nutzers im WebGL-Bereich. Befindet sich der Cursor über dem Raster, wird er zu einem Handzeiger, da die Nutzenden durch das Darstellen von Tönen interagieren können. Wird der Mauszeiger in dem leeren Bereich um das Raster herum platziert, wird er durch einen Richtungskreuz-Cursor ersetzt, um anzuzeigen, dass das Raster gedreht oder in Ebenen aufgelöst werden kann.

Bereit für den Auftritt?

LESS (ein CSS-Vorprozessor) und CodeKit (Webentwicklung auf Steroiden) reduzieren die Zeit, die für die Übersetzung von Designdateien in HTML/CSS benötigt wird. Damit können wir CSS viel vielseitiger organisieren, schreiben und optimieren und dabei Variablen, Mix-Ins (Funktionen) und sogar Mathematik nutzen.

Bühneneffekte

Mithilfe von CSS3-Übergängen und backbone.js haben wir einige ganz einfache Effekte erstellt, die die Anwendung zum Leben erwecken und Nutzern visuelle Warteschlangen bieten, die erkennen, welches Instrument sie verwenden.

Die Farben von Technitone.

Mit Backbone.js können wir Farbänderungsereignisse erfassen und die neue Farbe auf die entsprechenden DOM-Elemente anwenden. Die Farbstiländerungen wurden durch GPU-beschleunigte CSS3-Übergänge gehandhabt, die sich kaum oder gar nicht auf die Leistung auswirkten.

Die meisten Farbübergänge für Oberflächenelemente wurden durch Übergänge bei den Hintergrundfarben erzeugt. Über dieser Hintergrundfarbe platzieren wir Hintergrundbilder mit transparenten Bereichen, um die Hintergrundfarbe durchscheinen zu lassen.

HTML: Die Grundlage

Für die Demo benötigten wir drei Farbbereiche: zwei vom Nutzer ausgewählte Farbbereiche und einen dritten Bereich für gemischte Farben. Wir erstellten die einfachste DOM-Struktur, die CSS3-Übergänge und möglichst wenige HTTP-Anfragen für unsere Abbildung unterstützt.

<!-- Basic HTML Setup -->
<div class="illo color-mixed">
  <div class="illo color-primary"></div>
  <div class="illo color-secondary"></div>
</div>

CSS: Einfache Struktur mit Stil

Wir haben die absolute Positionierung verwendet, um jeden Bereich an der richtigen Position zu platzieren, und die Eigenschaft „background-position“ angepasst, um die Hintergrundillustration innerhalb jeder Region auszurichten. Dadurch sehen alle Bereiche (jeweils mit demselben Hintergrundbild) wie ein einzelnes Element aus.

.illo {
  background: url('../img/illo.png') no-repeat;
  top:        0;
  cursor:     pointer;
}
  .illo.color-primary, .illo.color-secondary {
    position: absolute;
    height:   100%;
  }
  .illo.color-primary {
    width:                350px;
    left:                 0;
    background-position:  top left;
  }
  .illo.color-secondary {
    width:                355px;
    right:                0;
    background-position:  top right;
  }

Es wurden GPU-beschleunigte Übergänge angewendet, die auf Farbänderungsereignisse warten. Wir haben die Dauer verlängert und das Easing für „.color-mixed“ angepasst, um den Eindruck zu erwecken, dass es einige Zeit dauert, bis die Farben gemischt wurden.

/* Apply Transitions To Backgrounds */
.color-primary, .color-secondary {
  -webkit-transition: background .5s linear;
  -moz-transition:    background .5s linear;
  -ms-transition:     background .5s linear;
  -o-transition:      background .5s linear;
}

.color-mixed {
  position:           relative;
  width:              750px;
  height:             600px;
  -webkit-transition: background 1.5s cubic-bezier(.78,0,.53,1);
  -moz-transition:    background 1.5s cubic-bezier(.78,0,.53,1);
  -ms-transition:     background 1.5s cubic-bezier(.78,0,.53,1);
  -o-transition:      background 1.5s cubic-bezier(.78,0,.53,1);
}

Unter HTML5Please finden Sie Informationen zur aktuellen Browserunterstützung und zu den empfohlenen Verwendungen für CSS3-Umstellungen.

JavaScript: So funktioniert's

Die dynamische Zuweisung von Farben ist einfach. Wir durchsuchen das DOM nach jedem Element mit unserer Farbklasse und legen die Hintergrundfarbe (background-color) basierend auf der Farbauswahl des Nutzers fest. Wir wenden unseren Übergangseffekt auf jedes Element im DOM an, indem wir eine Klasse hinzufügen. Dadurch entsteht eine Architektur, die einfach, flexibel und skalierbar ist.

function createPotion() {

    var primaryColor = $('.picker.color-primary > li.selected').css('background-color');
    var secondaryColor = $('.picker.color-secondary > li.selected').css('background-color');
    console.log(primaryColor, secondaryColor);
    $('.illo.color-primary').css('background-color', primaryColor);
    $('.illo.color-secondary').css('background-color', secondaryColor);

    var mixedColor = mixColors (
            parseColor(primaryColor),
            parseColor(secondaryColor)
    );

    $('.color-mixed').css('background-color', mixedColor);
}

Sobald die Primär- und die Sekundärfarbe ausgewählt sind, berechnen wir den gemischten Farbwert und weisen den resultierenden Wert dem entsprechenden DOM-Element zu.

// take our rgb(x,x,x) value and return an array of numeric values
function parseColor(value) {
    return (
            (value = value.match(/(\d+),\s*(\d+),\s*(\d+)/)))
            ? [value[1], value[2], value[3]]
            : [0,0,0];
}

// blend two rgb arrays into a single value
function mixColors(primary, secondary) {

    var r = Math.round( (primary[0] * .5) + (secondary[0] * .5) );
    var g = Math.round( (primary[1] * .5) + (secondary[1] * .5) );
    var b = Math.round( (primary[2] * .5) + (secondary[2] * .5) );

    return 'rgb('+r+', '+g+', '+b+')';
}

Illustration für die HTML/CSS-Architektur: Eine persönliche Note für drei Farbverschiebungsfelder

Unser Ziel war es, einen lustigen und realistischen Lichteffekt zu schaffen, der seine Integrität beibehalten konnte, auch wenn kontrastreiche Farben in benachbarten Farbbereichen platziert wurden.

Bei einer 24-Bit-PNG-Datei ist die Hintergrundfarbe unserer HTML-Elemente durch die transparenten Bereiche des Bildes sichtbar.

Bildtransparenzen

Die farbigen Felder erzeugen an den Stellen, an denen verschiedene Farben aufeinandertreffen, harte Kanten. Dies behindert realistische Lichteffekte und war eine der größeren Herausforderungen beim Entwerfen der Illustration.

Farbbereiche

Die Lösung bestand darin, die Illustration so zu gestalten, dass die Ränder der Farbbereiche nie durch die transparenten Bereiche durchscheinen können.

Kanten der Farbbereiche

Die Planung des Aufbaus war von entscheidender Bedeutung. Eine kurze Planungssitzung mit Designer, Entwickler und Illustrator half dem Team dabei, zu verstehen, wie alles gebaut werden muss, damit es zusammengebaut werden kann.

Die Photoshop-Datei ist ein Beispiel dafür, wie durch die Benennung von Ebenen Informationen zur CSS-Konstruktion vermittelt werden können.

Kanten der Farbbereiche

Encore

Für Nutzer ohne Chrome setzten wir uns das Ziel, den Kern der Anwendung in einem einzigen statischen Bild zusammenzufassen. Der Rasterknoten wurde zum Hero, die Hintergrundkacheln symbolisieren den Zweck der Anwendung und die in der Reflexion vorhandene Perspektive weist auf die immersive 3D-Umgebung des Rasters hin.

Kanten der Farbbereiche.

Wenn Sie mehr über Technitone erfahren möchten, besuchen Sie unseren Blog.

Das Band

Vielen Dank fürs Lesen, vielleicht haben wir bald mit dir zusammen eine Jamsession.