Qu’est-ce qu’un acouphène ?

Votre acouphène pulse-t-il, souffle-t-il, clique-t-il ou bat-il ? Cet outil gratuit vous aide à identifier et à faire correspondre le son que vous entendez, afin de pouvoir le décrire à votre médecin.

Une note concernant l'acouphène pulsatile et votre santé : L'acouphène pulsatile est différent du bourdonnement constant que la plupart des gens associent aux acouphènes. Parce qu'il implique souvent le flux sanguin, il peut parfois indiquer une condition sous-jacente qu'un médecin peut identifier et traiter. C'est une bonne nouvelle : contrairement à la plupart des acouphènes, l'acouphène pulsatile a souvent une cause spécifique et traitable. Nous recommandons à toute personne souffrant d'acouphène pulsatile d'en discuter avec son médecin, surtout s'il s'agit d'un symptôme nouveau.

Faire correspondre le son pulsatile

Mon acouphène pulse, souffle ou bat -- souvent au rythme de mon battement cardiaque.

Faire correspondre les cliquetis d'oreille

Mon oreille clique, craque, crépite ou vibre -- de manière régulière ou irrégulière.

Parcourir les préréglages de sons

Écoutez des exemples de sons de présentations courantes d'acouphène pulsatile et de cliquetis d'oreille.

Vérification rapide des symptômes

Ces questions nous aident à fournir des conseils pertinents. Vos réponses ne sont ni stockées ni transmises -- tout le traitement se fait dans votre navigateur.

1. Quand ce son a-t-il commencé ?

2. Le son est-il synchronisé avec votre battement cardiaque ?

3. Le son est-il dans une oreille ou les deux ?

4. Avez-vous l'un de ces symptômes associés ? (cochez tous ceux qui s'appliquent)

5. Avez-vous consulté un médecin au sujet de ce son ?


Balayeur de ville
Cliquez pour agrandir
Très bas — Bas — Moyen — Aigu
Plage de fréquence cardiaque au repos
Tapez au rythme de votre pouls ou de votre acouphène
Forme de pulsation :
Application Mobile ——— Silence complet entre les pulsations
Caractère du ton :
Large (soufflé) ——— Étroit (tonal)
Oreille :
Balayeur de ville
Cliquez pour agrandir
Caractère du clic :
Date d'ajout
Coups sourds ——— Clics nets et brillants
Oreille :

Qu’est-ce qu’un acouphène ?

Préréglages de cliquetis d'oreille

Profils enregistrés

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

Partagez ceci avec votre médecin

Votre profil sonore peut aider votre médecin à comprendre exactement ce que vous entendez. Vous pouvez exporter vos paramètres et les apporter à votre rendez-vous.

Quand consulter un médecin :

  • Votre acouphène pulsatile est récent (apparu au cours des dernières semaines)
  • Il n'est que dans une oreille
  • Il devient plus fort ou plus fréquent
  • Vous avez également des maux de tête, des changements de vision ou des vertiges

Quel médecin consulter : Un ORL (oto-rhino-laryngologiste) est généralement le premier spécialiste à évaluer l'acouphène pulsatile. Il peut prescrire des examens d'imagerie tels qu'une IRM/ARM, une angiographie par tomodensitométrie ou une échographie des artères du cou.

Qu’est-ce qu’un acouphène ?

L'acouphène pulsatile est un son rythmique perçu dans une ou les deux oreilles qui se synchronise généralement avec le battement cardiaque. Contrairement au bourdonnement, sifflement ou grésillement constant de l'acouphène tonal, l'acouphène pulsatile a un schéma temporel distinct. Les patients le décrivent généralement comme un son soufflant, battant, pulsant ou de flux qui suit le rythme de leur pouls.

La différence essentielle est que l'acouphène tonal est un son fantôme généré par le système nerveux, tandis que l'acouphène pulsatile a généralement une source sonore physique et identifiable -- le plus souvent un flux sanguin turbulent près de l'oreille. Cela rend l'acouphène pulsatile fondamentalement différent tant dans ses causes que dans sa signification clinique. Environ 10% des personnes souffrant d'acouphènes présentent des symptômes pulsatiles.

Qu’est-ce qu’un acouphène ?

