Was ist Tinnitus?

Pulsiert, rauscht, klickt oder pocht Ihr Tinnitus? Dieses kostenlose Werkzeug hilft Ihnen, das Geräusch, das Sie hören, abzugleichen und zu identifizieren, damit Sie es Ihrem Arzt beschreiben können.

Ein Hinweis zu pulsierendem Tinnitus und Ihrer Gesundheit: Pulsierender Tinnitus unterscheidet sich vom konstanten Klingeln, das die meisten Menschen mit Tinnitus verbinden. Da er oft mit dem Blutfluss zusammenhängt, kann er manchmal auf eine zugrunde liegende Erkrankung hinweisen, die ein Arzt identifizieren und behandeln kann. Das ist eine gute Nachricht -- im Gegensatz zu den meisten Tinnitus-Formen hat pulsierender Tinnitus oft eine spezifische, behandelbare Ursache. Wir empfehlen jedem mit pulsierendem Tinnitus, dies mit seinem Arzt zu besprechen, insbesondere wenn es ein neues Symptom ist.

Pulsierenden Klang abgleichen

Mein Tinnitus pulsiert, rauscht oder pocht -- oft im Rhythmus meines Herzschlags.

Ohrklicks abgleichen

Mein Ohr klickt, knackt, knistert oder flattert -- regelmäßig oder unregelmäßig.

Klangvorlagen durchsuchen

Hören Sie sich Beispielgeräusche häufiger Erscheinungsformen von pulsierendem Tinnitus und Ohrklicks an.

Schnelle Symptomprüfung

Diese Fragen helfen uns, relevante Hinweise zu geben. Ihre Antworten werden weder gespeichert noch übertragen -- die gesamte Verarbeitung erfolgt in Ihrem Browser.

1. Wann hat dieses Geräusch begonnen?

2. Ist das Geräusch mit Ihrem Herzschlag synchronisiert?

3. Ist das Geräusch in einem Ohr oder in beiden?

4. Haben Sie eines dieser begleitenden Symptome? (Kreuzen Sie alle zutreffenden an)

5. Haben Sie wegen dieses Geräuschs einen Arzt aufgesucht?


Stadtschwärmer
Zum Vergrößern klicken
Sehr niedrig — Niedrig — Mittel — Hoch
Ruheherzfrequenzbereich
Tippen Sie im Rhythmus Ihres Pulses oder Tinnitus
Pulsform:
Mobile Anwendung ——— Vollständige Stille zwischen den Pulsen
Klangcharakter:
Breit (hauchig) ——— Schmal (tonal)
Ohr:
Stadtschwärmer
Zum Vergrößern klicken
Klickcharakter:
Datum hinzugefügt
Dumpfe Schläge ——— Scharfe, helle Klicks
Ohr:

Was ist Tinnitus?

Ohrklick-Vorlagen

Gespeicherte Profile

No saved profiles yet. Use the "Save Profile" button after adjusting sounds to your liking.

Teilen Sie dies mit Ihrem Arzt

Ihr abgeglichenes Klangprofil kann Ihrem Arzt helfen, genau zu verstehen, was Sie hören. Sie können Ihre Einstellungen exportieren und zu Ihrem Termin mitbringen.

Wann Sie eine ärztliche Untersuchung aufsuchen sollten:

  • Ihr pulsierender Tinnitus ist neu (begann in den letzten Wochen)
  • Es ist nur in einem Ohr
  • Es wird lauter oder häufiger
  • Sie haben auch Kopfschmerzen, Sehveränderungen oder Schwindel

Welchen Arzt Sie aufsuchen sollten: Ein HNO-Arzt (Hals-Nasen-Ohren-Arzt) ist in der Regel der erste Spezialist, der pulsierenden Tinnitus untersucht. Er kann bildgebende Verfahren wie MRT/MRA, CT-Angiographie oder Ultraschall der Halsarterien anordnen.

Was ist Tinnitus?

Pulsierender Tinnitus ist ein rhythmisches Geräusch, das in einem oder beiden Ohren wahrgenommen wird und sich typischerweise mit dem Herzschlag synchronisiert. Im Gegensatz zum gleichmäßigen Klingeln, Summen oder Zischen des tonalen Tinnitus hat pulsierender Tinnitus ein deutliches zeitliches Muster. Patienten beschreiben es häufig als rauschendes, pochendes, pulsierendes oder strömendes Geräusch, das mit ihrem Puls Schritt hält.

Der wesentliche Unterschied besteht darin, dass tonaler Tinnitus ein Phantomgeräusch ist, das vom Nervensystem erzeugt wird, während pulsierender Tinnitus normalerweise eine physische, identifizierbare Geräuschquelle hat -- meistens turbulente Blutströmung in der Nähe des Ohrs. Dies macht pulsierenden Tinnitus sowohl in seinen Ursachen als auch in seiner klinischen Bedeutung grundlegend anders. Etwa 10% der Menschen mit Tinnitus erleben pulsierende Symptome.

Was ist Tinnitus?

Da pulsierender Tinnitus oft vom Blutfluss herrührt, sind seine Ursachen häufig vaskulärer Natur:

  • Atherosklerose: Verengung der Halsschlagader oder ihrer Verzweigungen in der Nähe des Ohrs, wodurch ein turbulenter Blutfluss entsteht.
  • Bluthochdruck: Eine erhöhte Kraft des Blutflusses kann das Geräusch von Blut, das durch Gefäße in der Nähe des Ohrs fließt, hörbar machen.
  • Arteriovenöse Malformationen: Abnormale Verbindungen zwischen Arterien und Venen in der Nähe des Ohrs, die einen turbulenten Hochgeschwindigkeitsfluss erzeugen.
  • Idiopathische intrakranielle Hypertension (IIH): Erhöhter Liquordruck, der Veränderungen der venösen Sinus und turbulenten Fluss verursacht.
  • Glomustumoren: Stark vaskularisierte gutartige Tumoren des Mittelohrs oder des Bulbus jugularis.
  • Mittelohr-Myoklonus: Unwillkürliche rhythmische Kontraktionen der Muskeln im Mittelohr.

Was ist Tinnitus?

Regulärer (tonaler) Tinnitus erzeugt ein konstantes Klingeln, Summen oder Zischen und wird typischerweise durch Hörverlust oder neurale Veränderungen verursacht. Im Gegensatz dazu erzeugt pulsierender Tinnitus ein rhythmisches Geräusch und hat eher eine identifizierbare, potenziell behandelbare physische Ursache. Die Notch-Klangtherapie von AudioNotch ist für tonalen Tinnitus konzipiert. Bei pulsierendem Tinnitus hat die Identifizierung und Behandlung der zugrunde liegenden Ursache durch ärztliche Untersuchung Priorität.

Was ist AudioNotch?

