diff --git a/README.md b/README.md index 9d1ced2..7bea050 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,7 @@ For development and building: ### Testing/CICD * Add/fix tests * Add CICD --> automated testing and branch rules + +## Docs +* Improve documentation --> `jsdoc`, `better-docs` +* Fix all docstrings `Slider` and `Dropdown` diff --git a/package.json b/package.json index a4d6250..dbbf58f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "babel-cli": "^6.26.0", "only-allow": "^1.1.1", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "^5.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fda85c..e40e91b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ importers: '@testing-library/user-event': ^14.4.3 babel-cli: ^6.26.0 only-allow: ^1.1.1 + prop-types: ^15.8.1 react: ^18.2.0 react-dom: ^18.2.0 react-scripts: ^5.0.1 @@ -17,6 +18,7 @@ importers: dependencies: babel-cli: 6.26.0 only-allow: 1.1.1 + prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-scripts: 5.0.1_w5u3lwoijcfmmatjhtyh5gmbwq @@ -3133,7 +3135,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.21.4 - caniuse-lite: 1.0.30001426 + caniuse-lite: 1.0.30001427 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -3644,7 +3646,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001426 + caniuse-lite: 1.0.30001427 electron-to-chromium: 1.4.284 node-releases: 2.0.6 update-browserslist-db: 1.0.10_browserslist@4.21.4 @@ -3728,13 +3730,13 @@ packages: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} dependencies: browserslist: 4.21.4 - caniuse-lite: 1.0.30001426 + caniuse-lite: 1.0.30001427 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 dev: false - /caniuse-lite/1.0.30001426: - resolution: {integrity: sha512-n7cosrHLl8AWt0wwZw/PJZgUg3lV0gk9LMI7ikGJwhyhgsd2Nb65vKvmSexCqq/J7rbH3mFG6yZZiPR5dLPW5A==} + /caniuse-lite/1.0.30001427: + resolution: {integrity: sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ==} dev: false /case-sensitive-paths-webpack-plugin/2.4.0: @@ -5562,7 +5564,7 @@ packages: eslint: 8.26.0 fs-extra: 9.1.0 glob: 7.2.3 - memfs: 3.4.8 + memfs: 3.4.9 minimatch: 3.1.2 schema-utils: 2.7.0 semver: 7.3.8 @@ -7576,8 +7578,8 @@ packages: engines: {node: '>= 0.6'} dev: false - /memfs/3.4.8: - resolution: {integrity: sha512-E8QAFfd4csESWOqKIpN+khILPFSAZwPR9S+DO/5UtJNcuanF1jLZz0oWUAPF7xd2c1r6dGjGx+jH1st+MFWufA==} + /memfs/3.4.9: + resolution: {integrity: sha512-3rm8kbrzpUGRyPKSGuk387NZOwQ90O4rI9tsWQkzNW7BLSnKGp23RsEsKK8N8QVCrtJoAMqy3spxHC4os4G6PQ==} engines: {node: '>= 4.0.0'} dependencies: fs-monkey: 1.0.3 @@ -10896,7 +10898,7 @@ packages: webpack: ^4.0.0 || ^5.0.0 dependencies: colorette: 2.0.19 - memfs: 3.4.8 + memfs: 3.4.9 mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.0.0 diff --git a/src/App.js b/src/App.js index 57ca442..7a475c7 100644 --- a/src/App.js +++ b/src/App.js @@ -8,9 +8,10 @@ export default function App() { const [osc1Playing, setOsc1Playing] = useState(false); const [osc2Playing, setOsc2Playing] = useState(false); const [osc3Playing, setOsc3Playing] = useState(false); - - // TODO: set these nicer - + /** + * TODO: look at removing isPlaying, setPlaying here + * Instantiate objects with refactored code, then pass to global + */ const osc1 = new InteractiveOscillator({ initOscType: "sine", initFreq: "73", diff --git a/src/oscillator.js b/src/oscillator.js index 5b0c498..204fecc 100644 --- a/src/oscillator.js +++ b/src/oscillator.js @@ -1,116 +1,89 @@ -import React, { useRef, useEffect, useState } from "react"; +import React, { useEffect } from "react"; import Dropdown from "./dropdown"; import Slider from "./slider"; +import SetupOscillator from "./setupOscillator"; +import SetupStates from "./setupOscillatorStates"; import "./audioStyles.scss"; +import PropTypes from "prop-types"; const oscillatorTypes = [ { label: "\u223F", value: "sine" }, { label: "\u2293", value: "square" }, { label: "\u3030", value: "sawtooth" }, { label: "^⌄", value: "triangle" }, - // { label: 'Custom', value: 'custom' }, ]; +/** + * Interactive Oscillator. + * Renders oscillators with gain, frequency sliders/input boxes + * Separate play pause buttons. + * @component + * @example + * const [oscPlaying, setOscPlaying] = useState(false); + * const osc = new InteractiveOscillator({ + * initOscType: "sine", + * initFreq: "73", + * minFreq: "20", + * maxFreq: "1000", + * id: "osc", + * isPlaying: osc1Playing, + * setPlaying: setOsc1Playing, + * }); + * return ( + *
+ * {osc} + *
+ * ) + */ export default function InteractiveOscillator(props) { - // TODO: reduce the calls here... Define out of fn - // setup default useState objects - // const props.(props.isPlaying && props.setPlaying) - const [freq, setFreq] = useState(props.initFreq); - const [oscType, setOscType] = useState(props.initOscType); - const [gain, setGain] = useState(1); - const audioContextRef = useRef(); - const gainNodeRef = useRef(); - const oscRef = useRef(); - const playingRef = useRef(false); - - // handler: freq slider - const onSlideFreq = (event, props) => { - console.log(`${props.id} Frequency set to ${event.target.value} Hz`); - setFreq(event.target.value); - }; - // handler: gain slider - const onSlideGain = (event, props) => { - console.log(`${props.id} Gain set to ${event.target.value}`); - setGain(event.target.value); - }; - // handler: oscType - const handleChangeOscType = (event, props) => { - console.log( - `${props.id} Oscillator changed from ${oscType} to ${event.target.value} wave type` - ); - setOscType(event.target.value); + // Call setup SetupOscillator and SetupStates + const { gainNodeRef, oscRef } = SetupOscillator(props); + const { freqState, oscType, gainState } = SetupStates(props); + //! State change handler // + // Callback should be useState setter fn + const handleStateChange = (event, cb) => { + console.log(`${event.target.id} set to ${event.target.value}`); + cb(event.target.value); }; + //! Instantiating Components // const oscSelector = new Dropdown({ label: "Shape: ", - initValue: oscType, - handleChange: (e) => handleChangeOscType(e, props), + initValue: oscType.get, + handleChange: (e) => handleStateChange(e, oscType.set), optionList: oscillatorTypes, id: `${props.id}-osc-type-dropdown`, }); const freqSlider = new Slider({ - val: freq, - onSlide: (e) => onSlideFreq(e, props), + val: freqState.get, + onSlide: (e) => handleStateChange(e, freqState.set), min: props.minFreq, max: props.maxFreq, label: `Frequency [Hz] (min: ${props.minFreq}, max: ${props.maxFreq})`, id: `${props.id}-freq-slider`, }); const gainSlider = new Slider({ - val: gain, - onSlide: (e) => onSlideGain(e, props), + val: gainState.get, + onSlide: (e) => handleStateChange(e, gainState.set), min: 0, max: 1, step: 0.01, label: `Gain (0-1)`, id: `${props.id}-gain-slider`, }); - - // initial osc starting - useEffect(() => { - const audioContext = new AudioContext(); - const osc = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); - // Connect and start - osc.connect(gainNode); - gainNode.connect(audioContext.destination); - osc.start(); - - // Create refs to updatable params, start suspended - gainNodeRef.current = gainNode; - oscRef.current = osc; - audioContextRef.current = audioContext; - audioContext.suspend(); - // Disconnect osc - return () => { - osc.disconnect(gainNode); - gainNode.disconnect(audioContext.destination); - audioContext.close(); - }; - }, []); + //! useEffects // // update oscType useEffect(() => { - if (oscRef.current) oscRef.current.type = oscType; - }, [oscType]); + if (oscRef.current) oscRef.current.type = oscType.get; + }, [oscType.get, oscRef]); // update freq values useEffect(() => { - if (oscRef.current) oscRef.current.frequency.value = freq; - }, [freq]); + if (oscRef.current) oscRef.current.frequency.value = freqState.get; + }, [freqState.get, oscRef]); // update gain values useEffect(() => { - if (gainNodeRef.current) gainNodeRef.current.gain.value = gain; - }, [gain]); - // updates play/pause state - useEffect(() => { - if (playingRef.current !== props.isPlaying) { - console.log( - `${props.id} oscillator ` + (playingRef.current ? "stopped" : "started") - ); - playingRef.current - ? audioContextRef.current.suspend() - : audioContextRef.current.resume(); - playingRef.current = !playingRef.current; - } - }, [props.isPlaying, props.id, props.setPlaying]); + if (gainNodeRef.current) gainNodeRef.current.gain.value = gainState.get; + }, [gainState.get, gainNodeRef]); + return (
{oscSelector} @@ -126,3 +99,20 @@ export default function InteractiveOscillator(props) {
); } + +InteractiveOscillator.propTypes = { + /** Initial frequency of oscillator [Hz] */ + initFreq: PropTypes.number, + /** Max frequency of oscillator [Hz] */ + maxFreq: PropTypes.number, + /** Min frequency of oscillator [Hz] */ + minFreq: PropTypes.number, + /** Initial oscillator waveshape */ + initOscType: PropTypes.oneOf(oscillatorTypes.map((item) => item.value)), + /** playing useState */ + isPlaying: PropTypes.bool, + /** sets playing useState */ + setPlaying: PropTypes.func, + /** id */ + id: PropTypes.string, +}; diff --git a/src/setupOscillator.js b/src/setupOscillator.js new file mode 100644 index 0000000..1dc4047 --- /dev/null +++ b/src/setupOscillator.js @@ -0,0 +1,69 @@ +import { useRef, useEffect } from "react"; +import PropTypes from "prop-types"; + +/** + * Sets up webAudioAPI oscillator object. + * New audio context, oscillator and gain node connected. + * Rendered paused on startup. + * @component + * @example + * const id = "my-osc" + * const [isPlaying, setPlaying] = useState() + * const { gainNodeRef, oscRef } = SetupOscillator({id:id, isPlaying:isPlaying,setPlaying:setPlaying}); + */ +export default function SetupOscillator(props) { + const audioContextRef = useRef(); + const gainNodeRef = useRef(); + const oscRef = useRef(); + const playingRef = useRef(false); + + // initial osc starting + useEffect(() => { + const audioContext = new AudioContext(); + const osc = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + // Connect and start + osc.connect(gainNode); + gainNode.connect(audioContext.destination); + osc.start(); + + // Create refs to updatable params, start suspended + gainNodeRef.current = gainNode; + oscRef.current = osc; + audioContextRef.current = audioContext; + audioContext.suspend(); + // Disconnect osc + return () => { + osc.disconnect(gainNode); + gainNode.disconnect(audioContext.destination); + audioContext.close(); + }; + }, []); + // updates play/pause state + useEffect(() => { + if (playingRef.current !== props.isPlaying) { + console.log( + `${props.id} oscillator ` + (playingRef.current ? "stopped" : "started") + ); + playingRef.current + ? audioContextRef.current.suspend() + : audioContextRef.current.resume(); + playingRef.current = !playingRef.current; + } + }, [props.isPlaying, props.id, props.setPlaying]); + + return { + audioContextRef: audioContextRef, + gainNodeRef: gainNodeRef, + oscRef: oscRef, + playingRef: playingRef, + }; +} +SetupOscillator.propTypes = { + /** playing useState */ + isPlaying: PropTypes.bool, + /** sets playing useState */ + setPlaying: PropTypes.func, + /** id */ + id: PropTypes.string, +}; diff --git a/src/setupOscillatorStates.js b/src/setupOscillatorStates.js new file mode 100644 index 0000000..2f73d88 --- /dev/null +++ b/src/setupOscillatorStates.js @@ -0,0 +1,32 @@ +import { useState } from "react"; +import PropTypes from "prop-types"; + +/** + * Sets up react states for oscillator. + * @component + * @example + * const freq = 440 + * const oscType = "sine" + * + * const oscRef = useRef() // Typically defined with `SetupOscillator` + * const { freqState, oscType, gainState } = SetupStates({initFreq:freq, initOscType:oscType}); + */ +export default function SetupStates(props) { + const [freq, setFreq] = useState(props.initFreq); + const [oscType, setOscType] = useState(props.initOscType); + const [gain, setGain] = useState(1); + + return { + freqState: { get: freq, set: setFreq }, + oscType: { get: oscType, set: setOscType }, + gainState: { get: gain, set: setGain }, + }; +} + +SetupStates.propTypes = { + /** Initial frequency of oscillator [Hz] */ + initFreq: PropTypes.number, + /** Initial oscillator waveshape */ + // TODO: set this from the oscillatorTypes struct + initOscType: PropTypes.oneOf(["sine", "square", "sawtooth", "triangle"]), +}; diff --git a/src/slider.js b/src/slider.js index a77702d..9b136c7 100644 --- a/src/slider.js +++ b/src/slider.js @@ -1,14 +1,14 @@ -export default function Slider({ min, max, step, val, onSlide, label }) { +export default function Slider({ min, max, step, val, onSlide, label, id }) { // requires min, mix, val, onSlider, label return (

- +
onSlide(val)} /> @@ -16,7 +16,7 @@ export default function Slider({ min, max, step, val, onSlide, label }) {