Wat is oorsuizen?

Pulseert, suist, klikt of bonkt uw tinnitus? Dit gratis hulpmiddel helpt u het geluid dat u hoort te identificeren en af te stemmen, zodat u het aan uw arts kunt beschrijven.

Een opmerking over pulserende tinnitus en uw gezondheid: Pulserende tinnitus verschilt van het constante piepen dat de meeste mensen met tinnitus associëren. Omdat het vaak verband houdt met de bloedstroom, kan het soms wijzen op een onderliggende aandoening die een arts kan identificeren en behandelen. Dit is goed nieuws -- in tegenstelling tot de meeste vormen van tinnitus heeft pulserende tinnitus vaak een specifieke, behandelbare oorzaak. Wij raden iedereen met pulserende tinnitus aan om dit met hun arts te bespreken, vooral als het een nieuw symptoom is.

Pulserend geluid afstemmen

Mijn tinnitus pulseert, suist of bonkt -- vaak op het ritme van mijn hartslag.

Oorklikken afstemmen

Mijn oor klikt, knalt, kraakt of fladdert -- regelmatig of onregelmatig.

Geluidspresets bekijken

Luister naar voorbeeldgeluiden van veelvoorkomende presentaties van pulserende tinnitus en oorklikken.

Snelle symptomencontrole

Deze vragen helpen ons relevante begeleiding te bieden. Uw antwoorden worden niet opgeslagen of verzonden -- alle verwerking vindt plaats in uw browser.

1. Wanneer is dit geluid begonnen?

2. Is het geluid gesynchroniseerd met uw hartslag?

3. Zit het geluid in één oor of beide?

4. Heeft u een van deze bijkomende symptomen? (vink alle toepasselijke aan)

5. Heeft u een arts geraadpleegd over dit geluid?


Stad Veger
Klik om te vergroten
Zeer laag — Laag — Midden — Hoog
Bereik van de rusthartslag
Tik mee op het ritme van uw pols of tinnitus
Pulsvorm:
Mobiele applicatie ——— Volledige stilte tussen de pulsen
Toonkarakter:
Breed (ademend) ——— Smal (tonaal)
Oor:
Stad Veger
Klik om te vergroten
Klikkarakter:
Datum toegevoegd
Doffe bonzen ——— Scherpe, heldere klikken
Oor:

Wat is oorsuizen?

Oorklik-presets

Opgeslagen profielen

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

Deel dit met uw arts

Uw afgestemde geluidsprofiel kan uw arts helpen precies te begrijpen wat u hoort. U kunt uw instellingen exporteren en meenemen naar uw afspraak.

Wanneer u medische evaluatie moet zoeken:

  • Uw pulserende tinnitus is nieuw (begonnen in de afgelopen weken)
  • Het zit alleen in één oor
  • Het wordt luider of frequenter
  • U heeft ook hoofdpijn, veranderingen in het gezichtsvermogen of duizeligheid

Welke arts u moet raadplegen: Een KNO-arts (keel-, neus- en oorarts) is meestal de eerste specialist die pulserende tinnitus beoordeelt. Hij kan beeldvormend onderzoek aanvragen zoals een MRI/MRA, CT-angiografie of echografie van de halsslagaders.

Wat is oorsuizen?

Pulserende tinnitus is een ritmisch geluid dat in één of beide oren wordt waargenomen en dat doorgaans synchroniseert met de hartslag. In tegenstelling tot het constante piepen, zoemen of sissen van tonale tinnitus heeft pulserende tinnitus een duidelijk tijdspatroon. Patiënten beschrijven het gewoonlijk als een suizend, bonkend, pulserend of stromend geluid dat gelijk loopt met hun pols.

Het belangrijkste verschil is dat tonale tinnitus een fantoomgeluid is dat door het zenuwstelsel wordt gegenereerd, terwijl pulserende tinnitus meestal een fysieke, identificeerbare geluidsbron heeft -- meestal turbulente bloedstroom nabij het oor. Dit maakt pulserende tinnitus fundamenteel anders in zowel de oorzaken als de klinische betekenis. Ongeveer 10% van de mensen met tinnitus ervaart pulserende symptomen.

Wat is oorsuizen?

