import { createDevice, type Device } from '@rnbo/js' import { Howl } from 'howler' import { ensureAudio } from '~/stores/audio' import { ANC } from '~/stores/interfaces/ANC' import { HeadsetType } from '~/stores/interfaces/HeadsetType' import { useUserStore } from '~/stores/user' function waitForCanPlay(audio: HTMLMediaElement): Promise { return new Promise((resolve) => { if (audio.readyState >= 3) { resolve() } else { const onCanPlay = () => { audio.removeEventListener('canplay', onCanPlay) resolve() } audio.addEventListener('canplay', onCanPlay) } }) } async function onFallback(reason: string, ctx?: AudioContext, src?:string): Promise { useNuxtApp().$logger.warn(`[Audio-Fallback] ⚠️ ${reason}`) if (reason.includes('captureStream nicht verfügbar')) { // → Safari oder alter Browser useNuxtApp().$logger.info('Nutze Fallback via decodeAudioData()...') // Trigger eigene Fallback-Logik, z. B.: if(ctx && src) return await createAudioSource(ctx, src) } else if (reason.includes('kein AudioTrack')) { useNuxtApp().$logger.warn('Audio-Stream hat keinen Track – evtl. nicht geladen?') // Optionale Retry-Logik hier? } else if (reason.includes('Audioelement hat einen Fehler')) { alert('Audio konnte nicht geladen werden. Bitte prüfen Sie die Quelle oder Ihre Netzwerkverbindung.') } else if (reason.includes('MediaStreamSource')) { useNuxtApp().$logger.error('Web Audio konnte keine Verbindung zum Stream herstellen.') if(ctx && src) return await createAudioSource(ctx, src) } else { useNuxtApp().$logger.error(`[Audio-Fallback] Unbekannter Fehler: ${reason}`) } return null } async function setupAudioSource( audioElement: HTMLMediaElement, audioContext: AudioContext): Promise { await ensureAudio() useNuxtApp().$logger.log("HTML5 Element vor dem umwandeln = ", {audioElement}) try { // 1. Quelle vorhanden? if (!audioElement.src && !audioElement.srcObject) { onFallback('Audioelement hat keine gültige Quelle') return null } // 2. Fehler prüfen if (audioElement.error) { onFallback('Audioelement hat einen Fehler (z. B. 404, CORS, etc.)', audioContext, audioElement.src ) return null } // 3. Versuche zu laden try { await waitForCanPlay(audioElement) audioElement.volume = 0.0 await audioElement.play() } catch (err) { useNuxtApp().$logger.info('Audio konnte nicht abgespielt werden (Autoplay blockiert?):', err) await ensureAudio() audioElement.volume = 0.0 await audioElement.play() } // 4. Ist ready? if (audioElement.readyState < 2) { setTimeout(() => { setupAudioSource(audioElement, audioContext) }, 500) onFallback('Audioelement ist nicht bereit (readyState < 2)') return null } // 5. Nutze den MediaStream const stream = getMediaStreamFromElement(audioElement) if (!stream) { onFallback('captureStream (inkl. mozCaptureStream) nicht verfügbar') if(audioElement.src) useNuxtApp().$logger.log("Audio Src is a string") if(audioElement.srcObject) useNuxtApp().$logger.log("Audio Src is a srcObject") return createAudioSource(audioContext, audioElement.src) } if (stream.getAudioTracks().length === 0) { onFallback('captureStream liefert keinen AudioTrack') return createAudioSource(audioContext, audioElement.src) } // 6. Teste node-Erstellung const node = audioContext.createMediaStreamSource(stream) return node } catch (error) { onFallback(`Fehler beim Aufbau der MediaStreamSource: ${error}`) return null } } function getMediaStreamFromElement(mediaElement: HTMLMediaElement): MediaStream | null { const element = mediaElement as HTMLMediaElement & { captureStream?: () => MediaStream mozCaptureStream?: () => MediaStream } if (typeof element.captureStream === 'function') { return element.captureStream() } else if (typeof element.mozCaptureStream === 'function') { useNuxtApp().$logger.warn('[AudioBridge] ⚠️ captureStream() nicht verfügbar, versuche mozCaptureStream() (Firefox-Fallback)') return element.mozCaptureStream() } useNuxtApp().$logger.warn('[AudioBridge] ❌ Weder captureStream() noch mozCaptureStream() verfügbar.') return null } async function createStreamingAudioSource (ctx: AudioContext, fileName: string): Promise { if(Howler.ctx != null) { Howler.ctx.close() } if(fileName == null || fileName == undefined) { useNuxtApp().$logger.error('Could not find file with name ' + fileName) } // Howler.ctx = ctx // Load your audio file with streaming enabled const baseURL = window.location.origin const sound: Howl = await new Howl({ src: [fileName], html5: false, // Enables streaming loop: true }) if(sound.state() === "unloaded") { useNuxtApp().$logger.warn('Could not find file with name, please check the following resource -> '+ fileName) } // Assuming you have the Web Audio API context (Howler.ctx) // and it's not null or undefined, you can proceed to create a custom AudioNode useNuxtApp().$logger.log( {sound }) return sound // Note: This is a simplified example. Depending on what you're doing, // you might not want to connect directly to the destination but to other nodes for further processing. } function getAttenuationFactor () { const user = useUserStore().user as any const anc = user.settings.anc_type const ht = user.settings.headphone_type if (anc === ANC.Yes && ht === HeadsetType.OverEar) { return 0.0562 } if (anc === ANC.No && ht === HeadsetType.OverEar) { return 0.5623 } if (anc === ANC.Yes && ht === HeadsetType.InEar) { return 0.1778 } if (anc === ANC.No && ht === HeadsetType.InEar) { return 0.0316 } return 0.5623 } function throttle void>( func: T, limit: number ): T { let inThrottle: boolean return function (this: any, ...args: any[]) { if (!inThrottle) { func.apply(this, args) inThrottle = true setTimeout(() => (inThrottle = false), limit) } } as T } async function createAudioSource ( ctx: AudioContext, path: string ): Promise { // Works but is slow const response: Response = await fetch(path) const source: AudioBufferSourceNode = ctx.createBufferSource() // Check if the partial content was successfully fetched if (response.status === 206 || response.status === 200) { // Retrieve the chunk as an ArrayBuffer const buffer: ArrayBuffer = await response.arrayBuffer() // Decode the audio data const decodedAudio: AudioBuffer = await ctx.decodeAudioData(buffer) // checkClipping(decodedAudio) // useNuxtApp().$logger.log('Noise decoded successfully') source.buffer = decodedAudio source.loop = true return source } else { throw new Error( `Failed to fetch audio chunk: HTTP status ${response.status}` ) } /* const decodedAudio: AudioBuffer = await fetchAndDecodeAudioChunk(ctx, path, startByte, endByte) const source: AudioBufferSourceNode = ctx.createBufferSource() source.buffer = decodedAudio return source */ } function createRNBODevice (ctx: AudioContext, patcher: any): Promise { return createDevice({ context: ctx, patcher }) } function connectAudioNodes ( from: AudioNode, to: AudioNode, output?: number, input?: number ): void { /* // useNuxtApp().$logger.log('Connecting the Node: ', { from }, ' with ' + from.numberOfInputs + ' inputs', ' and with ' + from.numberOfOutputs + ' outputs ... with ') // useNuxtApp().$logger.log('the Node: ', { to }, ' with ' + to.numberOfInputs + ' inputs', ' and with ' + to.numberOfOutputs + ' outputs ... defined Channels are o/i: ' + output + ' and ' + input) */ // useNuxtApp().$logger.log('new Node connected ', { allNodes }) try { if (output !== undefined && input !== undefined) { from.connect(to, output, input) // useNuxtApp().$logger.log('Connect specific channels o/i ' + output, input, { to }, { from }) } else if (output !== undefined) { from.connect(to, output) // useNuxtApp().$logger.log('Connect with specific output channel ' + output, { to }, { from }) } else { from.connect(to) // useNuxtApp().$logger.log('Connect without specific channels', { to }, { from }) } } catch (error) { // useNuxtApp().$logger.log('Error while connecting' + error) } } function checkClipping (buffer: AudioBuffer): boolean { let isClipping = false for ( let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++ ) { const element: Float32Array = buffer.getChannelData(channelNumber) // Iterate through buffer to check if any of the |values| exceeds 1. for (let i = 0; i < buffer.length; i++) { const absValue = Math.abs(element[i]) if (absValue >= 1.0) { isClipping = true break } } } return isClipping } function calculateNormalizedVolume (db: number) : number { const minDB = -18 const maxDB = 6 if (db < minDB || db > maxDB) { return Math.pow(10, db/ 20) } return Math.pow(10, db / 20) } function stereoToMono (stereoNode: AudioNode): AudioNode { if (stereoNode.channelCount !== 2) { useNuxtApp().$logger.warn('Not a stereo node. Mismatch of channels, expected 2 channels') return stereoNode } const context = stereoNode.context const splitter = context.createChannelSplitter(2) const gainL = context.createGain() const gainR = context.createGain() gainL.gain.value = 0.5 gainR.gain.value = 0.5 // ChannelMerger, aber wir nutzen nur den ersten Output-Kanal const merger = context.createChannelMerger(1) // Verkabelung stereoNode.connect(splitter) splitter.connect(gainL, 0) // Left channel splitter.connect(gainR, 1) // Right channel gainL.connect(merger, 0, 0) gainR.connect(merger, 0, 0) useNuxtApp().$logger.info('Stereo node umgewandelt') useNuxtApp().$logger.log({merger }) return merger } /** * Analysiert, ob ein Mikrofon innerhalb eines Zeitfensters aktiv war (z. B. Sprache, Geräusche). * * @param micNode - Eine MediaStreamAudioSourceNode (z. B. von getUserMedia) * @param durationMs - Die Dauer der Analyse in Millisekunden * @param threshold - Optional: Empfindlichkeit (0.01 ist guter Standardwert) * @returns Promise - true, wenn Aktivität erkannt wurde, sonst false NOT TESTED async function analyseMicActivity( micNode: MediaStreamAudioSourceNode, durationMs: number, threshold: number = 0.01 ): Promise { const audioContext = micNode.context const analyser = audioContext.createAnalyser() analyser.fftSize = 2048 const bufferLength = analyser.fftSize const dataArray = new Float32Array(bufferLength) micNode.connect(analyser) return new Promise((resolve) => { let activeDetected = false const startTime = performance.now() const analyze = () => { analyser.getFloatTimeDomainData(dataArray) // RMS berechnen let sum = 0 for (let i = 0; i < bufferLength; i++) { sum += dataArray[i] * dataArray[i] } const rms = Math.sqrt(sum / bufferLength) if (rms > threshold) { activeDetected = true } if (performance.now() - startTime < durationMs) { requestAnimationFrame(analyze) } else { micNode.disconnect(analyser) resolve(activeDetected) } } analyze() }) } */ /** * EXPORTS */ export { createAudioSource, createRNBODevice, createStreamingAudioSource, connectAudioNodes, checkClipping, getAttenuationFactor, throttle, calculateNormalizedVolume, stereoToMono, setupAudioSource, getMediaStreamFromElement }