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 (
@@ -359,153 +406,80 @@ export default function MelodyComposer() { style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }} >
- - + + | - - + + | - - +
-
- {steps.length} steps, {noteCount} notes -
- +
{steps.length} steps, {noteCount} notes
+
- -
-
-
- {!isPlaying ? ( +
+
+
+ {!isPlaying ? ( + + ) : ( + + )} - ) : ( - - )} - - | -
- -
-
-
- - - {stepDelayMs} ms - -
- setStepDelayMs(Number(e.target.value))} - className="w-full mt-2" - /> + |
-
-
- - - {noteDurationMs} ms - +
+
+
+ + + {speedBpm} bpm + {stepDelayMs} ms + +
+ setStepDelayMs(bpmToMs(Number(e.target.value)))} className="w-full mt-2" /> +
+ +
+
+ + {noteDurationMs} ms +
+ setNoteDurationMs(Number(e.target.value))} className="w-full mt-2" /> +
+ + | +
+
+ + {measureEvery || "Off"} +
+ setMeasureEvery(measureChoices[Number(e.target.value)] || 0)} className="w-full mt-2" />
- setNoteDurationMs(Number(e.target.value))} - className="w-full mt-2" - />
- + {currentStep >= 0 && ( +

+ Playing step {currentStep + 1}/{steps.length} + {activeBellsInCurrentStep.length ? ` (bells: ${activeBellsInCurrentStep.join(", ")})` : " (silence)"} +

+ )}
- - {currentStep >= 0 && ( -

- Playing step {currentStep + 1}/{steps.length} - {activeBellsInCurrentStep.length ? ` (bells: ${activeBellsInCurrentStep.join(", ")})` : " (silence)"} -

- )}
{steps.map((_, stepIndex) => { const isCurrent = stepIndex === currentStep; + const measureHit = measureEvery > 0 && (stepIndex + 1) % measureEvery === 0; + const measureBlockOdd = measureEvery > 0 && Math.floor(stepIndex / measureEvery) % 2 === 1; return ( - {stepIndex + 1} + + {stepMenuIndex === stepIndex && ( +
+ + + +
+ )} ); })} @@ -557,13 +578,18 @@ export default function MelodyComposer() { {steps.map((stepValue, stepIndex) => { const enabled = Boolean(stepValue & (1 << noteIndex)); const isCurrent = stepIndex === currentStep; + const measureHit = measureEvery > 0 && (stepIndex + 1) % measureEvery === 0; + const measureBlockOdd = measureEvery > 0 && Math.floor(stepIndex / measureEvery) % 2 === 1; return ( -

- Generated Output Preview -

-

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 && (