Ohrklicken bezieht sich auf wiederholte Klick-, Knack- oder Knistergeräusche, die im Ohr wahrgenommen werden. Diese Geräusche werden typischerweise durch unwillkürliche Kontraktionen der Mittelohrmuskeln, Eustachische Röhren-Dysfunktion oder Gaumenmyoklonus verursacht.

  • Mittelohr-Myoklonus: Rhythmisches Klicken durch unwillkürliche Kontraktionen des Musculus tensor tympani oder Musculus stapedius.
  • Eustachische Röhren-Dysfunktion: Klicken oder Knacken beim Schlucken, Gähnen oder bei Kieferbewegungen.
  • Gaumenmyoklonus: Rhythmisches Klicken durch Gaumenmuskelspasmen, oft auch für andere hörbar.
  • Kiefergelenkserkrankungen (TMJ): Klicken in Verbindung mit Kieferbewegungen, kein echter Tinnitus, wird aber häufig als Ohrklicken beschrieben.

Wenn das Ohrklicken anhaltend ist, sich verschlechtert oder von Hörverlust begleitet wird, wird ein Besuch bei einem HNO-Arzt empfohlen.

Haeufig gestellte Fragen

Was ist Tinnitus?

Pulsierender Tinnitus unterscheidet sich von regulärem Tinnitus, da er oft eine identifizierbare physische Ursache hat. Während viele Ursachen gutartig sind, können einige auf Erkrankungen hinweisen, die von einer Behandlung profitieren. Wir empfehlen jedem mit pulsierendem Tinnitus, dies mit seinem Arzt zu besprechen, insbesondere wenn es ein neues Symptom ist oder von Kopfschmerzen, Sehveränderungen oder Schwindel begleitet wird.

Kann pulsierender Tinnitus von selbst verschwinden?

Einige Fälle lösen sich von selbst, insbesondere solche, die mit vorübergehenden Zuständen wie Ohrinfektionen, Stress oder Bluthochdruck zusammenhängen. Da pulsierender Tinnitus jedoch manchmal auf eine zugrunde liegende Erkrankung hinweisen kann, ist es am besten, ihn von einem medizinischen Fachpersonal untersuchen zu lassen.

Wie klingt es?

Es wird häufig als rauschendes, pochendes, strömendes oder pulsierendes Geräusch beschrieben, das mit dem Herzschlag Schritt hält. Manche Menschen beschreiben es als das Hören ihres Herzschlags im Ohr. Unser obiger Klangenerator ermöglicht es Ihnen, diese Geräusche nachzubilden und abzugleichen, um Ihre Symptome einem Arzt besser beschreiben zu können.

Warum höre ich ein Klicken in meinem Ohr?

Ohrklicken kann mehrere Ursachen haben, darunter Mittelohr-Myoklonus, Eustachische Röhren-Dysfunktion, Tensor-Tympani-Syndrom oder Gaumenmyoklonus. Wenn das Klicken anhaltend oder störend ist, kann ein HNO-Arzt helfen, die Ursache festzustellen.

Sprechen Sie mit einem Arzt, wenn Sie Tinnitus haben.

Ja. Im Gegensatz zum konstanten Klingel-Tinnitus hat pulsierender Tinnitus oft eine spezifische, identifizierbare Ursache, die behandelbar sein kann. Dies ist besonders wichtig, wenn das Symptom neu ist, nur in einem Ohr auftritt, schlimmer wird oder von Kopfschmerzen, Sehveränderungen oder Schwindel begleitet wird. Ein HNO-Arzt ist in der Regel der beste erste Arzt, den Sie aufsuchen sollten.

Haftungsausschluss

Dieses Werkzeug wird ausschließlich zu Bildungs- und Klangabgleichszwecken bereitgestellt. Es ist kein Diagnosewerkzeug und kann die Ursache Ihres Tinnitus nicht identifizieren.

Wichtig: Pulsierender Tinnitus kann ein Symptom zugrunde liegender medizinischer Erkrankungen sein, von denen einige eine Behandlung erfordern. Im Gegensatz zum konstanten Klingel-Tinnitus hat pulsierender Tinnitus oft eine identifizierbare und behandelbare Ursache. Wir empfehlen dringend, dass jeder, der pulsierenden Tinnitus erlebt, einen medizinischen Fachmann konsultiert, insbesondere einen Hals-Nasen-Ohren-Arzt (HNO).

AudioNotch bietet keine medizinische Beratung, Diagnose oder Behandlung. Die Informationen und Werkzeuge auf dieser Seite sind kein Ersatz für eine professionelle medizinische Untersuchung.

Hören Sie auch ein konstantes Klingeln oder Summen?

Die Notch-Klangtherapie von AudioNotch ist für die Behandlung von gleichmäßigem tonalem Tinnitus konzipiert. Wenn Sie sowohl pulsierenden als auch tonalen Tinnitus erleben, können unsere Therapiewerkzeuge bei der tonalen Komponente helfen.

Frequenz-Visualisierer

Klangprofil speichern

Entdecken Sie weitere kostenlose Tinnitus-Tools

Frequenz-Visualisierer

Finden Sie die genaue Frequenz Ihres Tinnitus mit gefuehrter binaerer Suche und Oktavverwechslungserkennung.

Tinnitus-Handicap-Inventar

Fuehren Sie die klinisch validierte THI-Bewertung mit 25 Fragen durch. Verfolgen Sie den Schweregrad Ihres Tinnitus im Zeitverlauf.

Kostenloser Online-Hoertest

Erhalten Sie ein vollstaendiges Audiogramm mit kalibrierter Reintonaudiometrie. Testen Sie alle Standardfrequenzen von 250 Hz bis 8 kHz.


