diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5eb39a9..a7371c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,33 +9,69 @@ permissions: contents: write jobs: - build-windows: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm install - - run: npm run electron:build:win - env: - CSC_IDENTITY_AUTO_DISCOVERY: false - WIN_CSC_LINK: '' - - uses: softprops/action-gh-release@v2 - with: - files: releases/*.exe + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false # Ensures one OS failing doesn't kill the others + matrix: + include: + - os: windows-latest + command: npm run electron:build:win + artifact_pattern: "release/*.exe" + - os: macos-latest + # Added --universal here if you want to support both Intel and Apple Silicon + command: npm run electron:build:mac -- --universal + artifact_pattern: "release/*.dmg" + - os: ubuntu-latest + command: npm run electron:build:linux + artifact_pattern: "release/*.AppImage" + + env: + # This fixes the "GitHub Personal Access Token is not set" error + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Prevents errors related to missing code-signing certificates + CSC_IDENTITY_AUTO_DISCOVERY: false - build-mac: - runs-on: macos-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 20 - - run: npm install - - run: npm run electron:build:mac - env: - CSC_IDENTITY_AUTO_DISCOVERY: false - - uses: softprops/action-gh-release@v2 + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Build Application + run: ${{ matrix.command }} + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 with: - files: releases/*.dmg + name: artifacts-${{ matrix.os }} + path: ${{ matrix.artifact_pattern }} + if-no-files-found: error + + publish: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + # This downloads all "artifacts-*" into a folder named 'all-outputs' + path: all-outputs + merge-multiple: true + + - name: List files for debugging + run: ls -R all-outputs + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + # Point directly to the folder where all OS builds are merged + files: all-outputs/* + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 3571627..117cf03 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,42 @@ npm run electron:build:linux Output is placed in `frontend/release/`. +## Releasing / Tagging + +To create a GitHub release and trigger the CI build pipeline, create an annotated tag and push it to origin. The release workflow runs on tags matching `v*` (for example `v0.6.1`). + +Local tagging example: + +```bash +# update package.json version first if desired +git tag -a v0.6.1 -m "Release v0.6.1" +git push origin v0.6.1 +``` + +What the GitHub Action does (`.github/workflows/release.yml`): + +- Listens for pushed tags `v*` and runs a matrix build across Windows, macOS and Linux. +- macOS is built as a universal binary (`--universal`) so a single DMG supports both Intel and Apple Silicon. +- Each matrix job builds the installer using `electron-builder`, uploads its artifacts, and a final `publish` job aggregates all artifacts into one GitHub release. + +If you prefer to run builds locally before tagging, use the npm scripts in the repository root: + +```bash +# Windows NSIS +npm run electron:build:win + +# macOS DMG (universal) +npm run electron:build:mac -- --universal + +# Linux AppImage +npm run electron:build:linux +``` + +CI notes / troubleshooting +- The workflow uploads artifacts from `release/` into the release. Ensure `package.json` build `directories.output` matches the workflow's expected `release/` folder. +- If mac packaging for x64 on ARM-hosted runners fails, switch to `--universal` (already configured) or build x64 on an Intel runner. +- To test the workflow locally, consider using `nektos/act` or push a temporary tag like `vtest`. + ## Design Tokens All colors are defined in `frontend/tailwind.config.js` and can be referenced by name in any component. diff --git a/package-lock.json b/package-lock.json index fbc5e8c..b0380ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "whattheflat", - "version": "0.1.0", + "name": "jambuddy", + "version": "0.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "whattheflat", - "version": "0.1.0", + "name": "jambuddy", + "version": "0.6.2", "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..fae472b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "whattheflat", + "name": "jambuddy", "description": "A jam session companion app that provides a chromatic tuner, chord progressions, and more.", - "version": "0.6.0", + "version": "0.6.2", "private": true, "type": "module", "main": "electron/main.cjs", @@ -13,7 +13,7 @@ "electron:build": "vite build && electron-builder", "electron:build:win": "vite build && electron-builder --win --publish never", "electron:build:mac": "vite build && electron-builder --mac --publish never", - "electron:build:linux": "vite build && electron-builder --linux" + "electron:build:linux": "vite build && electron-builder --linux --publish never" }, "dependencies": { "audiomotion-analyzer": "^4.5.4", @@ -34,8 +34,8 @@ "wait-on": "^9.0.4" }, "build": { - "appId": "com.whattheflat.app", - "productName": "WhatTheFlat", + "appId": "com.jambuddy.app", + "productName": "JamBuddy", "files": [ "dist/**/*", "electron/**/*", diff --git a/src/App.jsx b/src/App.jsx index 5df8590..2e87c45 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -25,6 +25,8 @@ const DEFAULTS = { // Audio input minClarity: 0.80, minVolume: 0.01, + // Selected device (null = system default) + audioDeviceId: null, } function loadStored(key, fallback) { @@ -514,6 +516,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 ( +