¿Qué es Tinnitus?

¿Su tinnitus pulsa, silba, chasquea o late? Esta herramienta gratuita le ayuda a identificar y comparar el sonido que escucha, para que pueda describirlo a su médico.

Una nota sobre el tinnitus pulsátil y su salud: El tinnitus pulsátil es diferente del zumbido constante que la mayoría de las personas asocian con el tinnitus. Debido a que a menudo está relacionado con el flujo sanguíneo, a veces puede indicar una condición subyacente que un médico puede identificar y tratar. Esta es una buena noticia: a diferencia de la mayoría de los tipos de tinnitus, el tinnitus pulsátil a menudo tiene una causa específica y tratable. Recomendamos que cualquier persona con tinnitus pulsátil lo consulte con su médico, especialmente si es un síntoma nuevo.

Comparar sonido pulsante

Mi tinnitus pulsa, silba o golpea, a menudo al ritmo de mi latido cardíaco.

Comparar chasquidos en el oído

Mi oído chasquea, estalla, cruje o aletea, de forma regular o irregular.

Explorar presets de sonido

Escuche ejemplos de sonidos de presentaciones comunes de tinnitus pulsátil y chasquidos en el oído.

Evaluación rápida de síntomas

Estas preguntas nos ayudan a proporcionar orientación relevante. Sus respuestas no se almacenan ni se transmiten; todo el procesamiento se realiza en su navegador.

1. ¿Cuándo comenzó este sonido?

2. ¿El sonido se sincroniza con su latido cardíaco?

3. ¿El sonido está en un oído o en ambos?

4. ¿Tiene alguno de estos síntomas asociados? (marque todos los que correspondan)

5. ¿Ha consultado a un médico sobre este sonido?


Barrenderos de la Ciudad
Haga click para agrandar
Muy bajo — Bajo — Medio — Alto
Rango de frecuencia cardíaca en reposo
Toque al ritmo de su pulso o de su tinnitus
Forma de pulso:
Aplicación Móvil ——— Silencio completo entre pulsos
Carácter del tono:
Amplio (aspirado) ——— Estrecho (tonal)
Oído:
Barrenderos de la Ciudad
Haga click para agrandar
Carácter del clic:
Fecha añadida
Golpes sordos ——— Clics agudos y brillantes
Oído:

¿Qué es Tinnitus?

Presets de chasquidos en el oído

Perfiles guardados

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

Comparta esto con su médico

Su perfil de sonido puede ayudar a su médico a comprender exactamente lo que está escuchando. Puede exportar su configuración y llevarla a su cita.

Cuándo buscar evaluación médica:

  • Su tinnitus pulsátil es nuevo (comenzó en las últimas semanas)
  • Solo está en un oído
  • Se está volviendo más fuerte o más frecuente
  • También tiene dolores de cabeza, cambios en la visión o mareos

Qué tipo de médico consultar: Un otorrinolaringólogo (ORL) suele ser el primer especialista en evaluar el tinnitus pulsátil. Puede solicitar estudios de imagen como resonancia magnética/angiografía por resonancia magnética, angiografía por tomografía computarizada o ecografía de las arterias del cuello.

¿Qué es Tinnitus?

El tinnitus pulsátil es un sonido rítmico percibido en uno o ambos oídos que típicamente se sincroniza con el latido cardíaco. A diferencia del zumbido, pitido o silbido constante del tinnitus tonal, el tinnitus pulsátil tiene un patrón temporal distintivo. Los pacientes lo describen comúnmente como un sonido silbante, golpeante, pulsante o de flujo que sigue el ritmo de su pulso.

La diferencia clave es que el tinnitus tonal es un sonido fantasma generado por el sistema nervioso, mientras que el tinnitus pulsátil generalmente tiene una fuente de sonido física e identificable, más a menudo flujo sanguíneo turbulento cerca del oído. Esto hace que el tinnitus pulsátil sea fundamentalmente diferente tanto en sus causas como en su importancia clínica. Aproximadamente el 10% de las personas con tinnitus experimentan síntomas pulsátiles.

¿Qué es Tinnitus?