Étant donné que l'acouphène pulsatile provient souvent du flux sanguin, ses causes sont fréquemment vasculaires :

  • Athérosclérose : Rétrécissement de l'artère carotide ou de ses branches près de l'oreille, créant un flux sanguin turbulent.
  • Hypertension artérielle : L'augmentation de la force du flux sanguin peut rendre audible le son du sang circulant dans les vaisseaux près de l'oreille.
  • Malformations artério-veineuses : Connexions anormales entre les artères et les veines près de l'oreille, créant un flux turbulent à haute vitesse.
  • Hypertension intracrânienne idiopathique (HII) : Pression élevée du liquide céphalo-rachidien provoquant des modifications des sinus veineux et un flux turbulent.
  • Tumeurs glomiques : Tumeurs bénignes hautement vascularisées de l'oreille moyenne ou du bulbe jugulaire.
  • Myoclonie de l'oreille moyenne : Contractions rythmiques involontaires des muscles de l'oreille moyenne.

Qu’est-ce qu’un acouphène ?

L'acouphène régulier (tonal) produit un bourdonnement, sifflement ou grésillement constant et est généralement causé par une perte auditive ou des changements neuraux. En revanche, l'acouphène pulsatile produit un son rythmique et est plus susceptible d'avoir une cause physique identifiable et potentiellement traitable. La thérapie sonore à filtre encoche d'AudioNotch est conçue pour l'acouphène tonal. Pour l'acouphène pulsatile, la priorité est d'identifier et de traiter la cause sous-jacente par une évaluation médicale.

Ce qui est AudioNotch ?

Le cliquetis de l'oreille désigne des sons répétitifs de clics, de craquements ou de crépitements perçus dans l'oreille. Ces sons sont généralement causés par des contractions involontaires des muscles de l'oreille moyenne, un dysfonctionnement de la trompe d'Eustache ou un myoclonus palatin.

  • Myoclonie de l'oreille moyenne : Cliquetis rythmique dû aux contractions involontaires des muscles tenseur du tympan ou stapédien.
  • Dysfonctionnement de la trompe d'Eustache : Clics ou craquements lors de la déglutition, du bâillement ou des mouvements de la mâchoire.
  • Myoclonus palatin : Cliquetis rythmique dû à un spasme des muscles du palais, souvent audible par l'entourage.
  • Troubles de l'ATM : Cliquetis associés aux mouvements de la mâchoire, ce n'est pas un véritable acouphène mais souvent signalé comme un cliquetis de l'oreille.

Si le cliquetis de l'oreille est persistant, s'aggrave ou s'accompagne d'une perte auditive, une consultation chez un spécialiste ORL est recommandée.

Questions frequemment posees

Qu’est-ce qu’un acouphène ?

L'acouphène pulsatile est différent de l'acouphène régulier car il a souvent une cause physique identifiable. Bien que de nombreuses causes soient bénignes, certaines peuvent indiquer des conditions qui bénéficient d'un traitement. Nous recommandons à toute personne souffrant d'acouphène pulsatile d'en discuter avec son médecin, surtout s'il s'agit d'un symptôme nouveau ou accompagné de maux de tête, de changements de vision ou de vertiges.

L'acouphène pulsatile peut-il disparaître de lui-même ?

Certains cas se résolvent d'eux-mêmes, en particulier ceux liés à des conditions temporaires comme les infections de l'oreille, le stress ou l'hypertension artérielle. Cependant, comme l'acouphène pulsatile peut parfois indiquer une condition sous-jacente, il est préférable de le faire évaluer par un professionnel de la santé.

A quoi cela ressemble-t-il ?

Il est généralement décrit comme un son soufflant, battant, de flux ou pulsant qui suit le rythme de votre battement cardiaque. Certaines personnes le décrivent comme le fait d'entendre leur battement cardiaque dans l'oreille. Notre générateur de sons ci-dessus vous permet de recréer et de faire correspondre ces sons pour aider à décrire vos symptômes à un médecin.

Pourquoi j'entends des cliquetis dans mon oreille ?

Le cliquetis de l'oreille peut avoir plusieurs causes, notamment la myoclonie de l'oreille moyenne, le dysfonctionnement de la trompe d'Eustache, le syndrome du tenseur du tympan ou le myoclonus palatin. Si le cliquetis est persistant ou gênant, un spécialiste ORL peut aider à en déterminer la cause.

Consultez un médecin si vous souffrez d'acouphènes.