// ================================================================== // TRANSLATION OBJECT // ================================================================== var T = { clickToPlay: "Zum Vergrößern klicken", playing: "Play Store", detected: "Ausgewählte löschen", bpm: "BPM", confidenceGood: "Gut", confidenceFair: "Ausreichend", confidenceLow: "Niedrig", confidence: "Vertrauliche Informationen.", keepTapping: "Weiter tippen...", taps: "Tipps", tapAlongPrompt: "Tippen Sie im Rhythmus Ihres Pulses oder Tinnitus", belowRestingHR: "Unter der Ruheherzfrequenz", restingHRRange: "Ruheherzfrequenzbereich", elevatedHR: "Erhöhte / Belastungsherzfrequenz", weRecommendSeeingDoctor: "Wir empfehlen, eine Stunde am Tag AudioNotch anzuhören.", basedOnResponses: "Basierend auf Ihren Antworten umfassen Ihre Symptome einen kürzlichen Beginn und", whileManyBenign: "Obwohl es viele gutartige Ursachen für pulsierenden Tinnitus gibt, sollten diese Merkmale zeitnah von einem Arzt untersucht werden. Ein HNO-Arzt oder Neurologe kann bildgebende Untersuchungen durchführen, um die Ursache festzustellen.", noteNewPulsatile: "Neuer pulsierender Tinnitus ist es wert, bei Ihrem nächsten Besuch Ihrem Arzt gegenüber erwähnt zu werden, auch wenn Sie keine weiteren Symptome haben.", noteAssociatedSymptoms: "Sie haben begleitende Symptome erwähnt", recommendDiscussing: "Wir empfehlen, diese mit einem Arzt zu besprechen, um behandelbare Ursachen auszuschließen.", note: "Hinweis:", noSavedProfiles: "Noch keine gespeicherten Profile.", load: "Laden", loadAndCustomize: "Anpassen", deleteProfile: "Ausgewählte löschen", browserNotSupported: "Browser nicht unterstützt", browserNotSupportedMsg: "Ihr Browser unterstützt die für dieses Werkzeug erforderliche Web Audio API nicht. Bitte verwenden Sie einen modernen Browser wie Chrome, Firefox, Safari oder Edge.", headaches: "Kopfschmerzen", visionChanges: "Ändern", dizziness: "Schwindel", hearingLoss: "Hörverlust", facialNumbness: "Taubheit oder Schwäche im Gesicht", loadPreset: "Passwort zurücksetzen" }; // ================================================================== // PULSATILE TINNITUS & EAR CLICKING GENERATOR - AudioNotch // ================================================================== // --- PRESETS --- var PULSATILE_PRESETS = [ { id:'heartbeat-whoosh', name:'Heartbeat Whoosh', description:'Low-frequency whooshing synchronized with the pulse', params:{ baseFrequency:150, pulseRate:72, pulseShape:'soft-whoosh', pulseWidth:250, modulationDepth:85, toneCharacter:'filtered-noise', filterQ:5, volume:50, pan:0 }}, { id:'arterial-thump', name:'Arterial Thump', description:'Sharp thumping pulse felt deep in the ear', params:{ baseFrequency:100, pulseRate:72, pulseShape:'sharp-pulse', pulseWidth:150, modulationDepth:95, toneCharacter:'filtered-noise', filterQ:10, volume:50, pan:0 }}, { id:'venous-hum', name:'Venous Hum', description:'Continuous low hum with gentle pulsatile modulation', params:{ baseFrequency:120, pulseRate:72, pulseShape:'soft-whoosh', pulseWidth:300, modulationDepth:40, toneCharacter:'filtered-noise', filterQ:3, volume:50, pan:0 }}, { id:'rushing-waterfall', name:'Rushing / Waterfall', description:'Broadband rushing with rhythmic intensity changes', params:{ baseFrequency:300, pulseRate:75, pulseShape:'soft-whoosh', pulseWidth:280, modulationDepth:60, toneCharacter:'broadband', filterQ:5, volume:50, pan:0 }}, { id:'double-beat', name:'Double Beat', description:'Lub-dub heartbeat where both heart sounds are audible', params:{ baseFrequency:100, pulseRate:72, pulseShape:'double-pulse', pulseWidth:200, modulationDepth:90, toneCharacter:'filtered-noise', filterQ:8, volume:50, pan:0 }}, { id:'exercise-pulse', name:'Exercise Pulse', description:'Fast prominent pulsing during or after physical activity', params:{ baseFrequency:200, pulseRate:120, pulseShape:'sharp-pulse', pulseWidth:120, modulationDepth:80, toneCharacter:'filtered-noise', filterQ:6, volume:50, pan:0 }} ]; var CLICKING_PRESETS = [ { id:'rhythmic-clicking', name:'Rhythmic Clicking', description:'Regular metronome-like clicking from middle ear myoclonus', params:{ clickCharacter:'sharp-click', clickRate:3, clickMode:'regular', regularity:100, brightness:1000, pan:0, volume:50 }}, { id:'typewriter', name:'Typewriter Tinnitus', description:'Irregular clicking bursts resembling typewriter keys', params:{ clickCharacter:'sharp-click', clickRate:5, clickMode:'irregular', regularity:30, brightness:4000, pan:0, volume:50 }}, { id:'eustachian-pop', name:'Eustachian Pop', description:'Soft popping like swallowing or yawning', params:{ clickCharacter:'soft-pop', clickRate:1, clickMode:'irregular', regularity:50, brightness:200, pan:0, volume:50 }}, { id:'crackling-ear', name:'Crackling Ear', description:'Crackling or Rice Krispies sound', params:{ clickCharacter:'crackling', clickRate:2, clickMode:'irregular', regularity:20, brightness:1000, pan:0, volume:50 }}, { id:'fluttering', name:'Fluttering / Buzzing', description:'Rapid fluttering from high-rate muscle spasm', params:{ clickCharacter:'flutter', clickRate:20, clickMode:'regular', regularity:100, brightness:600, pan:0, volume:50 }}, { id:'palatal-myoclonus', name:'Palatal Myoclonus', description:'Regular clicking at 1-3 Hz from palatal muscle spasm', params:{ clickCharacter:'soft-pop', clickRate:2, clickMode:'regular', regularity:100, brightness:200, pan:0, volume:50 }} ]; // --- STATE --- var audioCtx = null; var pulseIsPlaying = false; var clickIsPlaying = false; var currentToolTab = 'pulsatile'; var screeningTarget = 'pulsatile'; // Pulsatile audio nodes var pulseSource = null; var pulseNoiseBuffer = null; var pulseGainNode = null; var pulseMasterGain = null; var pulsePanner = null; var pulseAnalyser = null; var pulseFilter = null; var pulseSchedulerId = null; var pulseNextTime = 0; // Clicking audio nodes var clickMasterGain = null; var clickPanner = null; var clickAnalyser = null; var clickSchedulerId = null; var clickNextTime = 0; // Tap tempo var tapTimes = []; var tapMaxTaps = 8; var tapTimeout = 3000; var tapMinInterval = 200; // Visualization var pulseAnimId = null; var clickAnimId = null; var pulseWaveAnimId = null; var clickWaveAnimId = null; // --- UTILITY --- function getAudioContext() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (audioCtx.state === 'suspended') { audioCtx.resume(); } return audioCtx; } function createNoiseBuffer(ctx) { if (pulseNoiseBuffer) return pulseNoiseBuffer; var sr = ctx.sampleRate; var buf = ctx.createBuffer(1, sr * 2, sr); var data = buf.getChannelData(0); for (var i = 0; i < data.length; i++) { data[i] = Math.random() * 2 - 1; } pulseNoiseBuffer = buf; return buf; } // Logarithmic frequency slider: map 0-1000 -> 50-2000 Hz logarithmically function sliderToFreq(val) { var minLog = Math.log(50); var maxLog = Math.log(2000); return Math.round(Math.exp(minLog + (val / 1000) * (maxLog - minLog))); } function freqToSlider(freq) { var minLog = Math.log(50); var maxLog = Math.log(2000); return Math.round(((Math.log(freq) - minLog) / (maxLog - minLog)) * 1000); } // --- SECTION/TAB MANAGEMENT --- function showSection(id) { var sections = document.querySelectorAll('.ptg-section'); for (var i = 0; i < sections.length; i++) { sections[i].classList.remove('active'); } var el = document.getElementById(id); if (el) el.classList.add('active'); } function switchToolTab(tab) { currentToolTab = tab; var tabs = document.querySelectorAll('.ptg-tab'); var contents = document.querySelectorAll('.ptg-tab-content'); for (var i = 0; i < tabs.length; i++) { tabs[i].classList.remove('active'); tabs[i].setAttribute('aria-selected','false'); } for (var i = 0; i < contents.length; i++) { contents[i].classList.remove('active'); } if (tab === 'pulsatile') { tabs[0].classList.add('active'); tabs[0].setAttribute('aria-selected','true'); document.getElementById('ptg-tab-pulsatile').classList.add('active'); } else if (tab === 'clicking') { tabs[1].classList.add('active'); tabs[1].setAttribute('aria-selected','true'); document.getElementById('ptg-tab-clicking').classList.add('active'); } else { tabs[2].classList.add('active'); tabs[2].setAttribute('aria-selected','true'); document.getElementById('ptg-tab-presets').classList.add('active'); loadPresetLibrary(); } } function toggleFAQ(el) { var item = el.parentElement; item.classList.toggle('open'); } // --- SCREENING --- function startScreening(target) { screeningTarget = target; showSection('ptg-screening'); } function skipScreening() { showSection('ptg-tool'); if (screeningTarget === 'clicking') switchToolTab('clicking'); else switchToolTab('pulsatile'); renderPresetRows(); } function submitScreening() { var sq1 = document.querySelector('input[name="sq1"]:checked'); var sq4boxes = document.querySelectorAll('input[name="sq4"]:checked'); var sq5 = document.querySelector('input[name="sq5"]:checked'); var onset = sq1 ? sq1.value : 'unknown'; var redFlags = []; for (var i = 0; i < sq4boxes.length; i++) { var v = sq4boxes[i].value; if (v !== 'none') redFlags.push(v); } var seenDoc = sq5 ? sq5.value : 'unknown'; var banner = document.getElementById('ptg-screening-warning-banner'); var flagNames = { headaches: T.headaches, vision: T.visionChanges, dizziness: T.dizziness, hearing_loss: T.hearingLoss, facial: T.facialNumbness }; if ((onset === 'days' || onset === 'weeks') && redFlags.length > 0) { var flagList = redFlags.map(function(f){ return flagNames[f] || f; }).join(', '); banner.innerHTML = '