Omdat pulserende tinnitus vaak voortkomt uit de bloedstroom, zijn de oorzaken vaak vasculair:

  • Atherosclerose: Vernauwing van de halsslagader of de vertakkingen ervan nabij het oor, waardoor turbulente bloedstroom ontstaat.
  • Hoge bloeddruk: De verhoogde kracht van de bloedstroom kan het geluid van bloed dat door bloedvaten nabij het oor stroomt hoorbaar maken.
  • Arterioveneuze malformaties: Abnormale verbindingen tussen slagaders en aders nabij het oor die een turbulente stroming met hoge snelheid veroorzaken.
  • Idiopathische intracraniële hypertensie (IIH): Verhoogde liquordruk die veranderingen in de veneuze sinussen en turbulente stroming veroorzaakt.
  • Glomustumoren: Sterk doorbloed goedaardige tumoren van het middenoor of de bulbus jugularis.
  • Middenoor-myoclonus: Onvrijwillige ritmische contracties van de spieren in het middenoor.

Wat is oorsuizen?

Reguliere (tonale) tinnitus produceert een constant piepen, zoemen of sissen en wordt doorgaans veroorzaakt door gehoorverlies of neurale veranderingen. Pulserende tinnitus daarentegen produceert een ritmisch geluid en heeft vaker een identificeerbare, potentieel behandelbare fysieke oorzaak. De notch-geluidstherapie van AudioNotch is ontworpen voor tonale tinnitus. Bij pulserende tinnitus heeft het identificeren en behandelen van de onderliggende oorzaak door middel van medische evaluatie prioriteit.

Wat is AudioNotch?

Oorklikken verwijst naar herhaalde klik-, knal- of krakende geluiden die in het oor worden waargenomen. Deze geluiden worden doorgaans veroorzaakt door onvrijwillige contracties van de middenoorspieren, disfunctie van de buis van Eustachius of palatale myoclonus.

  • Middenoor-myoclonus: Ritmisch klikken door onvrijwillige contracties van de musculus tensor tympani of musculus stapedius.
  • Disfunctie van de buis van Eustachius: Klikken of knallen bij het slikken, geeuwen of bij kaakbewegingen.
  • Palatale myoclonus: Ritmisch klikken door spasmen van de gehemeltespieren, vaak ook hoorbaar voor anderen.
  • TMJ-aandoeningen: Klikken in verband met kaakbewegingen, geen echte tinnitus maar wordt vaak gemeld als oorklikken.

Als het oorklikken aanhoudt, verergert of gepaard gaat met gehoorverlies, wordt een bezoek aan een KNO-arts aanbevolen.

Veelgestelde vragen

Wat is oorsuizen?

Pulserende tinnitus verschilt van reguliere tinnitus omdat het vaak een identificeerbare fysieke oorzaak heeft. Hoewel veel oorzaken goedaardig zijn, kunnen sommige wijzen op aandoeningen die baat hebben bij behandeling. Wij raden iedereen met pulserende tinnitus aan om dit met hun arts te bespreken, vooral als het een nieuw symptoom is of gepaard gaat met hoofdpijn, veranderingen in het gezichtsvermogen of duizeligheid.

Kan pulserende tinnitus vanzelf verdwijnen?

Sommige gevallen lossen vanzelf op, met name die welke verband houden met tijdelijke aandoeningen zoals oorinfecties, stress of hoge bloeddruk. Omdat pulserende tinnitus echter soms op een onderliggende aandoening kan wijzen, is het het beste om het door een zorgprofessional te laten beoordelen.

Hoe klinkt het?

Het wordt gewoonlijk beschreven als een suizend, bonkend, stromend of pulserend geluid dat gelijke tred houdt met uw hartslag. Sommige mensen beschrijven het als het horen van hun hartslag in hun oor. Onze geluidsgenerator hierboven laat u deze geluiden nabootsen en afstemmen om uw symptomen aan een arts te beschrijven.

Waarom hoor ik klikken in mijn oor?

Oorklikken kan verschillende oorzaken hebben, waaronder middenoor-myoclonus, disfunctie van de buis van Eustachius, tensor tympani-syndroom of palatale myoclonus. Als het klikken aanhoudt of hinderlijk is, kan een KNO-arts helpen de oorzaak vast te stellen.

Raadpleeg een arts als u tinnitus heeft.

