1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
196 lines
9.0 KiB
HTML
196 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Funk-Effekt Test</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',system-ui,sans-serif;background:#f5efe0;padding:2rem;max-width:700px;margin:auto}
|
|
h1{font-size:1.2rem;color:#2d5434;margin-bottom:1rem}
|
|
.row{display:flex;gap:.5rem;align-items:center;margin-bottom:.6rem;padding:.5rem;background:#fff;border-radius:8px}
|
|
.row button{padding:.4rem .8rem;background:#2d5434;color:#fff;border:none;border-radius:6px;font-size:.75rem;font-weight:700;cursor:pointer}
|
|
.row span{font-size:.75rem;color:#3d5165;flex:1}
|
|
.row .tag{font-size:.6rem;background:#dae8ec;padding:2px 6px;border-radius:4px;color:#4a7c8a;font-weight:600}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🎙️ Funk-Effekt Vergleich</h1>
|
|
<p style="font-size:.8rem;color:#6b7d8f;margin-bottom:.5rem">Gleicher TTS-Clip mit verschiedenen Filter-Ketten.</p>
|
|
<button onclick="initAudio()" style="padding:.5rem 1.2rem;background:#2d5434;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;margin-bottom:.5rem">🔊 Audio aktivieren</button>
|
|
<div id="status" style="font-size:.75rem;color:#8a8a8a;margin-bottom:1rem">Klicke "Audio aktivieren" um zu starten.</div>
|
|
|
|
<div id="tests"></div>
|
|
|
|
<script>
|
|
var audioCtx = null;
|
|
var testBuffer = null;
|
|
|
|
function initAudio() {
|
|
if (audioCtx) return;
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
|
var statusEl = document.getElementById('status');
|
|
statusEl.textContent = 'Lade Audio...';
|
|
|
|
var basePath = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1);
|
|
var audioUrl = basePath + 'sounds/radio/tower1.mp3';
|
|
statusEl.textContent = 'Lade: ' + audioUrl;
|
|
fetch(audioUrl)
|
|
.then(function(r) {
|
|
statusEl.textContent = 'Dekodiere... (Status: ' + r.status + ')';
|
|
return r.arrayBuffer();
|
|
})
|
|
.then(function(b) { return audioCtx.decodeAudioData(b); })
|
|
.then(function(buf) {
|
|
testBuffer = buf;
|
|
statusEl.textContent = 'Bereit! (' + buf.duration.toFixed(1) + 's, ' + buf.sampleRate + ' Hz)';
|
|
statusEl.style.color = '#5a8a5e';
|
|
// Auch alle Stimmen laden
|
|
var loaded = 0;
|
|
['alloy','echo','fable','nova','onyx','shimmer'].forEach(function(v){
|
|
var vUrl = basePath + 'sounds/radio/voice_'+v+'.mp3';
|
|
fetch(vUrl)
|
|
.then(function(r){ if(!r.ok) throw new Error(v+': HTTP '+r.status); return r.arrayBuffer(); })
|
|
.then(function(b){ return audioCtx.decodeAudioData(b); })
|
|
.then(function(buf){ voiceBuffers[v]=buf; loaded++; statusEl.textContent='Bereit! Stimmen: '+loaded+'/6'; })
|
|
.catch(function(e){ console.error('Voice load fail:', v, e); statusEl.textContent += ' ('+v+' fehlt)'; });
|
|
});
|
|
})
|
|
.catch(function(e) {
|
|
statusEl.textContent = 'FEHLER: ' + e.message;
|
|
statusEl.style.color = '#c85c4a';
|
|
});
|
|
}
|
|
|
|
// Lade verschiedene Stimmen zum Vergleich
|
|
var voiceBuffers = {};
|
|
function loadVoice(name, url) {
|
|
fetch(basePath + url).then(function(r){return r.arrayBuffer()}).then(function(b){return audioCtx.decodeAudioData(b)}).then(function(buf){voiceBuffers[name]=buf;});
|
|
}
|
|
|
|
var EFFECTS = [
|
|
{name:'Original (kein Effekt)', tag:'clean', build: function(ctx){
|
|
var g = ctx.createGain(); g.gain.value = 1.0;
|
|
return {input:g, output:g};
|
|
}},
|
|
{name:'Nur Highpass 800 Hz', tag:'HP800', build: function(ctx){
|
|
var hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=800;
|
|
return {input:hp, output:hp};
|
|
}},
|
|
{name:'Bandpass 2500 Hz (Q=1)', tag:'BP2500', build: function(ctx){
|
|
var bp = ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=2500; bp.Q.value=1;
|
|
return {input:bp, output:bp};
|
|
}},
|
|
{name:'HP 800 + Peak 2500 + LP 4000', tag:'HP+Peak+LP', build: function(ctx){
|
|
var hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=800;
|
|
var pk = ctx.createBiquadFilter(); pk.type='peaking'; pk.frequency.value=2500; pk.gain.value=8; pk.Q.value=1.5;
|
|
var lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=4000;
|
|
hp.connect(pk); pk.connect(lp);
|
|
return {input:hp, output:lp};
|
|
}},
|
|
{name:'HP 800 + Peak 2500 + LP 4000 + Soft Distortion', tag:'Funk v1', build: function(ctx){
|
|
var hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=800;
|
|
var pk = ctx.createBiquadFilter(); pk.type='peaking'; pk.frequency.value=2500; pk.gain.value=8; pk.Q.value=1.5;
|
|
var lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=4000;
|
|
var dist = ctx.createWaveShaper();
|
|
var c = new Float32Array(256);
|
|
for(var i=0;i<256;i++){var x=(i/128)-1;c[i]=Math.tanh(x*1.5);}
|
|
dist.curve = c;
|
|
hp.connect(pk); pk.connect(lp); lp.connect(dist);
|
|
return {input:hp, output:dist};
|
|
}},
|
|
{name:'HP 600 + Peak 3000 + LP 3500 + Hard Clip', tag:'Funk v2', build: function(ctx){
|
|
var hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=600;
|
|
var pk = ctx.createBiquadFilter(); pk.type='peaking'; pk.frequency.value=3000; pk.gain.value=12; pk.Q.value=2;
|
|
var lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=3500;
|
|
var dist = ctx.createWaveShaper();
|
|
var c = new Float32Array(256);
|
|
for(var i=0;i<256;i++){var x=(i/128)-1;c[i]=x>0.6?0.6:x<-0.6?-0.6:x;}
|
|
dist.curve = c;
|
|
hp.connect(pk); pk.connect(lp); lp.connect(dist);
|
|
return {input:hp, output:dist};
|
|
}},
|
|
{name:'Walkie-Talkie (BP 1800, Q=3, Dist)', tag:'Walkie', build: function(ctx){
|
|
var bp = ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=1800; bp.Q.value=3;
|
|
var dist = ctx.createWaveShaper();
|
|
var c = new Float32Array(256);
|
|
for(var i=0;i<256;i++){var x=(i/128)-1;c[i]=Math.sign(x)*Math.pow(Math.abs(x),0.6);}
|
|
dist.curve = c;
|
|
bp.connect(dist);
|
|
return {input:bp, output:dist};
|
|
}},
|
|
{name:'AM Radio (HP 400, BP 1500 Q=2, LP 3000)', tag:'AM Radio', build: function(ctx){
|
|
var hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=400;
|
|
var bp = ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=1500; bp.Q.value=2;
|
|
var lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=3000;
|
|
hp.connect(bp); bp.connect(lp);
|
|
return {input:hp, output:lp};
|
|
}},
|
|
];
|
|
|
|
var html = '<h2 style="font-size:.9rem;color:#2d5434;margin:1rem 0 .5rem">Filter-Effekte (Tower-Stimme)</h2>';
|
|
EFFECTS.forEach(function(e, i){
|
|
html += '<div class="row"><button onclick="playEffect('+i+')">▶</button><span>'+e.name+'</span><span class="tag">'+e.tag+'</span></div>';
|
|
});
|
|
|
|
// Stimmenvergleich
|
|
var VOICES = ['alloy','echo','fable','nova','onyx','shimmer'];
|
|
var VOICE_DESC = {
|
|
alloy:'Neutral, warm', echo:'Männlich, tief', fable:'Britisch, weich',
|
|
nova:'Weiblich, klar', onyx:'Männlich, voll (aktuell)', shimmer:'Weiblich, hell'
|
|
};
|
|
html += '<h2 style="font-size:.9rem;color:#2d5434;margin:1.5rem 0 .5rem">Stimmen-Vergleich (mit Walkie-Effekt)</h2>';
|
|
VOICES.forEach(function(v){
|
|
html += '<div class="row"><button onclick="playVoice(\''+v+'\')">▶</button><span><strong>'+v+'</strong> — '+VOICE_DESC[v]+'</span><span class="tag">Stimme</span></div>';
|
|
});
|
|
html += '<h2 style="font-size:.9rem;color:#2d5434;margin:1.5rem 0 .5rem">Stimmen ohne Effekt (zum Vergleich)</h2>';
|
|
VOICES.forEach(function(v){
|
|
html += '<div class="row"><button onclick="playVoiceClean(\''+v+'\')">▶</button><span><strong>'+v+'</strong> — '+VOICE_DESC[v]+' (clean)</span><span class="tag">Clean</span></div>';
|
|
});
|
|
|
|
document.getElementById('tests').innerHTML = html;
|
|
|
|
function playVoice(voice) {
|
|
if (!audioCtx || !voiceBuffers[voice]) { alert('Lade zuerst Audio.'); return; }
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
var src = audioCtx.createBufferSource();
|
|
src.buffer = voiceBuffers[voice];
|
|
// Walkie-Talkie Effekt
|
|
var bp = audioCtx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=1800; bp.Q.value=3;
|
|
var dist = audioCtx.createWaveShaper();
|
|
var c = new Float32Array(256);
|
|
for(var i=0;i<256;i++){var x=(i/128)-1;c[i]=Math.sign(x)*Math.pow(Math.abs(x),0.6);}
|
|
dist.curve = c;
|
|
var gain = audioCtx.createGain(); gain.gain.value = 3.0;
|
|
src.connect(bp); bp.connect(dist); dist.connect(gain); gain.connect(audioCtx.destination);
|
|
src.start(0);
|
|
}
|
|
|
|
function playVoiceClean(voice) {
|
|
if (!audioCtx || !voiceBuffers[voice]) { alert('Lade zuerst Audio.'); return; }
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
var src = audioCtx.createBufferSource();
|
|
src.buffer = voiceBuffers[voice];
|
|
var gain = audioCtx.createGain(); gain.gain.value = 1.5;
|
|
src.connect(gain); gain.connect(audioCtx.destination);
|
|
src.start(0);
|
|
}
|
|
|
|
function playEffect(idx) {
|
|
if (!audioCtx) initAudio();
|
|
if (!testBuffer) { alert('Audio noch nicht geladen. Klicke zuerst "Audio aktivieren".'); return; }
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
var src = audioCtx.createBufferSource();
|
|
src.buffer = testBuffer;
|
|
var chain = EFFECTS[idx].build(audioCtx);
|
|
var gain = audioCtx.createGain(); gain.gain.value = 2.5;
|
|
src.connect(chain.input);
|
|
chain.output.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
src.start(0);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|