add value calculator, listeneer and microphone logic
parent
3c9d382794
commit
d28e7a34a8
|
@ -0,0 +1,63 @@
|
||||||
|
[
|
||||||
|
"MacBook Pro-Mikrofon",
|
||||||
|
"MacBook Pro-Mikrofon (Built-in)",
|
||||||
|
"MacBook Air-Mikrofon",
|
||||||
|
"MacBook Air-Mikrofon (Built-in)",
|
||||||
|
"iMac-Mikrofon",
|
||||||
|
"iMac-Mikrofon (Built-in)",
|
||||||
|
"Studio Display-Mikrofon",
|
||||||
|
"Studio Display-Mikrofon (Built-in)",
|
||||||
|
"Surface-Mikrofon",
|
||||||
|
"Surface-Mikrofon (Built-in)",
|
||||||
|
"Internal Microphone Array",
|
||||||
|
"Internal Microphone Array (Built-in)",
|
||||||
|
"Realtek Audio Device",
|
||||||
|
"Realtek Audio Device (Built-in)",
|
||||||
|
"Dell Microphone Array",
|
||||||
|
"Dell Microphone Array (Built-in)",
|
||||||
|
"HP Wide Vision HD Microphone",
|
||||||
|
"HP Wide Vision HD Microphone (Built-in)",
|
||||||
|
"ThinkPad Microphone",
|
||||||
|
"ThinkPad Microphone (Built-in)",
|
||||||
|
"Realtek High Definition Audio",
|
||||||
|
"Realtek High Definition Audio (Built-in)",
|
||||||
|
"ASUS Microphone Array",
|
||||||
|
"ASUS Microphone Array (Built-in)",
|
||||||
|
"Acer HD Microphone",
|
||||||
|
"Acer HD Microphone (Built-in)",
|
||||||
|
"Razer Internal Microphone",
|
||||||
|
"Razer Internal Microphone (Built-in)",
|
||||||
|
"Blue Yeti Microphone",
|
||||||
|
"Blue Yeti Microphone (Built-in)",
|
||||||
|
"RØDE NT-USB",
|
||||||
|
"RØDE NT-USB (Built-in)",
|
||||||
|
"Shure MV7",
|
||||||
|
"Shure MV7 (Built-in)",
|
||||||
|
"HyperX QuadCast",
|
||||||
|
"HyperX QuadCast (Built-in)",
|
||||||
|
"Logitech Webcam Microphone",
|
||||||
|
"Logitech Webcam Microphone (Built-in)",
|
||||||
|
"Built-in Microphone",
|
||||||
|
"Internal Mic",
|
||||||
|
"Internal Mic (Built-in)",
|
||||||
|
"Primary Mic",
|
||||||
|
"Primary Mic (Built-in)",
|
||||||
|
"Secondary Mic",
|
||||||
|
"Secondary Mic (Built-in)",
|
||||||
|
"Microphone Array",
|
||||||
|
"Microphone Array (Built-in)",
|
||||||
|
"iPhone Microphone",
|
||||||
|
"iPhone Microphone (Built-in)",
|
||||||
|
"iPad Microphone",
|
||||||
|
"iPad Microphone (Built-in)",
|
||||||
|
"Front Microphone",
|
||||||
|
"Front Microphone (Built-in)",
|
||||||
|
"Back Microphone",
|
||||||
|
"Back Microphone (Built-in)",
|
||||||
|
"Bottom Microphone",
|
||||||
|
"Bottom Microphone (Built-in)",
|
||||||
|
"Top Microphone",
|
||||||
|
"Top Microphone (Built-in)",
|
||||||
|
"Internal Microphone",
|
||||||
|
"Internal Microphone (Built-in)"
|
||||||
|
]
|
|
@ -0,0 +1,40 @@
|
||||||
|
export function setupPlayButton(
|
||||||
|
sources: Map<number, AudioBufferSourceNode>,
|
||||||
|
gains: Map<number, GainNode>,
|
||||||
|
context: AudioContext
|
||||||
|
) {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.textContent = '▶ Play All'
|
||||||
|
button.style.fontSize = '1.5rem'
|
||||||
|
button.style.padding = '10px'
|
||||||
|
button.style.marginTop = '20px'
|
||||||
|
|
||||||
|
button.onclick = () => {
|
||||||
|
const startTime = context.currentTime + 0.1 // 100ms in Zukunft
|
||||||
|
const rampTime = 2
|
||||||
|
|
||||||
|
sources.forEach((source, freq) => {
|
||||||
|
const gainNode = gains.get(freq)
|
||||||
|
if (!gainNode) return
|
||||||
|
|
||||||
|
// Ramp up GainNode
|
||||||
|
const gain = gainNode.gain
|
||||||
|
gain.setValueAtTime(0.001, startTime)
|
||||||
|
gain.exponentialRampToValueAtTime(1.0, startTime + rampTime)
|
||||||
|
|
||||||
|
// Start BufferSource
|
||||||
|
try {
|
||||||
|
source.start(startTime)
|
||||||
|
console.log(`Started ${freq}Hz at ${startTime.toFixed(2)}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Start error for ${freq}Hz`, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
button.disabled = true
|
||||||
|
button.textContent = '🔊 Playing...'
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(button)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// composables/useRnboParamStore.ts
|
||||||
|
const parameterValues = new Map<string, number>()
|
||||||
|
|
||||||
|
export const useRnboParamStore = () => ({
|
||||||
|
get: (id: string) => parameterValues.get(id),
|
||||||
|
set: (id: string, val: number) => parameterValues.set(id, val),
|
||||||
|
all: () => parameterValues
|
||||||
|
})
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useRnboParamStore } from './composables/RNBOParameterStore'
|
||||||
|
import type { Device } from '@rnbo/js'
|
||||||
|
|
||||||
|
export function attachDBValueListener(noiseDevice: Device, freq: number) {
|
||||||
|
const paramStore = useRnboParamStore()
|
||||||
|
const deviceId = freq.toString() || 'unknown'
|
||||||
|
createDisplayElement(deviceId)
|
||||||
|
|
||||||
|
console.log(`🔊 Attaching message listener for freq ${freq} on device ${deviceId}`)
|
||||||
|
|
||||||
|
noiseDevice.messageEvent.subscribe((ev: any) => {
|
||||||
|
if (ev.tag === 'out1') {
|
||||||
|
const payload = Array.isArray(ev.payload) ? ev.payload[0] : ev.payload
|
||||||
|
paramStore.set(deviceId, payload)
|
||||||
|
updateDisplay(deviceId, payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDisplayElement(id: string) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.id = id
|
||||||
|
div.style.fontFamily = 'monospace'
|
||||||
|
div.style.padding = '4px'
|
||||||
|
div.style.marginBottom = '2px'
|
||||||
|
div.style.border = '1px solid #ccc'
|
||||||
|
div.style.width = 'max-content'
|
||||||
|
div.textContent = `${id}: (waiting...)`
|
||||||
|
document.body.appendChild(div)
|
||||||
|
return div
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDisplay(id: string, value: number) {
|
||||||
|
const el = document.getElementById(id)
|
||||||
|
if (el) {
|
||||||
|
el.textContent = `${id}: ${value.toFixed(2)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* Attempts to find a preferred microphone device from the available devices.
|
||||||
|
*
|
||||||
|
* This method iterates through a list of preferred microphone names and tries to match them
|
||||||
|
* against the labels of available audio input devices. It uses a case-insensitive regex match,
|
||||||
|
* allowing for partial matches. The method includes safety measures such as input sanitization,
|
||||||
|
* length limits, and timeout protection against potential regex-based attacks.
|
||||||
|
*
|
||||||
|
* If a match is found, it returns the corresponding MediaDeviceInfo object.
|
||||||
|
* If no match is found, it returns undefined, indicating that the default device should be used.
|
||||||
|
*
|
||||||
|
* @returns {MediaDeviceInfo | undefined} The matched device info or undefined if no match is found.
|
||||||
|
*/
|
||||||
|
import microphoneList from '../assets/microphone_list.json'
|
||||||
|
|
||||||
|
export async function getPreferredDevice (): Promise<MediaDeviceInfo | undefined> {
|
||||||
|
// 1) Berechtigung anfragen, sonst sind Labels leer
|
||||||
|
try {
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
|
} catch {
|
||||||
|
window.alert("Keine Zugriff aufs Mikrofon.")
|
||||||
|
}
|
||||||
|
const availableDevices = await updateAvailableDevices()
|
||||||
|
for (const preferredMic of microphoneList) {
|
||||||
|
if (preferredMic.length > 200) {
|
||||||
|
// Limit the length of preferred mic names
|
||||||
|
console.warn(
|
||||||
|
`Skipping overly long preferred microphone name: ${preferredMic.substring(
|
||||||
|
0,
|
||||||
|
20
|
||||||
|
)}...`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize the preferred microphone name
|
||||||
|
const sanitizedMicName = preferredMic.replace(/[^\w\s-]/gi, '')
|
||||||
|
// Create a case-insensitive regular expression from the sanitized name
|
||||||
|
const preferredMicRegex = new RegExp(sanitizedMicName, 'i')
|
||||||
|
const matchingDevice = availableDevices.find(
|
||||||
|
(device: { label: string }) => {
|
||||||
|
// Use a timeout to prevent long-running regex matches
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
return false
|
||||||
|
}, 1000) // 1 second timeout
|
||||||
|
|
||||||
|
const match = preferredMicRegex.test(device.label)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (matchingDevice) {
|
||||||
|
// Found preferred microphone
|
||||||
|
return matchingDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function updates the List of possible microphone sources.
|
||||||
|
*/
|
||||||
|
async function updateAvailableDevices (): Promise<MediaDeviceInfo[]> {
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
|
const newDevices = devices.filter(
|
||||||
|
device => device.kind === 'audioinput'
|
||||||
|
)
|
||||||
|
if (newDevices.length === 0) {
|
||||||
|
console.warn(
|
||||||
|
'No audio input devices found. This might be due to missing permissions or no connected devices.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return newDevices
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error enumerating devices:', error)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { createDevice, type Device } from '@rnbo/js'
|
||||||
|
import { attachDBValueListener } from './dbValueListener'
|
||||||
|
import controlValuesPatcher from './assets/patch/LAF-Controll-Values_Simple_Band1000.rnbopat.export.json'
|
||||||
|
import { fetchOrLoadBuffer, createBufferSource } from './composables/BufferStore'
|
||||||
|
import { setupPlayButton } from './composables/Player';
|
||||||
|
import { getPreferredDevice } from './lib/microphone';
|
||||||
|
|
||||||
|
interface DeviceInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
device: Device;
|
||||||
|
audioNode: AudioNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create context & master gain
|
||||||
|
const ctx = new AudioContext() // match your assets
|
||||||
|
const mobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
console.log("AudioContext created")
|
||||||
|
|
||||||
|
//
|
||||||
|
// Gains to control the audio
|
||||||
|
//
|
||||||
|
const masterGain = ctx.createGain()
|
||||||
|
|
||||||
|
const bands = mobile ? [150, 1500, 8000] : [63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
||||||
|
const devices = new Map<number, DeviceInfo>()
|
||||||
|
const sources = new Map<number, AudioBufferSourceNode>()
|
||||||
|
const gains = new Map<number, GainNode>()
|
||||||
|
|
||||||
|
// Audio Files
|
||||||
|
const urlsWebM = mobile ? [
|
||||||
|
{ name: bands[0], url: 'audio/masking/3bands/low_band_256kbps.webm' },
|
||||||
|
{ name: bands[1], url: 'audio/masking/3bands/mid_band_256kbps.webm' },
|
||||||
|
{ name: bands[2], url: 'audio/masking/3bands/high_band_256kbps.webm' },
|
||||||
|
] :
|
||||||
|
[
|
||||||
|
{ name: bands[0], url: 'audio/masking/9bands/Quellrauschen63.webm' },
|
||||||
|
{ name: bands[1], url: 'audio/masking/9bands/Quellrauschen125.webm' },
|
||||||
|
{ name: bands[2], url: 'audio/masking/9bands/Quellrauschen250.webm' },
|
||||||
|
{ name: bands[3], url: 'audio/masking/9bands/Quellrauschen500.webm' },
|
||||||
|
{ name: bands[4], url: 'audio/masking/9bands/Quellrauschen1000.webm' },
|
||||||
|
{ name: bands[5], url: 'audio/masking/9bands/Quellrauschen2000.webm' },
|
||||||
|
{ name: bands[6], url: 'audio/masking/9bands/Quellrauschen4000.webm' },
|
||||||
|
{ name: bands[7], url: 'audio/masking/9bands/Quellrauschen8000.webm' },
|
||||||
|
{ name: bands[8], url: 'audio/masking/9bands/Quellrauschen16000.webm' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
|
||||||
|
const newDevices = await Promise.all(
|
||||||
|
bands.map(async (freq, index) => {
|
||||||
|
const device = await createBandDevice(freq, ctx) as DeviceInfo
|
||||||
|
await fetchOrLoadBuffer(freq.toString(),
|
||||||
|
urlsWebM[index].url,
|
||||||
|
ctx
|
||||||
|
)
|
||||||
|
const sourceNode = await createBufferSource(freq.toString(),ctx)
|
||||||
|
const bandGainNode = ctx.createGain()
|
||||||
|
// patch them together
|
||||||
|
sourceNode.node?.connect(bandGainNode)
|
||||||
|
bandGainNode.connect(masterGain)
|
||||||
|
//update the source list
|
||||||
|
if(sourceNode.node instanceof AudioBufferSourceNode && bandGainNode instanceof GainNode ) {
|
||||||
|
sources.set(freq, sourceNode.node)
|
||||||
|
gains.set(freq, bandGainNode)
|
||||||
|
}
|
||||||
|
return {freq, device}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
newDevices.forEach(({freq, device}) => {
|
||||||
|
devices.set(freq, device)
|
||||||
|
})
|
||||||
|
console.log("RNBO Devices created")
|
||||||
|
|
||||||
|
await initMicrophone()
|
||||||
|
devices.forEach((devices, freq)=> {
|
||||||
|
try{
|
||||||
|
if(devices && devices.device) {
|
||||||
|
attachDBValueListener(devices.device,freq)
|
||||||
|
}
|
||||||
|
}catch {
|
||||||
|
console.warn("couldnt attach rnbo listener")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log("Everything is setup, display a play button")
|
||||||
|
setupPlayButton(sources, gains,ctx)
|
||||||
|
masterGain.connect(ctx.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the Microphone to use it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function initMicrophone() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const preferredDevice = await getPreferredDevice()
|
||||||
|
console.log("INIT MIC", { preferredDevice})
|
||||||
|
if(preferredDevice){
|
||||||
|
console.log("prefered device !!! ")
|
||||||
|
const constraints: MediaStreamConstraints = {
|
||||||
|
audio: { deviceId: { exact: preferredDevice.deviceId }, echoCancellation: false, noiseSuppression: false,
|
||||||
|
autoGainControl: false },
|
||||||
|
video: false
|
||||||
|
}
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(
|
||||||
|
constraints
|
||||||
|
)
|
||||||
|
const source = ctx.createMediaStreamSource(stream)
|
||||||
|
console.log("SOURCE MICROFON", {source})
|
||||||
|
const splitter = ctx.createChannelSplitter(source.channelCount)
|
||||||
|
|
||||||
|
try {
|
||||||
|
source.connect(splitter)
|
||||||
|
devices.forEach((device, freq) => {
|
||||||
|
splitter.connect(device.audioNode, 0, 0)
|
||||||
|
console.log('🎤 Mikrofon mono-signal wurde an RNBO-Device mit Freq: '+freq+' geroutet.')
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error!: connecting the microphone to deivces... connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}else {
|
||||||
|
console.log("Das Gerät wurde nicht gefunden. Lieber nochmal den Mikrofon-Zugriff checken")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Fehler beim Zugriff auf Mikrofon:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function creates a RNBODevice with a given frequency. If the microphone is connected
|
||||||
|
* this devices sends out control Values as messages.
|
||||||
|
* @param freq
|
||||||
|
* @param ctx
|
||||||
|
* @returns RNBO Device
|
||||||
|
*/
|
||||||
|
async function createBandDevice(freq: number, ctx: AudioContext): Promise<DeviceInfo> {
|
||||||
|
const patcher = await controlValuesPatcher as any
|
||||||
|
const rnboDevice = await createDevice({
|
||||||
|
context: ctx,
|
||||||
|
patcher
|
||||||
|
})
|
||||||
|
const param = rnboDevice.parametersById.get('centerFrequency')
|
||||||
|
param.value = freq
|
||||||
|
console.log("Device created for "+ freq)
|
||||||
|
return { id: freq, name: ""+freq, device: rnboDevice, audioNode: rnboDevice.node }
|
||||||
|
}
|
||||||
|
|
||||||
|
init() // hey ho lets go
|
||||||
|
|
||||||
|
// resume only after interaction inside the iframe (mobile safe)
|
||||||
|
window.addEventListener('click', () => ctx.resume(), { once: true })
|
||||||
|
console.log("Await unlock context")
|
Loading…
Reference in New Issue