rnbo patch as speaker and with 6 inputs working example

rapp/detached-control-value-iframe
Robert Rapp 2025-07-01 12:48:35 +02:00
parent f97d3136fe
commit c5f605395b
8 changed files with 544 additions and 173 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>9 x single-band rnbo patches</title> <title>channel pass through rnbo patches</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,11 @@
let CONNECTED = false
export function setupPlayButton( export function setupPlayButton(
sources: Map<number, AudioBufferSourceNode>, source: AudioNode[],
gains: Map<number, GainNode>, gain: GainNode,
context: AudioContext context: AudioContext
) { ) {
const button = document.createElement('button') const button = document.createElement('button')
button.textContent = '▶ Play All' button.textContent = '▶ Connect Source'
button.style.fontSize = '1.5rem' button.style.fontSize = '1.5rem'
button.style.padding = '10px' button.style.padding = '10px'
button.style.marginTop = '20px' button.style.marginTop = '20px'
@ -12,32 +13,22 @@ export function setupPlayButton(
button.onclick = () => { button.onclick = () => {
const startTime = context.currentTime + 0.1 // 100ms in Zukunft const startTime = context.currentTime + 0.1 // 100ms in Zukunft
const rampTime = 2 const rampTime = 2
// 1⃣ Master-Gain auf 0 dB hochrampen if(CONNECTED){
master.gain.setValueAtTime(0.001, startTime) source.forEach((n) => {
master.gain.exponentialRampToValueAtTime(1.0, startTime + rampTime) if(n instanceof AudioBufferSourceNode) {
n.disconnect()
sources.forEach((source, freq) => { button.textContent = 'Disconnected...'
const gainNode = gains.get(freq) CONNECTED=false
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)
} }
}) })
}else {
button.disabled = true source.forEach(n => n.start())
button.textContent = '🔊 Playing...' gain.gain.setValueAtTime(0.001, startTime)
if(source instanceof AudioBufferSourceNode) source.start(startTime)
gain.gain.linearRampToValueAtTime(1, startTime+rampTime)
button.textContent = '🔊 Connected and Playing...'
CONNECTED=true
}
} }
document.body.appendChild(button) document.body.appendChild(button)
} }

View File

