Skip to content

Commit

Permalink
FEAT: big refactor of InteractiveOscillator (#9)
Browse files Browse the repository at this point in the history
## Features
* FEAT: create `SetupOscillator` comp
* FEAT: create `SetupStates` comp
* FEAT: improve docstrings and comments
* FEAT: remove legacy instantiation, use new methods
## Docs
* DOCS: Adds prop-types
  • Loading branch information
jordyjwilliams authored Oct 30, 2022
1 parent ac71565 commit 3333730
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 93 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 11 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
142 changes: 66 additions & 76 deletions src/oscillator.js
Original file line number Diff line number Diff line change
@@ -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 (
* <div id="osc-div" htmlFor="osc">
* {osc}
* </div>
* )
*/
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 (
<div>
{oscSelector}
Expand All @@ -126,3 +99,20 @@ export default function InteractiveOscillator(props) {
</div>
);
}

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,
};
69 changes: 69 additions & 0 deletions src/setupOscillator.js
Original file line number Diff line number Diff line change
@@ -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,
};
32 changes: 32 additions & 0 deletions src/setupOscillatorStates.js
Original file line number Diff line number Diff line change
@@ -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"]),
};
Loading

0 comments on commit 3333730

Please sign in to comment.