Add Twelve-Tone Synthesizer - Dodekaphonie nach Schönberg

Complete web-based synthesizer implementing Arnold Schönberg's
twelve-tone technique (Dodekaphonie) with:

- PHP backend for tone row generation and matrix calculation
- JavaScript Web Audio API for real-time sound synthesis
- Four row transformations: Original, Retrograde, Inversion, RI
- Convolver-based reverb effect with adjustable wet/dry mix
- Real-time audio visualization (waveform and spectrum)
- Interactive controls for tempo, octave, attack, release
- Multiple waveform options (sine, triangle, square, sawtooth)
- Full 12x12 twelve-tone matrix display
- Automatic continuous playback with random transformations
This commit is contained in:
Claude
2025-12-13 16:26:02 +00:00
parent b50ef8bc00
commit a93e940b71
2 changed files with 1000 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# Zwölfton-Synthesizer (Dodekaphonie)
Ein interaktiver Web-Synthesizer basierend auf Arnold Schönbergs Zwölftontechnik.
## Features
- **Automatische Zwölfton-Komposition**: Generiert fortlaufend Musik nach den Regeln der Dodekaphonie
- **Vier Reihenformen**: Original, Krebs (Retrograde), Umkehrung (Inversion), Krebsumkehrung
- **Reverb-Effekt**: Einstellbarer Hall mit Convolver-basierter Impulsantwort
- **Web Audio API**: Echtzeit-Klangsynthese im Browser
- **Audio-Visualisierung**: Wellenform und Frequenzspektrum in Echtzeit
- **Zwölftonmatrix**: Vollständige 12x12-Matrix aller möglichen Transpositionen
## Die Zwölftontechnik
Die Dodekaphonie wurde von Arnold Schönberg um 1921 entwickelt:
1. Alle 12 chromatischen Töne werden gleichberechtigt verwendet
2. Kein Ton darf wiederholt werden, bevor alle anderen gespielt wurden
3. Die Grundreihe erscheint in vier Formen:
- **Original (O)**: Die Grundreihe
- **Krebs (R)**: Rückwärts gespielt
- **Umkehrung (I)**: Intervalle gespiegelt
- **Krebsumkehrung (RI)**: Kombination aus Krebs und Umkehrung
4. Jede Form kann auf alle 12 Stufen transponiert werden (48 mögliche Reihen)
## Installation
1. PHP-Server starten (PHP 7.4+ erforderlich):
```bash
cd twelve-tone-synthesizer
php -S localhost:8000
```
2. Browser öffnen: `http://localhost:8000`
## Bedienung
- **Starten**: Startet die automatische Wiedergabe der Zwölftonreihe
- **Stoppen**: Beendet die Wiedergabe
- **Neue Reihe**: Generiert eine zufällige neue Zwölftonreihe
### Klangparameter
- **Tempo (BPM)**: Geschwindigkeit der Notenwiedergabe (40-300)
- **Oktave**: Tonhöhenbereich (2-6)
- **Reverb**: Hallanteil (0-100%)
- **Attack**: Einschwingzeit der Töne
- **Release**: Ausklingzeit der Töne
- **Wellenform**: Sinus, Dreieck, Rechteck, Sägezahn
## Technologie
- **PHP**: Backend für Zwölftonreihen-Generierung und Matrix-Berechnung
- **JavaScript**: Web Audio API für Klangsynthese
- **ConvolverNode**: Realistische Reverb-Simulation
- **Canvas API**: Audio-Visualisierung
## Lizenz
MIT License
+939
View File
@@ -0,0 +1,939 @@
<?php
/**
* Zwölfton-Synthesizer (Dodekaphonie)
* Nach der Lehre von Arnold Schönberg
*
* Regeln der Zwölftontechnik:
* 1. Alle 12 chromatischen Töne müssen verwendet werden
* 2. Kein Ton darf wiederholt werden, bevor alle anderen gespielt wurden
* 3. Die Reihe kann transformiert werden: Original, Krebs, Umkehrung, Krebsumkehrung
* 4. Transposition auf alle 12 Stufen ist erlaubt
*/
class TwelveToneGenerator {
private array $noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
private array $originalRow;
public function __construct() {
$this->originalRow = $this->generateRandomRow();
}
/**
* Generiert eine zufällige Zwölftonreihe
*/
public function generateRandomRow(): array {
$row = range(0, 11);
shuffle($row);
return $row;
}
/**
* Gibt die Grundreihe zurück
*/
public function getOriginalRow(): array {
return $this->originalRow;
}
/**
* Krebs (Retrograde) - Reihe rückwärts
*/
public function getRetrograde(): array {
return array_reverse($this->originalRow);
}
/**
* Umkehrung (Inversion) - Intervalle gespiegelt
*/
public function getInversion(): array {
$inversion = [];
$firstNote = $this->originalRow[0];
foreach ($this->originalRow as $note) {
$interval = $note - $firstNote;
$invertedNote = ($firstNote - $interval + 12) % 12;
$inversion[] = $invertedNote;
}
return $inversion;
}
/**
* Krebsumkehrung (Retrograde Inversion)
*/
public function getRetrogradeInversion(): array {
return array_reverse($this->getInversion());
}
/**
* Transponiert eine Reihe um n Halbtöne
*/
public function transpose(array $row, int $semitones): array {
return array_map(function($note) use ($semitones) {
return ($note + $semitones) % 12;
}, $row);
}
/**
* Konvertiert Notennummern zu Notennamen
*/
public function toNoteNames(array $row): array {
return array_map(function($note) {
return $this->noteNames[$note];
}, $row);
}
/**
* Generiert die komplette Zwölftonmatrix (12x12)
*/
public function generateMatrix(): array {
$matrix = [];
$inversion = $this->getInversion();
for ($i = 0; $i < 12; $i++) {
$transposition = $inversion[$i];
$matrix[$i] = $this->transpose($this->originalRow, $transposition);
}
return $matrix;
}
/**
* Gibt alle Daten als JSON zurück
*/
public function toJSON(): string {
return json_encode([
'original' => $this->originalRow,
'retrograde' => $this->getRetrograde(),
'inversion' => $this->getInversion(),
'retrogradeInversion' => $this->getRetrogradeInversion(),
'noteNames' => $this->toNoteNames($this->originalRow),
'matrix' => $this->generateMatrix()
]);
}
}
// API-Endpoint für neue Reihe
if (isset($_GET['action']) && $_GET['action'] === 'generate') {
header('Content-Type: application/json');
$generator = new TwelveToneGenerator();
echo $generator->toJSON();
exit;
}
$generator = new TwelveToneGenerator();
$initialData = $generator->toJSON();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zwölfton-Synthesizer | Dodekaphonie nach Schönberg</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #e0e0e0;
overflow-x: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 30px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
margin-bottom: 30px;
}
h1 {
font-size: 2.5em;
background: linear-gradient(90deg, #00d4ff, #7b2cbf, #ff6b6b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.subtitle {
color: #888;
font-size: 1.1em;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
button {
padding: 15px 30px;
font-size: 1.1em;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
}
.btn-primary {
background: linear-gradient(135deg, #00d4ff, #0099cc);
color: #fff;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.3);
}
.btn-secondary {
background: linear-gradient(135deg, #7b2cbf, #5a189a);
color: #fff;
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(123, 44, 191, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #ff6b6b, #ee5a5a);
color: #fff;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(255, 107, 107, 0.3);
}
.panel {
background: rgba(255,255,255,0.05);
border-radius: 20px;
padding: 25px;
margin-bottom: 25px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
.panel h2 {
margin-bottom: 20px;
color: #00d4ff;
font-size: 1.3em;
}
.row-display {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.note-box {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 212, 255, 0.1);
border: 2px solid rgba(0, 212, 255, 0.3);
border-radius: 10px;
font-weight: bold;
font-size: 1.2em;
transition: all 0.3s ease;
}
.note-box.active {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
border-color: #fff;
transform: scale(1.2);
box-shadow: 0 0 30px rgba(0, 212, 255, 0.5);
}
.note-box.played {
background: rgba(123, 44, 191, 0.3);
border-color: rgba(123, 44, 191, 0.5);
}
.sliders {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.slider-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.slider-group label {
display: flex;
justify-content: space-between;
color: #aaa;
}
input[type="range"] {
width: 100%;
height: 8px;
border-radius: 4px;
background: rgba(255,255,255,0.1);
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
cursor: pointer;
}
.visualizer-container {
height: 200px;
background: rgba(0,0,0,0.3);
border-radius: 15px;
overflow: hidden;
position: relative;
}
#visualizer {
width: 100%;
height: 100%;
}
.transformation-select {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
.transform-btn {
padding: 10px 20px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 25px;
color: #e0e0e0;
cursor: pointer;
transition: all 0.3s ease;
}
.transform-btn.active {
background: linear-gradient(135deg, #00d4ff, #7b2cbf);
border-color: transparent;
}
.transform-btn:hover {
background: rgba(0, 212, 255, 0.3);
}
.status {
text-align: center;
padding: 15px;
background: rgba(0,0,0,0.2);
border-radius: 10px;
margin-top: 20px;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
background: #666;
}
.status-indicator.playing {
background: #00ff88;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.matrix-container {
overflow-x: auto;
}
.matrix {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 3px;
min-width: 500px;
}
.matrix-cell {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 212, 255, 0.1);
border-radius: 5px;
font-size: 0.9em;
font-weight: 500;
}
.info-text {
background: rgba(0,0,0,0.2);
padding: 15px;
border-radius: 10px;
margin-top: 15px;
font-size: 0.9em;
color: #aaa;
line-height: 1.6;
}
footer {
text-align: center;
padding: 30px;
color: #666;
border-top: 1px solid rgba(255,255,255,0.1);
margin-top: 30px;
}
@media (max-width: 768px) {
h1 { font-size: 1.8em; }
.note-box { width: 45px; height: 45px; font-size: 1em; }
.controls { flex-direction: column; align-items: center; }
button { width: 100%; max-width: 300px; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Zwölfton-Synthesizer</h1>
<p class="subtitle">Dodekaphonie nach Arnold Schönberg</p>
</header>
<div class="controls">
<button id="startBtn" class="btn-primary">▶ Starten</button>
<button id="stopBtn" class="btn-danger">◼ Stoppen</button>
<button id="newRowBtn" class="btn-secondary">🎲 Neue Reihe</button>
</div>
<div class="panel">
<h2>Aktuelle Zwölftonreihe</h2>
<div class="transformation-select">
<button class="transform-btn active" data-transform="original">Original (O)</button>
<button class="transform-btn" data-transform="retrograde">Krebs (R)</button>
<button class="transform-btn" data-transform="inversion">Umkehrung (I)</button>
<button class="transform-btn" data-transform="retrogradeInversion">Krebsumkehrung (RI)</button>
</div>
<div id="rowDisplay" class="row-display"></div>
</div>
<div class="panel">
<h2>Klangparameter</h2>
<div class="sliders">
<div class="slider-group">
<label>Tempo (BPM): <span id="tempoValue">120</span></label>
<input type="range" id="tempo" min="40" max="300" value="120">
</div>
<div class="slider-group">
<label>Oktave: <span id="octaveValue">4</span></label>
<input type="range" id="octave" min="2" max="6" value="4">
</div>
<div class="slider-group">
<label>Reverb: <span id="reverbValue">50</span>%</label>
<input type="range" id="reverb" min="0" max="100" value="50">
</div>
<div class="slider-group">
<label>Attack: <span id="attackValue">0.05</span>s</label>
<input type="range" id="attack" min="1" max="500" value="50">
</div>
<div class="slider-group">
<label>Release: <span id="releaseValue">0.3</span>s</label>
<input type="range" id="release" min="50" max="2000" value="300">
</div>
<div class="slider-group">
<label>Wellenform:</label>
<select id="waveform" style="padding: 8px; border-radius: 5px; background: rgba(255,255,255,0.1); color: #e0e0e0; border: 1px solid rgba(255,255,255,0.2);">
<option value="sine">Sinus</option>
<option value="triangle">Dreieck</option>
<option value="square">Rechteck</option>
<option value="sawtooth" selected>Sägezahn</option>
</select>
</div>
</div>
</div>
<div class="panel">
<h2>Audio-Visualisierung</h2>
<div class="visualizer-container">
<canvas id="visualizer"></canvas>
</div>
<div class="status">
<span id="statusIndicator" class="status-indicator"></span>
<span id="statusText">Bereit zum Starten</span>
</div>
</div>
<div class="panel">
<h2>Zwölftonmatrix</h2>
<div class="matrix-container">
<div id="matrix" class="matrix"></div>
</div>
<div class="info-text">
<strong>Die Zwölftontechnik (Dodekaphonie):</strong><br>
Entwickelt von Arnold Schönberg um 1921. Alle 12 Halbtöne der chromatischen Tonleiter
werden gleichberechtigt verwendet. Die Grundreihe kann in vier Formen erscheinen:
<strong>Original (O)</strong> - die Grundreihe,
<strong>Krebs (R)</strong> - rückwärts gespielt,
<strong>Umkehrung (I)</strong> - Intervalle gespiegelt,
<strong>Krebsumkehrung (RI)</strong> - Kombination aus Krebs und Umkehrung.
Jede Form kann auf alle 12 Stufen transponiert werden (48 mögliche Reihen).
</div>
</div>
<footer>
<p>Zwölfton-Synthesizer &copy; 2024 | Basierend auf der Dodekaphonie von Arnold Schönberg</p>
</footer>
</div>
<script>
// Initiale Daten vom PHP-Backend
let rowData = <?= $initialData ?>;
// Audio-Kontext und Knoten
let audioContext = null;
let masterGain = null;
let reverbGain = null;
let dryGain = null;
let convolver = null;
let analyser = null;
let isPlaying = false;
let currentNoteIndex = 0;
let playInterval = null;
let currentTransform = 'original';
// Frequenzen für alle Noten (A4 = 440Hz)
const noteFrequencies = {
0: 261.63, // C
1: 277.18, // C#
2: 293.66, // D
3: 311.13, // D#
4: 329.63, // E
5: 349.23, // F
6: 369.99, // F#
7: 392.00, // G
8: 415.30, // G#
9: 440.00, // A
10: 466.16, // A#
11: 493.88 // B
};
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
/**
* Initialisiert den Audio-Kontext
*/
async function initAudio() {
if (audioContext) return;
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Master Gain
masterGain = audioContext.createGain();
masterGain.gain.value = 0.5;
masterGain.connect(audioContext.destination);
// Analyser für Visualisierung
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.connect(masterGain);
// Dry/Wet Gain für Reverb
dryGain = audioContext.createGain();
reverbGain = audioContext.createGain();
dryGain.connect(analyser);
reverbGain.connect(analyser);
// Convolver für Reverb
convolver = audioContext.createConvolver();
convolver.connect(reverbGain);
// Generiere Impulsantwort für Reverb
await createReverbImpulse();
updateReverbMix();
startVisualization();
}
/**
* Erstellt eine synthetische Impulsantwort für den Reverb
*/
async function createReverbImpulse() {
const sampleRate = audioContext.sampleRate;
const length = sampleRate * 3; // 3 Sekunden Reverb
const impulse = audioContext.createBuffer(2, length, sampleRate);
for (let channel = 0; channel < 2; channel++) {
const channelData = impulse.getChannelData(channel);
for (let i = 0; i < length; i++) {
// Exponentieller Decay mit etwas Rauschen
const decay = Math.pow(1 - i / length, 2);
channelData[i] = (Math.random() * 2 - 1) * decay;
}
}
convolver.buffer = impulse;
}
/**
* Aktualisiert das Dry/Wet-Verhältnis des Reverbs
*/
function updateReverbMix() {
const reverbAmount = document.getElementById('reverb').value / 100;
dryGain.gain.value = 1 - reverbAmount * 0.5;
reverbGain.gain.value = reverbAmount;
}
/**
* Spielt eine Note
*/
function playNote(noteNumber) {
if (!audioContext) return;
const octave = parseInt(document.getElementById('octave').value);
const frequency = noteFrequencies[noteNumber] * Math.pow(2, octave - 4);
const waveform = document.getElementById('waveform').value;
const attack = document.getElementById('attack').value / 1000;
const release = document.getElementById('release').value / 1000;
// Oszillator
const osc = audioContext.createOscillator();
osc.type = waveform;
osc.frequency.value = frequency;
// Gain für ADSR-Hüllkurve
const gainNode = audioContext.createGain();
gainNode.gain.value = 0;
// Verbindungen
osc.connect(gainNode);
gainNode.connect(dryGain);
gainNode.connect(convolver);
const now = audioContext.currentTime;
// Attack
gainNode.gain.linearRampToValueAtTime(0.7, now + attack);
// Release
gainNode.gain.linearRampToValueAtTime(0, now + attack + release);
osc.start(now);
osc.stop(now + attack + release + 0.1);
}
/**
* Startet die automatische Wiedergabe
*/
async function startPlaying() {
await initAudio();
if (isPlaying) return;
isPlaying = true;
currentNoteIndex = 0;
updateStatus(true);
playNextNote();
}
/**
* Stoppt die Wiedergabe
*/
function stopPlaying() {
isPlaying = false;
if (playInterval) {
clearTimeout(playInterval);
playInterval = null;
}
updateStatus(false);
resetNoteDisplay();
}
/**
* Spielt die nächste Note in der Reihe
*/
function playNextNote() {
if (!isPlaying) return;
const row = getCurrentRow();
const noteNumber = row[currentNoteIndex];
// Visuelle Hervorhebung
updateNoteDisplay(currentNoteIndex);
// Note spielen
playNote(noteNumber);
// Nächste Note vorbereiten
currentNoteIndex++;
if (currentNoteIndex >= 12) {
currentNoteIndex = 0;
// Zufällig Transformation wechseln (optional)
if (Math.random() > 0.7) {
const transforms = ['original', 'retrograde', 'inversion', 'retrogradeInversion'];
currentTransform = transforms[Math.floor(Math.random() * transforms.length)];
updateTransformButtons();
displayRow();
}
}
// Tempo berechnen
const bpm = parseInt(document.getElementById('tempo').value);
const interval = 60000 / bpm;
playInterval = setTimeout(playNextNote, interval);
}
/**
* Gibt die aktuelle Reihenform zurück
*/
function getCurrentRow() {
switch (currentTransform) {
case 'retrograde': return rowData.retrograde;
case 'inversion': return rowData.inversion;
case 'retrogradeInversion': return rowData.retrogradeInversion;
default: return rowData.original;
}
}
/**
* Aktualisiert die visuelle Darstellung der aktuellen Note
*/
function updateNoteDisplay(activeIndex) {
const boxes = document.querySelectorAll('.note-box');
boxes.forEach((box, index) => {
box.classList.remove('active');
if (index < activeIndex) {
box.classList.add('played');
} else {
box.classList.remove('played');
}
if (index === activeIndex) {
box.classList.add('active');
}
});
}
/**
* Setzt die Notenanzeige zurück
*/
function resetNoteDisplay() {
const boxes = document.querySelectorAll('.note-box');
boxes.forEach(box => {
box.classList.remove('active', 'played');
});
}
/**
* Aktualisiert den Status-Anzeiger
*/
function updateStatus(playing) {
const indicator = document.getElementById('statusIndicator');
const text = document.getElementById('statusText');
if (playing) {
indicator.classList.add('playing');
text.textContent = 'Spielt...';
} else {
indicator.classList.remove('playing');
text.textContent = 'Gestoppt';
}
}
/**
* Zeigt die Zwölftonreihe an
*/
function displayRow() {
const container = document.getElementById('rowDisplay');
const row = getCurrentRow();
container.innerHTML = row.map((note, index) => `
<div class="note-box" data-note="${note}" data-index="${index}">
${noteNames[note]}
</div>
`).join('');
}
/**
* Zeigt die Zwölftonmatrix an
*/
function displayMatrix() {
const container = document.getElementById('matrix');
const matrix = rowData.matrix;
container.innerHTML = matrix.flat().map(note => `
<div class="matrix-cell">${noteNames[note]}</div>
`).join('');
}
/**
* Aktualisiert die Transformations-Buttons
*/
function updateTransformButtons() {
document.querySelectorAll('.transform-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.transform === currentTransform);
});
}
/**
* Generiert eine neue Reihe vom Server
*/
async function generateNewRow() {
try {
const response = await fetch('?action=generate');
rowData = await response.json();
displayRow();
displayMatrix();
currentNoteIndex = 0;
resetNoteDisplay();
} catch (error) {
console.error('Fehler beim Generieren:', error);
}
}
/**
* Audio-Visualisierung
*/
function startVisualization() {
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
resize();
window.addEventListener('resize', resize);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function draw() {
requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
// Hintergrund mit Fade-Effekt
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, width, height);
// Wellenform zeichnen
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(0, 212, 255, 0.8)';
ctx.beginPath();
const sliceWidth = width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * height / 2;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(width, height / 2);
ctx.stroke();
// Frequenzspektrum
analyser.getByteFrequencyData(dataArray);
const barWidth = (width / 64) * 1.5;
let barX = 0;
for (let i = 0; i < 64; i++) {
const barHeight = (dataArray[i] / 255) * height * 0.7;
const gradient = ctx.createLinearGradient(0, height - barHeight, 0, height);
gradient.addColorStop(0, 'rgba(123, 44, 191, 0.8)');
gradient.addColorStop(1, 'rgba(0, 212, 255, 0.8)');
ctx.fillStyle = gradient;
ctx.fillRect(barX, height - barHeight, barWidth - 2, barHeight);
barX += barWidth;
}
}
draw();
}
// Event Listeners
document.getElementById('startBtn').addEventListener('click', startPlaying);
document.getElementById('stopBtn').addEventListener('click', stopPlaying);
document.getElementById('newRowBtn').addEventListener('click', generateNewRow);
document.querySelectorAll('.transform-btn').forEach(btn => {
btn.addEventListener('click', () => {
currentTransform = btn.dataset.transform;
updateTransformButtons();
displayRow();
currentNoteIndex = 0;
resetNoteDisplay();
});
});
// Slider Updates
document.getElementById('tempo').addEventListener('input', e => {
document.getElementById('tempoValue').textContent = e.target.value;
});
document.getElementById('octave').addEventListener('input', e => {
document.getElementById('octaveValue').textContent = e.target.value;
});
document.getElementById('reverb').addEventListener('input', e => {
document.getElementById('reverbValue').textContent = e.target.value;
if (audioContext) updateReverbMix();
});
document.getElementById('attack').addEventListener('input', e => {
document.getElementById('attackValue').textContent = (e.target.value / 1000).toFixed(2);
});
document.getElementById('release').addEventListener('input', e => {
document.getElementById('releaseValue').textContent = (e.target.value / 1000).toFixed(2);
});
// Initialisierung
displayRow();
displayMatrix();
</script>
</body>
</html>