commit bd28ca810eab6d2303217ade018939a0b6cf5f2d
parent 539500c7df344f7c530fb5120f43a0a34cecb1af
Author: Massimo Siboldi <mdsiboldi@gmail.com>
Date: Mon, 19 Feb 2018 21:41:29 -0800
Too many changes for one commit, but i'll list them out
sorry, future me
- knob broken into param and wheel, now param has room for a child
element, which is sometimes wheel.
- now Param can be fixed to a certain precision.
- more data associated with ADSR to have better control over look /
feel and data ranges.
- metroMs becomes bpm, and it becomes a setTimeout loop to allow for
changing bpm. bpm has a cool param UI, soon to be with a blinking
light. there is an improperly named and / or hacky variable over there.
- a custom checkbox starts to exist, without the accesibility of
normal checkboxes. focusable custom checkbox is on the way.
- now the App does the combining of waveforms ceremony, so that Synth
can use it as well as other canvasses (performance mode)
- edited some default settings for bug fixes and looks
Diffstat:
13 files changed, 377 insertions(+), 150 deletions(-)
diff --git a/src/App/index.js b/src/App/index.js
@@ -3,7 +3,8 @@ import WaveEditor from '../WaveEditor/';
import WaveManager from '../WaveManager/';
import Synth from '../Synth/';
import CircleButton from '../CircleButton/';
-import Knob from '../Knob/';
+import Param from '../Param/';
+import Wheel from '../Wheel/';
import './App.css';
import consts from '../consts.js';
import helpers from '../helpers.js';
@@ -27,7 +28,7 @@ const immObjArray = {
const newArr = arr.slice();
newArr.splice(idx, 1);
return newArr;
- }
+ }
}
const boolArray = {
@@ -47,18 +48,45 @@ const boolArray = {
}
}
+const adsrProperties = [
+ {
+ name: 'attack',
+ suffix: 's',
+ maxVal: 10
+ },
+ {
+ name: 'decay',
+ suffix: 's',
+ maxVal: 10
+ },
+ {
+ name: 'sustain',
+ maxVal: 2
+ },
+ {
+ name: 'release',
+ suffix: 's',
+ maxVal: 30,
+ }
+];
+
const Adsr = (props) => (
- <div>
- {['a', 'd', 's', 'r'].map((letter) => (
- <Knob
- val={props.adsr[letter]}
- minVal={0}
- maxVal={3}
- step={0.1}
- update={(newVal) => {props.update(letter, newVal)}}
- />
- ))}
+ <div style="display: inline-block;">
+ {adsrProperties.map((aspect) => (
+ <Param
+ suffix={aspect.suffix || ''}
+ precision={1}
+ val={props.adsr[aspect.name]}
+ name={aspect.name}
+ minVal={0}
+ maxVal={aspect.maxVal}
+ step={0.1}
+ update={(newVal) => {props.update(aspect.name, newVal)}}
+ >
+ <Wheel percent={props.adsr[aspect.name] / aspect.maxVal} />
+ </Param>
+ ))}
</div>
)
@@ -68,26 +96,27 @@ class App extends Component {
super();
let initBeats = 4;
this.state = {
- metroMs: 250,
+ bpm: 120,
beat: 0,
waveforms: [{
+ active: true,
waveform: initialWave.slice(),
- beats: boolArray.create(initBeats)
+ beats: boolArray.update(boolArray.create(initBeats), 0, true)
}],
numBeats: initBeats,
editingWaveformIdx: 0,
adsr: {
- a: 0.3,
- d: 1,
- s: 0.4,
- r: 1
+ attack: 0.3,
+ decay: 1,
+ sustain: 0.4,
+ release: 1
}
}
}
- updateAdsr = (letter, val) => {
+ updateAdsr = (aspect, val) => {
this.setState({
- adsr: Object.assign({}, this.state.adsr, {[letter]: val})
+ adsr: Object.assign({}, this.state.adsr, {[aspect]: val})
});
}
@@ -100,6 +129,19 @@ class App extends Component {
return accum;
}, [])
+ //TODO: for some reason, this function ends up modifying the array passed in as a prop to WaveEditor. I probably need to break editor out into viewer as well. But in any case, there should be a nice way to observe changes in the array or at least communicate when a change has happened.
+ totalWaveform = () => {
+ const allWaves = this.activeWaveforms();
+ if (allWaves.length === 0) {
+ return new Array(consts.BUF_SIZE).fill(0);
+ }
+ const firstWave = allWaves.shift();
+ return allWaves.reduce(
+ (totalArray, currWaveform, i) => totalArray.map((val, j) => ((val * (i + 1)) + currWaveform[j]) / (i + 2)),
+ firstWave
+ )
+ }
+
updateWaveform = (idx = this.state.editingWaveformIdx, opts) => {
this.setState({
waveforms: immObjArray.update(this.state.waveforms, idx, opts)
@@ -124,7 +166,7 @@ class App extends Component {
waveform,
beats: boolArray.create(this.state.numBeats)
});
-
+
const state = {
waveforms
};
@@ -149,15 +191,18 @@ class App extends Component {
}
metro = () => {
+ this.setState({
+ interval: true
+ });
const loop = () => {
- this.setState({
- beat: (this.state.beat + 1) % this.state.numBeats
- })
+ if (this.state.interval) {
+ this.setState({
+ beat: (this.state.beat + 1) % this.state.numBeats
+ })
+ window.setTimeout(loop, (1 / this.state.bpm) * 60000);
+ }
}
- const interval = window.setInterval(loop, this.state.metroMs);
- this.setState({
- interval
- })
+ loop();
}
stopMetro = () => {
@@ -165,7 +210,7 @@ class App extends Component {
window.clearInterval(this.state.interval);
}
this.setState({
- interval: null,
+ interval: false,
beat: 0
})
}
@@ -175,69 +220,91 @@ class App extends Component {
console.log('wow i got through', e.key);
}
+ setBpm = (newBpm) => {
+ this.setState({
+ bpm: newBpm
+ });
+ }
+
render() {
const waves = this.state.waveforms.map((form, idx) => {
return (
- <WaveManager
- activate={this.changeEditingWaveform.bind(this, idx)}
- remove={this.removeWaveform.bind(this, idx)}
- duplicate={() => {
- let pleaseActivate = false;
- if (this.state.editingWaveformIdx === idx) {
- pleaseActivate = true;
- }
- this.addWaveform(this.state.waveforms[idx].waveform.slice(), idx + 1, pleaseActivate);
- }}
- activated={idx === this.state.editingWaveformIdx}
- beats={this.state.waveforms[idx].beats}
- beat={this.state.beat}
- updateBeat={(i, val) => {
- this.updateWaveform(idx, {
- beats: boolArray.update(
- this.state.waveforms[idx].beats,
- i,
- val
- )
- });
- }}
- ></WaveManager>
+ <WaveManager
+ activate={this.changeEditingWaveform.bind(this, idx)}
+ remove={this.removeWaveform.bind(this, idx)}
+ duplicate={() => {
+ let pleaseActivate = false;
+ if (this.state.editingWaveformIdx === idx) {
+ pleaseActivate = true;
+ }
+ this.addWaveform(this.state.waveforms[idx].waveform.slice(), idx + 1, pleaseActivate);
+ }}
+ activated={idx === this.state.editingWaveformIdx}
+ waveform={this.state.waveforms[idx].waveform}
+ beats={this.state.waveforms[idx].beats}
+ beat={this.state.beat}
+ updateBeat={(i, val) => {
+ this.updateWaveform(idx, {
+ beats: boolArray.update(
+ this.state.waveforms[idx].beats,
+ i,
+ val
+ )
+ });
+ }}
+ ></WaveManager>
);
})
return (
- <div
- className="App"
- onKeyDown={this.keyHandler}
- >
- <h1>synthing</h1>
- <WaveEditor
- mouseData={this.state.mouseData}
- waveform={this.editingWaveform()}
- updateWaveform={(waveform) => {
- this.updateWaveform(this.state.editingWaveformIdx, {waveform});
- }}
- ></WaveEditor>
- <div class="global-controls">
- <CircleButton
- active={this.state.interval}
- action={this.metro}
- disabled={this.state.interval}
- >
- <div class="triangle"></div>
- </CircleButton>
- <CircleButton
- active={!this.state.interval}
- action={this.stopMetro}
- >
- <div class="rectangle"></div>
- </CircleButton>
- <button onClick={() => {this.setBeats(this.state.numBeats + 1)}}>+ beat</button>
- <button onClick={() => {this.setBeats(this.state.numBeats - 1)}}>- beat</button>
- <Adsr adsr={this.state.adsr} update={this.updateAdsr} />
- </div>
- {waves}
- <button onClick={() => this.addWaveform()}>+</button>
- <Synth waveforms={this.activeWaveforms()} adsr={this.state.adsr}></Synth>
- </div>
+ <div
+ className="App"
+ onKeyDown={this.keyHandler}
+ >
+ <h1>synthing</h1>
+ <WaveEditor
+ mouseData={this.state.mouseData}
+ waveform={this.editingWaveform()}
+ updateWaveform={(waveform) => {
+ this.updateWaveform(this.state.editingWaveformIdx, {waveform});
+ }}
+ ></WaveEditor>
+ <div class="global-controls">
+ <CircleButton
+ active={this.state.interval}
+ action={this.metro}
+ disabled={this.state.interval}
+ >
+ <div class="triangle"></div>
+ </CircleButton>
+ <CircleButton
+ active={!this.state.interval}
+ action={this.stopMetro}
+ >
+ <div class="rectangle"></div>
+ </CircleButton>
+ <Param
+ precision={0}
+ name="bpm"
+ minVal={20}
+ maxVal={600}
+ step={1}
+ val={this.state.bpm}
+ update={this.setBpm}
+ />
+ <Param
+ name="beats"
+ minVal="1"
+ maxVal={16}
+ step="1"
+ val={this.state.numBeats}
+ update={this.setBeats}
+ />
+ <Adsr adsr={this.state.adsr} update={this.updateAdsr} />
+ </div>
+ {waves}
+ <button onClick={() => this.addWaveform()}>+</button>
+ <Synth waveform={this.totalWaveform()} adsr={this.state.adsr}></Synth>
+ </div>
);
}
}
diff --git a/src/CheckBox/index.js b/src/CheckBox/index.js
@@ -0,0 +1,18 @@
+import { h } from 'preact';
+import helpers from '../helpers';
+import './style.css';
+
+const handleToggle = (update, checked, ev) => {
+ update(!checked);
+}
+
+export default (props) => {
+ return (
+ <div
+ class={`${props.class} checkbox`}
+ onClick={helpers.partial(handleToggle, props.update, props.checked)}
+ >
+ {props.checked ? <div class="checkbox-circle"></div> : null }
+ </div>
+ );
+}
diff --git a/src/CheckBox/style.css b/src/CheckBox/style.css
@@ -0,0 +1,23 @@
+.checkbox {
+ background: #f1ddd8;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ margin: 5px;
+}
+.checkbox .checkbox-circle {
+ width: 25px;
+ height: 25px;
+ border-radius: 999px;
+ background: #ff735e;
+}
+
+.checkbox.beat {
+ background: #ff735e;
+}
+
+.checkbox.beat .checkbox-circle {
+ background: #f1ddd8;
+}
diff --git a/src/Knob/index.js b/src/Knob/index.js
@@ -1,34 +0,0 @@
-import { h, Component} from 'preact';
-import helpers from '../helpers.js';
-
-export default class Knob extends Component {
- componentDidMount() {
- const handleMove = (ev) => {
- let step = this.props.step || 0.5;
- let newVal = this.props.val - (ev.movementY * step);
- if (typeof(this.props.minVal) === 'number' && (newVal < this.props.minVal)) {
- newVal = this.props.minVal;
- } else if (typeof(this.props.maxVal) === 'number' && (newVal > this.props.maxVal)) {
- newVal = this.props.maxVal;
- }
- this.props.update(newVal);
- }
- this.knobRef.addEventListener('dblclick', (ev) => {
- console.log('double');
- this.knobRef.setAttribute('contenteditable', true);
- });
- this.knobRef.addEventListener('mousedown', (ev) => {
- ev.preventDefault();
- document.addEventListener('mousemove', handleMove);
- helpers.oneTime(document, 'mouseup', (ev) => {
- document.removeEventListener('mousemove', handleMove);
- });
- });
- }
- render() {
- return (
- <div ref={(knob) => {this.knobRef = knob}}>{this.props.val}</div>
- )
- }
-}
-
diff --git a/src/Param/index.js b/src/Param/index.js
@@ -0,0 +1,49 @@
+import { h, Component} from 'preact';
+import helpers from '../helpers.js';
+import './style.css';
+
+export default class Param extends Component {
+ componentDidMount() {
+ const handleMove = (ev) => {
+ let step = this.props.step || 0.5;
+ let newVal = this.props.val - (ev.movementY * step);
+ if (typeof(this.props.minVal) === 'number' && (newVal < this.props.minVal)) {
+ newVal = this.props.minVal;
+ } else if (typeof(this.props.maxVal) === 'number' && (newVal > this.props.maxVal)) {
+ newVal = this.props.maxVal;
+ }
+ this.props.update(newVal);
+ }
+ this.paramRef.addEventListener('mousedown', (ev) => {
+ ev.preventDefault();
+ document.addEventListener('mousemove', handleMove);
+ helpers.oneTime(document, 'mouseup', (ev) => {
+ document.removeEventListener('mousemove', handleMove);
+ });
+ });
+ }
+ handleChange(e) {
+ this.props.update(e.target.value);
+ }
+ getNumString() {
+ return (this.props.precision ?
+ this.props.val.toFixed(this.props.precision) :
+ this.props.val) + (this.props.suffix || '');
+ }
+ render() {
+ const inputId = `param-${this.props.name}`;
+ return (
+ <div class="param" ref={(param) => {this.paramRef = param}}>
+ <label for={inputId}>{this.props.name}</label>
+ <input
+ type="text"
+ id={inputId}
+ value={this.getNumString()}
+ onChange={this.handleChange}
+ ></input>
+ {this.props.children}
+ </div>
+ )
+ }
+}
+
diff --git a/src/Param/style.css b/src/Param/style.css
@@ -0,0 +1,33 @@
+.param {
+ position: relative;
+ display: inline-block;
+ padding: 10px;
+ padding-right: 46px;
+ box-sizing: border-box;
+}
+
+.param > * {
+ cursor: ns-resize;
+}
+
+.param label {
+ display: block;
+ font-size: 11px;
+ text-align: left;
+ position: relative;
+ left: -1px;
+}
+
+.param input {
+ border: 0;
+ width: 60px;
+ height: 22px;
+ font-size: 16px;
+ text-align: center;
+}
+
+.param > canvas {
+ position: absolute;
+ right: 5px;
+ top: 15px;
+}
diff --git a/src/Polyphonic.js b/src/Polyphonic.js
@@ -13,10 +13,10 @@ export default class Polyphonic {
let osc = this.audioContext.createOscillator();
let gain = this.audioContext.createGain();
let envelope = new Envelope(this.audioContext, {
- attackTime: adsr.a,
- decayTime: adsr.d,
- sustainLevel: adsr.s,
- releaseTime: adsr.r,
+ attackTime: adsr.attack,
+ decayTime: adsr.decay,
+ sustainLevel: adsr.sustain,
+ releaseTime: adsr.release,
maxLevel: 0.4
});
diff --git a/src/Synth/index.js b/src/Synth/index.js
@@ -25,7 +25,7 @@ export default class Synth extends Component {
return false;
}
componentWillReceiveProps(newProps) {
- updateAudio(newProps.waveforms);
+ updateAudio(newProps.waveform);
}
render() {
return null;
@@ -42,8 +42,8 @@ function combineWaveforms(waveforms) {
return res;
}
-function updateAudio(waveforms) {
- const waveform = combineWaveforms(waveforms);
+function updateAudio(waveform) {
+ //const waveform = combineWaveforms(waveforms);
FFT.forward(waveform);
const periodicWave = ac.createPeriodicWave(
new Float32Array(FFT.real),
diff --git a/src/WaveManager/index.js b/src/WaveManager/index.js
@@ -1,30 +1,38 @@
import { h, Component } from 'preact';
+import CheckBox from '../CheckBox/';
+import WaveTable from '../WaveTable/';
+import helpers from '../helpers';
import './style.css';
export default class WaveManager extends Component {
render() {
return (
<div class="wave-manager">
- <div class="buttons">
- {this.props.activated ? 'x' : ''}
- <button onClick={this.props.activate}>activate</button>
- <button onClick={this.props.remove}>remove</button>
- <button onClick={this.props.duplicate}>dupe</button>
- </div>
- <div class="beats">
- {this.props.beats.map((val, idx) => {
- return (
- <span class={this.props.beat === idx ? 'beat' : ''}>
- <input
- type="checkbox"
- checked={val}
- onChange={(ev) => {this.props.updateBeat(
- idx,
- ev.target.checked)}}
- ></input>
- </span>
- )})}
- </div>
+ <div class="buttons">
+ {this.props.activated ? (
+ <div>
+ <button onClick={this.props.remove}>remove</button>
+ <button onClick={this.props.duplicate}>dupe</button>
+ </div>
+ ) : ''}
+ </div>
+ <div class="beats">
+ <div onClick={this.props.activate}>
+ <WaveTable
+ height={40}
+ width={75}
+ waveform={this.props.waveform.slice()}
+ />
+ </div>
+
+ {this.props.beats.map((val, idx) => {
+ return (
+ <CheckBox
+ class={this.props.beat === idx ? 'beat' : ''}
+ checked={val}
+ update={helpers.partial(this.props.updateBeat, idx)} />
+ )})}
+ </div>
</div>
);
}
diff --git a/src/WaveManager/style.css b/src/WaveManager/style.css
@@ -1,3 +1,11 @@
-.beat {
- background: red;
+.beats {
+ display: flex;
+}
+
+.beats > * {
+ margin: 5px;
+}
+
+.beats > *:first-child {
+ margin-right: 10px;
}
diff --git a/src/Wheel/index.js b/src/Wheel/index.js
@@ -0,0 +1,54 @@
+import { h, Component} from 'preact';
+
+const drawCircle = (canvas, percent) => {
+ const context = canvas.getContext('2d');
+ const color = '#ff735e';
+
+ context.lineWidth = 6;
+ context.clearRect(0,0,36,36);
+ context.beginPath();
+ context.strokeStyle = "#00000011";
+
+ if (percent === 0) {
+ context.arc(18, 18, 15, 0, 10);
+ context.stroke();
+ }
+ else {
+ const begin = -(Math.PI / 2);
+ const end = 2 * Math.PI * percent - Math.PI / 2;
+
+ context.arc(18, 18, 15, end, begin);
+ context.stroke();
+ context.beginPath();
+ context.strokeStyle = color;
+ context.arc(18, 18, 15, begin, end);
+ context.stroke();
+ }
+
+}
+
+export default class Wheel extends Component {
+ componentWillUpdate() {
+ return false;
+ }
+
+ componentDidMount() {
+ drawCircle(this.wheelRef, this.props.percent);
+ }
+
+ componentWillReceiveProps(newProps) {
+ if (newProps.percent !== this.props.percent);
+ drawCircle(this.wheelRef, newProps.percent);
+ }
+
+ render() {
+ return (
+ <canvas
+ class="wheel"
+ height="36"
+ width="36"
+ ref={(ref) => {this.wheelRef = ref}}
+ ></canvas>
+ );
+ }
+}
diff --git a/src/consts.js b/src/consts.js
@@ -1,3 +1,3 @@
export default {
- BUF_SIZE: 1024
+ BUF_SIZE: 256
}
diff --git a/src/helpers.js b/src/helpers.js
@@ -1,4 +1,5 @@
export default {
+ partial: (f, ...args) => (...moreArgs) => f(...args, ...moreArgs),
linear: (m, x, b) => (m * x) + b,
bounded: (val, min, max) => val < min ? min : (val > max ? max : val),
scale: (buf, amt) => buf.map(val => val * amt),