diff --git a/package-lock.json b/package-lock.json index fbc5e8c..8975003 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "whattheflat", - "version": "0.1.0", + "name": "jambuddy", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "whattheflat", - "version": "0.1.0", + "name": "jambuddy", + "version": "0.6.0", "dependencies": { "audiomotion-analyzer": "^4.5.4", "pitchy": "^4.1.0", @@ -2189,6 +2189,70 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", diff --git a/package.json b/package.json index 4dc7417..73e7d49 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "whattheflat", + "name": "jambuddy", "description": "A jam session companion app that provides a chromatic tuner, chord progressions, and more.", "version": "0.6.0", "private": true, diff --git a/src/App.jsx b/src/App.jsx index 87d7ea8..e9a3ab8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,6 +24,8 @@ const DEFAULTS = { // Audio input minClarity: 0.80, minVolume: 0.01, + // Selected device (null = system default) + audioDeviceId: null, } function loadStored(key, fallback) { @@ -508,6 +510,7 @@ export default function App() { isListening={isListening} minClarity={config.minClarity} minVolume={config.minVolume} + audioDeviceId={config.audioDeviceId} onPermissionError={() => { setMicError(true) setIsListening(false) diff --git a/src/components/AudioCapture.jsx b/src/components/AudioCapture.jsx index c3b67ae..14141de 100644 --- a/src/components/AudioCapture.jsx +++ b/src/components/AudioCapture.jsx @@ -81,7 +81,7 @@ function detectBassPC(freqData, sampleRate, fftSize) { return ((bestMidi % 12) + 12) % 12 } -export default function AudioCapture({ onNote, onChroma, onOnset, onWaveform, isListening, minClarity = 0.80, minVolume = 0.01, onPermissionError }) { +export default function AudioCapture({ onNote, onChroma, onOnset, onWaveform, isListening, minClarity = 0.80, minVolume = 0.01, onPermissionError, audioDeviceId = null }) { const audioCtxRef = useRef(null) const timeBufRef = useRef(null) const freqBufRef = useRef(null) @@ -121,8 +121,34 @@ export default function AudioCapture({ onNote, onChroma, onOnset, onWaveform, is stop() let stream try { - stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + // Helpful debug: list available media devices before requesting permission + try { + if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { + const devices = await navigator.mediaDevices.enumerateDevices() + const audioIns = devices.filter(d => d.kind === 'audioinput') + console.log('Audio inputs available:', audioIns) + } + } catch (e) { + console.warn('enumerateDevices failed', e) + } + + const constraints = audioDeviceId + ? { audio: { deviceId: { exact: audioDeviceId } } } + : { audio: true } + + console.log('Requesting getUserMedia with constraints:', constraints) + stream = await navigator.mediaDevices.getUserMedia(constraints) } catch (err) { + // If permission denied or other error, surface extra diagnostics when possible + console.warn('getUserMedia failed', err) + try { + if (navigator.permissions && navigator.permissions.query) { + const p = await navigator.permissions.query({ name: 'microphone' }) + console.log('microphone permission state:', p.state) + } + } catch (e) { + // ignore; not all environments support Permissions API for microphone + } onPermissionErrorRef.current?.(err) return } @@ -135,6 +161,14 @@ export default function AudioCapture({ onNote, onChroma, onOnset, onWaveform, is audioCtxRef.current = ctx const source = ctx.createMediaStreamSource(stream) + // Debug: log the acquired audio tracks and labels/deviceIds + try { + const tracks = stream.getAudioTracks() + console.log('Acquired audio tracks:', tracks.map(t => ({ label: t.label, id: t.id, enabled: t.enabled, muted: t.muted }))) + } catch (e) { + console.warn('Could not inspect stream tracks', e) + } + // Small analyser — pitch detection needs fast time-domain data const pa = ctx.createAnalyser() pa.fftSize = PITCH_FFT diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx index 34f714f..f109aa2 100644 --- a/src/components/Settings.jsx +++ b/src/components/Settings.jsx @@ -1,3 +1,5 @@ +import { useRef, useEffect, useState } from 'react' + const SETTINGS = [ { section: 'Chord Detection', @@ -70,12 +72,10 @@ const SETTINGS = [ }, ] -import { useRef } from 'react' - export default function Settings({ config, onChange, onClose, onReset, monoColor, onMonoColorChange }) { // Snapshot on mount so Cancel can restore - const savedConfig = useRef(config) - const savedMono = useRef(monoColor) + const savedConfig = useRef(config) + const savedMono = useRef(monoColor) function handleCancel() { Object.entries(savedConfig.current).forEach(([k, v]) => onChange(k, v)) @@ -83,6 +83,41 @@ export default function Settings({ config, onChange, onClose, onReset, monoColor onClose() } + function DeviceSelector({ config, onChange }) { + const [devices, setDevices] = useState([]) + + async function refresh() { + try { + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return + const list = await navigator.mediaDevices.enumerateDevices() + setDevices(list.filter(d => d.kind === 'audioinput')) + } catch (e) { + console.warn('enumerateDevices failed', e) + } + } + + useEffect(() => { refresh() }, []) + + return ( +