From 17ff36bdfc476a1c314ab8232b257cdefbe51446 Mon Sep 17 00:00:00 2001 From: Oscar Date: Fri, 26 Jul 2024 14:23:37 +0200 Subject: [PATCH] [MOD] Migrate to Vite --- compress-cra.json | 12 - package.json | 3 +- postcss.config.cjs | 3 + postcss.config.js | 7 - src/{App.js => App.jsx} | 0 .../controls/{Dropdown.js => Dropdown.jsx} | 80 ++-- .../controls/{Keyboard.js => Keyboard.jsx} | 162 ++++---- src/components/controls/{Knob.js => Knob.jsx} | 104 ++--- .../controls/{Octave.js => Octave.jsx} | 46 +-- .../controls/{Selector.js => Selector.jsx} | 116 +++--- .../controls/{SeqInput.js => SeqInput.jsx} | 88 ++--- .../controls/{Slider.js => Slider.jsx} | 136 +++---- .../controls/{Switch.js => Switch.jsx} | 104 ++--- .../controls/{Tempo.js => Tempo.jsx} | 128 +++--- .../controls/{Wheel.js => Wheel.jsx} | 144 +++---- .../layout/{Display.js => Display.jsx} | 260 ++++++------ .../layout/{Footer.js => Footer.jsx} | 72 ++-- .../layout/{Header.js => Header.jsx} | 370 +++++++++--------- .../layout/{Memory.js => Memory.jsx} | 34 +- .../layout/{Section.js => Section.jsx} | 158 ++++---- .../screens/{Message.js => Message.jsx} | 18 +- .../screens/{Sequencer.js => Sequencer.jsx} | 88 ++--- src/hooks/{useLayout.js => useLayout.jsx} | 244 ++++++------ src/hooks/{useMidi.js => useMidi.jsx} | 294 +++++++------- src/hooks/{useNTS.js => useNTS.jsx} | 338 ++++++++-------- .../{useSequencer.js => useSequencer.jsx} | 236 +++++------ src/{index.js => index.jsx} | 0 src/views/{Live.js => Live.jsx} | 80 ++-- src/views/{Synth.js => Synth.jsx} | 58 +-- src/views/modals/{Banks.js => Banks.jsx} | 80 ++-- src/views/modals/{Info.js => Info.jsx} | 168 ++++---- .../modals/{Settings.js => Settings.jsx} | 140 +++---- vite.config.js | 93 +++++ 33 files changed, 1971 insertions(+), 1893 deletions(-) delete mode 100644 compress-cra.json create mode 100644 postcss.config.cjs delete mode 100644 postcss.config.js rename src/{App.js => App.jsx} (100%) rename src/components/controls/{Dropdown.js => Dropdown.jsx} (98%) rename src/components/controls/{Keyboard.js => Keyboard.jsx} (98%) rename src/components/controls/{Knob.js => Knob.jsx} (97%) rename src/components/controls/{Octave.js => Octave.jsx} (98%) rename src/components/controls/{Selector.js => Selector.jsx} (97%) rename src/components/controls/{SeqInput.js => SeqInput.jsx} (97%) rename src/components/controls/{Slider.js => Slider.jsx} (97%) rename src/components/controls/{Switch.js => Switch.jsx} (97%) rename src/components/controls/{Tempo.js => Tempo.jsx} (98%) rename src/components/controls/{Wheel.js => Wheel.jsx} (97%) rename src/components/layout/{Display.js => Display.jsx} (98%) rename src/components/layout/{Footer.js => Footer.jsx} (98%) rename src/components/layout/{Header.js => Header.jsx} (98%) rename src/components/layout/{Memory.js => Memory.jsx} (97%) rename src/components/layout/{Section.js => Section.jsx} (97%) rename src/components/screens/{Message.js => Message.jsx} (96%) rename src/components/screens/{Sequencer.js => Sequencer.jsx} (98%) rename src/hooks/{useLayout.js => useLayout.jsx} (97%) rename src/hooks/{useMidi.js => useMidi.jsx} (97%) rename src/hooks/{useNTS.js => useNTS.jsx} (97%) rename src/hooks/{useSequencer.js => useSequencer.jsx} (97%) rename src/{index.js => index.jsx} (100%) rename src/views/{Live.js => Live.jsx} (97%) rename src/views/{Synth.js => Synth.jsx} (97%) rename src/views/modals/{Banks.js => Banks.jsx} (97%) rename src/views/modals/{Info.js => Info.jsx} (98%) rename src/views/modals/{Settings.js => Settings.jsx} (97%) create mode 100644 vite.config.js diff --git a/compress-cra.json b/compress-cra.json deleted file mode 100644 index b9bbb10..0000000 --- a/compress-cra.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "algorithms": ["br", "gz"], - "filetypes": [ - ".html", - ".js", - ".css", - ".svg", - ".png", - ".tff" - ], - "directory": "/build" -} \ No newline at end of file diff --git a/package.json b/package.json index f22c1d0..f21837c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "nts-web", "description": "A web controller for the Korg NTS-1", - "version": "3.0.1", + "version": "4.0.0", + "type": "module", "author": { "name": "Oscar R.C.", "email": "oscarrc.web@gmail.com", diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..552dbe0 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("tailwindcss"), require("autoprefixer")], +}; \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index dbb4fda..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: { - 'postcss-import': {}, - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/src/App.js b/src/App.jsx similarity index 100% rename from src/App.js rename to src/App.jsx diff --git a/src/components/controls/Dropdown.js b/src/components/controls/Dropdown.jsx similarity index 98% rename from src/components/controls/Dropdown.js rename to src/components/controls/Dropdown.jsx index 014897f..a21dc04 100644 --- a/src/components/controls/Dropdown.js +++ b/src/components/controls/Dropdown.jsx @@ -1,41 +1,41 @@ -import { SlArrowDown } from "react-icons/sl"; -import switchButton from "../../assets/switch.png"; - -const Dropdown = ({ id, label, value = 0, options, switchValue, isActive, onChange, onSwitch }) => { - const handleSelection = (i) => { - document.activeElement.blur() - onChange && onChange(i) - } - - return ( -
- { label && } - { !isNaN(switchValue) && - onSwitch(e.target.checked) } - checked={ isActive || false } - className="input-switch" - data-diameter="60" - data-src={ switchButton } - /> - } -
- -
    - { - options?.map((option, index) => { - return
  • handleSelection(index) } className="px-2 py-1 cursor-pointer hover:bg-base-100 w-full">{ option }
  • - }) - } -
-
-
- ) -} - +import { SlArrowDown } from "react-icons/sl"; +import switchButton from "../../assets/switch.png"; + +const Dropdown = ({ id, label, value = 0, options, switchValue, isActive, onChange, onSwitch }) => { + const handleSelection = (i) => { + document.activeElement.blur() + onChange && onChange(i) + } + + return ( +
+ { label && } + { !isNaN(switchValue) && + onSwitch(e.target.checked) } + checked={ isActive || false } + className="input-switch" + data-diameter="60" + data-src={ switchButton } + /> + } +
+ +
    + { + options?.map((option, index) => { + return
  • handleSelection(index) } className="px-2 py-1 cursor-pointer hover:bg-base-100 w-full">{ option }
  • + }) + } +