' + T.weRecommendSeeingDoctor + ' ' + T.basedOnResponses + ' ' + flagList + '. ' + T.whileManyBenign + '

'; banner.classList.remove('ptg-hidden'); } else if (onset === 'days' || onset === 'weeks') { banner.innerHTML = '

' + T.note + ' ' + T.noteNewPulsatile + '

'; banner.classList.remove('ptg-hidden'); } else if (seenDoc === 'no' && redFlags.length > 0) { banner.innerHTML = '

' + T.note + ' ' + T.noteAssociatedSymptoms + ' (' + redFlags.map(function(f){ return flagNames[f] || f; }).join(', ') + '). ' + T.recommendDiscussing + '

'; banner.classList.remove('ptg-hidden'); } else { banner.classList.add('ptg-hidden'); } showSection('ptg-tool'); if (screeningTarget === 'clicking') switchToolTab('clicking'); else switchToolTab('pulsatile'); renderPresetRows(); } // Mutual exclusion: "None of the above" deselects symptoms and vice versa function onSymptomCheck(el) { var allBoxes = document.querySelectorAll('input[name="sq4"]'); if (el.value === 'none' && el.checked) { for (var i = 0; i < allBoxes.length; i++) { if (allBoxes[i].value !== 'none') allBoxes[i].checked = false; } } else if (el.value !== 'none' && el.checked) { for (var i = 0; i < allBoxes.length; i++) { if (allBoxes[i].value === 'none') allBoxes[i].checked = false; } } } // --- PULSATILE TINNITUS AUDIO ENGINE --- function getPulseParams() { var freqSlider = document.getElementById('ptg-freq'); return { baseFrequency: sliderToFreq(parseInt(freqSlider.value)), pulseRate: parseInt(document.getElementById('ptg-bpm').value), pulseShape: (document.querySelector('input[name="pulseShape"]:checked') || {}).value || 'soft-whoosh', pulseWidth: parseInt(document.getElementById('ptg-pw').value) / 1000, modulationDepth: parseInt(document.getElementById('ptg-mod').value) / 100, toneCharacter: (document.querySelector('input[name="toneChar"]:checked') || {}).value || 'filtered-noise', filterQ: parseInt(document.getElementById('ptg-filterq').value), volume: parseInt(document.getElementById('ptg-vol-pulse').value) / 100, pan: parseFloat((document.querySelector('input[name="pulsePan"]:checked') || {}).value || '0') }; } function buildPulseGraph() { var ctx = getAudioContext(); var p = getPulseParams(); // Cleanup old nodes destroyPulseGraph(); // Create nodes pulseGainNode = ctx.createGain(); pulseGainNode.gain.value = 0.0001; pulseMasterGain = ctx.createGain(); pulseMasterGain.gain.value = p.volume; pulsePanner = ctx.createStereoPanner(); pulsePanner.pan.value = p.pan; pulseAnalyser = ctx.createAnalyser(); pulseAnalyser.fftSize = 2048; // Source if (p.toneCharacter === 'sine') { pulseSource = ctx.createOscillator(); pulseSource.type = 'sine'; pulseSource.frequency.value = p.baseFrequency; pulseSource.connect(pulseGainNode); pulseSource.start(); } else if (p.toneCharacter === 'filtered-noise') { var noiseBuf = createNoiseBuffer(ctx); pulseSource = ctx.createBufferSource(); pulseSource.buffer = noiseBuf; pulseSource.loop = true; pulseFilter = ctx.createBiquadFilter(); pulseFilter.type = 'bandpass'; pulseFilter.frequency.value = p.baseFrequency; pulseFilter.Q.value = p.filterQ; pulseSource.connect(pulseFilter); pulseFilter.connect(pulseGainNode); pulseSource.start(); } else if (p.toneCharacter === 'broadband') { var noiseBuf = createNoiseBuffer(ctx); pulseSource = ctx.createBufferSource(); pulseSource.buffer = noiseBuf; pulseSource.loop = true; pulseSource.connect(pulseGainNode); pulseSource.start(); } else if (p.toneCharacter === 'click-impulse') { var noiseBuf = createNoiseBuffer(ctx); pulseSource = ctx.createBufferSource(); pulseSource.buffer = noiseBuf; pulseSource.loop = true; pulseFilter = ctx.createBiquadFilter(); pulseFilter.type = 'bandpass'; pulseFilter.frequency.value = p.baseFrequency; pulseFilter.Q.value = 15; pulseSource.connect(pulseFilter); pulseFilter.connect(pulseGainNode); pulseSource.start(); } // Chain: pulseGain -> masterGain -> panner -> analyser -> destination pulseGainNode.connect(pulseMasterGain); pulseMasterGain.connect(pulsePanner); pulsePanner.connect(pulseAnalyser); pulseAnalyser.connect(ctx.destination); } function destroyPulseGraph() { if (pulseSource) { try { pulseSource.stop(); } catch(e){} try { pulseSource.disconnect(); } catch(e){} pulseSource = null; } if (pulseFilter) { try { pulseFilter.disconnect(); } catch(e){} pulseFilter = null; } if (pulseGainNode) { try { pulseGainNode.disconnect(); } catch(e){} pulseGainNode = null; } if (pulseMasterGain) { try { pulseMasterGain.disconnect(); } catch(e){} pulseMasterGain = null; } if (pulsePanner) { try { pulsePanner.disconnect(); } catch(e){} pulsePanner = null; } if (pulseAnalyser) { try { pulseAnalyser.disconnect(); } catch(e){} pulseAnalyser = null; } } function schedulePulse(startTime, p) { if (!pulseGainNode) return; var gain = pulseGainNode.gain; var minGain = Math.max(1.0 - p.modulationDepth, 0.0001); var maxGain = 1.0; var dur = p.pulseWidth; switch (p.pulseShape) { case 'soft-whoosh': gain.setValueAtTime(minGain, startTime); gain.linearRampToValueAtTime(maxGain, startTime + dur * 0.35); gain.linearRampToValueAtTime(maxGain, startTime + dur * 0.55); gain.linearRampToValueAtTime(minGain, startTime + dur * 0.9); break; case 'sharp-pulse': gain.setValueAtTime(minGain, startTime); gain.linearRampToValueAtTime(maxGain, startTime + dur * 0.05); gain.exponentialRampToValueAtTime(Math.max(minGain, 0.001), startTime + dur * 0.9); break; case 'click': var clickDur = Math.min(dur, 0.05); gain.setValueAtTime(minGain, startTime); gain.linearRampToValueAtTime(maxGain, startTime + 0.002); gain.linearRampToValueAtTime(minGain, startTime + clickDur); break; case 'double-pulse': var firstDur = dur * 0.25; var gap = 0.2; var secondDur = dur * 0.2; gain.setValueAtTime(minGain, startTime); gain.linearRampToValueAtTime(maxGain, startTime + firstDur * 0.1); gain.exponentialRampToValueAtTime(Math.max(minGain, 0.001), startTime + firstDur); var secondStart = startTime + firstDur + gap; var secondMax = minGain + (maxGain - minGain) * 0.6; gain.setValueAtTime(Math.max(minGain, 0.001), secondStart); gain.linearRampToValueAtTime(secondMax, secondStart + secondDur * 0.1); gain.exponentialRampToValueAtTime(Math.max(minGain, 0.001), secondStart + secondDur); break; } } function startPulseScheduler() { var ctx = getAudioContext(); pulseNextTime = ctx.currentTime + 0.05; pulseSchedulerId = setInterval(function() { if (!pulseGainNode || !audioCtx) return; var p = getPulseParams(); var lookAhead = 0.1; var beatInterval = 60.0 / p.pulseRate; while (pulseNextTime < audioCtx.currentTime + lookAhead) { schedulePulse(pulseNextTime, p); pulseNextTime += beatInterval; } }, 25); } function stopPulseScheduler() { if (pulseSchedulerId) { clearInterval(pulseSchedulerId); pulseSchedulerId = null; } } function togglePulsatilePlay() { if (pulseIsPlaying) { stopPulseScheduler(); destroyPulseGraph(); pulseIsPlaying = false; document.getElementById('ptg-play-pulse-icon').className = 'fa fa-play'; document.getElementById('ptg-play-pulse').classList.remove('playing'); document.getElementById('ptg-play-status-pulse').textContent = T.clickToPlay; stopPulseVisualization(); } else { if (clickIsPlaying) { toggleClickingPlay(); } buildPulseGraph(); startPulseScheduler(); pulseIsPlaying = true; document.getElementById('ptg-play-pulse-icon').className = 'fa fa-pause'; document.getElementById('ptg-play-pulse').classList.add('playing'); document.getElementById('ptg-play-status-pulse').textContent = T.playing; startPulseVisualization(); } } // --- CLICKING AUDIO ENGINE --- function getClickParams() { return { clickCharacter: (document.querySelector('input[name="clickChar"]:checked') || {}).value || 'sharp-click', clickRate: parseInt(document.getElementById('ptg-clickrate').value), clickMode: (document.querySelector('input[name="clickMode"]:checked') || {}).value || 'regular', regularity: parseInt(document.getElementById('ptg-regularity').value) / 100, brightness: parseInt(document.getElementById('ptg-brightness').value), pan: parseFloat((document.querySelector('input[name="clickPan"]:checked') || {}).value || '0'), volume: parseInt(document.getElementById('ptg-vol-click').value) / 100 }; } function createClickBuffer(ctx, character) { var sr = ctx.sampleRate; var buf, data; switch (character) { case 'sharp-click': buf = ctx.createBuffer(1, Math.floor(sr * 0.005), sr); data = buf.getChannelData(0); data[0] = 1.0; for (var i = 1; i < data.length; i++) { data[i] = Math.exp(-i / (sr * 0.001)); } return buf; case 'soft-pop': buf = ctx.createBuffer(1, Math.floor(sr * 0.02), sr); data = buf.getChannelData(0); for (var i = 0; i < data.length; i++) { var t = i / sr; data[i] = Math.sin(2 * Math.PI * 200 * t) * Math.exp(-t / 0.005); } return buf; case 'crackling': buf = ctx.createBuffer(1, Math.floor(sr * 0.08), sr); data = buf.getChannelData(0); var numClicks = 3 + Math.floor(Math.random() * 5); for (var c = 0; c < numClicks; c++) { var pos = Math.floor(Math.random() * data.length); var amp = 0.3 + Math.random() * 0.7; data[pos] = amp; for (var i = 1; i < Math.min(Math.floor(sr * 0.003), data.length - pos); i++) { data[pos + i] += amp * Math.exp(-i / (sr * 0.0008)); } } return buf; case 'flutter': buf = ctx.createBuffer(1, Math.floor(sr * 0.1), sr); data = buf.getChannelData(0); var modFreq = 25; for (var i = 0; i < data.length; i++) { var t = i / sr; data[i] = (Math.random() * 2 - 1) * Math.exp(-t / 0.05) * (0.5 + 0.5 * Math.sin(2 * Math.PI * modFreq * t)); } return buf; default: buf = ctx.createBuffer(1, Math.floor(sr * 0.005), sr); data = buf.getChannelData(0); data[0] = 1.0; for (var i = 1; i < data.length; i++) { data[i] = Math.exp(-i / (sr * 0.001)); } return buf; } } function playOneClick(ctx, time, cp) { var buf = createClickBuffer(ctx, cp.clickCharacter); var source = ctx.createBufferSource(); source.buffer = buf; var filter = ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = cp.brightness; filter.Q.value = 1; source.connect(filter); filter.connect(clickMasterGain); source.start(time); } function buildClickGraph() { var ctx = getAudioContext(); var cp = getClickParams(); destroyClickGraph(); clickMasterGain = ctx.createGain(); clickMasterGain.gain.value = cp.volume; clickPanner = ctx.createStereoPanner(); clickPanner.pan.value = cp.pan; clickAnalyser = ctx.createAnalyser(); clickAnalyser.fftSize = 2048; clickMasterGain.connect(clickPanner); clickPanner.connect(clickAnalyser); clickAnalyser.connect(ctx.destination); } function destroyClickGraph() { if (clickMasterGain) { try { clickMasterGain.disconnect(); } catch(e){} clickMasterGain = null; } if (clickPanner) { try { clickPanner.disconnect(); } catch(e){} clickPanner = null; } if (clickAnalyser) { try { clickAnalyser.disconnect(); } catch(e){} clickAnalyser = null; } } function startClickScheduler() { var ctx = getAudioContext(); clickNextTime = ctx.currentTime + 0.05; clickSchedulerId = setInterval(function() { if (!clickMasterGain || !audioCtx) return; var cp = getClickParams(); var lookAhead = 0.15; var baseInterval = 1.0 / cp.clickRate; while (clickNextTime < audioCtx.currentTime + lookAhead) { playOneClick(audioCtx, clickNextTime, cp); if (cp.clickMode === 'regular') { clickNextTime += baseInterval; } else { var jitter = (1 - cp.regularity) * (-Math.log(Math.random() + 0.0001) * baseInterval); var regularPart = cp.regularity * baseInterval; clickNextTime += Math.max(regularPart + jitter, 0.03); } } }, 25); } function stopClickScheduler() { if (clickSchedulerId) { clearInterval(clickSchedulerId); clickSchedulerId = null; } } function toggleClickingPlay() { if (clickIsPlaying) { stopClickScheduler(); destroyClickGraph(); clickIsPlaying = false; document.getElementById('ptg-play-click-icon').className = 'fa fa-play'; document.getElementById('ptg-play-click').classList.remove('playing'); document.getElementById('ptg-play-status-click').textContent = T.clickToPlay; stopClickVisualization(); } else { if (pulseIsPlaying) { togglePulsatilePlay(); } buildClickGraph(); startClickScheduler(); clickIsPlaying = true; document.getElementById('ptg-play-click-icon').className = 'fa fa-pause'; document.getElementById('ptg-play-click').classList.add('playing'); document.getElementById('ptg-play-status-click').textContent = T.playing; startClickVisualization(); } } // --- TAP TEMPO --- function handleTap() { var now = performance.now(); if (tapTimes.length > 0 && (now - tapTimes[tapTimes.length - 1]) > tapTimeout) { tapTimes = []; } tapTimes.push(now); if (tapTimes.length > tapMaxTaps) tapTimes.shift(); var result = computeTapBPM(); if (result) { var confLabel = result.confidence > 0.8 ? T.confidenceGood : result.confidence > 0.5 ? T.confidenceFair : T.confidenceLow; document.getElementById('ptg-tap-result').innerHTML = T.detected + ' ' + result.bpm + ' ' + T.bpm + ' (' + T.confidence + ' ' + confLabel + ')'; var fill = document.getElementById('ptg-tap-confidence-fill'); fill.style.width = (result.confidence * 100) + '%'; fill.style.background = result.confidence > 0.8 ? 'var(--ptg-success)' : result.confidence > 0.5 ? 'var(--ptg-warning)' : 'var(--ptg-danger)'; var bpmSlider = document.getElementById('ptg-bpm'); var clampedBpm = Math.max(30, Math.min(200, result.bpm)); bpmSlider.value = clampedBpm; updatePulseSliderDisplay(); } else { document.getElementById('ptg-tap-result').textContent = T.keepTapping + ' (' + tapTimes.length + ' ' + T.taps + ')'; } } function computeTapBPM() { if (tapTimes.length < 2) return null; var intervals = []; for (var i = 1; i < tapTimes.length; i++) { var interval = tapTimes[i] - tapTimes[i - 1]; if (interval >= tapMinInterval) intervals.push(interval); } if (intervals.length === 0) return null; intervals.sort(function(a, b) { return a - b; }); var median = intervals[Math.floor(intervals.length / 2)]; var bpm = Math.round(60000 / median); var mean = intervals.reduce(function(a, b) { return a + b; }, 0) / intervals.length; var variance = intervals.reduce(function(sum, val) { return sum + Math.pow(val - mean, 2); }, 0) / intervals.length; var stdDev = Math.sqrt(variance); var confidence = 1 - Math.min(stdDev / mean, 1); return { bpm: bpm, confidence: confidence, tapCount: tapTimes.length }; } // --- VISUALIZATION --- function startPulseVisualization() { var circle = document.getElementById('ptg-pulse-circle'); function animatePulseCircle() { if (!pulseIsPlaying || !audioCtx) return; var p = getPulseParams(); var beatInterval = 60.0 / p.pulseRate; var elapsed = audioCtx.currentTime % beatInterval; var phase = elapsed / beatInterval; var intensity = 0; var dur = p.pulseWidth; var timeInBeat = phase * beatInterval; if (timeInBeat < dur) { var envPhase = timeInBeat / dur; switch (p.pulseShape) { case 'soft-whoosh': if (envPhase < 0.35) intensity = envPhase / 0.35; else if (envPhase < 0.55) intensity = 1; else if (envPhase < 0.9) intensity = 1 - ((envPhase - 0.55) / 0.35); else intensity = 0; break; case 'sharp-pulse': if (envPhase < 0.05) intensity = envPhase / 0.05; else intensity = Math.exp(-(envPhase - 0.05) * 5); break; case 'click': intensity = envPhase < 0.2 ? 1 : 0; break; case 'double-pulse': if (envPhase < 0.25) intensity = envPhase < 0.05 ? envPhase / 0.05 : Math.exp(-(envPhase - 0.05) * 8); else if (envPhase < 0.6) intensity = 0; else intensity = (envPhase < 0.65 ? (envPhase - 0.6) / 0.05 : Math.exp(-(envPhase - 0.65) * 8)) * 0.6; break; } } intensity *= p.modulationDepth; var scale = 0.85 + intensity * 0.3; var opacity = 0.3 + intensity * 0.7; circle.style.transform = 'scale(' + scale + ')'; circle.style.opacity = opacity; circle.style.borderColor = 'rgba(255,119,0,' + (0.3 + intensity * 0.7) + ')'; pulseAnimId = requestAnimationFrame(animatePulseCircle); } pulseAnimId = requestAnimationFrame(animatePulseCircle); var canvas = document.getElementById('ptg-waveform-pulse'); var canvasCtx = canvas.getContext('2d'); function drawPulseWaveform() { if (!pulseIsPlaying || !pulseAnalyser) return; canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; var bufLen = pulseAnalyser.frequencyBinCount; var dataArray = new Uint8Array(bufLen); pulseAnalyser.getByteTimeDomainData(dataArray); canvasCtx.fillStyle = '#12121f'; canvasCtx.fillRect(0, 0, canvas.width, canvas.height); canvasCtx.lineWidth = 2; canvasCtx.strokeStyle = '#FF7700'; canvasCtx.beginPath(); var sliceWidth = canvas.width / bufLen; var x = 0; for (var i = 0; i < bufLen; i++) { var v = dataArray[i] / 128.0; var y = v * canvas.height / 2; if (i === 0) canvasCtx.moveTo(x, y); else canvasCtx.lineTo(x, y); x += sliceWidth; } canvasCtx.lineTo(canvas.width, canvas.height / 2); canvasCtx.stroke(); pulseWaveAnimId = requestAnimationFrame(drawPulseWaveform); } pulseWaveAnimId = requestAnimationFrame(drawPulseWaveform); } function stopPulseVisualization() { if (pulseAnimId) { cancelAnimationFrame(pulseAnimId); pulseAnimId = null; } if (pulseWaveAnimId) { cancelAnimationFrame(pulseWaveAnimId); pulseWaveAnimId = null; } var circle = document.getElementById('ptg-pulse-circle'); circle.style.transform = 'scale(1)'; circle.style.opacity = '1'; circle.style.borderColor = 'var(--ptg-primary)'; var canvas = document.getElementById('ptg-waveform-pulse'); var canvasCtx = canvas.getContext('2d'); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; canvasCtx.fillStyle = '#12121f'; canvasCtx.fillRect(0, 0, canvas.width, canvas.height); } function startClickVisualization() { var canvas = document.getElementById('ptg-waveform-click'); var canvasCtx = canvas.getContext('2d'); function drawClickWaveform() { if (!clickIsPlaying || !clickAnalyser) return; canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; var bufLen = clickAnalyser.frequencyBinCount; var dataArray = new Uint8Array(bufLen); clickAnalyser.getByteTimeDomainData(dataArray); canvasCtx.fillStyle = '#12121f'; canvasCtx.fillRect(0, 0, canvas.width, canvas.height); canvasCtx.lineWidth = 2; canvasCtx.strokeStyle = '#FF7700'; canvasCtx.beginPath(); var sliceWidth = canvas.width / bufLen; var x = 0; for (var i = 0; i < bufLen; i++) { var v = dataArray[i] / 128.0; var y = v * canvas.height / 2; if (i === 0) canvasCtx.moveTo(x, y); else canvasCtx.lineTo(x, y); x += sliceWidth; } canvasCtx.lineTo(canvas.width, canvas.height / 2); canvasCtx.stroke(); clickWaveAnimId = requestAnimationFrame(drawClickWaveform); } clickWaveAnimId = requestAnimationFrame(drawClickWaveform); } function stopClickVisualization() { if (clickWaveAnimId) { cancelAnimationFrame(clickWaveAnimId); clickWaveAnimId = null; } var canvas = document.getElementById('ptg-waveform-click'); var canvasCtx = canvas.getContext('2d'); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; canvasCtx.fillStyle = '#12121f'; canvasCtx.fillRect(0, 0, canvas.width, canvas.height); } // --- SLIDER DISPLAY UPDATES --- function updatePulseSliderDisplay() { var freq = sliderToFreq(parseInt(document.getElementById('ptg-freq').value)); document.getElementById('ptg-freq-val').textContent = freq + ' Hz'; var bpm = parseInt(document.getElementById('ptg-bpm').value); document.getElementById('ptg-bpm-val').textContent = bpm + ' BPM'; var hint = bpm < 60 ? T.belowRestingHR : bpm <= 100 ? T.restingHRRange : T.elevatedHR; document.getElementById('ptg-bpm-hint').textContent = hint; document.getElementById('ptg-pw-val').textContent = document.getElementById('ptg-pw').value + ' ms'; document.getElementById('ptg-mod-val').textContent = document.getElementById('ptg-mod').value + '%'; document.getElementById('ptg-filterq-val').textContent = document.getElementById('ptg-filterq').value; document.getElementById('ptg-vol-pulse-val').textContent = document.getElementById('ptg-vol-pulse').value + '%'; var tc = (document.querySelector('input[name="toneChar"]:checked') || {}).value; document.getElementById('ptg-filterq-group').style.display = (tc === 'filtered-noise') ? 'block' : 'none'; if (pulseIsPlaying && pulseMasterGain) { var p = getPulseParams(); pulseMasterGain.gain.setTargetAtTime(p.volume, audioCtx.currentTime, 0.02); pulsePanner.pan.setTargetAtTime(p.pan, audioCtx.currentTime, 0.02); if (pulseFilter) { pulseFilter.frequency.setTargetAtTime(p.baseFrequency, audioCtx.currentTime, 0.02); pulseFilter.Q.setTargetAtTime(p.filterQ, audioCtx.currentTime, 0.02); } if (pulseSource && pulseSource.frequency) { pulseSource.frequency.setTargetAtTime(p.baseFrequency, audioCtx.currentTime, 0.02); } } } function updateClickSliderDisplay() { document.getElementById('ptg-clickrate-val').textContent = document.getElementById('ptg-clickrate').value + ' /sec'; document.getElementById('ptg-regularity-val').textContent = document.getElementById('ptg-regularity').value + '%'; document.getElementById('ptg-brightness-val').textContent = document.getElementById('ptg-brightness').value + ' Hz'; document.getElementById('ptg-vol-click-val').textContent = document.getElementById('ptg-vol-click').value + '%'; if (clickIsPlaying && clickMasterGain) { var cp = getClickParams(); clickMasterGain.gain.setTargetAtTime(cp.volume, audioCtx.currentTime, 0.02); clickPanner.pan.setTargetAtTime(cp.pan, audioCtx.currentTime, 0.02); } } function updateClickModeUI() { var mode = (document.querySelector('input[name="clickMode"]:checked') || {}).value; document.getElementById('ptg-regularity-group').style.display = (mode === 'irregular') ? 'block' : 'none'; } function onToneCharacterChange() { updatePulseSliderDisplay(); if (pulseIsPlaying) { stopPulseScheduler(); destroyPulseGraph(); buildPulseGraph(); startPulseScheduler(); } } // --- PRESETS --- function renderPresetRows() { var pulseRow = document.getElementById('ptg-pulsatile-presets'); var clickRow = document.getElementById('ptg-clicking-presets'); pulseRow.innerHTML = ''; clickRow.innerHTML = ''; PULSATILE_PRESETS.forEach(function(pr) { var card = document.createElement('div'); card.className = 'ptg-preset-card'; card.setAttribute('role', 'button'); card.setAttribute('tabindex', '0'); card.setAttribute('aria-label', T.loadPreset + ' ' + pr.name); card.innerHTML = '
' + pr.name + '
' + pr.description + '
'; card.onclick = function() { loadPulsatilePreset(pr); }; pulseRow.appendChild(card); }); CLICKING_PRESETS.forEach(function(pr) { var card = document.createElement('div'); card.className = 'ptg-preset-card'; card.setAttribute('role', 'button'); card.setAttribute('tabindex', '0'); card.setAttribute('aria-label', T.loadPreset + ' ' + pr.name); card.innerHTML = '
' + pr.name + '
' + pr.description + '
'; card.onclick = function() { loadClickingPreset(pr); }; clickRow.appendChild(card); }); } function loadPulsatilePreset(pr) { var p = pr.params; document.getElementById('ptg-freq').value = freqToSlider(p.baseFrequency); document.getElementById('ptg-bpm').value = p.pulseRate; document.getElementById('ptg-pw').value = p.pulseWidth; document.getElementById('ptg-mod').value = p.modulationDepth; document.getElementById('ptg-filterq').value = p.filterQ; document.getElementById('ptg-vol-pulse').value = p.volume; var shapeRadios = document.querySelectorAll('input[name="pulseShape"]'); for (var i = 0; i < shapeRadios.length; i++) { shapeRadios[i].checked = shapeRadios[i].value === p.pulseShape; } var toneRadios = document.querySelectorAll('input[name="toneChar"]'); for (var i = 0; i < toneRadios.length; i++) { toneRadios[i].checked = toneRadios[i].value === p.toneCharacter; } var panRadios = document.querySelectorAll('input[name="pulsePan"]'); for (var i = 0; i < panRadios.length; i++) { panRadios[i].checked = panRadios[i].value === String(p.pan); } updatePulseSliderDisplay(); if (pulseIsPlaying) { onToneCharacterChange(); } } function loadClickingPreset(pr) { var p = pr.params; document.getElementById('ptg-clickrate').value = p.clickRate; document.getElementById('ptg-regularity').value = p.regularity; document.getElementById('ptg-brightness').value = p.brightness; document.getElementById('ptg-vol-click').value = p.volume; var charRadios = document.querySelectorAll('input[name="clickChar"]'); for (var i = 0; i < charRadios.length; i++) { charRadios[i].checked = charRadios[i].value === p.clickCharacter; } var modeRadios = document.querySelectorAll('input[name="clickMode"]'); for (var i = 0; i < modeRadios.length; i++) { modeRadios[i].checked = modeRadios[i].value === p.clickMode; } var panRadios = document.querySelectorAll('input[name="clickPan"]'); for (var i = 0; i < panRadios.length; i++) { panRadios[i].checked = panRadios[i].value === String(p.pan); } updateClickModeUI(); updateClickSliderDisplay(); } function loadPresetLibrary() { var pulseGrid = document.getElementById('ptg-presets-pulsatile-grid'); var clickGrid = document.getElementById('ptg-presets-clicking-grid'); pulseGrid.innerHTML = ''; clickGrid.innerHTML = ''; PULSATILE_PRESETS.forEach(function(pr) { var col = document.createElement('div'); col.className = 'col-md-4 col-sm-6'; col.innerHTML = '