@ -1,129 +1,291 @@
import { createDevice, type Device } from '@rnbo/js' import RNBO, { createDevice, TimeNow } from '@rnbo/js'
import { attachDBValueListener } from './dbValueListener' import { fetchOrLoadBuffer } from './composables/BufferStore'
import { fetchOrLoadBuffer, createBufferSource } from './composables/BufferStore'
import { setupPlayButton } from './composables/Player'; import { setupPlayButton } from './composables/Player';
import { getPreferredDevice } from './lib/microphone';
import { getAudioContext } from './audio/context' import { getAudioContext } from './audio/context'
import patcherUrl from '../public/patches/controlvalue/LAF-Controll-Values_Simple_Band1000.rnbopat.export.json?url' import patcherUrl from '../public/patches/passthrough_6Kanal_Test.rnbopat.export.json?url'
interface DeviceInfo {
id: number;
name: string;
device: Device;
audioNode: AudioNode;
}
// RNBO // RNBO
// RNBO // RNBO
let patcherPromise: Promise<any> let patcherPromise: Promise<any>
// create context & master gain
const mobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
//
// Gains to control the audio
//
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 getPatcher() { async function getPatcher() {
if (!patcherPromise) patcherPromise = fetch(patcherUrl).then(r => r.json()) if (!patcherPromise) patcherPromise = fetch(patcherUrl).then(r => r.json())
return patcherPromise return patcherPromise
} }
async function init() { async function init() {
// Schritt 0: Mic-Recht
await initMicrophone()
// Schritt 1: Kontext // Schritt 1: Kontext
const ctx = getAudioContext() const ctx = getAudioContext()
await ctx.suspend() await ctx.suspend()
initMicrophone(ctx)
// Schritt 2: Fetch Patcher // Schritt 2: Fetch Patcher
await getPatcher() await getPatcher()
// Schritt 3: Erstelle alle Devices // Schritt 3: Erstelle alle Devices
const newDevices = await Promise.all( const patcher = await getPatcher()
bands.map(async (freq, index) => { const rnboDevice = await createDevice({
const device = await createBandDevice(freq, ctx) as DeviceInfo context: ctx,
patcher
})
// Initiale Werte
const events = {
/**
* Schickt einen numerischen Event-Input an das Gerät.
* @param {string} name genau wie im Max-Patch benannt
* @param {number} val
* @param {number} [when=context.currentTime] Sekunden relativ zur AudioClock
*/
send(name: any, val: any, when = TimeNow) {
const event = new RNBO.MessageEvent(when, name, val)
rnboDevice.scheduleEvent(event)
console.log(`Input Event ${name} schedueled.`)
},
};
// PARAMETER
// PARAMETER
// PARAMETER
let in7 = 0 // Hier soll das aus dem jeweiligen Textfeld
let in8 = 0
let in9 = 0
let in10 = 0
let in11 = 0
let in12 = 0
events.send("in7", in7) // channel 1 auf mute
events.send("in8", in8) // channel 2 auf mute
events.send("in9", in9) // channel 3 auf mute
events.send("in10", in10) // channel 4 auf mute
events.send("in11", in11) // channel 5 auf mute
events.send("in12", in12) // channel 6 auf mute
console.log("Alle Ins gesetzt")
// Schritt 4 Fetch alle AudioBuffer // Schritt 4 Fetch alle AudioBuffer
await fetchOrLoadBuffer(freq.toString(), const buffer1 = await fetchOrLoadBuffer("100000",
urlsWebM[index].url, 'audio/LMusik_RSprache.mp3',
ctx ctx
) )
const sourceNode = await createBufferSource(freq.toString(),ctx)
const bandGainNode = ctx.createGain() const buffer2 = await fetchOrLoadBuffer("100001",
'audio/stereo-test1.mp3',
ctx
)
const buffer3 = await fetchOrLoadBuffer("100002",
'audio/stereo-test2.wav',
ctx
)
const sourceNode1 = ctx.createBufferSource() // stereo
const sourceNode2 = ctx.createBufferSource() // stereo
const sourceNode3 = ctx.createBufferSource() // stereo
sourceNode1.buffer = buffer1
sourceNode2.buffer = buffer2
sourceNode3.buffer = buffer3
sourceNode1.loop = true
sourceNode2.loop = true
sourceNode3.loop = true
const channelMergerL = ctx.createChannelMerger()
const channelMergerR = ctx.createChannelMerger()
// CHANNEL INTERPRETATION AND COUNT MODE
channelMergerL.channelInterpretation = 'speakers'
channelMergerR.channelInterpretation = 'speakers'
rnboDevice.node.channelCountMode = 'max'
rnboDevice.node.channelInterpretation = 'speakers'
const inputGainNode = ctx.createGain()
// Connecte Audios auf ein Merger
const { left: sourceNode1L, right: sourceNode1R } = splitStereoIntoMonoChannels(sourceNode1)
const {left: sourceNode2L, right: sourceNode2R } = splitStereoIntoMonoChannels(sourceNode2)
const {left: sourceNode3L, right: sourceNode3R } = splitStereoIntoMonoChannels(sourceNode3)
console.log({sourceNode1L, sourceNode1R }, {sourceNode2L, sourceNode2R },{sourceNode3L, sourceNode3R })
console.log({ channelMergerL, channelMergerR })
// sourceNode1L.connect(ctx.destination, 0, 0)
sourceNode1L.connect(rnboDevice.node, 0, 0)
sourceNode1R.connect(rnboDevice.node, 0, 1)
sourceNode2L.connect(rnboDevice.node, 0, 2)
sourceNode2R.connect(rnboDevice.node, 0 ,3)
sourceNode3L.connect(rnboDevice.node, 0 ,4)
sourceNode3R.connect(rnboDevice.node, 0, 5)
const channelSplitter = ctx.createChannelSplitter(6)
channelMergerL.connect(channelSplitter)
// Schritt 5 Verbinde AudioNodes // Schritt 5 Verbinde AudioNodes
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}
}
))
registerDevicesWithListeners(newDevices)
console.log("RNBO Devices created")
devices.forEach((devices, freq)=> { // channelMerger.connect(ctx.destination) // 6
try{
if(devices && devices.device) { rnboDevice.node.connect(ctx.destination)
attachDBValueListener(devices.device,freq) console.log("RNBO Device created connected with input source")
}
}catch {
console.warn("couldnt attach rnbo listener")
}
})
console.log("Everything is setup, display a play button") console.log("Everything is setup, display a play button")
setupPlayButton(sources, gains,ctx) setupPlayButton([sourceNode1,sourceNode2,sourceNode3], inputGainNode , ctx)
masterGain.connect(ctx.destination) // UI
const ui = document.createElement('div')
ui.style.padding = '20px'
// Report-Infos anhängen
ui.appendChild(createNodeInspector('RNBO Device', rnboDevice.node))
ui.appendChild(createNodeInspectorAndToggle('Scape Source LEFT 1', sourceNode1L, 0, events))
ui.appendChild(createNodeInspectorAndToggle('Scape Source 2', sourceNode2, 1, events))
ui.appendChild(createNodeInspectorAndToggle('Scape Source 3', sourceNode3, 2, events))
ui.appendChild(createNodeInspector('ChannelMerger', channelMergerL))
ui.appendChild(createNodeInspector('ChannelMerger', channelMergerR))
ui.appendChild(createNodeInspector('channelSplitter', channelSplitter))
console.log("rnboDevice.numInputChannels= "+ rnboDevice.numInputChannels)
console.log("rnboDevice.numOutputChannels = "+ rnboDevice.numOutputChannels)
console.log("rnboDevice.node.numberOfInputs = "+ rnboDevice.node.numberOfInputs)
console.log("rnboDevice.node.numberOfOutputs = "+ rnboDevice.node.numberOfOutputs)
document.body.appendChild(ui)
} }
init() // hey ho lets go
/**
* Enables the Microphone to use it.
*/
function registerDevicesWithListeners(newDevices: { freq: number, device: any }[]) { // resume only after interaction inside the iframe (mobile safe)
const devices = new Map<number, any>() window.addEventListener('click', () => getAudioContext().resume(), { once: true })
console.log("Await unlock context")
newDevices.forEach(({ freq, device }) => {
devices.set(freq, device)
try { function createNodeInspector(label: string, node: AudioNode): HTMLElement {
if (device) { const container = document.createElement('div')
attachDBValueListener(device, freq) container.style.marginTop = '10px'
container.style.padding = '8px'
container.style.border = '1px solid #ccc'
container.style.borderRadius = '6px'
container.style.fontFamily = 'monospace'
container.style.background = '#f9f9f9'
const title = document.createElement('strong')
title.textContent = `🎧 ${label}`
container.appendChild(title)
const list = document.createElement('ul')
list.style.listStyle = 'none'
list.style.padding = '4px'
list.style.margin = '4px 0'
const addRow = (key: string, value: string | number) => {
const li = document.createElement('li')
li.textContent = `${key}: ${value}`
list.appendChild(li)
} }
} catch (err) {
console.warn(`Could not attach RNBO listener for ${freq}Hz`, err) addRow('channelCount', node.channelCount)
addRow('channelCountMode', node.channelCountMode)
addRow('channelInterpretation', node.channelInterpretation)
addRow('numberOfInputs', node.numberOfInputs)
addRow('numberOfOutputs', node.numberOfOutputs)
container.appendChild(list)
return container
} }
function createNodeInspectorAndToggle(label: string, node: AudioNode, sourceIndex: number, events: { send: (tag: string, value: number) => void }): HTMLElement {
const container = document.createElement('div')
container.style.marginTop = '10px'
container.style.padding = '8px'
container.style.border = '1px solid #ccc'
container.style.borderRadius = '6px'
container.style.fontFamily = 'monospace'
container.style.background = '#f9f9f9'
const title = document.createElement('strong')
title.textContent = `🎧 ${label}`
container.appendChild(title)
const list = document.createElement('ul')
list.style.listStyle = 'none'
list.style.padding = '4px'
list.style.margin = '4px 0'
const addRow = (key: string, value: string | number) => {
const li = document.createElement('li')
li.textContent = `${key}: ${value}`
list.appendChild(li)
}
addRow('channelCount', node.channelCount)
addRow('channelCountMode', node.channelCountMode)
addRow('channelInterpretation', node.channelInterpretation)
addRow('numberOfInputs', node.numberOfInputs)
addRow('numberOfOutputs', node.numberOfOutputs)
container.appendChild(list)
if(node instanceof AudioBufferSourceNode || node instanceof GainNode) {
// Button-UI für L/R Kanalsteuerung (toggle 012)
const buttonContainer = document.createElement('div')
buttonContainer.style.marginTop = '8px'
;['L', 'R'].forEach((side, i) => {
const modeBtn = document.createElement('button')
modeBtn.textContent = `Mute ${side}: 0`
modeBtn.style.marginRight = '8px'
let currentMode = 0
const paramTag = `in${7 + sourceIndex * 2 + i}` // z.B. in7, in8, ..., in12
modeBtn.addEventListener('click', () => {
currentMode = (currentMode + 1) % 3
modeBtn.textContent = `Mute ${side}: ${currentMode}`
events.send(paramTag, currentMode)
console.log(`🔁 Sent ${paramTag} = ${currentMode}`)
}) })
console.log("✅ RNBO Devices registered and listeners attached") buttonContainer.appendChild(modeBtn)
return devices })
container.appendChild(buttonContainer)
}
return container
}
function splitStereoIntoMonoChannels(node: AudioNode): { left: GainNode, right: GainNode } {
console.log("BEFORE SPLIT: channelCount -> "+ node.channelCount)
const ctx = getAudioContext()
const splitter = ctx.createChannelSplitter(2)
node.connect(splitter)
const leftGain = ctx.createGain()
leftGain.channelCount = 1
leftGain.channelCountMode = 'explicit'
leftGain.channelInterpretation = 'discrete'
leftGain.gain.setValueAtTime(1,leftGain.context.currentTime)
const rightGain = ctx.createGain()
rightGain.channelCount = 1
rightGain.channelCountMode = 'explicit'
rightGain.channelInterpretation = 'discrete'
rightGain.gain.setValueAtTime(1,rightGain.context.currentTime)
splitter.connect(leftGain, 0) // Kanal 0 = Links
splitter.connect(rightGain, 1) // Kanal 1 = Rechts
console.log("AFTER SPLIT: channelCount -> ",{ left: leftGain, right: rightGain } )
return { left: leftGain, right: rightGain }
} }
@ -131,15 +293,21 @@ function registerDevicesWithListeners(newDevices: { freq: number, device: any }[
* Enables the Microphone to use it. * Enables the Microphone to use it.
*/ */
async function initMicrophone() { async function initMicrophone(ctx:AudioContext) {
try {
await navigator.mediaDevices.getUserMedia({ audio: true })
} catch {
window.alert("Keine Zugriff aufs Mikrofon.")
}
const availableDevices = await updateAvailableDevices()
console.log("MIKROFONE ", {availableDevices})
const preferredDevice = availableDevices[3]
const deviceJabra = "fdab2256f4654ac7c3fb7c7f334d88a6e468cf155fb00bb4c33d0f2f2479f653"
const externesMic = "276fc63a34034d08b7d5957cda539d268d20598e1708932112de199d85cfed1f"
try { try {
const preferredDevice = await getPreferredDevice()
console.log("INIT MIC", { preferredDevice})
if(preferredDevice){
console.log("prefered device !!! ") console.log("prefered device !!! ")
const constraints: MediaStreamConstraints = { const constraints: MediaStreamConstraints = {
audio: { deviceId: { exact: preferredDevice.deviceId }, echoCancellation: false, noiseSuppression: false, audio: { deviceId: { exact: "276fc63a34034d08b7d5957cda539d268d20598e1708932112de199d85cfed1f" }, echoCancellation: false, noiseSuppression: false,
autoGainControl: false }, autoGainControl: false },
video: false video: false
} }
@ -147,53 +315,32 @@ async function initMicrophone() {
constraints constraints
) )
const source = ctx.createMediaStreamSource(stream) const source = ctx.createMediaStreamSource(stream)
createNodeInspector("MIKROFON", source)
console.log("SOURCE MICROFON", {source}) 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) { } catch (error) {
console.error("Error!: connecting the microphone to deivces... connected") 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 function updates the List of possible microphone sources.
* this devices sends out control Values as messages.
* @param freq
* @param ctx
* @returns RNBO Device
*/ */
async function createBandDevice(freq: number, ctx: AudioContext): Promise<DeviceInfo> { async function updateAvailableDevices (): Promise<MediaDeviceInfo[]> {
const patcher = await getPatcher() try {
const rnboDevice = await createDevice({ const devices = await navigator.mediaDevices.enumerateDevices()
context: ctx, const newDevices = devices.filter(
patcher device => device.kind === 'audioinput'
}) )
const param = rnboDevice.parametersById.get('centerFrequency') if (newDevices.length === 0) {
param.value = freq console.warn(
console.log("Device created for "+ freq) 'No audio input devices found. This might be due to missing permissions or no connected devices.'
return { id: freq, name: ""+freq, device: rnboDevice, audioNode: rnboDevice.node } )
}
return newDevices
} catch (error) {
console.error('Error enumerating devices:', error)
}
return []
} }
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")