Ja. In tegenstelling tot constant piepende tinnitus heeft pulserende tinnitus vaak een specifieke, identificeerbare oorzaak die behandelbaar kan zijn. Dit is vooral belangrijk als het symptoom nieuw is, slechts in één oor zit, erger wordt, of gepaard gaat met hoofdpijn, veranderingen in het gezichtsvermogen of duizeligheid. Een KNO-arts is doorgaans de beste eerste arts om te raadplegen.

Disclaimer

Dit hulpmiddel wordt uitsluitend aangeboden voor educatieve doeleinden en het afstemmen van geluiden. Het is geen diagnostisch hulpmiddel en kan de oorzaak van uw tinnitus niet vaststellen.

Belangrijk: Pulserende tinnitus kan een symptoom zijn van onderliggende medische aandoeningen, waarvan sommige behandeling vereisen. In tegenstelling tot constant piepende tinnitus heeft pulserende tinnitus vaak een identificeerbare en behandelbare oorzaak. Wij raden iedereen die pulserende tinnitus ervaart sterk aan om een zorgprofessional te raadplegen, met name een keel-, neus- en oorarts (KNO-arts).

AudioNotch biedt geen medisch advies, diagnose of behandeling. De informatie en hulpmiddelen op deze pagina zijn geen vervanging voor professionele medische evaluatie.

Hoort u ook een constant piepen of zoemen?

De notch-geluidstherapie van AudioNotch is ontworpen om constante tonale tinnitus te behandelen. Als u zowel pulserende als tonale tinnitus ervaart, kunnen onze therapiehulpmiddelen helpen bij de tonale component.

Frequentievisualisatie

Geluidsprofiel opslaan

Ontdek meer gratis tinnitus-tools

Frequentievisualisatie

Vind de exacte frequentie van uw tinnitus met begeleide binaire zoekopdracht en octaafverwarringsdetectie.

Tinnitus Handicap Inventaris

Doe de klinisch gevalideerde THI-beoordeling van 25 vragen. Volg de ernst van uw tinnitus in de loop van de tijd.

Gratis online gehoortest

Krijg een volledig audiogram met gekalibreerde toonaudiometrie. Test alle standaardfrequenties van 250 Hz tot 8 kHz.


// ================================================================== // TRANSLATION OBJECT // ================================================================== var T = { clickToPlay: "Klik om te vergroten", playing: "Play Store", detected: "Verwijder selectie", bpm: "BPM", confidenceGood: "Goed", confidenceFair: "Redelijk", confidenceLow: "Laag", confidence: "Vertrouwelijke informatie.", keepTapping: "Blijf tikken...", taps: "tikken", tapAlongPrompt: "Tik mee op het ritme van uw pols of tinnitus", belowRestingHR: "Onder de rusthartslag", restingHRRange: "Bereik van de rusthartslag", elevatedHR: "Verhoogde / inspanningshartslag", weRecommendSeeingDoctor: "We raden aan dat u elke dag een uur naar AudioNotch luistert.", basedOnResponses: "Op basis van uw antwoorden omvatten uw symptomen een recent begin en", whileManyBenign: "Hoewel er veel goedaardige oorzaken zijn voor pulserende tinnitus, is het de moeite waard om deze kenmerken snel door een arts te laten beoordelen. Een KNO-arts of neuroloog kan beeldvormende onderzoeken uitvoeren om de oorzaak vast te stellen.", noteNewPulsatile: "Nieuwe pulserende tinnitus is het vermelden waard bij uw arts tijdens uw volgende bezoek, zelfs als u geen andere symptomen heeft.", noteAssociatedSymptoms: "U heeft bijkomende symptomen genoemd", recommendDiscussing: "Wij raden aan deze met een arts te bespreken om behandelbare oorzaken uit te sluiten.", note: "Opmerking:", noSavedProfiles: "Nog geen opgeslagen profielen.", load: "Laden", loadAndCustomize: "Aanpassen", deleteProfile: "Verwijder selectie", browserNotSupported: "Browser niet ondersteund", browserNotSupportedMsg: "Uw browser ondersteunt de Web Audio API die vereist is voor dit hulpmiddel niet. Gebruik een moderne browser zoals Chrome, Firefox, Safari of Edge.", headaches: "hoofdpijn", visionChanges: "Wijzigen", dizziness: "duizeligheid", hearingLoss: "gehoorverlies", facialNumbness: "gevoelloosheid of zwakte in het gezicht", loadPreset: "Wachtwoord herstellen" }; // ================================================================== // 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(); }