' + pr.name + '

' + pr.description + '

'; pulseGrid.appendChild(col); }); CLICKING_PRESETS.forEach(function(pr) { var col = document.createElement('div'); col.className = 'col-md-4 col-sm-6'; col.innerHTML = '

' + pr.name + '

' + pr.description + '

'; clickGrid.appendChild(col); }); loadSavedProfiles(); } // --- SAVE/LOAD PROFILES --- function openSaveModal() { document.getElementById('ptg-save-modal').classList.add('active'); document.getElementById('ptg-profile-name').focus(); } function closeSaveModal() { document.getElementById('ptg-save-modal').classList.remove('active'); document.getElementById('ptg-profile-name').value = ''; document.getElementById('ptg-profile-notes').value = ''; } function saveProfile() { var name = document.getElementById('ptg-profile-name').value.trim(); if (!name) { document.getElementById('ptg-profile-name').style.borderColor = 'var(--ptg-danger)'; return; } var notes = document.getElementById('ptg-profile-notes').value.trim(); var profile = { id: 'user-' + Date.now(), name: name, notes: notes, createdAt: new Date().toISOString(), toolTab: currentToolTab }; if (currentToolTab === 'pulsatile') { profile.params = getPulseParams(); profile.params.modulationDepth = profile.params.modulationDepth * 100; profile.params.pulseWidth = profile.params.pulseWidth * 1000; profile.params.volume = profile.params.volume * 100; } else { profile.params = getClickParams(); profile.params.regularity = profile.params.regularity * 100; profile.params.volume = profile.params.volume * 100; } var profiles = JSON.parse(localStorage.getItem('ptg-profiles') || '[]'); profiles.push(profile); localStorage.setItem('ptg-profiles', JSON.stringify(profiles)); closeSaveModal(); loadSavedProfiles(); } function loadSavedProfiles() { var container = document.getElementById('ptg-saved-profiles'); var profiles = JSON.parse(localStorage.getItem('ptg-profiles') || '[]'); if (profiles.length === 0) { container.innerHTML = '

