synthing

a waveform sequencing synth on the web
Log | Files | Refs | Submodules

commit d403e80f994b8a43a863dc3783ed65172e585741
parent ffeadc2d9b4cee98aa35b6e351240226600f829b
Author: Massimo Siboldi <mdsiboldi@gmail.com>
Date:   Mon, 12 Mar 2018 00:35:24 -0700

move rest of state to redux, finally

Diffstat:
Msrc/App/index.js | 156+++++++++++--------------------------------------------------------------------
Msrc/consts.js | 8++++++--
Msrc/helpers.js | 33+++++++++++++++++++++++++++++++++
Msrc/index.js | 8++++++--
Msrc/store.js | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
5 files changed, 154 insertions(+), 155 deletions(-)

diff --git a/src/App/index.js b/src/App/index.js @@ -11,46 +11,6 @@ import '../iconfont/style.css'; import consts from '../consts.js'; import helpers from '../helpers.js'; -const initialWave = new Array(consts.BUF_SIZE) - .fill(0) - .map((val, i) => Math.sin(i / consts.BUF_SIZE * Math.PI * 2)); - -const immObjArray = { - update: (arr, idx, opts) => { - const newArr = arr.slice(); - newArr[idx] = Object.assign({}, arr[idx], opts) - return newArr; - }, - add: (arr, idx, opts) => { - const newArr = arr.slice(); - newArr.splice(idx, 0, opts); - return newArr; - }, - remove: (arr, idx) => { - const newArr = arr.slice(); - newArr.splice(idx, 1); - return newArr; - } -} - -const boolArray = { - setLength: (ba, newLength) => { - let newBa = ba.slice(0, newLength); - if (ba.length < newLength) { - newBa = newBa.concat(new Array(newLength - ba.length).fill(false)); - } - return newBa; - }, create: (length) => { - return new Array(length).fill(false); - }, - update: (ba, idx, val) => { - const newBa = ba.slice(); - newBa[idx] = val; - return newBa; - } -} - - const Adsr = (props) => ( <div style="display: inline-block;"> {consts.adsrProperties.map((aspect) => ( @@ -70,27 +30,13 @@ const Adsr = (props) => ( </div> ) - class App extends Component { - constructor() { - super(); - this.state = { - tones: [{ - active: true, - waveform: initialWave.slice(), - mix: 0.7, - mute: false, - solo: false, - beats: boolArray.update(boolArray.create(4), 0, true) - }], - } - } - - editingWaveform = () => this.state.tones[this.props.editingToneIdx].waveform + editingWaveform = () => this.props.tones[this.props.editingToneIdx].waveform + //TODO these can be in redux too, using reselect activeTones = () => { let hasSolo = false; - const waves = this.state.tones.reduce((accum, val) => { + const waves = this.props.tones.reduce((accum, val) => { let group = 'rest'; if (val.solo) { hasSolo = true; @@ -131,106 +77,46 @@ class App extends Component { ); } - updateTone = (idx = this.props.editingToneIdx, opts) => { - this.setState({ - tones: immObjArray.update(this.state.tones, idx, opts) - }); - } - - removeTone = (idx) => { - const tones = immObjArray.remove(this.state.tones, idx); - this.setState({ tones }); - this.props.setEditingToneIdx(Math.min( - this.props.editingToneIdx, - tones.length - 1 - )); - } - - changeEditingTone = (i) => this.props.set({editingToneIdx: i}) - - addTone = ( - waveform = initialWave.slice(), - at = this.state.tones.length, - isEditing = false - ) => { - const tones = immObjArray.add(this.state.tones, at, { - waveform, - beats: boolArray.create(this.props.numBeats), - mix: 0.7, - mute: false, - solo: false - }); - - const state = { - tones - }; - - if (isEditing) { - state.editingToneIdx = at; - } - - this.setState(state); - } - - setBeats = (newNumBeats) => { - newNumBeats = Math.max(newNumBeats, 1); - this.props.setNumBeats(newNumBeats); - this.setState({ - tones: this.state.tones.map((val, idx) => { - let ret = Object.assign({}, val, { - beats: boolArray.setLength(val.beats, newNumBeats) - }); - return ret; - }) - }) - } - keyHandler(e) { //TODO handle global commands, maybe some modal stuff even wow console.log('wow i got through', e.key); } render() { - const tones = this.state.tones.map((form, idx) => { + const tones = this.props.tones.map((form, idx) => { return ( <WaveManager activate={this.props.setEditingToneIdx.bind(null, idx)} - remove={this.removeTone.bind(this, idx)} + remove={this.props.deleteTone.bind(null, idx)} duplicate={() => { let pleaseActivate = false; if (this.props.editingToneIdx === idx) { pleaseActivate = true; } - this.addTone(this.state.tones[idx].waveform.slice(), idx + 1, pleaseActivate); + this.props.addTone(this.props.tones[idx].waveform.slice(), idx + 1, pleaseActivate); }} activated={idx === this.props.editingToneIdx} - tone={this.state.tones[idx]} + tone={this.props.tones[idx]} beat={this.props.beat} toggleMute={() => { - this.updateTone(idx, { - mute: !this.state.tones[idx].mute - }) + this.props.setToneProperty(idx, 'mute', !this.props.tones[idx].mute); }} toggleSolo={() => { - this.updateTone(idx, { - solo: !this.state.tones[idx].solo - }) + this.props.setToneProperty(idx, 'solo', !this.props.tones[idx].solo); }} updateBeat={(i, val) => { - this.updateTone(idx, { - beats: boolArray.update( - this.state.tones[idx].beats, + this.props.setToneProperty( + idx, + 'beats', + helpers.boolArray.update( + this.props.tones[idx].beats, i, val ) - }); - }} - mix={this.state.tones[idx].mix} - updateMix={(mix) => { - this.updateTone(idx, { - mix - }); + ); }} + mix={this.props.tones[idx].mix} + updateMix={(mix) => {this.props.setToneProperty(idx, 'mix', mix)}} ></WaveManager> ); }) @@ -247,7 +133,7 @@ class App extends Component { mouseData={this.state.mouseData} waveform={this.editingWaveform()} updateWaveform={(waveform) => { - this.updateTone(this.props.editingToneIdx, {waveform}); + this.props.setToneProperty(this.props.editingToneIdx, 'waveform', waveform); }} ></WaveEditor> <div class="global-controls"> @@ -275,11 +161,11 @@ class App extends Component { /> <Param name="beats" - minVal="1" + minVal={3} maxVal={16} step="1" val={this.props.numBeats} - update={this.setBeats} + update={this.props.setNumBeats} /> <Adsr adsr={this.props.adsr} update={this.props.setAdsrProperty} /> <HSlider value={this.props.volume} update={this.props.setVolume} /> @@ -287,7 +173,7 @@ class App extends Component { <div class="wave-manager-container"> {tones} </div> - <button onClick={() => this.addTone()}>+</button> + <button onClick={() => this.props.addTone()}>+</button> <Synth waveform={this.totalWaveform()} volume={this.props.volume} diff --git a/src/consts.js b/src/consts.js @@ -1,5 +1,6 @@ +const BUF_SIZE = 256; export default { - BUF_SIZE: 256, + BUF_SIZE, adsrProperties: [ { name: 'attack', @@ -20,5 +21,8 @@ export default { suffix: 's', maxVal: 30, } - ] + ], + initialWave: new Array(BUF_SIZE) + .fill(0) + .map((val, i) => Math.sin(i / BUF_SIZE * Math.PI * 2)) } diff --git a/src/helpers.js b/src/helpers.js @@ -42,5 +42,38 @@ export default { } }); }) + }, + boolArray: { + setLength: (ba, newLength) => { + let newBa = ba.slice(0, newLength); + if (ba.length < newLength) { + newBa = newBa.concat(new Array(newLength - ba.length).fill(false)); + } + return newBa; + }, create: (length) => { + return new Array(length).fill(false); + }, + update: (ba, idx, val) => { + const newBa = ba.slice(); + newBa[idx] = val; + return newBa; + } + }, + immObjArray: { + update: (arr, idx, opts) => { + const newArr = arr.slice(); + newArr[idx] = Object.assign({}, arr[idx], opts) + return newArr; + }, + add: (arr, idx, opts) => { + const newArr = arr.slice(); + newArr.splice(idx, 0, opts); + return newArr; + }, + remove: (arr, idx) => { + const newArr = arr.slice(); + newArr.splice(idx, 1); + return newArr; + } } }; diff --git a/src/index.js b/src/index.js @@ -6,7 +6,7 @@ import 'preact/devtools'; import { Provider, connect } from 'preact-redux'; import { store } from './store.js'; -const ConnectedApp = connect(state => Object.assign({}, state.global, {adsr: state.adsr}), { +const ConnectedApp = connect(state => state, { setVolume: (value) => ({type: 'SET_GLOBAL_VOLUME', value}), setBpm: (value) => ({type: 'SET_GLOBAL_BPM', value}), setBeat: (value) => ({type: 'SET_GLOBAL_BEAT', value}), @@ -28,7 +28,11 @@ const ConnectedApp = connect(state => Object.assign({}, state.global, {adsr: sta loop(); }, stopMetro: () => ({type: 'STOP_METRO'}), - setAdsrProperty: (property, value) => ({type: 'SET_ADSR_PROPERTY', property, value}) + setAdsrProperty: (property, value) => ({type: 'SET_ADSR_PROPERTY', property, value}), + addTone: (waveform, idx, activate) => ({type: 'ADD_TONE', waveform, idx, activate}), + setToneProperty: (idx, property, value) => ({type: 'SET_TONE_PROPERTY', idx, property, value}), + deleteTone: (idx) => ({type: 'DELETE_TONE', idx}) + })(App); const InformedApp = <Provider store={store}><ConnectedApp /></Provider>; diff --git a/src/store.js b/src/store.js @@ -1,22 +1,39 @@ -import { createStore, combineReducers, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import consts from './consts.js'; +import helpers from './helpers.js'; +const numBeats = 4; const initialState = { - global: { - volume: 0.7, - bpm: 120, - beat: 0, - playing: false, - numBeats: 4, - editingToneIdx: 0 - }, + volume: 0.7, + bpm: 120, + beat: 0, + playing: false, + numBeats, + editingToneIdx: 0, adsr: { attack: 0.3, decay: 1, sustain: 0.4, release: 1 - } + }, + tones: [{ + active: true, + waveform: consts.initialWave.slice(), + mix: 0.7, + mute: false, + solo: false, + beats: helpers.boolArray.update(helpers.boolArray.create(numBeats), 0, true) + }], +}; + +const newTone = { + active: false, + waveform: consts.initialWave.slice(), + mix: 0.7, + mute: false, + solo: false, + beats: helpers.boolArray.create(numBeats) }; const adsrReducer = (state, action) => { @@ -24,7 +41,6 @@ const adsrReducer = (state, action) => { const propertiesAllowed = consts.adsrProperties.map(val => val.name); switch (action.type) { case 'SET_ADSR_PROPERTY': - console.log(action); if (propertiesAllowed.indexOf(action.property) > -1) { updates[action.property] = action.value } @@ -33,7 +49,53 @@ const adsrReducer = (state, action) => { break; } return Object.assign({}, state, updates); -} +}; + +const tonesReducer = (state = [], action) => { + switch (action.type) { + case 'ADD_TONE': + // idx: optional, defaults to end + // waveform: optional, defaults to sine wave + // activate: optional, defaults active to false + const idx = action.idx === undefined ? state.length : action.idx; + + const newToneProps = {}; + if (action.waveform) + newToneProps.waveform = action.waveform; + if (action.activate) + newToneProps.active = true; + + return helpers.immObjArray.add( + state, + idx, + Object.assign({}, newTone, newToneProps) + ); + case 'SET_TONE_PROPERTY': + // idx: required + // property: required + // value: required + const propertiesAllowed = Object.keys(initialState.tones[0]); + if (propertiesAllowed.indexOf(action.property) > -1) { + return helpers.immObjArray.update(state, action.idx, { + [action.property]: action.value + }); + } + else return state; + case 'DELETE_TONE': + // idx: required + return helpers.immObjArray.remove(state, action.idx); + case 'SET_GLOBAL_NUM_BEATS': + // TODO save beats and just change the view, instead of deleting them + return state.map((val, idx) => { + let ret = Object.assign({}, val, { + beats: helpers.boolArray.setLength(val.beats, action.value) + }); + return ret; + }); + default: + return state; + } +}; const globalReducer = (state, action) => { const updates = {}; @@ -63,18 +125,28 @@ const globalReducer = (state, action) => { case 'SET_EDITING_TONE_IDX': updates.editingToneIdx = action.value; break; + case 'DELETE_TONE': + updates.editingToneIdx = Math.min( + state.editingToneIdx, + // 1 for length, 1 for deleted tone. + // min length of tones array should be 2 before a delete... + state.tones.length - 2 + ); + break; default: break; } return Object.assign({}, state, updates); } -const reducers = { - global: globalReducer, - adsr: adsrReducer +const reducer = (state = initialState, action) => { + return Object.assign({}, state, globalReducer(state, action), { + tones: tonesReducer(state.tones, action), + adsr: adsrReducer(state.adsr, action) + }); }; -const store = createStore(combineReducers(reducers), initialState, applyMiddleware(thunk)); +const store = createStore(reducer, initialState, applyMiddleware(thunk)); export default store; export { store };