Debido a que el tinnitus pulsátil a menudo se origina por el flujo sanguíneo, sus causas son frecuentemente vasculares:

  • Aterosclerosis: Estrechamiento de la arteria carótida o sus ramas cerca del oído, creando un flujo sanguíneo turbulento.
  • Presión arterial alta: El aumento de la fuerza del flujo sanguíneo puede hacer audible el sonido de la sangre que se mueve a través de los vasos cerca del oído.
  • Malformaciones arteriovenosas: Conexiones anormales entre arterias y venas cerca del oído que crean un flujo turbulento de alta velocidad.
  • Hipertensión intracraneal idiopática (HII): Presión elevada del líquido cefalorraquídeo que causa cambios en los senos venosos y flujo turbulento.
  • Tumores glómicos: Tumores benignos altamente vascularizados del oído medio o del bulbo yugular.
  • Mioclonía del oído medio: Contracciones rítmicas involuntarias de los músculos del oído medio.

¿Qué es Tinnitus?

El tinnitus regular (tonal) produce un zumbido, pitido o silbido constante y es típicamente causado por pérdida auditiva o cambios neurales. En contraste, el tinnitus pulsátil produce un sonido rítmico y es más probable que tenga una causa física identificable y potencialmente tratable. La terapia de sonido con filtro de muesca de AudioNotch está diseñada para el tinnitus tonal. Para el tinnitus pulsátil, la prioridad es identificar y tratar la causa subyacente mediante evaluación médica.

¿Qué es AudioNotch?

Los chasquidos en el oído se refieren a sonidos repetitivos de clic, estallido o crepitación percibidos en el oído. Estos sonidos son típicamente causados por contracciones involuntarias de los músculos del oído medio, disfunción de la trompa de Eustaquio o mioclonía palatina.

  • Mioclonía del oído medio: Chasquidos rítmicos por contracciones involuntarias de los músculos tensor del tímpano o estapedio.
  • Disfunción de la trompa de Eustaquio: Chasquidos o estallidos al tragar, bostezar o con el movimiento de la mandíbula.
  • Mioclonía palatina: Chasquidos rítmicos por espasmo de los músculos del paladar, a menudo audibles para otras personas.
  • Trastornos de la ATM: Chasquidos asociados con el movimiento de la mandíbula, no es tinnitus verdadero pero se reporta frecuentemente como chasquidos en el oído.

Si los chasquidos en el oído son persistentes, empeoran o van acompañados de pérdida auditiva, se recomienda una visita a un especialista en otorrinolaringología.

Preguntas frecuentes

¿Qué es Tinnitus?

El tinnitus pulsátil es diferente del tinnitus regular porque a menudo tiene una causa física identificable. Si bien muchas causas son benignas, algunas pueden indicar condiciones que se benefician del tratamiento. Recomendamos que cualquier persona con tinnitus pulsátil lo consulte con su médico, especialmente si es un síntoma nuevo o está acompañado de dolores de cabeza, cambios en la visión o mareos.

¿Puede el tinnitus pulsátil desaparecer por sí solo?

Algunos casos se resuelven por sí solos, particularmente aquellos relacionados con condiciones temporales como infecciones de oído, estrés o presión arterial alta. Sin embargo, debido a que el tinnitus pulsátil a veces puede indicar una condición subyacente, es mejor que sea evaluado por un profesional de la salud.

¿Como suena?

Comúnmente se describe como un sonido silbante, golpeante, de flujo o pulsante que sigue el ritmo de su latido cardíaco. Algunas personas lo describen como escuchar su latido cardíaco en el oído. Nuestro generador de sonido anterior le permite recrear y comparar estos sonidos para ayudar a describir sus síntomas a un médico.

¿Por qué escucho chasquidos en mi oído?

Los chasquidos en el oído pueden tener varias causas, incluyendo mioclonía del oído medio, disfunción de la trompa de Eustaquio, síndrome del tensor del tímpano o mioclonía palatina. Si los chasquidos son persistentes o molestos, un especialista en otorrinolaringología puede ayudar a determinar la causa.

Consulte a un médico si tiene tinnitus.