' + T.noSavedProfiles + '

'; return; } container.innerHTML = ''; profiles.forEach(function(prof, idx) { var item = document.createElement('div'); item.className = 'ptg-profile-item'; item.innerHTML = '
' + prof.name + ' (' + prof.toolTab + ')
' + new Date(prof.createdAt).toLocaleDateString() + (prof.notes ? ' - ' + prof.notes : '') + '
'; container.appendChild(item); }); } function loadSavedProfile(idx) { var profiles = JSON.parse(localStorage.getItem('ptg-profiles') || '[]'); var prof = profiles[idx]; if (!prof) return; if (prof.toolTab === 'pulsatile') { switchToolTab('pulsatile'); loadPulsatilePreset(prof); } else { switchToolTab('clicking'); loadClickingPreset(prof); } } function deleteSavedProfile(idx) { var profiles = JSON.parse(localStorage.getItem('ptg-profiles') || '[]'); profiles.splice(idx, 1); localStorage.setItem('ptg-profiles', JSON.stringify(profiles)); loadSavedProfiles(); } function exportJSON() { var data; if (currentToolTab === 'pulsatile') { data = { type: 'pulsatile', params: getPulseParams(), exportedAt: new Date().toISOString(), tool: 'AudioNotch Pulsatile Tinnitus Generator' }; } else { data = { type: 'clicking', params: getClickParams(), exportedAt: new Date().toISOString(), tool: 'AudioNotch Pulsatile Tinnitus Generator' }; } var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'audionotch-sound-profile.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // --- EVENT LISTENERS --- function initEventListeners() { var pulseSliders = ['ptg-freq', 'ptg-bpm', 'ptg-pw', 'ptg-mod', 'ptg-filterq', 'ptg-vol-pulse']; pulseSliders.forEach(function(id) { var el = document.getElementById(id); if (el) el.addEventListener('input', updatePulseSliderDisplay); }); var toneRadios = document.querySelectorAll('input[name="toneChar"]'); for (var i = 0; i < toneRadios.length; i++) { toneRadios[i].addEventListener('change', onToneCharacterChange); } var panRadios = document.querySelectorAll('input[name="pulsePan"]'); for (var i = 0; i < panRadios.length; i++) { panRadios[i].addEventListener('change', updatePulseSliderDisplay); } var clickSliders = ['ptg-clickrate', 'ptg-regularity', 'ptg-brightness', 'ptg-vol-click']; clickSliders.forEach(function(id) { var el = document.getElementById(id); if (el) el.addEventListener('input', updateClickSliderDisplay); }); var clickPanRadios = document.querySelectorAll('input[name="clickPan"]'); for (var i = 0; i < clickPanRadios.length; i++) { clickPanRadios[i].addEventListener('change', updateClickSliderDisplay); } var cards = document.querySelectorAll('.ptg-card-hover'); for (var i = 0; i < cards.length; i++) { cards[i].addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); } }); } document.getElementById('ptg-save-modal').addEventListener('click', function(e) { if (e.target === this) closeSaveModal(); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeSaveModal(); }); } // --- CLEANUP ON PAGE LEAVE --- window.addEventListener('beforeunload', function() { if (pulseIsPlaying) { stopPulseScheduler(); destroyPulseGraph(); } if (clickIsPlaying) { stopClickScheduler(); destroyClickGraph(); } if (audioCtx) { try { audioCtx.close(); } catch(e){} } }); // --- INIT --- function init() { if (!window.AudioContext && !window.webkitAudioContext) { document.getElementById('ptg-intro').innerHTML = '

' + T.browserNotSupported + '

' + T.browserNotSupportedMsg + '

'; } updatePulseSliderDisplay(); updateClickSliderDisplay(); renderPresetRows(); initEventListeners(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }