diff --git a/.gitignore b/.gitignore index 787ce99..2b352ba 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ dist/ .DS_Store Thumbs.db -MAIN-APP-REFERENCE/ \ No newline at end of file +MAIN-APP-REFERENCE/ +SecondaryApps/ \ No newline at end of file diff --git a/ClaudePromptExample b/ClaudePromptExample deleted file mode 100644 index 852f9b1..0000000 --- a/ClaudePromptExample +++ /dev/null @@ -1,10 +0,0 @@ -You are working on the bellsystems-cp project at ~/bellsystems-cp/. -Read BellSystems_AdminPanel_Strategy.md for the full project strategy. - -We are now building Phase X — [Phase Name]. - -Review the existing codebase first, then implement the following: -[list of tasks for that phase] - -Ask me before making any major architectural decisions. -Commit when done. \ No newline at end of file diff --git a/VesperPlus.png b/VesperPlus.png deleted file mode 100644 index 57797e1..0000000 Binary files a/VesperPlus.png and /dev/null differ diff --git a/frontend/src/melodies/MelodyComposer.jsx b/frontend/src/melodies/MelodyComposer.jsx index ab1394a..ebdbac6 100644 --- a/frontend/src/melodies/MelodyComposer.jsx +++ b/frontend/src/melodies/MelodyComposer.jsx @@ -6,7 +6,8 @@ const MAX_NOTES = 16; const NOTE_LABELS = "ABCDEFGHIJKLMNOP"; function bellFrequency(bellNumber) { - return 880 * Math.pow(Math.pow(2, 1 / 12), -2 * (bellNumber - 1)); + // One octave every 8 notes. + return 880 * Math.pow(2, -((bellNumber - 1) / 8)); } function stepToNotation(stepValue) { @@ -22,6 +23,14 @@ function stepToHex(stepValue) { return `0x${(stepValue >>> 0).toString(16).toUpperCase().padStart(4, "0")}`; } +function msToBpm(ms) { + return Math.max(1, Math.round(60000 / Math.max(1, Number(ms) || 1))); +} + +function bpmToMs(bpm) { + return Math.max(20, Math.round(60000 / Math.max(1, Number(bpm) || 1))); +} + function interpolateHue(t) { const stops = [ [0.0, 190], @@ -103,6 +112,7 @@ export default function MelodyComposer() { const [noteCount, setNoteCount] = useState(8); const [stepDelayMs, setStepDelayMs] = useState(280); const [noteDurationMs, setNoteDurationMs] = useState(110); + const [measureEvery, setMeasureEvery] = useState(0); const [loopEnabled, setLoopEnabled] = useState(true); const [isPlaying, setIsPlaying] = useState(false); const [currentStep, setCurrentStep] = useState(-1); @@ -114,6 +124,7 @@ export default function MelodyComposer() { const [deployError, setDeployError] = useState(""); const [deploying, setDeploying] = useState(false); const [noteColors, setNoteColors] = useState([]); + const [stepMenuIndex, setStepMenuIndex] = useState(null); const audioCtxRef = useRef(null); const playbackRef = useRef(null); @@ -121,6 +132,7 @@ export default function MelodyComposer() { const stepDelayRef = useRef(stepDelayMs); const noteDurationRef = useRef(noteDurationMs); const loopEnabledRef = useRef(loopEnabled); + const stepMenuRef = useRef(null); useEffect(() => { stepsRef.current = steps; @@ -137,6 +149,16 @@ export default function MelodyComposer() { useEffect(() => { loopEnabledRef.current = loopEnabled; }, [loopEnabled]); + useEffect(() => { + if (stepMenuIndex == null) return undefined; + const onDocClick = (e) => { + if (stepMenuRef.current && !stepMenuRef.current.contains(e.target)) { + setStepMenuIndex(null); + } + }; + document.addEventListener("mousedown", onDocClick); + return () => document.removeEventListener("mousedown", onDocClick); + }, [stepMenuIndex]); const ensureAudioContext = useCallback(() => { if (!audioCtxRef.current || audioCtxRef.current.state === "closed") { @@ -238,6 +260,28 @@ export default function MelodyComposer() { }; const clearAll = () => setSteps((prev) => prev.map(() => 0)); + const insertStepAt = (index) => { + setSteps((prev) => { + const next = [...prev]; + next.splice(index, 0, 0); + return next; + }); + setCurrentStep((prev) => (prev >= index ? prev + 1 : prev)); + }; + const deleteStepAt = (index) => { + setSteps((prev) => { + if (prev.length <= 1) return prev; + const next = [...prev]; + next.splice(index, 1); + return next; + }); + setCurrentStep((prev) => { + if (prev < 0) return prev; + if (prev === index) return -1; + if (prev > index) return prev - 1; + return prev; + }); + }; const handlePlay = () => { if (!stepsRef.current.length) return; @@ -317,6 +361,9 @@ export default function MelodyComposer() { } return active; }, [currentStep, noteCount, steps]); + const speedBpm = msToBpm(stepDelayMs); + const measureChoices = [0, 4, 8, 16, 32]; + const measureSliderIdx = Math.max(0, measureChoices.indexOf(measureEvery)); return (
+ Playing step {currentStep + 1}/{steps.length} + {activeBellsInCurrentStep.length ? ` (bells: ${activeBellsInCurrentStep.join(", ")})` : " (silence)"} +
+ )}- Playing step {currentStep + 1}/{steps.length} - {activeBellsInCurrentStep.length ? ` (bells: ${activeBellsInCurrentStep.join(", ")})` : " (silence)"} -
- )}CSV-like notation
+Generated CSV notation
PROGMEM values
+Generated PROGMEM Values
{hasAnyFilter
- ? `Filtered - Showing ${displayRows.length} / ${allMelodyCount || melodies.length} Melodies | ${offlineTaggedCount} Melodies tagged for Offline`
+ ? `Filtered - Showing ${displayRows.length} / ${allMelodyCount || melodies.length} Melodies | ${offlineTaggedCount} Offline-tagged`
: `Showing all (${allMelodyCount || melodies.length}) Melodies | ${offlineTaggedCount} Melodies tagged for Offline`}
{canEdit && (