+
+
+ ) +} + export default Dropdown; \ No newline at end of file diff --git a/src/components/controls/Keyboard.js b/src/components/controls/Keyboard.jsx similarity index 98% rename from src/components/controls/Keyboard.js rename to src/components/controls/Keyboard.jsx index 02f68b0..e2c2959 100644 --- a/src/components/controls/Keyboard.js +++ b/src/components/controls/Keyboard.jsx @@ -1,82 +1,82 @@ -import { Fragment, useEffect, useRef, useState } from "react" - -import { octaveLayout } from "../../config/layout"; -import { useLayout } from "../../hooks/useLayout" -import { useMidi } from "../../hooks/useMidi"; -import { useSequencer } from "../../hooks/useSequencer"; - -const Keyboard = () => { - const { breakpoint } = useLayout(); - const { playNote, octave, input, passthrough } = useMidi(); - const { isRecording, stepStart, stepEnd } = useSequencer(); - const [ octaves, setOctaves ] = useState( octaveLayout[breakpoint] ); - const [ activeNote, setActiveNote ] = useState(null); - - const keyboardRef = useRef(null); - const notes = ["C", "D", "E", "F", "G", "A", "B"] - - const onPlayStart = (e) => { - window.navigator.vibrate && window.navigator.vibrate(10); - isRecording && stepStart(e.target.dataset.note, 0); - playNote(e.target.dataset.note); - } - - const onPlayEnd = (e) => { - isRecording && stepEnd(); - playNote(e.target.dataset.note, false); - e.target.blur(); - } - - const setNote = (e) => { - const note = `${e.note._name}${!!e.note._accidental ? e.note._accidental : ""}${e.note._octave}` - if(e.type === "noteon") setActiveNote(note) - else setActiveNote(false) - } - - const events = { - onTouchStart: onPlayStart, - onTouchEnd: onPlayEnd, - onMouseDown: onPlayStart, - onMouseUp: onPlayEnd, - onMouseLeave: onPlayEnd, - onTouchCancel: onPlayEnd - } - - useEffect(() => { - setOctaves(octaveLayout[breakpoint]) - }, [breakpoint]); - - useEffect(() => { - !input?.hasListener("noteon", setNote) && input?.addListener("noteon", setNote) - !input?.hasListener("noteoff", setNote) && input?.addListener("noteoff", setNote) - !passthrough?.hasListener("noteon", setNote) && passthrough?.addListener("noteon", setNote) - !passthrough?.hasListener("noteoff", setNote) && passthrough?.addListener("noteoff", setNote) - - return () => { - input?.hasListener("noteon", setNote) && input?.removeListener("noteon", setNote) - input?.hasListener("noteoff", setNote) && input?.removeListener("noteoff", setNote) - passthrough?.hasListener("noteon", setNote) && passthrough?.removeListener("noteon", setNote) - passthrough?.hasListener("noteoff", setNote) && passthrough?.removeListener("noteoff", setNote) - } - }, [input, passthrough]) - - return ( -
- { - [...Array(octaves).keys()].map((o) => { - return [...Array(7).keys()].map((n) => { - return ( - - { [1,2,4,5,6].includes(n + 1) && } - - - ) - }) - }) - } - { octaves && } -
- ) -} - +import { Fragment, useEffect, useRef, useState } from "react" + +import { octaveLayout } from "../../config/layout"; +import { useLayout } from "../../hooks/useLayout" +import { useMidi } from "../../hooks/useMidi"; +import { useSequencer } from "../../hooks/useSequencer"; + +const Keyboard = () => { + const { breakpoint } = useLayout(); + const { playNote, octave, input, passthrough } = useMidi(); + const { isRecording, stepStart, stepEnd } = useSequencer(); + const [ octaves, setOctaves ] = useState( octaveLayout[breakpoint] ); + const [ activeNote, setActiveNote ] = useState(null); + + const keyboardRef = useRef(null); + const notes = ["C", "D", "E", "F", "G", "A", "B"] + + const onPlayStart = (e) => { + window.navigator.vibrate && window.navigator.vibrate(10); + isRecording && stepStart(e.target.dataset.note, 0); + playNote(e.target.dataset.note); + } + + const onPlayEnd = (e) => { + isRecording && stepEnd(); + playNote(e.target.dataset.note, false); + e.target.blur(); + } + + const setNote = (e) => { + const note = `${e.note._name}${!!e.note._accidental ? e.note._accidental : ""}${e.note._octave}` + if(e.type === "noteon") setActiveNote(note) + else setActiveNote(false) + } + + const events = { + onTouchStart: onPlayStart, + onTouchEnd: onPlayEnd, + onMouseDown: onPlayStart, + onMouseUp: onPlayEnd, + onMouseLeave: onPlayEnd, + onTouchCancel: onPlayEnd + } + + useEffect(() => { + setOctaves(octaveLayout[breakpoint]) + }, [breakpoint]); + + useEffect(() => { + !input?.hasListener("noteon", setNote) && input?.addListener("noteon", setNote) + !input?.hasListener("noteoff", setNote) && input?.addListener("noteoff", setNote) + !passthrough?.hasListener("noteon", setNote) && passthrough?.addListener("noteon", setNote) + !passthrough?.hasListener("noteoff", setNote) && passthrough?.addListener("noteoff", setNote) + + return () => { + input?.hasListener("noteon", setNote) && input?.removeListener("noteon", setNote) + input?.hasListener("noteoff", setNote) && input?.removeListener("noteoff", setNote) + passthrough?.hasListener("noteon", setNote) && passthrough?.removeListener("noteon", setNote) + passthrough?.hasListener("noteoff", setNote) && passthrough?.removeListener("noteoff", setNote) + } + }, [input, passthrough]) + + return ( +
+ { + [...Array(octaves).keys()].map((o) => { + return [...Array(7).keys()].map((n) => { + return ( + + { [1,2,4,5,6].includes(n + 1) && } + + + ) + }) + }) + } + { octaves && } +
+ ) +} + export default Keyboard \ No newline at end of file diff --git a/src/components/controls/Knob.js b/src/components/controls/Knob.jsx similarity index 97% rename from src/components/controls/Knob.js rename to src/components/controls/Knob.jsx index 60420c3..491d990 100644 --- a/src/components/controls/Knob.js +++ b/src/components/controls/Knob.jsx @@ -1,53 +1,53 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import knob from "../../assets/knob.png"; - -const Knob = ({id, value = 0, min = 0, max = 127, step = 1, label, onChange}) => { - const [ currentValue, setValue ] = useState(value ? value : min); - const knobRef = useRef(null); - - const handleValue = useCallback((e) => { - setValue(e.target.value); - onChange && onChange(parseInt(e.target.value)); - }, [onChange]) - - useEffect(() => setValue(value), [value]); - - useEffect(() => { - const currentKnob = knobRef.current; - - currentKnob.addEventListener("change", handleValue) - currentKnob.addEventListener("input", handleValue) - - return () => { - currentKnob.removeEventListener("change", handleValue) - currentKnob.removeEventListener("input", handleValue) - } - }, [handleValue]) - - return ( -
- { label && } -
- -
-
{ value }
-
- ) -} - +import { useCallback, useEffect, useRef, useState } from "react"; + +import knob from "../../assets/knob.png"; + +const Knob = ({id, value = 0, min = 0, max = 127, step = 1, label, onChange}) => { + const [ currentValue, setValue ] = useState(value ? value : min); + const knobRef = useRef(null); + + const handleValue = useCallback((e) => { + setValue(e.target.value); + onChange && onChange(parseInt(e.target.value)); + }, [onChange]) + + useEffect(() => setValue(value), [value]); + + useEffect(() => { + const currentKnob = knobRef.current; + + currentKnob.addEventListener("change", handleValue) + currentKnob.addEventListener("input", handleValue) + + return () => { + currentKnob.removeEventListener("change", handleValue) + currentKnob.removeEventListener("input", handleValue) + } + }, [handleValue]) + + return ( +
+ { label && } +
+ +
+
{ value }
+
+ ) +} + export default Knob; \ No newline at end of file diff --git a/src/components/controls/Octave.js b/src/components/controls/Octave.jsx similarity index 98% rename from src/components/controls/Octave.js rename to src/components/controls/Octave.jsx index e0370c7..3a4d924 100644 --- a/src/components/controls/Octave.js +++ b/src/components/controls/Octave.jsx @@ -1,24 +1,24 @@ -import { FaCaretDown, FaCaretUp } from "react-icons/fa"; - -const Octave = ({ octave, setOctave }) => { - const handleClick = (q) => { - if (window.navigator.vibrate){ - let vibration = (q < 0 && octave > 0) || (q > 0 && octave < 6) ? 10 : 50 - window.navigator.vibrate(vibration); - } - if(q < 0 && octave > 0) setOctave(o => o - 1) - if(q > 0 && octave < 11) setOctave(o => o + 1) - } - - return ( -
- -
- { octave } -
- -
- ) -} - +import { FaCaretDown, FaCaretUp } from "react-icons/fa"; + +const Octave = ({ octave, setOctave }) => { + const handleClick = (q) => { + if (window.navigator.vibrate){ + let vibration = (q < 0 && octave > 0) || (q > 0 && octave < 6) ? 10 : 50 + window.navigator.vibrate(vibration); + } + if(q < 0 && octave > 0) setOctave(o => o - 1) + if(q > 0 && octave < 11) setOctave(o => o + 1) + } + + return ( +
+ +
+ { octave } +
+ +
+ ) +} + export default Octave; \ No newline at end of file diff --git a/src/components/controls/Selector.js b/src/components/controls/Selector.jsx similarity index 97% rename from src/components/controls/Selector.js rename to src/components/controls/Selector.jsx index 08ec253..aeeaab2 100644 --- a/src/components/controls/Selector.js +++ b/src/components/controls/Selector.jsx @@ -1,59 +1,59 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import selector from "../../assets/selector.png"; - -const Selector = ({id, value = 0, options, label, onChange, display}) => { - const [ currentValue, setValue ] = useState(value ? value : 0); - const selectorRef = useRef(null); - - const handleValue = useCallback((e) => { - // window.navigator.vibrate && window.navigator.vibrate(10); - setValue(e.target.value); - onChange && onChange(parseInt(e.target.value)); - }, [onChange]) - - useEffect(() => setValue(value), [value]); - useEffect(() => { // TODO: debug selector not being updated en realtime - if(selectorRef.current) selectorRef.current.max = options.length - 1; - if(typeof selectorRef?.current?.refresh === "function") selectorRef.current?.refresh() - if(typeof selectorRef?.current?.redraw === "function") selectorRef.current?.redraw(); - }, [options.length, selectorRef.current?.max]); - - useEffect(() => { - const currentKnob = selectorRef.current; - - currentKnob.addEventListener("change", handleValue) - currentKnob.addEventListener("input", handleValue) - - return () => { - currentKnob.removeEventListener("change", handleValue) - currentKnob.removeEventListener("input", handleValue) - } - }, [handleValue]) - - return ( -
- { label && } -
- -
- { display &&
{ display }
} -
- ) -} - +import { useCallback, useEffect, useRef, useState } from "react"; + +import selector from "../../assets/selector.png"; + +const Selector = ({id, value = 0, options, label, onChange, display}) => { + const [ currentValue, setValue ] = useState(value ? value : 0); + const selectorRef = useRef(null); + + const handleValue = useCallback((e) => { + // window.navigator.vibrate && window.navigator.vibrate(10); + setValue(e.target.value); + onChange && onChange(parseInt(e.target.value)); + }, [onChange]) + + useEffect(() => setValue(value), [value]); + useEffect(() => { // TODO: debug selector not being updated en realtime + if(selectorRef.current) selectorRef.current.max = options.length - 1; + if(typeof selectorRef?.current?.refresh === "function") selectorRef.current?.refresh() + if(typeof selectorRef?.current?.redraw === "function") selectorRef.current?.redraw(); + }, [options.length, selectorRef.current?.max]); + + useEffect(() => { + const currentKnob = selectorRef.current; + + currentKnob.addEventListener("change", handleValue) + currentKnob.addEventListener("input", handleValue) + + return () => { + currentKnob.removeEventListener("change", handleValue) + currentKnob.removeEventListener("input", handleValue) + } + }, [handleValue]) + + return ( +
+ { label && } +
+ +
+ { display &&
{ display }
} +
+ ) +} + export default Selector; \ No newline at end of file diff --git a/src/components/controls/SeqInput.js b/src/components/controls/SeqInput.jsx similarity index 97% rename from src/components/controls/SeqInput.js rename to src/components/controls/SeqInput.jsx index a300346..9134299 100644 --- a/src/components/controls/SeqInput.js +++ b/src/components/controls/SeqInput.jsx @@ -1,45 +1,45 @@ -import { BsCaretDownFill, BsCaretUpFill } from "react-icons/bs"; - -const SeqInput = ({label, value, options, validation, min, max, onChange, className}) => { - const handleValue = (val) => { - if(options && isNaN(val)) value = min; - if(!isNaN(min) && parseInt(val) < min ) val = min; - if(!isNaN(max) && parseInt(val) > max) val = max; - onChange && onChange(val); - } - - const handleValidation = (val) => { - if(!validation) return; - const regex = new RegExp(validation, 'g'); - !regex.test(val) && onChange() - } - - return ( -
- handleValue(e.target.value) } - onBlur={ (e) => handleValidation(e.target.value) } - disabled={options} - /> - { - (options?.length || (!isNaN(min) && !isNaN(max))) && -
- - -
- } -
- ) -} - +import { BsCaretDownFill, BsCaretUpFill } from "react-icons/bs"; + +const SeqInput = ({label, value, options, validation, min, max, onChange, className}) => { + const handleValue = (val) => { + if(options && isNaN(val)) value = min; + if(!isNaN(min) && parseInt(val) < min ) val = min; + if(!isNaN(max) && parseInt(val) > max) val = max; + onChange && onChange(val); + } + + const handleValidation = (val) => { + if(!validation) return; + const regex = new RegExp(validation, 'g'); + !regex.test(val) && onChange() + } + + return ( +
+ handleValue(e.target.value) } + onBlur={ (e) => handleValidation(e.target.value) } + disabled={options} + /> + { + (options?.length || (!isNaN(min) && !isNaN(max))) && +
+ + +
+ } +
+ ) +} + export default SeqInput; \ No newline at end of file diff --git a/src/components/controls/Slider.js b/src/components/controls/Slider.jsx similarity index 97% rename from src/components/controls/Slider.js rename to src/components/controls/Slider.jsx index 8b16434..e488bc3 100644 --- a/src/components/controls/Slider.js +++ b/src/components/controls/Slider.jsx @@ -1,69 +1,69 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -const Slider = ({id, value = 50, min = 0, max = 100, step = 1, defaultValue = 0, autoReturn = false, label, onChange}) => { - const [ currentValue, setValue ] = useState(value ? value : (max - min) / 2 ); - const sliderRef = useRef(null); - - const handleValue = useCallback((e) => { - if (window.navigator.vibrate) window.navigator.vibrate(10); - setValue(e.target.value); - onChange && onChange(e.target.value); - },[onChange]) - - const resetValue = useCallback( (e) => { - setValue(defaultValue); - onChange && onChange(defaultValue); - }, [defaultValue, onChange]); - - useEffect(() => { setValue(value) }, [value]); - - useEffect(() => { - if(!autoReturn) return; - - const currentSlider = sliderRef.current; - currentSlider.addEventListener("mouseup", resetValue); - currentSlider.addEventListener("mouseleave", resetValue); - currentSlider.addEventListener("touchend", resetValue); - currentSlider.addEventListener("touchcancel", resetValue); - - return () => { - currentSlider.removeEventListener("mouseup", resetValue); - currentSlider.removeEventListener("mouseleave", resetValue); - currentSlider.removeEventListener("touchstart", resetValue); - currentSlider.addEventListener("touchcancel", resetValue); - } - }, [autoReturn, resetValue]) - - useEffect(() => { - const currentSlider = sliderRef.current; - - currentSlider.addEventListener("change", handleValue) - currentSlider.addEventListener("input", handleValue) - - return () => { - currentSlider.removeEventListener("change", handleValue) - currentSlider.removeEventListener("input", handleValue) - } - }, [handleValue]) - - - return ( - - { label && } - - - ) -} - +import { useCallback, useEffect, useRef, useState } from "react"; + +const Slider = ({id, value = 50, min = 0, max = 100, step = 1, defaultValue = 0, autoReturn = false, label, onChange}) => { + const [ currentValue, setValue ] = useState(value ? value : (max - min) / 2 ); + const sliderRef = useRef(null); + + const handleValue = useCallback((e) => { + if (window.navigator.vibrate) window.navigator.vibrate(10); + setValue(e.target.value); + onChange && onChange(e.target.value); + },[onChange]) + + const resetValue = useCallback( (e) => { + setValue(defaultValue); + onChange && onChange(defaultValue); + }, [defaultValue, onChange]); + + useEffect(() => { setValue(value) }, [value]); + + useEffect(() => { + if(!autoReturn) return; + + const currentSlider = sliderRef.current; + currentSlider.addEventListener("mouseup", resetValue); + currentSlider.addEventListener("mouseleave", resetValue); + currentSlider.addEventListener("touchend", resetValue); + currentSlider.addEventListener("touchcancel", resetValue); + + return () => { + currentSlider.removeEventListener("mouseup", resetValue); + currentSlider.removeEventListener("mouseleave", resetValue); + currentSlider.removeEventListener("touchstart", resetValue); + currentSlider.addEventListener("touchcancel", resetValue); + } + }, [autoReturn, resetValue]) + + useEffect(() => { + const currentSlider = sliderRef.current; + + currentSlider.addEventListener("change", handleValue) + currentSlider.addEventListener("input", handleValue) + + return () => { + currentSlider.removeEventListener("change", handleValue) + currentSlider.removeEventListener("input", handleValue) + } + }, [handleValue]) + + + return ( + + { label && } + + + ) +} + export default Slider; \ No newline at end of file diff --git a/src/components/controls/Switch.js b/src/components/controls/Switch.jsx similarity index 97% rename from src/components/controls/Switch.js rename to src/components/controls/Switch.jsx index c165c58..f58ea56 100644 --- a/src/components/controls/Switch.js +++ b/src/components/controls/Switch.jsx @@ -1,53 +1,53 @@ -import { useCallback, useEffect, useRef } from "react"; - -import switchButton from "../../assets/switch.png"; - -const Switch = ({ id, isActive = false, isMomentary = false, label, inline, onChange }) => { - - const switchRef = useRef(null); - - const toggle = useCallback((e) => { - switchRef.current.checked = !switchRef.current.checked; - onChange && ["mouseup","touchend"].includes(e.type) && onChange(switchRef.current.checked); - }, [onChange]) - - const handleValue = (e) => { - onChange(e.target.checked); - } - - useEffect(() => { - if(!isMomentary) return; - const currentSwitch = switchRef.current; - currentSwitch.addEventListener("mousedown", toggle); - currentSwitch.addEventListener("mouseup", toggle); - currentSwitch.addEventListener("touchstart", toggle); - currentSwitch.addEventListener("touchend", toggle); - - return () => { - currentSwitch.removeEventListener("mousedown", toggle); - currentSwitch.removeEventListener("mouseup", toggle); - currentSwitch.removeEventListener("touchstart", toggle); - currentSwitch.removeEventListener("touchend", toggle); - } - }, [isMomentary, toggle]) - - return ( -
- { label && } -
- -
-
- ) -} - +import { useCallback, useEffect, useRef } from "react"; + +import switchButton from "../../assets/switch.png"; + +const Switch = ({ id, isActive = false, isMomentary = false, label, inline, onChange }) => { + + const switchRef = useRef(null); + + const toggle = useCallback((e) => { + switchRef.current.checked = !switchRef.current.checked; + onChange && ["mouseup","touchend"].includes(e.type) && onChange(switchRef.current.checked); + }, [onChange]) + + const handleValue = (e) => { + onChange(e.target.checked); + } + + useEffect(() => { + if(!isMomentary) return; + const currentSwitch = switchRef.current; + currentSwitch.addEventListener("mousedown", toggle); + currentSwitch.addEventListener("mouseup", toggle); + currentSwitch.addEventListener("touchstart", toggle); + currentSwitch.addEventListener("touchend", toggle); + + return () => { + currentSwitch.removeEventListener("mousedown", toggle); + currentSwitch.removeEventListener("mouseup", toggle); + currentSwitch.removeEventListener("touchstart", toggle); + currentSwitch.removeEventListener("touchend", toggle); + } + }, [isMomentary, toggle]) + + return ( +
+ { label && } +
+ +
+
+ ) +} + export default Switch; \ No newline at end of file diff --git a/src/components/controls/Tempo.js b/src/components/controls/Tempo.jsx similarity index 98% rename from src/components/controls/Tempo.js rename to src/components/controls/Tempo.jsx index 9ca47d3..6f1e71a 100644 --- a/src/components/controls/Tempo.js +++ b/src/components/controls/Tempo.jsx @@ -1,65 +1,65 @@ -import { BsDash, BsPlus } from "react-icons/bs"; - -const Tempo = ({ tempo, metronome, barLength, onTempoChange, onBarChange, onToggle }) => { - const setTempo = (e) => { - const value = parseInt(e) - if(typeof value === "number" && value > 0) onTempoChange(value) - } - - const setBar = (e) => { - const value = parseInt(e) - if(typeof value === "number" && value > 0) onBarChange(value) - } - - const toggleMetronome = (e) => { - onToggle(!metronome) - } - - return ( -
-
- -
- - setTempo(e.target.value) } - className="bg-neutral w-full text-center bg-grid font-sevenSegment text-2xl text-accent outline focus:outline-base-100 focus:outline-offset-1 focus:outline-1 focus:border-none border-none focus:ring-0 outline-base-100 outline-offset-1 outline-1 px-2" - /> - -
-
-
- -
- - setBar(e.target.value) } - className="bg-neutral w-full text-center bg-grid font-sevenSegment text-2xl text-accent outline focus:outline-base-100 focus:outline-offset-1 focus:outline-1 focus:border-none border-none focus:ring-0 outline-base-100 outline-offset-1 outline-1 px-2" - /> - -
-
-
- -
-
- ) -} - +import { BsDash, BsPlus } from "react-icons/bs"; + +const Tempo = ({ tempo, metronome, barLength, onTempoChange, onBarChange, onToggle }) => { + const setTempo = (e) => { + const value = parseInt(e) + if(typeof value === "number" && value > 0) onTempoChange(value) + } + + const setBar = (e) => { + const value = parseInt(e) + if(typeof value === "number" && value > 0) onBarChange(value) + } + + const toggleMetronome = (e) => { + onToggle(!metronome) + } + + return ( +
+
+ +
+ + setTempo(e.target.value) } + className="bg-neutral w-full text-center bg-grid font-sevenSegment text-2xl text-accent outline focus:outline-base-100 focus:outline-offset-1 focus:outline-1 focus:border-none border-none focus:ring-0 outline-base-100 outline-offset-1 outline-1 px-2" + /> + +
+
+
+ +
+ + setBar(e.target.value) } + className="bg-neutral w-full text-center bg-grid font-sevenSegment text-2xl text-accent outline focus:outline-base-100 focus:outline-offset-1 focus:outline-1 focus:border-none border-none focus:ring-0 outline-base-100 outline-offset-1 outline-1 px-2" + /> + +
+
+
+ +
+
+ ) +} + export default Tempo; \ No newline at end of file diff --git a/src/components/controls/Wheel.js b/src/components/controls/Wheel.jsx similarity index 97% rename from src/components/controls/Wheel.js rename to src/components/controls/Wheel.jsx index 0944b47..ce91194 100644 --- a/src/components/controls/Wheel.js +++ b/src/components/controls/Wheel.jsx @@ -1,73 +1,73 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -import wheel from "../../assets/wheel.png"; - -const Wheel = ({id, defaultValue = 50, min = 0, max = 100, step = 1, autoReturn = false, label, onChange}) => { - const [ value, setValue ] = useState(defaultValue ? defaultValue : (max - min) / 2 ); - const wheelRef = useRef(null); - - const handleValue = useCallback((e) => { - if (window.navigator.vibrate) window.navigator.vibrate(5); - setValue(e.target.value); - onChange(e.target.value); - }, [onChange]) - - const resetValue = useCallback( () => { - setValue(defaultValue); - }, [defaultValue]); - - useEffect(() => { - // onChange(value) - }, [value, onChange]) - - useEffect(() => { - if(!autoReturn) return; - - const currentWheel = wheelRef.current; - currentWheel.addEventListener("mouseup", resetValue); - currentWheel.addEventListener("mouseleave", resetValue); - currentWheel.addEventListener("touchend", resetValue); - - return () => { - currentWheel.removeEventListener("mouseup", resetValue); - currentWheel.removeEventListener("mouseleave", resetValue); - currentWheel.removeEventListener("touchstart", resetValue); - } - }, [autoReturn, resetValue]) - - useEffect(() => { - const currentWheel = wheelRef.current; - - currentWheel.addEventListener("change", handleValue) - currentWheel.addEventListener("input", handleValue) - - return () => { - currentWheel.removeEventListener("change", handleValue) - currentWheel.removeEventListener("input", handleValue) - } - }, [handleValue]) - - - return ( - - { label && } - - - ) -} - +import { useCallback, useEffect, useRef, useState } from "react"; + +import wheel from "../../assets/wheel.png"; + +const Wheel = ({id, defaultValue = 50, min = 0, max = 100, step = 1, autoReturn = false, label, onChange}) => { + const [ value, setValue ] = useState(defaultValue ? defaultValue : (max - min) / 2 ); + const wheelRef = useRef(null); + + const handleValue = useCallback((e) => { + if (window.navigator.vibrate) window.navigator.vibrate(5); + setValue(e.target.value); + onChange(e.target.value); + }, [onChange]) + + const resetValue = useCallback( () => { + setValue(defaultValue); + }, [defaultValue]); + + useEffect(() => { + // onChange(value) + }, [value, onChange]) + + useEffect(() => { + if(!autoReturn) return; + + const currentWheel = wheelRef.current; + currentWheel.addEventListener("mouseup", resetValue); + currentWheel.addEventListener("mouseleave", resetValue); + currentWheel.addEventListener("touchend", resetValue); + + return () => { + currentWheel.removeEventListener("mouseup", resetValue); + currentWheel.removeEventListener("mouseleave", resetValue); + currentWheel.removeEventListener("touchstart", resetValue); + } + }, [autoReturn, resetValue]) + + useEffect(() => { + const currentWheel = wheelRef.current; + + currentWheel.addEventListener("change", handleValue) + currentWheel.addEventListener("input", handleValue) + + return () => { + currentWheel.removeEventListener("change", handleValue) + currentWheel.removeEventListener("input", handleValue) + } + }, [handleValue]) + + + return ( + + { label && } + + + ) +} + export default Wheel; \ No newline at end of file diff --git a/src/components/layout/Display.js b/src/components/layout/Display.jsx similarity index 98% rename from src/components/layout/Display.js rename to src/components/layout/Display.jsx index 25b7d1e..b4b4334 100644 --- a/src/components/layout/Display.js +++ b/src/components/layout/Display.jsx @@ -1,131 +1,131 @@ -import { BsCaretDownFill, BsCaretUpFill, BsDash, BsFillCircleFill, BsFillPauseFill, BsPlayFill, BsPlus } from "react-icons/bs"; -import { useCallback, useEffect, useState } from "react"; - -import Message from "../screens/Message"; -import Sequencer from "../screens/Sequencer"; -import { messages } from "../../config/display"; -import { useMidi } from "../../hooks/useMidi"; -import { useNTS } from "../../hooks/useNTS"; -import { useSequencer } from "../../hooks/useSequencer"; - -const Display = () => { - const { enabled, input, output, passthrough, octave, playNote, stopAll } = useMidi(); - const { bank, bankNames, state, sendControlChange } = useNTS(); - const { step, steps, setStep, bars, setBars, isPlaying, setIsPlaying, isRecording, setIsRecording, tempo, sequence, setSequence, barLength } = useSequencer(); - const [ message, setMessage ] = useState(null); - const [ bpmIndicator, setBpmIndicator ] = useState(1) - - const handleUp = () => { - window.navigator.vibrate && window.navigator.vibrate(step > 0 ? 10 : 50); - if(step > 0) setStep(s => s-1); - else setStep(steps - 1); - } - - const handleDown = () => { - window.navigator.vibrate && window.navigator.vibrate(10); - if(step < steps - 1) setStep(s => s+1); - else setStep(0); - } - - const addBar = () => { - window.navigator.vibrate && window.navigator.vibrate(10); - setBars(b => b + 1); - } - - const removeBar = () => { - window.navigator.vibrate && window.navigator.vibrate(step > 0 ? 10 : 50); - bars > 1 && setBars(b => b - 1); - } - - const playStep = useCallback((step) => { - let duration = step.length * 60000/tempo; - - if(!step?.note) return; - if(!isNaN(step?.bank) && step.bank !== bank) { - let b = JSON.parse(localStorage.getItem(`BANK_${step.bank}`)) - if(!b) return; - Object.keys(b).forEach(cc => sendControlChange(parseInt(cc), b[cc])); - }; - - playNote(step.note, true, false, duration); - }, [bank, playNote, sendControlChange, tempo]) - - const togglePlay = () => { - window.navigator.vibrate && window.navigator.vibrate(10); - - if(isPlaying){ - Object.keys(state).forEach(cc => sendControlChange(parseInt(cc), state[cc])); - } - - setIsPlaying(p => !p); - } - - const toggleRecording = () => { - window.navigator.vibrate && window.navigator.vibrate(10); - setIsRecording(r => !r) - } - - const setScreenMessage = (message, timed) => { - setMessage(message); - if(timed) return setTimeout(() => setMessage(null), 5000) - } - - useEffect(() => { - if(!enabled) return setMessage(messages["midi"]); - else if(!input || !output) return setMessage(messages["nodevice"]); - else if(passthrough) setScreenMessage(messages["newdevice"], true); - else setMessage(null); - - }, [enabled, input, output, passthrough]) - - useEffect(() => { - let interval; - interval = setInterval(() => setBpmIndicator(b => !b), (60000/tempo)/2 ); - return () => clearInterval(interval); - }, [tempo]); - - useEffect(() => { - if(!isPlaying) return - if(!sequence?.[step]?.note) return; - playStep(sequence?.[step]); - }, [isPlaying, playStep, step, sequence, stopAll]) - - return ( -
-
-
-
{bankNames?.[bank] ? bankNames?.[bank] : `Bank ${bank < 10 && 0 }${bank}`}
-
Octave {octave}
-
{tempo} BPM
-
- { - message ? - : - - } -
-
-
- - -
-
- - -
- - -
-
- ) -} - +import { BsCaretDownFill, BsCaretUpFill, BsDash, BsFillCircleFill, BsFillPauseFill, BsPlayFill, BsPlus } from "react-icons/bs"; +import { useCallback, useEffect, useState } from "react"; + +import Message from "../screens/Message"; +import Sequencer from "../screens/Sequencer"; +import { messages } from "../../config/display"; +import { useMidi } from "../../hooks/useMidi"; +import { useNTS } from "../../hooks/useNTS"; +import { useSequencer } from "../../hooks/useSequencer"; + +const Display = () => { + const { enabled, input, output, passthrough, octave, playNote, stopAll } = useMidi(); + const { bank, bankNames, state, sendControlChange } = useNTS(); + const { step, steps, setStep, bars, setBars, isPlaying, setIsPlaying, isRecording, setIsRecording, tempo, sequence, setSequence, barLength } = useSequencer(); + const [ message, setMessage ] = useState(null); + const [ bpmIndicator, setBpmIndicator ] = useState(1) + + const handleUp = () => { + window.navigator.vibrate && window.navigator.vibrate(step > 0 ? 10 : 50); + if(step > 0) setStep(s => s-1); + else setStep(steps - 1); + } + + const handleDown = () => { + window.navigator.vibrate && window.navigator.vibrate(10); + if(step < steps - 1) setStep(s => s+1); + else setStep(0); + } + + const addBar = () => { + window.navigator.vibrate && window.navigator.vibrate(10); + setBars(b => b + 1); + } + + const removeBar = () => { + window.navigator.vibrate && window.navigator.vibrate(step > 0 ? 10 : 50); + bars > 1 && setBars(b => b - 1); + } + + const playStep = useCallback((step) => { + let duration = step.length * 60000/tempo; + + if(!step?.note) return; + if(!isNaN(step?.bank) && step.bank !== bank) { + let b = JSON.parse(localStorage.getItem(`BANK_${step.bank}`)) + if(!b) return; + Object.keys(b).forEach(cc => sendControlChange(parseInt(cc), b[cc])); + }; + + playNote(step.note, true, false, duration); + }, [bank, playNote, sendControlChange, tempo]) + + const togglePlay = () => { + window.navigator.vibrate && window.navigator.vibrate(10); + + if(isPlaying){ + Object.keys(state).forEach(cc => sendControlChange(parseInt(cc), state[cc])); + } + + setIsPlaying(p => !p); + } + + const toggleRecording = () => { + window.navigator.vibrate && window.navigator.vibrate(10); + setIsRecording(r => !r) + } + + const setScreenMessage = (message, timed) => { + setMessage(message); + if(timed) return setTimeout(() => setMessage(null), 5000) + } + + useEffect(() => { + if(!enabled) return setMessage(messages["midi"]); + else if(!input || !output) return setMessage(messages["nodevice"]); + else if(passthrough) setScreenMessage(messages["newdevice"], true); + else setMessage(null); + + }, [enabled, input, output, passthrough]) + + useEffect(() => { + let interval; + interval = setInterval(() => setBpmIndicator(b => !b), (60000/tempo)/2 ); + return () => clearInterval(interval); + }, [tempo]); + + useEffect(() => { + if(!isPlaying) return + if(!sequence?.[step]?.note) return; + playStep(sequence?.[step]); + }, [isPlaying, playStep, step, sequence, stopAll]) + + return ( +
+
+
+
{bankNames?.[bank] ? bankNames?.[bank] : `Bank ${bank < 10 && 0 }${bank}`}
+
Octave {octave}
+
{tempo} BPM
+
+ { + message ? + : + + } +
+
+
+ + +
+
+ + +
+ + +
+
+ ) +} + export default Display; \ No newline at end of file diff --git a/src/components/layout/Footer.js b/src/components/layout/Footer.jsx similarity index 98% rename from src/components/layout/Footer.js rename to src/components/layout/Footer.jsx index 08327cf..0420605 100644 --- a/src/components/layout/Footer.js +++ b/src/components/layout/Footer.jsx @@ -1,37 +1,37 @@ -import { FaBug, FaDownload, FaHeart, FaInfo } from "react-icons/fa" - -import { SiKofi } from "react-icons/si"; -import { lazy } from "react"; -import { useLayout } from "../../hooks/useLayout"; - -const Footer = () => { - const { handleModal, supportsPWA, installPWA} = useLayout(); - - const openInfo = () => { - const Info = lazy(() => import('../../views/modals/Info')); - handleModal(); - } - - return ( -
-

Made with by Oscar R.C.

-

This page is not affiliated or endorsed by Korg

-
- Buy me a coffee -
-
- -
- { supportsPWA && -
- -
- } -
-
-
-
- ) -} - +import { FaBug, FaDownload, FaHeart, FaInfo } from "react-icons/fa" + +import { SiKofi } from "react-icons/si"; +import { lazy } from "react"; +import { useLayout } from "../../hooks/useLayout"; + +const Footer = () => { + const { handleModal, supportsPWA, installPWA} = useLayout(); + + const openInfo = () => { + const Info = lazy(() => import('../../views/modals/Info')); + handleModal(); + } + + return ( +
+

Made with by Oscar R.C.

+

This page is not affiliated or endorsed by Korg

+
+ Buy me a coffee +
+
+ +
+ { supportsPWA && +
+ +
+ } +
+
+
+
+ ) +} + export default Footer \ No newline at end of file diff --git a/src/components/layout/Header.js b/src/components/layout/Header.jsx similarity index 98% rename from src/components/layout/Header.js rename to src/components/layout/Header.jsx index 2dff2b5..205a6fb 100644 --- a/src/components/layout/Header.js +++ b/src/components/layout/Header.jsx @@ -1,186 +1,186 @@ -import { FaCog, FaFile, FaRandom, FaSave } from "react-icons/fa" -import { MdPiano, MdPianoOff } from "react-icons/md" -import { lazy, useRef } from "react"; - -import { GiMetronome } from "react-icons/gi" -import Tempo from "../../components/controls/Tempo"; -import korg from '../../assets/korg.svg'; -import { useLayout } from "../../hooks/useLayout"; -import { useNTS } from "../../hooks/useNTS"; -import { useSequencer } from "../../hooks/useSequencer"; - -const Header = () => { - const dataSelectorRef = useRef(null); - const bankSelectorRef = useRef(null); - const seqSelectorRef = useRef(null); - const { restoreBank, randomize, bank, bankNames } = useNTS(); - const { bottomDrawer, setBottomDrawer, handleModal } = useLayout(); - const { sequence, setSequence, tempo, setTempo, metronome, setMetronome, bars, setBars, barLength, setBarLength } = useSequencer(); - - const openRenameBanks = () => { - const Banks = lazy(() => import('../../views/modals/Banks')); - handleModal(); - } - - const openSettings = () => { - const Settings = lazy(() => import('../../views/modals/Settings')); - handleModal(); - } - - const toggleLive = () => setBottomDrawer(b => !b); - - const downloadFile = (data, extension, name) => { - const contentType = `application/${extension}+json;charset=utf-8;`; - - if (window.navigator && window.navigator.msSaveOrOpenBlob) { - let blob = new Blob([decodeURIComponent(encodeURI(JSON.stringify(data)))], { type: contentType }); - navigator.msSaveOrOpenBlob(blob, `${name}.${extension}`); - } else { - let a = document.createElement('a'); - a.download = `${name}.${extension}`; - a.href = 'data:' + contentType + ',' + encodeURIComponent(JSON.stringify(data)); - a.target = '_blank'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - } - - const loadFile = (e) => { - return new Promise((resolve, reject) => { - const file = e.target.files[0]; - const reader = new FileReader(); - - reader.onload = (e) => { - const data = JSON.parse(e.target.result); - resolve(data) - }; - - reader.onerror = reject; - reader.readAsText(file); - }) - } - - const importData = async (e) => { - const data = await loadFile(e); - Object.keys(data.bank).forEach(b => restoreBank(b, data.bank[b])) - if(data.seq) setSequence(data.seq); - if(data.tempo) setTempo(data.tempo); - if(data.barLength) setBarLength(data.barLength); - if(data.bars) setBars(data.bars); - } - - const importBank = async (e) => { - const data = await loadFile(e); - restoreBank(bank, data) - }; - - const importSequence = async (e) => { - const data = await loadFile(e); - setSequence(data) - }; - - const exportData = () => { - const data = { - bank: [], - seq: JSON.parse(localStorage.getItem(`SEQ`)) || sequence, - tempo: JSON.parse(localStorage.getItem(`TEMPO`)) || tempo, - barLength: JSON.parse(localStorage.getItem(`BAR`)) || barLength, - bars: JSON.parse(localStorage.getItem(`BARS`)) || bars - }; - - [...Array(15).keys()].forEach( (b) => { - const bank = JSON.parse(localStorage.getItem(`BANK_${b}`)); - - if(bank) data.bank[b] = { - name: bankNames?.[b], - values: bank - }; - }); - - downloadFile(data, "ntsweb", "DATA"); - } - - const exportBank = () => { - const data = { - name: bankNames?.[bank], - values: JSON.parse(localStorage.getItem(`BANK_${bank}`)) - } - - downloadFile(data, "ntsbank", data?.name || `BANK_${bank}`); - } - - const exportSequence = () => { - const data = JSON.parse(localStorage.getItem(`SEQ`)); - downloadFile(data, "ntsseq", `SEQUENCE`); - } - - const clearSequence = () => setSequence({}); - - return ( -
- -
-
    -
  • - -
      -
    • - - -
    • -
    • - - -
    • -
    • - -
    • -
    • - - -
    • -
    • - -
    • -
    -
  • -
  • - -
      -
    • -
    • -
    • -
    -
  • -
  • - -
  • -
  • - -
    - -
    -
  • -
  • - -
  • -
  • - -
  • -
-
-
- ) -} - +import { FaCog, FaFile, FaRandom, FaSave } from "react-icons/fa" +import { MdPiano, MdPianoOff } from "react-icons/md" +import { lazy, useRef } from "react"; + +import { GiMetronome } from "react-icons/gi" +import Tempo from "../../components/controls/Tempo"; +import korg from '../../assets/korg.svg'; +import { useLayout } from "../../hooks/useLayout"; +import { useNTS } from "../../hooks/useNTS"; +import { useSequencer } from "../../hooks/useSequencer"; + +const Header = () => { + const dataSelectorRef = useRef(null); + const bankSelectorRef = useRef(null); + const seqSelectorRef = useRef(null); + const { restoreBank, randomize, bank, bankNames } = useNTS(); + const { bottomDrawer, setBottomDrawer, handleModal } = useLayout(); + const { sequence, setSequence, tempo, setTempo, metronome, setMetronome, bars, setBars, barLength, setBarLength } = useSequencer(); + + const openRenameBanks = () => { + const Banks = lazy(() => import('../../views/modals/Banks')); + handleModal(); + } + + const openSettings = () => { + const Settings = lazy(() => import('../../views/modals/Settings')); + handleModal(); + } + + const toggleLive = () => setBottomDrawer(b => !b); + + const downloadFile = (data, extension, name) => { + const contentType = `application/${extension}+json;charset=utf-8;`; + + if (window.navigator && window.navigator.msSaveOrOpenBlob) { + let blob = new Blob([decodeURIComponent(encodeURI(JSON.stringify(data)))], { type: contentType }); + navigator.msSaveOrOpenBlob(blob, `${name}.${extension}`); + } else { + let a = document.createElement('a'); + a.download = `${name}.${extension}`; + a.href = 'data:' + contentType + ',' + encodeURIComponent(JSON.stringify(data)); + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } + + const loadFile = (e) => { + return new Promise((resolve, reject) => { + const file = e.target.files[0]; + const reader = new FileReader(); + + reader.onload = (e) => { + const data = JSON.parse(e.target.result); + resolve(data) + }; + + reader.onerror = reject; + reader.readAsText(file); + }) + } + + const importData = async (e) => { + const data = await loadFile(e); + Object.keys(data.bank).forEach(b => restoreBank(b, data.bank[b])) + if(data.seq) setSequence(data.seq); + if(data.tempo) setTempo(data.tempo); + if(data.barLength) setBarLength(data.barLength); + if(data.bars) setBars(data.bars); + } + + const importBank = async (e) => { + const data = await loadFile(e); + restoreBank(bank, data) + }; + + const importSequence = async (e) => { + const data = await loadFile(e); + setSequence(data) + }; + + const exportData = () => { + const data = { + bank: [], + seq: JSON.parse(localStorage.getItem(`SEQ`)) || sequence, + tempo: JSON.parse(localStorage.getItem(`TEMPO`)) || tempo, + barLength: JSON.parse(localStorage.getItem(`BAR`)) || barLength, + bars: JSON.parse(localStorage.getItem(`BARS`)) || bars + }; + + [...Array(15).keys()].forEach( (b) => { + const bank = JSON.parse(localStorage.getItem(`BANK_${b}`)); + + if(bank) data.bank[b] = { + name: bankNames?.[b], + values: bank + }; + }); + + downloadFile(data, "ntsweb", "DATA"); + } + + const exportBank = () => { + const data = { + name: bankNames?.[bank], + values: JSON.parse(localStorage.getItem(`BANK_${bank}`)) + } + + downloadFile(data, "ntsbank", data?.name || `BANK_${bank}`); + } + + const exportSequence = () => { + const data = JSON.parse(localStorage.getItem(`SEQ`)); + downloadFile(data, "ntsseq", `SEQUENCE`); + } + + const clearSequence = () => setSequence({}); + + return ( +
+ +
+
    +
  • + +
      +
    • + + +
    • +
    • + + +
    • +
    • + +
    • +
    • + + +
    • +
    • + +
    • +
    +
  • +
  • + +
      +
    • +
    • +
    • +
    +
  • +
  • + +
  • +
  • + +
    + +
    +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ ) +} + export default Header \ No newline at end of file diff --git a/src/components/layout/Memory.js b/src/components/layout/Memory.jsx similarity index 97% rename from src/components/layout/Memory.js rename to src/components/layout/Memory.jsx index b132dfe..770752d 100644 --- a/src/components/layout/Memory.js +++ b/src/components/layout/Memory.jsx @@ -1,18 +1,18 @@ -import { useNTS } from "../../hooks/useNTS"; - -const Memory = () => { - const { bank, setBank, bankNames } = useNTS(); - - return ( -
-

MEMORY

-
- { [...Array(16).keys()].map((k) => { - return - }) } -
-
- ) -} - +import { useNTS } from "../../hooks/useNTS"; + +const Memory = () => { + const { bank, setBank, bankNames } = useNTS(); + + return ( +
+

MEMORY

+
+ { [...Array(16).keys()].map((k) => { + return + }) } +
+
+ ) +} + export default Memory; \ No newline at end of file diff --git a/src/components/layout/Section.js b/src/components/layout/Section.jsx similarity index 97% rename from src/components/layout/Section.js rename to src/components/layout/Section.jsx index 9825944..c2a62f2 100644 --- a/src/components/layout/Section.js +++ b/src/components/layout/Section.jsx @@ -1,80 +1,80 @@ -import Dropdown from "../controls/Dropdown"; -import Knob from "../controls/Knob"; -import Selector from "../controls/Selector"; -import Switch from "../controls/Switch"; -import { useNTS } from "../../hooks/useNTS"; - -const Section = ({ section }) => { - const { state, setState, controls } = useNTS(); - - const renderControl = (cc, section, type = false) => { - const control = controls[cc] - const currentValue = isNaN(control.switch) ? state[cc] : state[cc].value; - - switch( type || control.type ){ - case "knob": - return setState(cc, value) } - min={control.min} - max={control.max} - /> - case "dropdown": - return setState(cc, isNaN(control.switch) ? value : { ...state[cc], value }) } - onSwitch={ value => setState(cc, { ...state[cc], active: value }) } - />; - case "selector": - return setState(cc, isNaN(control.switch) ? value : { ...state[cc], value } ) } - /> - case "switch": - return setState(cc, { ...state[cc], active: value }) } - /> - default: - return
- } - } - - return ( - section && -
-

{ section.label }

-
- { section.controls?.map(control => renderControl(control, section.label, section?.type )) } -
- { section?.sections?.map(section => ( -
-

{ section.label }

-
- { section.controls.map(control => renderControl(control, section.label)) } -
-
- )) } -
- ) -} - +import Dropdown from "../controls/Dropdown"; +import Knob from "../controls/Knob"; +import Selector from "../controls/Selector"; +import Switch from "../controls/Switch"; +import { useNTS } from "../../hooks/useNTS"; + +const Section = ({ section }) => { + const { state, setState, controls } = useNTS(); + + const renderControl = (cc, section, type = false) => { + const control = controls[cc] + const currentValue = isNaN(control.switch) ? state[cc] : state[cc].value; + + switch( type || control.type ){ + case "knob": + return setState(cc, value) } + min={control.min} + max={control.max} + /> + case "dropdown": + return setState(cc, isNaN(control.switch) ? value : { ...state[cc], value }) } + onSwitch={ value => setState(cc, { ...state[cc], active: value }) } + />; + case "selector": + return setState(cc, isNaN(control.switch) ? value : { ...state[cc], value } ) } + /> + case "switch": + return setState(cc, { ...state[cc], active: value }) } + /> + default: + return
+ } + } + + return ( + section && +
+

{ section.label }

+
+ { section.controls?.map(control => renderControl(control, section.label, section?.type )) } +
+ { section?.sections?.map(section => ( +
+

{ section.label }

+
+ { section.controls.map(control => renderControl(control, section.label)) } +
+
+ )) } +
+ ) +} + export default Section; \ No newline at end of file diff --git a/src/components/screens/Message.js b/src/components/screens/Message.jsx similarity index 96% rename from src/components/screens/Message.js rename to src/components/screens/Message.jsx index c51346e..00c60fa 100644 --- a/src/components/screens/Message.js +++ b/src/components/screens/Message.jsx @@ -1,10 +1,10 @@ -const Message = ({ message }) => { - return ( -
-

{ message?.title }

-

{ message?.text }

-
- ) -} - +const Message = ({ message }) => { + return ( +
+

{ message?.title }

+

{ message?.text }

+
+ ) +} + export default Message \ No newline at end of file diff --git a/src/components/screens/Sequencer.js b/src/components/screens/Sequencer.jsx similarity index 98% rename from src/components/screens/Sequencer.js rename to src/components/screens/Sequencer.jsx index 57355b1..ab7ab94 100644 --- a/src/components/screens/Sequencer.js +++ b/src/components/screens/Sequencer.jsx @@ -1,45 +1,45 @@ -import { BsCaretRightFill } from "react-icons/bs"; -import SeqInput from "../controls/SeqInput"; -import { useEffect } from "react"; - -const Sequencer = ({step, setStep, steps, sequence, setSequence, banks, barLength }) => { - useEffect(() => { - document.getElementById(`step-${step}`).scrollIntoView({block: "nearest", inline: "nearest", behavior: "smooth"}); - }, [step, steps]) - - return ( -
- { [...Array(steps).keys()].map(k => ( -
setStep(k)} id={`step-${k}`} key={k} className={`grid grid-cols-4 ${ k < steps - 1 && ((k + 1) % barLength === 0 ? 'border-b-4' : 'border-b')} border-accent cursor-pointer`}> -
{k === step && } {k < 10 && '0'}{k}
- setSequence( s => ({...s, [k]: { ...s[k], note: v }}) ) } - /> - setSequence( s => ({...s, [k]: { ...s[k], length: parseInt(v) }}) ) } - /> - setSequence( s => ({...s, [k]: { ...s[k], bank: parseInt(v) }}) ) } - /> -
- )) } -
- ) -} - +import { BsCaretRightFill } from "react-icons/bs"; +import SeqInput from "../controls/SeqInput"; +import { useEffect } from "react"; + +const Sequencer = ({step, setStep, steps, sequence, setSequence, banks, barLength }) => { + useEffect(() => { + document.getElementById(`step-${step}`).scrollIntoView({block: "nearest", inline: "nearest", behavior: "smooth"}); + }, [step, steps]) + + return ( +
+ { [...Array(steps).keys()].map(k => ( +
setStep(k)} id={`step-${k}`} key={k} className={`grid grid-cols-4 ${ k < steps - 1 && ((k + 1) % barLength === 0 ? 'border-b-4' : 'border-b')} border-accent cursor-pointer`}> +
{k === step && } {k < 10 && '0'}{k}
+ setSequence( s => ({...s, [k]: { ...s[k], note: v }}) ) } + /> + setSequence( s => ({...s, [k]: { ...s[k], length: parseInt(v) }}) ) } + /> + setSequence( s => ({...s, [k]: { ...s[k], bank: parseInt(v) }}) ) } + /> +
+ )) } +
+ ) +} + export default Sequencer; \ No newline at end of file diff --git a/src/hooks/useLayout.js b/src/hooks/useLayout.jsx similarity index 97% rename from src/hooks/useLayout.js rename to src/hooks/useLayout.jsx index 8e14bbc..54541eb 100644 --- a/src/hooks/useLayout.js +++ b/src/hooks/useLayout.jsx @@ -1,123 +1,123 @@ -import { Suspense, createContext, useContext, useEffect, useRef, useState } from "react"; - -const getWindowDimensions = () => { - const { innerWidth: width, innerHeight: height } = window; - return { width, height }; -} - -const getCurrentBreakpoint = () => { - let currentBreakpoint = "xs"; - let biggestBreakpointValue = 0; - - const screens = { - 'sm': '640px', - 'md': '768px', - 'lg': '1024px', - 'xl': '1280px', - '2xl': '1536px' - } - - Object.keys(screens).forEach( breakpoint => { - const breakpointValue = parseInt(screens[breakpoint].slice(0, -2)); - - if (breakpointValue > biggestBreakpointValue && window.innerWidth >= breakpointValue ) { - biggestBreakpointValue = breakpointValue; - currentBreakpoint = breakpoint; - } - }) - - return currentBreakpoint; -} - -const LayoutContext = createContext(); - -const LayoutProvider = ({children}) => { - const [ windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); - const [ breakpoint, setBreakpoint ] = useState(getCurrentBreakpoint()); - const [ bottomDrawer, setBottomDrawer ] = useState(false); - const [ modal, setModal ] = useState(false); - const [ isFullWidth, setIsFullWidth ] = useState(false); - const [ modalContent, setModalContent ] = useState(false); - const [supportsPWA, setSupportsPWA] = useState(false); - const [promptInstall, setPromptInstall] = useState(null); - - const overlay = useRef(null); - const modalOpen = "fixed top-0 left-0 right-0 bottom-0 min-w-screen min-h-screen bg-neutral bg-opacity-75 z-50 opacity-100 visible"; - - const handleModal = (content = false, fullWidth = false ) => { - setModal(content ? true : false); - setIsFullWidth(fullWidth); - setModalContent(content || null ); - } - - const toggleModal = (event) => { - if(event.target === overlay.current || event.key === "Escape") handleModal(); - } - - const installPWA = evt => { - evt.preventDefault(); - if(promptInstall) promptInstall.prompt(); - promptInstall.userChoice.then((choiceResult) => { - if (choiceResult.outcome === 'accepted') setSupportsPWA(false); - }); - }; - - useEffect(() => { - const handler = e => { - e.preventDefault(); - setSupportsPWA(true); - setPromptInstall(e); - }; - window.addEventListener("beforeinstallprompt", handler); - }, []); - - useEffect(() => { - document.addEventListener("keydown", toggleModal, false); - return () => document.removeEventListener("keydown", toggleModal, false); - }) - - useEffect(() => { - if(modal) document.body.classList.add("overflow-hidden"); - else document.body.classList.remove("overflow-hidden"); - }, [modal]) - - useEffect(() => { - const handleResize = () => { - setWindowDimensions(getWindowDimensions()); - setBreakpoint(getCurrentBreakpoint()); - } - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return ( - - { - windowDimensions.width < 380 ? -
-

Window is to small!!

-

Make your window wider or get a device with a bigger screen

-
: - <> - { children } -
toggleModal(e) }> -
-
}> - { modal ? modalContent : null } - -
- - - } - - ) -} - -const useLayout = () => { - const context = useContext(LayoutContext); - if(context === undefined) throw new Error("useLayout must be used within a LayoutProvider") - return context; -} - +import { Suspense, createContext, useContext, useEffect, useRef, useState } from "react"; + +const getWindowDimensions = () => { + const { innerWidth: width, innerHeight: height } = window; + return { width, height }; +} + +const getCurrentBreakpoint = () => { + let currentBreakpoint = "xs"; + let biggestBreakpointValue = 0; + + const screens = { + 'sm': '640px', + 'md': '768px', + 'lg': '1024px', + 'xl': '1280px', + '2xl': '1536px' + } + + Object.keys(screens).forEach( breakpoint => { + const breakpointValue = parseInt(screens[breakpoint].slice(0, -2)); + + if (breakpointValue > biggestBreakpointValue && window.innerWidth >= breakpointValue ) { + biggestBreakpointValue = breakpointValue; + currentBreakpoint = breakpoint; + } + }) + + return currentBreakpoint; +} + +const LayoutContext = createContext(); + +const LayoutProvider = ({children}) => { + const [ windowDimensions, setWindowDimensions] = useState(getWindowDimensions()); + const [ breakpoint, setBreakpoint ] = useState(getCurrentBreakpoint()); + const [ bottomDrawer, setBottomDrawer ] = useState(false); + const [ modal, setModal ] = useState(false); + const [ isFullWidth, setIsFullWidth ] = useState(false); + const [ modalContent, setModalContent ] = useState(false); + const [supportsPWA, setSupportsPWA] = useState(false); + const [promptInstall, setPromptInstall] = useState(null); + + const overlay = useRef(null); + const modalOpen = "fixed top-0 left-0 right-0 bottom-0 min-w-screen min-h-screen bg-neutral bg-opacity-75 z-50 opacity-100 visible"; + + const handleModal = (content = false, fullWidth = false ) => { + setModal(content ? true : false); + setIsFullWidth(fullWidth); + setModalContent(content || null ); + } + + const toggleModal = (event) => { + if(event.target === overlay.current || event.key === "Escape") handleModal(); + } + + const installPWA = evt => { + evt.preventDefault(); + if(promptInstall) promptInstall.prompt(); + promptInstall.userChoice.then((choiceResult) => { + if (choiceResult.outcome === 'accepted') setSupportsPWA(false); + }); + }; + + useEffect(() => { + const handler = e => { + e.preventDefault(); + setSupportsPWA(true); + setPromptInstall(e); + }; + window.addEventListener("beforeinstallprompt", handler); + }, []); + + useEffect(() => { + document.addEventListener("keydown", toggleModal, false); + return () => document.removeEventListener("keydown", toggleModal, false); + }) + + useEffect(() => { + if(modal) document.body.classList.add("overflow-hidden"); + else document.body.classList.remove("overflow-hidden"); + }, [modal]) + + useEffect(() => { + const handleResize = () => { + setWindowDimensions(getWindowDimensions()); + setBreakpoint(getCurrentBreakpoint()); + } + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return ( + + { + windowDimensions.width < 380 ? +
+

Window is to small!!

+

Make your window wider or get a device with a bigger screen

+
: + <> + { children } +
toggleModal(e) }> +
+
}> + { modal ? modalContent : null } + +
+ + + } + + ) +} + +const useLayout = () => { + const context = useContext(LayoutContext); + if(context === undefined) throw new Error("useLayout must be used within a LayoutProvider") + return context; +} + export { useLayout, LayoutProvider }; \ No newline at end of file diff --git a/src/hooks/useMidi.js b/src/hooks/useMidi.jsx similarity index 97% rename from src/hooks/useMidi.js rename to src/hooks/useMidi.jsx index 002995a..99a4a04 100644 --- a/src/hooks/useMidi.js +++ b/src/hooks/useMidi.jsx @@ -1,148 +1,148 @@ -import { channelList, defaultChannels, defaultDevices } from "../config/midi"; -import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useState } from "react"; - -import { WebMidi } from 'webmidi'; - -const MidiContext = createContext(); - -const DeviceReducer = (state, action) => { - switch (action.type) { - case "All": - return { - ...state, - inputDevices: action.payload.inputDevices, - outputDevices: action.payload.outputDevices, - passthroughDevices: action.payload.passthroughDevices - } - case "Input": - return { ...state, input: action.payload } - case "Output": - return { ...state, output: action.payload } - case "Passthrough": - return { ...state, passthrough: action.payload } - default: - break; - } -} - -const ChannelReducer = (state, action) => { - switch (action.type) { - case "All": - return action.payload; - case "Input": - return { ...state, input: action.payload } - case "Output": - return { ...state, output: action.payload } - case "Passthrough": - return { ...state, passthrough: action.payload } - default: - break; - } -} - -const MidiProvider = ({ children }) => { - const [devices, setDevices] = useReducer(DeviceReducer, defaultDevices ); - const [channels, setChannels] = useReducer(ChannelReducer, defaultChannels); - const [enabled, setEnabled] = useState(WebMidi.enabled); - const [octave, setOctave] = useState(parseInt(localStorage.getItem("OCTAVE")) || 3); - - const input = useMemo(() => devices.inputDevices[devices.input], [devices.input, devices.inputDevices]); - const output = useMemo(() => devices.outputDevices[devices.output], [devices.output, devices.outputDevices]); - const passthrough = useMemo(() => devices.passthroughDevices[devices.passthrough], [devices.passthrough, devices.passthroughDevices]); - - const init = useCallback(async () => { - await WebMidi.enable({ sysex: true }); - setEnabled(WebMidi.enabled); - parseDevices(); - }, []) - - const parseDevices = () => { - const currentDevices = { - inputDevices : WebMidi._inputs.filter(d => d._midiInput.name.includes("NTS")), - outputDevices : WebMidi._outputs.filter(d => d._midiOutput.name.includes("NTS")), - passthroughDevices : WebMidi._inputs.filter(d => !d._midiInput.name.includes("NTS")) - } - - if(currentDevices.inputDevices.length) setDevices({ type: "Input", payload: 0 }); - if(currentDevices.outputDevices.length) setDevices({ type: "Output", payload: 0 }); - if(currentDevices.passthroughDevices.length) setDevices({ type: "Passthrough", payload: 0 }); - - setDevices({type:"All", payload: currentDevices}); - } - - const playNote = (note, play = true, velocity = false, duration = false) => { - let options = { - channels: channelList[channels.output], - ...( velocity && { velocity } ), - ...( duration && { duration } ) - } - - if(output){ - if(play) output.playNote(note, options); - else output.stopNote(note, options); - } - } - - const stopAll = () => { - output && output.sendAllNotesOff({ - channels: channelList[channels.output] - }); - } - - const controlChange = (cc, value) => { - if(output) output.sendControlChange(cc, value, { channels: channelList[channels.output] } ); - } - - const pitchBend = (value) => { - if(output) output.sendPitchBend(value, { channels: channelList[channels.output] }); - } - - useEffect(() => { init() }, [init]); - useEffect(() => { localStorage.setItem("OCTAVE", octave) }, [octave]); - - useEffect(() => { - !WebMidi.hasListener("connected", parseDevices) && WebMidi.addListener("connected", parseDevices) - !WebMidi.hasListener("disconnected", parseDevices) && WebMidi.addListener("disconnected", parseDevices) - - return () => { - WebMidi.hasListener("connected", parseDevices) && WebMidi.removeListener("connected", parseDevices) - WebMidi.hasListener("disconnected", parseDevices) && WebMidi.removeListener("disconnected", parseDevices) - } - }, []); - - useEffect(() => { - if(!passthrough) return; - !passthrough.hasForwarder(output) && passthrough.addForwarder(output); - - return () => passthrough.hasForwarder(output) && passthrough.removeForwarder(output); - }, [passthrough, output]) - - return ( - - { children } - - ) -} - -const useMidi = () => { - const context = useContext(MidiContext); - if(context === undefined) throw new Error("useMidi must be used within a MidiProvider") - return context; -} - +import { channelList, defaultChannels, defaultDevices } from "../config/midi"; +import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useState } from "react"; + +import { WebMidi } from 'webmidi'; + +const MidiContext = createContext(); + +const DeviceReducer = (state, action) => { + switch (action.type) { + case "All": + return { + ...state, + inputDevices: action.payload.inputDevices, + outputDevices: action.payload.outputDevices, + passthroughDevices: action.payload.passthroughDevices + } + case "Input": + return { ...state, input: action.payload } + case "Output": + return { ...state, output: action.payload } + case "Passthrough": + return { ...state, passthrough: action.payload } + default: + break; + } +} + +const ChannelReducer = (state, action) => { + switch (action.type) { + case "All": + return action.payload; + case "Input": + return { ...state, input: action.payload } + case "Output": + return { ...state, output: action.payload } + case "Passthrough": + return { ...state, passthrough: action.payload } + default: + break; + } +} + +const MidiProvider = ({ children }) => { + const [devices, setDevices] = useReducer(DeviceReducer, defaultDevices ); + const [channels, setChannels] = useReducer(ChannelReducer, defaultChannels); + const [enabled, setEnabled] = useState(WebMidi.enabled); + const [octave, setOctave] = useState(parseInt(localStorage.getItem("OCTAVE")) || 3); + + const input = useMemo(() => devices.inputDevices[devices.input], [devices.input, devices.inputDevices]); + const output = useMemo(() => devices.outputDevices[devices.output], [devices.output, devices.outputDevices]); + const passthrough = useMemo(() => devices.passthroughDevices[devices.passthrough], [devices.passthrough, devices.passthroughDevices]); + + const init = useCallback(async () => { + await WebMidi.enable({ sysex: true }); + setEnabled(WebMidi.enabled); + parseDevices(); + }, []) + + const parseDevices = () => { + const currentDevices = { + inputDevices : WebMidi._inputs.filter(d => d._midiInput.name.includes("NTS")), + outputDevices : WebMidi._outputs.filter(d => d._midiOutput.name.includes("NTS")), + passthroughDevices : WebMidi._inputs.filter(d => !d._midiInput.name.includes("NTS")) + } + + if(currentDevices.inputDevices.length) setDevices({ type: "Input", payload: 0 }); + if(currentDevices.outputDevices.length) setDevices({ type: "Output", payload: 0 }); + if(currentDevices.passthroughDevices.length) setDevices({ type: "Passthrough", payload: 0 }); + + setDevices({type:"All", payload: currentDevices}); + } + + const playNote = (note, play = true, velocity = false, duration = false) => { + let options = { + channels: channelList[channels.output], + ...( velocity && { velocity } ), + ...( duration && { duration } ) + } + + if(output){ + if(play) output.playNote(note, options); + else output.stopNote(note, options); + } + } + + const stopAll = () => { + output && output.sendAllNotesOff({ + channels: channelList[channels.output] + }); + } + + const controlChange = (cc, value) => { + if(output) output.sendControlChange(cc, value, { channels: channelList[channels.output] } ); + } + + const pitchBend = (value) => { + if(output) output.sendPitchBend(value, { channels: channelList[channels.output] }); + } + + useEffect(() => { init() }, [init]); + useEffect(() => { localStorage.setItem("OCTAVE", octave) }, [octave]); + + useEffect(() => { + !WebMidi.hasListener("connected", parseDevices) && WebMidi.addListener("connected", parseDevices) + !WebMidi.hasListener("disconnected", parseDevices) && WebMidi.addListener("disconnected", parseDevices) + + return () => { + WebMidi.hasListener("connected", parseDevices) && WebMidi.removeListener("connected", parseDevices) + WebMidi.hasListener("disconnected", parseDevices) && WebMidi.removeListener("disconnected", parseDevices) + } + }, []); + + useEffect(() => { + if(!passthrough) return; + !passthrough.hasForwarder(output) && passthrough.addForwarder(output); + + return () => passthrough.hasForwarder(output) && passthrough.removeForwarder(output); + }, [passthrough, output]) + + return ( + + { children } + + ) +} + +const useMidi = () => { + const context = useContext(MidiContext); + if(context === undefined) throw new Error("useMidi must be used within a MidiProvider") + return context; +} + export { MidiProvider, useMidi } \ No newline at end of file diff --git a/src/hooks/useNTS.js b/src/hooks/useNTS.jsx similarity index 97% rename from src/hooks/useNTS.js rename to src/hooks/useNTS.jsx index 9ade7c0..fd52e98 100644 --- a/src/hooks/useNTS.js +++ b/src/hooks/useNTS.jsx @@ -1,170 +1,170 @@ -import { createContext, useCallback, useContext, useEffect, useReducer, useState } from 'react' -import { defaultControls, defaultValues, sysex, verifyValues } from "../config/synth"; - -import { useMidi } from './useMidi'; - -const NTSContext = createContext(); - -const NTSReducer = (state, action) => { - let updated; - const { bank, value } = action.payload; - - if(action.type === "bank") updated = value; - else if(state[action.type] === undefined) updated = state; - else updated = { ...state, [action.type]: value } - - localStorage.setItem(`BANK_${bank}`, JSON.stringify(updated)); - - return updated; -} - -const NTSProvider = ({ children }) => { - const { input, output, passthrough, channels } = useMidi(); - const [ controls, setControls ] = useState(JSON.parse(localStorage.getItem("CONTROLS")) || defaultControls); - const [ bank, setBank ] = useState(parseInt(localStorage.getItem("BANK")) || 0); - const [ bankNames, setBankNames] = useState(JSON.parse(localStorage.getItem("BANKS")) || {}); - const [ state, dispatch ] = useReducer(NTSReducer, defaultValues(controls, true)); - - const randomize = () => { - const random = defaultValues(defaultControls, true); - Object.keys(random).forEach( cc => sendControlChange(parseInt(cc), random[cc]) ); - dispatch({ type: "bank", payload: { bank, value: random } }) - }; - - const decode = (data) => { - let name = data.slice(30, data.length -1 ); - let decoded = ""; - name.forEach(e => { if(e) decoded = decoded + String.fromCharCode(e) }); - return decoded.replace(/[^a-zA-Z0-9 -]/g, "") - } - - const restoreBank = (b, data) => { - // if(!verifyValues(data, controls)) return; - if(b === bank){ - dispatch({type: "bank", payload: { bank, value: data.values } }); - Object.keys(data.values).forEach( cc => sendControlChange(parseInt(cc), data.values[cc]) ); - } - else localStorage.setItem(`BANK_${b}`, JSON.stringify(data.values)); - - if(data.name){ - localStorage.setItem("BANKS", JSON.stringify({...bankNames, [b]: data.name})) - setBankNames( n => ({ ...n, [b]: data.name })); - }else{ - localStorage.setItem("BANKS", JSON.stringify({...bankNames, [b]: null})) - setBankNames( n => ({ ...n, [b]: null })); - } - } - - const renameBank = (b, name) => { - const banks = {...bankNames, [b]: name } - setBankNames(banks); - localStorage.setItem("BANKS", JSON.stringify(banks)); - } - - const receiveControlChange = useCallback(( event ) => { - const { rawValue, value, controller: { number }} = event; - const control = controls[number]; - const hasSwitch = !isNaN(control.switch); - let parsed = control?.options ? - Math.round(value * (control.options.length + (hasSwitch ? 0 : -1))) : - control.min && control.max ? Math.round(control.min + ((control.max - control.min) / 127) * rawValue) : rawValue; - - if(hasSwitch) parsed = { ...state[number], ...( control.switch === rawValue ? { active: false } : { value: parsed, active: true })} - - dispatch({ type:number, payload: { bank, value: parsed } }) - }, [bank, controls, state]); - - const sendControlChange = useCallback((cc, value) => { - const control = controls[cc]; - const hasSwitch = !isNaN(control?.switch); - const isActive = value?.active === undefined ? true : value?.active; - const val = hasSwitch ? (value.value >= control.switch ? value.value + 1 : value.value) : value; - const options = control.options ? control.options.length + (hasSwitch ? 0 : -1 ) : 1; - const step = control.options ? Math.floor( 127 / options ) : 1; - const parsed = control?.options ? - val === options ? 127 : val * step : - control.min && control.max ? Math.round((127 / (control.max - control.min)) * (val - control.min)) : val; - - output && output.sendControlChange(cc, isActive ? parsed : control.switch, { channels: channels.output || null }) - dispatch({type: cc, payload: { bank, value } }) - }, [bank, channels.output, controls, output]); - - useEffect(() => { - const b = JSON.parse(localStorage.getItem(`BANK_${bank}`)) || defaultValues(controls, true); - - localStorage.setItem("BANK", bank); - Object.keys(b).forEach( cc => sendControlChange(parseInt(cc), b[cc])); - dispatch({type: "bank", payload: { bank, value: b }}); - }, [bank, controls, sendControlChange]); // Switch to another bank or create it if it doesn't exists - - useEffect(() => { - input && !input.hasListener("controlchange", receiveControlChange ) && input.addListener("controlchange", receiveControlChange ) - passthrough && !passthrough.hasListener("controlchange", receiveControlChange ) && passthrough.addListener("controlchange", receiveControlChange ) - - return () => { - input && input.hasListener("controlchange", receiveControlChange ) && input.removeListener("controlchange", receiveControlChange ); - passthrough && passthrough.hasListener("controlchange", receiveControlChange ) && passthrough.removeListener("controlchange", receiveControlChange ) - } - }, [receiveControlChange, input, passthrough]); // Listen for input from NTS or passthrough device - - useEffect(() => { - let type = 0; - let bank = 0; - let controls = defaultControls; - const index = [88, 89, 90, 53]; - - const get = (e) => { - if (e.data.length === 53){ - const decoded = decode(e.data) - !controls[index[type-1]]?.options.includes(decoded) && controls[index[type-1]]?.options.push(decode(e.data)) - }; - - if(bank < 16){ - bank++ - output.sendSysex(sysex.vendor, [48 + sysex.channel, 0, 1, sysex.device, 25, type, bank]); - }else if(type < 4){ - bank = 0; - type ++ - output.sendSysex(sysex.vendor, [48 + sysex.channel, 0, 1, sysex.device, 25, type, bank]); - }else{ - setTimeout(()=> { - input.removeListener("sysex", channels.input, get); - localStorage.setItem("CONTROLS", JSON.stringify(controls)); - setControls(controls) - }, 200); - } - } - - if(!input || !output) return setControls(defaultControls); - input.addListener("sysex", channels.input, get); - output.sendSysex(sysex.vendor, [80, 0, 2]); - output.sendSysex(sysex.vendor, [48 + sysex.channel, 0, 1, sysex.device, 25, 1, 0]); - }, [channels.input, input, output]) // Get user oscilators and effects - - return ( - - { children } - - ) -} - -const useNTS = () => { - const context = useContext(NTSContext); - if(context === undefined) throw new Error("useNTS must be used within a NTSProvider") - return context; -} - +import { createContext, useCallback, useContext, useEffect, useReducer, useState } from 'react' +import { defaultControls, defaultValues, sysex, verifyValues } from "../config/synth"; + +import { useMidi } from './useMidi'; + +const NTSContext = createContext(); + +const NTSReducer = (state, action) => { + let updated; + const { bank, value } = action.payload; + + if(action.type === "bank") updated = value; + else if(state[action.type] === undefined) updated = state; + else updated = { ...state, [action.type]: value } + + localStorage.setItem(`BANK_${bank}`, JSON.stringify(updated)); + + return updated; +} + +const NTSProvider = ({ children }) => { + const { input, output, passthrough, channels } = useMidi(); + const [ controls, setControls ] = useState(JSON.parse(localStorage.getItem("CONTROLS")) || defaultControls); + const [ bank, setBank ] = useState(parseInt(localStorage.getItem("BANK")) || 0); + const [ bankNames, setBankNames] = useState(JSON.parse(localStorage.getItem("BANKS")) || {}); + const [ state, dispatch ] = useReducer(NTSReducer, defaultValues(controls, true)); + + const randomize = () => { + const random = defaultValues(defaultControls, true); + Object.keys(random).forEach( cc => sendControlChange(parseInt(cc), random[cc]) ); + dispatch({ type: "bank", payload: { bank, value: random } }) + }; + + const decode = (data) => { + let name = data.slice(30, data.length -1 ); + let decoded = ""; + name.forEach(e => { if(e) decoded = decoded + String.fromCharCode(e) }); + return decoded.replace(/[^a-zA-Z0-9 -]/g, "") + } + + const restoreBank = (b, data) => { + // if(!verifyValues(data, controls)) return; + if(b === bank){ + dispatch({type: "bank", payload: { bank, value: data.values } }); + Object.keys(data.values).forEach( cc => sendControlChange(parseInt(cc), data.values[cc]) ); + } + else localStorage.setItem(`BANK_${b}`, JSON.stringify(data.values)); + + if(data.name){ + localStorage.setItem("BANKS", JSON.stringify({...bankNames, [b]: data.name})) + setBankNames( n => ({ ...n, [b]: data.name })); + }else{ + localStorage.setItem("BANKS", JSON.stringify({...bankNames, [b]: null})) + setBankNames( n => ({ ...n, [b]: null })); + } + } + + const renameBank = (b, name) => { + const banks = {...bankNames, [b]: name } + setBankNames(banks); + localStorage.setItem("BANKS", JSON.stringify(banks)); + } + + const receiveControlChange = useCallback(( event ) => { + const { rawValue, value, controller: { number }} = event; + const control = controls[number]; + const hasSwitch = !isNaN(control.switch); + let parsed = control?.options ? + Math.round(value * (control.options.length + (hasSwitch ? 0 : -1))) : + control.min && control.max ? Math.round(control.min + ((control.max - control.min) / 127) * rawValue) : rawValue; + + if(hasSwitch) parsed = { ...state[number], ...( control.switch === rawValue ? { active: false } : { value: parsed, active: true })} + + dispatch({ type:number, payload: { bank, value: parsed } }) + }, [bank, controls, state]); + + const sendControlChange = useCallback((cc, value) => { + const control = controls[cc]; + const hasSwitch = !isNaN(control?.switch); + const isActive = value?.active === undefined ? true : value?.active; + const val = hasSwitch ? (value.value >= control.switch ? value.value + 1 : value.value) : value; + const options = control.options ? control.options.length + (hasSwitch ? 0 : -1 ) : 1; + const step = control.options ? Math.floor( 127 / options ) : 1; + const parsed = control?.options ? + val === options ? 127 : val * step : + control.min && control.max ? Math.round((127 / (control.max - control.min)) * (val - control.min)) : val; + + output && output.sendControlChange(cc, isActive ? parsed : control.switch, { channels: channels.output || null }) + dispatch({type: cc, payload: { bank, value } }) + }, [bank, channels.output, controls, output]); + + useEffect(() => { + const b = JSON.parse(localStorage.getItem(`BANK_${bank}`)) || defaultValues(controls, true); + + localStorage.setItem("BANK", bank); + Object.keys(b).forEach( cc => sendControlChange(parseInt(cc), b[cc])); + dispatch({type: "bank", payload: { bank, value: b }}); + }, [bank, controls, sendControlChange]); // Switch to another bank or create it if it doesn't exists + + useEffect(() => { + input && !input.hasListener("controlchange", receiveControlChange ) && input.addListener("controlchange", receiveControlChange ) + passthrough && !passthrough.hasListener("controlchange", receiveControlChange ) && passthrough.addListener("controlchange", receiveControlChange ) + + return () => { + input && input.hasListener("controlchange", receiveControlChange ) && input.removeListener("controlchange", receiveControlChange ); + passthrough && passthrough.hasListener("controlchange", receiveControlChange ) && passthrough.removeListener("controlchange", receiveControlChange ) + } + }, [receiveControlChange, input, passthrough]); // Listen for input from NTS or passthrough device + + useEffect(() => { + let type = 0; + let bank = 0; + let controls = defaultControls; + const index = [88, 89, 90, 53]; + + const get = (e) => { + if (e.data.length === 53){ + const decoded = decode(e.data) + !controls[index[type-1]]?.options.includes(decoded) && controls[index[type-1]]?.options.push(decode(e.data)) + }; + + if(bank < 16){ + bank++ + output.sendSysex(sysex.vendor, [48 + sysex.channel, 0, 1, sysex.device, 25, type, bank]); + }else if(type < 4){ + bank = 0; + type ++ + output.sendSysex(sysex.vendor, [48 + sysex.channel, 0, 1, sysex.device, 25, type, bank]); + }else{ + setTimeout(()=> { + input.removeListener("sysex", channels.input, get); + localStorage.setItem("CONTROLS", JSON.stringify(controls)); + setControls(controls) + }, 200); + } + } + + if(!input || !output) return setControls(defaultControls); + input.addListener("sysex", channels.input, get); + output.sendSysex(sysex.vendor, [80, 0, 2]); + output.sendSysex(sysex.vendor, [48 + sysex.channel, 0, 1, sysex.device, 25, 1, 0]); + }, [channels.input, input, output]) // Get user oscilators and effects + + return ( + + { children } + + ) +} + +const useNTS = () => { + const context = useContext(NTSContext); + if(context === undefined) throw new Error("useNTS must be used within a NTSProvider") + return context; +} + export { NTSProvider, useNTS }; \ No newline at end of file diff --git a/src/hooks/useSequencer.js b/src/hooks/useSequencer.jsx similarity index 97% rename from src/hooks/useSequencer.js rename to src/hooks/useSequencer.jsx index 7ed7a78..c7b691b 100644 --- a/src/hooks/useSequencer.js +++ b/src/hooks/useSequencer.jsx @@ -1,119 +1,119 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; - -const SequencerContext = createContext(); - -const SequencerProvider = ({children}) => { - //TODO: sequencer - const [barLength, setBarLength] = useState(parseInt(localStorage.getItem("BAR")) || 4); - const [bars, setBars] = useState(parseInt(localStorage.getItem("BARS")) || 1); - const [step, setStep] = useState(0); - const [sequence, setSequence] = useState(JSON.parse(localStorage.getItem("SEQ")) || {}); - const [isPlaying, setIsPlaying] = useState(false); - const [isRecording, setIsRecording] = useState(false); - const [metronome, setMetronome] = useState(true); - const [tempo, setTempo] = useState(parseInt(localStorage.getItem("TEMPO")) || 60); - const steps = useMemo(() => bars * barLength, [bars, barLength]) - const prevStep = useRef(0); - - const audioContext = useRef(new AudioContext()); - - const playBeat = useCallback((step) => { - const osc = audioContext.current.createOscillator(); - const envelope = audioContext.current.createGain(); - const time = audioContext.current.currentTime; - - osc.frequency.value = (step % barLength === 0) ? 1000 : 800; - envelope.gain.value = 1; - envelope.gain.exponentialRampToValueAtTime(1, time + 0.001); - envelope.gain.exponentialRampToValueAtTime(0.001, time + 0.02); - - osc.connect(envelope); - envelope.connect(audioContext.current.destination); - - osc.start(time); - osc.stop(time + 0.03); - }, [barLength]) - - const stepStart = (note,bank) => { - prevStep.current = step; - - setSequence( s => ({ - ...s, - [step]: { - note, - bank, - length: 1 - }} - )) - } - - const stepEnd = () => { - const length = step - prevStep.current + 1; - - setSequence( s => ({ - ...s, - [prevStep.current]: { - ...s[prevStep.current], - length: length < 1 ? 1 : length - } - })) - } - - const clearStep = (step) => { - let s = sequence; - delete s[step]; - setSequence(s); - } - - useEffect(() => { !isPlaying && setIsRecording(false) }, [isPlaying]); - useEffect(() => { isPlaying && isRecording && metronome && playBeat(step) }, [isPlaying, isRecording, metronome, playBeat, step]); - - useEffect(() => { localStorage.setItem("TEMPO", tempo) }, [tempo]); - useEffect(() => { localStorage.setItem("BAR", barLength) }, [barLength]); - useEffect(() => { localStorage.setItem("BARS", bars) }, [bars]); - useEffect(() => { localStorage.setItem("SEQ", JSON.stringify(sequence)) }, [sequence]); - - useEffect(() => { - let interval; - - if(isPlaying) interval = setInterval(() => setStep(s => s === steps - 1 ? 0 : s + 1), 60000/tempo); - else clearInterval(interval); - - return () => clearInterval(interval); - }, [isPlaying, isRecording, playBeat, setStep, tempo, metronome, steps]); - - return ( - - {children} - - ) -} - -const useSequencer = () => { - const context = useContext(SequencerContext); - if(context === undefined) throw new Error("useSequencer must be used within a SequencerProvider") - return context; -} - +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; + +const SequencerContext = createContext(); + +const SequencerProvider = ({children}) => { + //TODO: sequencer + const [barLength, setBarLength] = useState(parseInt(localStorage.getItem("BAR")) || 4); + const [bars, setBars] = useState(parseInt(localStorage.getItem("BARS")) || 1); + const [step, setStep] = useState(0); + const [sequence, setSequence] = useState(JSON.parse(localStorage.getItem("SEQ")) || {}); + const [isPlaying, setIsPlaying] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [metronome, setMetronome] = useState(true); + const [tempo, setTempo] = useState(parseInt(localStorage.getItem("TEMPO")) || 60); + const steps = useMemo(() => bars * barLength, [bars, barLength]) + const prevStep = useRef(0); + + const audioContext = useRef(new AudioContext()); + + const playBeat = useCallback((step) => { + const osc = audioContext.current.createOscillator(); + const envelope = audioContext.current.createGain(); + const time = audioContext.current.currentTime; + + osc.frequency.value = (step % barLength === 0) ? 1000 : 800; + envelope.gain.value = 1; + envelope.gain.exponentialRampToValueAtTime(1, time + 0.001); + envelope.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + + osc.connect(envelope); + envelope.connect(audioContext.current.destination); + + osc.start(time); + osc.stop(time + 0.03); + }, [barLength]) + + const stepStart = (note,bank) => { + prevStep.current = step; + + setSequence( s => ({ + ...s, + [step]: { + note, + bank, + length: 1 + }} + )) + } + + const stepEnd = () => { + const length = step - prevStep.current + 1; + + setSequence( s => ({ + ...s, + [prevStep.current]: { + ...s[prevStep.current], + length: length < 1 ? 1 : length + } + })) + } + + const clearStep = (step) => { + let s = sequence; + delete s[step]; + setSequence(s); + } + + useEffect(() => { !isPlaying && setIsRecording(false) }, [isPlaying]); + useEffect(() => { isPlaying && isRecording && metronome && playBeat(step) }, [isPlaying, isRecording, metronome, playBeat, step]); + + useEffect(() => { localStorage.setItem("TEMPO", tempo) }, [tempo]); + useEffect(() => { localStorage.setItem("BAR", barLength) }, [barLength]); + useEffect(() => { localStorage.setItem("BARS", bars) }, [bars]); + useEffect(() => { localStorage.setItem("SEQ", JSON.stringify(sequence)) }, [sequence]); + + useEffect(() => { + let interval; + + if(isPlaying) interval = setInterval(() => setStep(s => s === steps - 1 ? 0 : s + 1), 60000/tempo); + else clearInterval(interval); + + return () => clearInterval(interval); + }, [isPlaying, isRecording, playBeat, setStep, tempo, metronome, steps]); + + return ( + + {children} + + ) +} + +const useSequencer = () => { + const context = useContext(SequencerContext); + if(context === undefined) throw new Error("useSequencer must be used within a SequencerProvider") + return context; +} + export { useSequencer, SequencerProvider }; \ No newline at end of file diff --git a/src/index.js b/src/index.jsx similarity index 100% rename from src/index.js rename to src/index.jsx diff --git a/src/views/Live.js b/src/views/Live.jsx similarity index 97% rename from src/views/Live.js rename to src/views/Live.jsx index 27d7bc6..293ca7c 100644 --- a/src/views/Live.js +++ b/src/views/Live.jsx @@ -1,41 +1,41 @@ -import { useEffect, useRef } from "react"; - -import Keyboard from "../components/controls/Keyboard"; -import { MdClose } from "react-icons/md"; -import Octave from "../components/controls/Octave"; -import Slider from "../components/controls/Slider"; -import { useLayout } from "../hooks/useLayout"; -import { useMidi } from "../hooks/useMidi"; - -const Live = () => { - const { bottomDrawer, setBottomDrawer } = useLayout(); - const { octave, setOctave, pitchBend } = useMidi(); - const drawerRef = useRef(null); - - useEffect(() => { - if(bottomDrawer) document.body.style.marginBottom = `${drawerRef.current.clientHeight}px` - else document.body.style.marginBottom = null; - }, [bottomDrawer]) - - return ( - - ) -} - +import { useEffect, useRef } from "react"; + +import Keyboard from "../components/controls/Keyboard"; +import { MdClose } from "react-icons/md"; +import Octave from "../components/controls/Octave"; +import Slider from "../components/controls/Slider"; +import { useLayout } from "../hooks/useLayout"; +import { useMidi } from "../hooks/useMidi"; + +const Live = () => { + const { bottomDrawer, setBottomDrawer } = useLayout(); + const { octave, setOctave, pitchBend } = useMidi(); + const drawerRef = useRef(null); + + useEffect(() => { + if(bottomDrawer) document.body.style.marginBottom = `${drawerRef.current.clientHeight}px` + else document.body.style.marginBottom = null; + }, [bottomDrawer]) + + return ( + + ) +} + export default Live; \ No newline at end of file diff --git a/src/views/Synth.js b/src/views/Synth.jsx similarity index 97% rename from src/views/Synth.js rename to src/views/Synth.jsx index 6a31ccb..793999e 100644 --- a/src/views/Synth.js +++ b/src/views/Synth.jsx @@ -1,30 +1,30 @@ -import Display from "../components/layout/Display"; -import Memory from "../components/layout/Memory"; -import Section from "../components/layout/Section" -import { defaultLayout } from "../config/layout"; - -const Synth = () => { - return ( -
-
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- ) -} - +import Display from "../components/layout/Display"; +import Memory from "../components/layout/Memory"; +import Section from "../components/layout/Section" +import { defaultLayout } from "../config/layout"; + +const Synth = () => { + return ( +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ ) +} + export default Synth; \ No newline at end of file diff --git a/src/views/modals/Banks.js b/src/views/modals/Banks.jsx similarity index 97% rename from src/views/modals/Banks.js rename to src/views/modals/Banks.jsx index 6a1b5de..4661db3 100644 --- a/src/views/modals/Banks.js +++ b/src/views/modals/Banks.jsx @@ -1,41 +1,41 @@ -import { useLayout } from "../../hooks/useLayout"; -import { useNTS } from "../../hooks/useNTS"; - -const Banks = () => { - const { handleModal } = useLayout(); - const { renameBank, bankNames } = useNTS(); - - const close = (e) => { - e.preventDefault(); - handleModal(); - } - - return ( -
-
-

Rename Banks

-

Click on any bank to rename it

-
- { [...Array(16).keys()].map((k) => { - return ( -
- renameBank(k, e.target.value) } - className="bg-neutral w-full text-center bg-grid font-sevenSegment input-sm text-xl p-1 text-accent outline focus:outline-base-100 focus:outline-offset-1 focus:outline-1 focus:border-none border-none focus:ring-0 outline-base-100 outline-offset-1 outline-1 px-2" - /> -
- ) - }) } -
- -
-
-
-
- ) -} - +import { useLayout } from "../../hooks/useLayout"; +import { useNTS } from "../../hooks/useNTS"; + +const Banks = () => { + const { handleModal } = useLayout(); + const { renameBank, bankNames } = useNTS(); + + const close = (e) => { + e.preventDefault(); + handleModal(); + } + + return ( +
+
+

Rename Banks

+

Click on any bank to rename it

+
+ { [...Array(16).keys()].map((k) => { + return ( +
+ renameBank(k, e.target.value) } + className="bg-neutral w-full text-center bg-grid font-sevenSegment input-sm text-xl p-1 text-accent outline focus:outline-base-100 focus:outline-offset-1 focus:outline-1 focus:border-none border-none focus:ring-0 outline-base-100 outline-offset-1 outline-1 px-2" + /> +
+ ) + }) } +
+ +
+
+
+
+ ) +} + export default Banks \ No newline at end of file diff --git a/src/views/modals/Info.js b/src/views/modals/Info.jsx similarity index 98% rename from src/views/modals/Info.js rename to src/views/modals/Info.jsx index 8e6be36..7e8fe7b 100644 --- a/src/views/modals/Info.js +++ b/src/views/modals/Info.jsx @@ -1,85 +1,85 @@ -import { AiFillWindows } from "react-icons/ai"; -import { BsCaretRightFill } from "react-icons/bs"; -import { SiLinux } from "react-icons/si"; -import { useLayout } from "../../hooks/useLayout"; -import { useState } from "react"; - -const Info = () => { - const { handleModal, supportsPWA, installPWA } = useLayout(); - - const [ active, setActive ] = useState(0); - const sections = ["How it works", "Compatibility", "Installation"] - console.log(process.env.npm_package_version) - return( -
-
-
- { sections.map((s, i) => ) } -
-
-
- -
-
-

This app makes use of the Web Midi API and the AudioContext API.

-

Make sure your browser support those by checking the links below.

- -

Due on how Apple handle simple and standard things, this app will not work on any Mac OS or iOS devices.

-
-
-
- - Play Store - - { - supportsPWA && - - PWA - - } -
- OR -
-
- - -
-
- - -
-
-
-
-
- -
-
-
- ) -} - +import { AiFillWindows } from "react-icons/ai"; +import { BsCaretRightFill } from "react-icons/bs"; +import { SiLinux } from "react-icons/si"; +import { useLayout } from "../../hooks/useLayout"; +import { useState } from "react"; + +const Info = () => { + const { handleModal, supportsPWA, installPWA } = useLayout(); + + const [ active, setActive ] = useState(0); + const sections = ["How it works", "Compatibility", "Installation"] + console.log(process.env.npm_package_version) + return( +
+
+
+ { sections.map((s, i) => ) } +
+
+
+ +
+
+

This app makes use of the Web Midi API and the AudioContext API.

+

Make sure your browser support those by checking the links below.

+ +

Due on how Apple handle simple and standard things, this app will not work on any Mac OS or iOS devices.

+
+
+
+ + Play Store + + { + supportsPWA && + + PWA + + } +
+ OR +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+ ) +} + export default Info; \ No newline at end of file diff --git a/src/views/modals/Settings.js b/src/views/modals/Settings.jsx similarity index 97% rename from src/views/modals/Settings.js rename to src/views/modals/Settings.jsx index 76ab667..b55518f 100644 --- a/src/views/modals/Settings.js +++ b/src/views/modals/Settings.jsx @@ -1,71 +1,71 @@ -import Dropdown from "../../components/controls/Dropdown"; -import { channelList } from "../../config/midi"; -import { useLayout } from "../../hooks/useLayout"; -import { useMidi } from "../../hooks/useMidi"; - -const Settings = () => { - const { devices, setDevices, channels, setChannels } = useMidi(); - const { handleModal } = useLayout(); - - const close = (e) => { - e.preventDefault(); - handleModal(); - } - - return ( -
-
-

MIDI Settings

-
-
-

NTS Input

- d.name ) } - value={ devices.input } - onChange={ (value) => setDevices({ type: "Input", payload: value }) } - /> - setChannels({ type:"Input", payload: value }) } - /> -
-
-

