Skip to content

Commit

Permalink
FEAT: Adds Global controls section. (#7)
Browse files Browse the repository at this point in the history
## Features
* FEAT: pass `isPlaying` and `setPlaying` to osc directly
* FEAT: nicer structure
* FEAT: `toggleOscillator` -> `useState`
* FEAT: started refactor, small UI tweaks

## Docs
* DOC: update `README.md` and enhance `TODOs`, typo fix
  • Loading branch information
jordyjwilliams authored Oct 29, 2022
1 parent e9e637b commit be7fd33
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 61 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This project uses [pnpm](https://pnpm.io/) for package management.
* To install this globally run `npm i -g pnpm`

## Screenshots
### Singular `InteractiveOscilator`
### Singular `InteractiveOscillator`
![Single Oscillator](./screenshots/osc.png)
### Default `App.js`
![Triple Oscillator](./screenshots/tri_osc.png)
Expand All @@ -34,6 +34,13 @@ For development and building:
* Quick proof of concept/tutorial/teaching aid for myself.

## TODO
* add CSS / make this look nice
* oscillator to view created waveform in realtime
* add tests
### UI
* Add CSS / make this look nice
* Osc/scope to view created waveform in realtime

### Audio
* Gain sliders for each osc

### Testing/CICD
* Add/fix tests
* Add CICD --> automated testing and branch rules
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Inter-Reactive-Oscillator</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"short_name": "React App",
"short_name": "InterReactiveOscillator",
"name": "Create React App Sample",
"icons": [
{
Expand Down
78 changes: 54 additions & 24 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,72 @@
import InteractiveOscillator from "./oscillator";
import GlobalPlayPause from "./globalPlayPause";
import React, { useState } from "react";

window.AudioContext = window.AudioContext || window.webkitAudioContext;

export default function App() {
const [osc1Playing, setOsc1Playing] = useState(false);
const [osc2Playing, setOsc2Playing] = useState(false);
const [osc3Playing, setOsc3Playing] = useState(false);

// TODO: set these nicer

const osc1 = new InteractiveOscillator({
initOscType: "sine",
initFreq: "73",
minFreq: "20",
maxFreq: "1000",
id: "osc-1",
isPlaying: osc1Playing,
setPlaying: setOsc1Playing,
});
const osc2 = new InteractiveOscillator({
initOscType: "square",
initFreq: "77",
minFreq: "20",
maxFreq: "1000",
id: "osc-2",
isPlaying: osc2Playing,
setPlaying: setOsc2Playing,
});
const osc3 = new InteractiveOscillator({
initOscType: "triangle",
initFreq: "75",
minFreq: "20",
maxFreq: "1000",
id: "osc-3",
isPlaying: osc3Playing,
setPlaying: setOsc3Playing,
});
const globalPP = new GlobalPlayPause({
playStates: [
{ isPlaying: osc1Playing, setPlaying: setOsc1Playing },
{ isPlaying: osc2Playing, setPlaying: setOsc2Playing },
{ isPlaying: osc3Playing, setPlaying: setOsc3Playing },
],
id: "global-play-pause",
});
return (
<div
id="osc-grid"
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gridGap: 20,
padding: 50,
}}
>
<div>
<InteractiveOscillator
initOscType="sine"
initFreq="261.63"
minFreq="20"
maxFreq="1000"
id="osc-1"
/>
<div id="osc-1-div" htmlFor="osc-1">
{osc1}
</div>
<div id="osc-2-div" htmlFor="osc-2">
{osc2}
</div>
<div>
<InteractiveOscillator
initOscType="triangle"
initFreq="329.63"
minFreq="20"
maxFreq="1000"
id="osc-2"
/>
<div id="osc-3-div" htmlFor="osc-3">
{osc3}
</div>
<div>
<InteractiveOscillator
initOscType="square"
initFreq="392.00"
minFreq="20"
maxFreq="1000"
id="osc-3"
/>
<div id="global-div" htmlFor="global-play-pause">
{globalPP}
</div>
</div>
);
Expand Down
38 changes: 38 additions & 0 deletions src/globalPlayPause.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import "./audioStyles.scss";
import React, { useState, useEffect } from "react";

export default function GlobalPlayPause(props) {
// props.playStates: array of each InteractiveOscillator
// isPlaying and setPlaying keys
const [isPlayingGlobal, setPlayingGlobal] = useState(false);

// Get state changes of oscillators
useEffect(() => {
setPlayingGlobal(
props.playStates.map((oscPlayData) => oscPlayData.isPlaying).some(Boolean)
);
}, [props.playStates, isPlayingGlobal]);

const handleGlobalPlayPause = () => {
console.log(`All oscillators ` + (isPlayingGlobal ? "stopped" : "started"));
props.playStates.map((oscPlayData) =>
oscPlayData.setPlaying(!isPlayingGlobal)
);
};

return (
<div>
<label htmlFor="global-play-pause">
Global Controls:
<br />
<button
onClick={handleGlobalPlayPause}
id="global-play-pause"
className={
isPlayingGlobal ? "play-pause-button paused" : "play-pause-button"
}
/>
</label>
</div>
);
}
68 changes: 37 additions & 31 deletions src/oscillator.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,42 @@ const oscillatorTypes = [
];

export default function InteractiveOscillator(props) {
// TODO: reduce the calls here... Define out of fn
// setup default useState objects
const [isPlaying, setPlaying] = useState(false);
// const props.(props.isPlaying && props.setPlaying)
const [freq, setFreq] = useState(props.initFreq);
const [oscType, setOscType] = useState(props.initOscType);
const audioContextRef = useRef();
const oscRef = useRef();
const [oscType, setOscType] = useState(props.initOscType);
const playingRef = useRef(false);

// handler for frequency slider
const onSlideFreq = (event) => {
const onSlideFreq = (event, props) => {
console.log(`${props.id} Frequency set to ${event.target.value} Hz`);
setFreq(event.target.value);
};
// handler for switching type
const handleChangeOscType = (event) => {
const handleChangeOscType = (event, props) => {
console.log(
`${props.id} Oscillator changed from ${oscType} to ${event.target.value} wave type`
);
setOscType(event.target.value);
};
const oscSelector = new Dropdown({
label: "Shape: ",
initValue: oscType,
handleChange: (e) => handleChangeOscType(e, props),
optionList: oscillatorTypes,
id: `${props.id}OscTypeDropdown`,
});
const freqSlider = new Slider({
val: freq,
onSlide: (e) => onSlideFreq(e, props),
min: props.minFreq,
max: props.maxFreq,
label: `Frequency [Hz] (min: ${props.minFreq}, max: ${props.maxFreq})`,
id: `${props.id}FreqSlider`,
});

// initial osc starting
useEffect(() => {
Expand All @@ -56,38 +73,27 @@ export default function InteractiveOscillator(props) {
if (oscRef.current) oscRef.current.frequency.value = freq;
}, [freq]);
// Play/Pause
const toggleOscillator = () => {
if (isPlaying) {
console.log(`${props.id} oscillator stopped`);
audioContextRef.current.suspend();
} else {
console.log(`${props.id} oscillator started`);
audioContextRef.current.resume();
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;
}
setPlaying((play) => !play);
};
}, [props.isPlaying, props.id, props.setPlaying]);
return (
<div>
<Dropdown
label="Osc Type: "
initValue={oscType}
handleChange={handleChangeOscType}
optionList={oscillatorTypes}
id={`${props.id}-osc-type-dropdown`}
/>
<Slider
val={freq}
onSlide={onSlideFreq}
min={props.minFreq}
max={props.maxFreq}
label={`Current Frequency [Hz] (min: ${props.minFreq}, max: ${props.maxFreq})`}
id={`${props.id}-freq-slider`}
/>
{oscSelector}
{freqSlider}
<button
onClick={toggleOscillator}
data-playing={isPlaying}
onClick={() => props.setPlaying((play) => !play)}
id={`${props.id}-play-pause`}
className={isPlaying ? "play-pause-button paused" : "play-pause-button"}
className={
props.isPlaying ? "play-pause-button paused" : "play-pause-button"
}
></button>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/slider.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ export default function Slider({ min, max, val, onSlide, label }) {
<div>
<br />
<label htmlFor="numeric-input-slider-val">{label}</label>
<br />
<input
type="number"
id="numeric-input-slider-val"
htmlFor="slider-input"
value={val}
onChange={(val) => onSlide(val)}
/>
<br />
<input
name="slider"
type="range"
Expand Down

0 comments on commit be7fd33

Please sign in to comment.