adding mic selection in the settings
This commit is contained in:
Generated
+68
-4
@@ -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",
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
|
||||
const SETTINGS = [
|
||||
{
|
||||
section: 'Chord Detection',
|
||||
@@ -70,8 +72,6 @@ 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)
|
||||
@@ -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 (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3 items-center">
|
||||
<select
|
||||
value={config.audioDeviceId ?? ''}
|
||||
onChange={e => onChange('audioDeviceId', e.target.value === '' ? null : e.target.value)}
|
||||
className="appearance-none bg-surface border border-border hover:border-gray-500 focus:border-accent focus:outline-none rounded-lg pl-3 pr-7 py-1 text-sm text-gray-200 cursor-pointer transition-colors w-full"
|
||||
>
|
||||
<option value="">System default</option>
|
||||
{devices.map((d, i) => (
|
||||
<option key={d.deviceId || i} value={d.deviceId}>{d.label || `Microphone ${i + 1}`}</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={refresh} className="px-3 py-1 rounded-lg border border-border text-sm text-gray-400">Refresh</button>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">If device labels are empty, grant microphone permission first and hit Refresh.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-surface z-50 overflow-y-auto">
|
||||
<div className="max-w-2xl mx-auto px-6 py-8">
|
||||
@@ -175,6 +210,14 @@ export default function Settings({ config, onChange, onClose, onReset, monoColor
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Audio device selector */}
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-widest text-gray-500 mb-4 border-b border-border pb-2">
|
||||
Microphone
|
||||
</h3>
|
||||
<DeviceSelector config={config} onChange={onChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,17 @@ export function useAudioTuner() {
|
||||
const startListening = async () => {
|
||||
if (isListening) return
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
// Read saved device selection from config (if present)
|
||||
let deviceId = null
|
||||
try {
|
||||
const cfg = JSON.parse(localStorage.getItem('wtf_config') || '{}')
|
||||
deviceId = cfg.audioDeviceId || null
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
const constraints = deviceId ? { audio: { deviceId: { exact: deviceId } } } : { audio: true }
|
||||
console.log('Tuner requesting getUserMedia with', constraints)
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints)
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)()
|
||||
const analyser = ctx.createAnalyser()
|
||||
analyser.fftSize = 4096
|
||||
|
||||
Reference in New Issue
Block a user