NTS Output

- d.name ) } - value={ devices.output } - onChange={ (value) => setDevices({ type: "Output", payload: value }) } - /> - setChannels({ type:"Output", payload: value }) } - /> -
-
-

Passthrough

- d.name ) } - value={ devices.passthrough } - onChange={ (value) => setDevices({ type: "Passthrough", payload: value }) } - /> - setChannels({ type:"Passthrough", payload: value }) } - /> -
-
- -
-
-
-
- ) -} - +import Dropdown from "../../components/controls/Dropdown"; +import { channelList } from "../../config/midi"; +import { useLayout } from "../../hooks/useLayout"; +import { useMidi } from "../../hooks/useMidi"; + +const Settings = () => { + const { devices, setDevices, channels, setChannels } = useMidi(); + const { handleModal } = useLayout(); + + const close = (e) => { + e.preventDefault(); + handleModal(); + } + + return ( +
+
+

MIDI Settings

+
+
+

NTS Input

+ d.name ) } + value={ devices.input } + onChange={ (value) => setDevices({ type: "Input", payload: value }) } + /> + setChannels({ type:"Input", payload: value }) } + /> +
+
+

NTS Output

+ d.name ) } + value={ devices.output } + onChange={ (value) => setDevices({ type: "Output", payload: value }) } + /> + setChannels({ type:"Output", payload: value }) } + /> +
+
+