Sí. A diferencia del tinnitus de zumbido constante, el tinnitus pulsátil a menudo tiene una causa específica e identificable que puede ser tratable. Esto es especialmente importante si el síntoma es nuevo, solo en un oído, empeora o está acompañado de dolores de cabeza, cambios en la visión o mareos. Un especialista en otorrinolaringología suele ser el mejor primer médico al que acudir.

Descargo de Responsabilidad

Esta herramienta se proporciona únicamente con fines educativos y de comparación de sonidos. No es una herramienta de diagnóstico y no puede identificar la causa de su tinnitus.

Importante: El tinnitus pulsátil puede ser un síntoma de condiciones médicas subyacentes, algunas de las cuales requieren tratamiento. A diferencia del tinnitus de zumbido constante, el tinnitus pulsátil a menudo tiene una causa identificable y tratable. Recomendamos encarecidamente que cualquier persona que experimente tinnitus pulsátil consulte con un profesional de la salud, particularmente un especialista en otorrinolaringología (ORL).

AudioNotch no proporciona asesoramiento médico, diagnóstico ni tratamiento. La información y las herramientas de esta página no sustituyen la evaluación médica profesional.

¿También escucha un zumbido o pitido constante?

La terapia de sonido con filtro de muesca de AudioNotch está diseñada para tratar el tinnitus tonal constante. Si experimenta tanto tinnitus pulsátil como tonal, nuestras herramientas de terapia pueden ayudar con el componente tonal.

Frecuencia (Hz)

Guardar perfil de sonido

Explore mas herramientas gratuitas para el tinnitus

Frecuencia (Hz)

Encuentre la frecuencia exacta de su tinnitus con busqueda binaria guiada y deteccion de confusion de octavas.

Inventario de Discapacidad por Tinnitus

Realice la evaluacion THI de 25 preguntas clinicamente validada. Haga seguimiento de la gravedad de su tinnitus a lo largo del tiempo.

Prueba de audicion en linea gratuita

Obtenga un audiograma completo con audiometria tonal calibrada. Pruebe todas las frecuencias estandar de 250 Hz a 8 kHz.


// ================================================================== // TRANSLATION OBJECT // ================================================================== var T = { clickToPlay: "Haga click para agrandar", playing: "Play Store", detected: "Eliminar Selección", bpm: "BPM", confidenceGood: "Bueno", confidenceFair: "Aceptable", confidenceLow: "Bajo", confidence: "Información Confidencial", keepTapping: "Siga tocando...", taps: "toques", tapAlongPrompt: "Toque al ritmo de su pulso o de su tinnitus", belowRestingHR: "Por debajo de la frecuencia cardíaca en reposo", restingHRRange: "Rango de frecuencia cardíaca en reposo", elevatedHR: "Frecuencia cardíaca elevada / de ejercicio", weRecommendSeeingDoctor: "Recomendamos escuchar AudioNotch una hora al día.", basedOnResponses: "Según sus respuestas, sus síntomas incluyen aparición reciente y", whileManyBenign: "Si bien existen muchas causas benignas del tinnitus pulsátil, estas características merecen ser evaluadas por un médico de manera oportuna. Un especialista en otorrinolaringología o neurólogo puede realizar estudios de imagen para determinar la causa.", noteNewPulsatile: "El tinnitus pulsátil nuevo merece ser mencionado a su médico en su próxima visita, incluso si no tiene otros síntomas.", noteAssociatedSymptoms: "Usted mencionó síntomas asociados", recommendDiscussing: "Recomendamos discutir estos síntomas con un médico para descartar causas tratables.", note: "Nota:", noSavedProfiles: "Aún no hay perfiles guardados.", load: "Cargar", loadAndCustomize: "Personalizar", deleteProfile: "Eliminar Selección", browserNotSupported: "Navegador no compatible", browserNotSupportedMsg: "Su navegador no es compatible con la Web Audio API requerida para esta herramienta. Por favor, utilice un navegador moderno como Chrome, Firefox, Safari o Edge.", headaches: "dolores de cabeza", visionChanges: "Cambiar", dizziness: "mareos", hearingLoss: "pérdida auditiva", facialNumbness: "entumecimiento o debilidad facial", loadPreset: "Restablecimiento de contraseña" }; // ================================================================== // 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(); }