Oui. Contrairement à l'acouphène à bourdonnement constant, l'acouphène pulsatile a souvent une cause spécifique et identifiable qui peut être traitable. C'est particulièrement important si le symptôme est nouveau, dans une seule oreille, s'aggrave ou s'accompagne de maux de tête, de changements de vision ou de vertiges. Un spécialiste ORL est généralement le meilleur premier médecin à consulter.

Clause de non-responsabilité

Cet outil est fourni à des fins éducatives et de correspondance sonore uniquement. Ce n'est pas un outil de diagnostic et il ne peut pas identifier la cause de votre acouphène.

Important : L'acouphène pulsatile peut être le symptôme de conditions médicales sous-jacentes, dont certaines nécessitent un traitement. Contrairement à l'acouphène à bourdonnement constant, l'acouphène pulsatile a souvent une cause identifiable et traitable. Nous recommandons fortement à toute personne souffrant d'acouphène pulsatile de consulter un professionnel de la santé, en particulier un spécialiste en oto-rhino-laryngologie (ORL).

AudioNotch ne fournit pas de conseils médicaux, de diagnostic ou de traitement. Les informations et les outils de cette page ne remplacent pas une évaluation médicale professionnelle.

Entendez-vous également un bourdonnement ou sifflement constant ?

La thérapie sonore à filtre encoche d'AudioNotch est conçue pour traiter l'acouphène tonal constant. Si vous souffrez à la fois d'acouphène pulsatile et tonal, nos outils thérapeutiques peuvent aider avec la composante tonale.

Visualiseur de fréquence

Enregistrer le profil sonore

Decouvrez d'autres outils gratuits pour les acouphenes

Visualiseur de fréquence

Trouvez la frequence exacte de vos acouphenes grace a une recherche binaire guidee et a la detection de confusion d'octaves.

Inventaire de Handicap lie aux Acouphenes

Passez l'evaluation THI de 25 questions cliniquement validee. Suivez la severite de vos acouphenes au fil du temps.

Test auditif en ligne gratuit

Obtenez un audiogramme complet avec une audiometrie tonale calibree. Testez toutes les frequences standard de 250 Hz a 8 kHz.


// ================================================================== // TRANSLATION OBJECT // ================================================================== var T = { clickToPlay: "Cliquez pour agrandir", playing: "Play Store", detected: "Supprimer la sélection", bpm: "BPM", confidenceGood: "Bon", confidenceFair: "Passable", confidenceLow: "Bas", confidence: "Informations confidentielles.", keepTapping: "Continuez à taper...", taps: "tapotements", tapAlongPrompt: "Tapez au rythme de votre pouls ou de votre acouphène", belowRestingHR: "En dessous de la fréquence cardiaque au repos", restingHRRange: "Plage de fréquence cardiaque au repos", elevatedHR: "Fréquence cardiaque élevée / d'exercice", weRecommendSeeingDoctor: "Nous vous recommandons d'écouter AudioNotch une heure par jour. ", basedOnResponses: "D'après vos réponses, vos symptômes incluent une apparition récente et", whileManyBenign: "Bien qu'il existe de nombreuses causes bénignes d'acouphène pulsatile, ces caractéristiques méritent d'être évaluées rapidement par un médecin. Un spécialiste ORL ou un neurologue peut effectuer des examens d'imagerie pour déterminer la cause.", noteNewPulsatile: "Un nouvel acouphène pulsatile mérite d'être mentionné à votre médecin lors de votre prochaine visite, même si vous n'avez pas d'autres symptômes.", noteAssociatedSymptoms: "Vous avez mentionné des symptômes associés", recommendDiscussing: "Nous recommandons d'en discuter avec un médecin pour écarter les causes traitables.", note: "Remarque :", noSavedProfiles: "Aucun profil enregistré pour le moment.", load: "Charger", loadAndCustomize: "Personnaliser", deleteProfile: "Supprimer la sélection", browserNotSupported: "Navigateur non pris en charge", browserNotSupportedMsg: "Votre navigateur ne prend pas en charge l'API Web Audio requise pour cet outil. Veuillez utiliser un navigateur moderne comme Chrome, Firefox, Safari ou Edge.", headaches: "maux de tête", visionChanges: "Changer", dizziness: "vertiges", hearingLoss: "perte auditive", facialNumbness: "engourdissement ou faiblesse du visage", loadPreset: "Réinitialisation du mot de passe" }; // ================================================================== // 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(); }