Passthrough

+ d.name ) } + value={ devices.passthrough } + onChange={ (value) => setDevices({ type: "Passthrough", payload: value }) } + /> + setChannels({ type:"Passthrough", payload: value }) } + /> +
+
+ +
+
+
+
+ ) +} + export default Settings \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..b67fcb1 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,93 @@ +import { defineConfig, loadEnv } from 'vite' + +import { VitePWA } from 'vite-plugin-pwa' +import react from '@vitejs/plugin-react' +import viteCompression from 'vite-plugin-compression'; + +export default ({ mode }) => { + let env = loadEnv(mode, process.cwd()); + + return defineConfig({ + base: '/', + server: { + open: true, + port: 3000, + host: true, + watch: { + usePolling: true + } + }, + build: { + outDir: "build", + emptyOutDir: true + }, + plugins: [, + react(), + VitePWA({ + registerType: 'autoUpdate', + manifest: { + "short_name": "NTS-web", + "name": "NTS-1 web controller", + "description": "An NTS-1 web controller, editor and sequencer", + "screenshots": [ + { + "src": "static/media/feature.png", + "type": "image/png", + "sizes": "1024x500", + "form_factor": "wide" + } + ], + "categories": ["Music", "Midi", "Synthesizer"], + "dir": "ltr", + "lang": "en", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64", + "type": "image/x-icon" + }, + { + "src": "static/media/icons/icon192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "static/media/icons/icon512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "static/media/icons/icon.png", + "type": "image/png", + "sizes": "1024x1024", + "purpose": "maskable" + } + ], + "scope": "/", + "start_url": ".", + "display": "standalone", + "theme_color": "#f3cc62", + "background_color": "#212122", + "orientation": "any", + "related_applications": [ + { + "platform": "web", + "url": "https://nts-web.oscarrc.me" + }, + { + "platform": "play", + "url": "https://play.google.com/store/apps/details?id=me.oscarrc.nts_web.twa", + "id": "me.oscarrc.nts_web.twa" + } + ] + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'], + maximumFileSizeToCacheInBytes: 3000000, + runtimeCaching: [] + } + }), + viteCompression() + ] + }) +} \ No newline at end of file