diff --git a/src/components/DeviceSelector.js b/src/components/DeviceSelector.js
index a959f7b..15a744a 100644
--- a/src/components/DeviceSelector.js
+++ b/src/components/DeviceSelector.js
@@ -1,40 +1,25 @@
-import React, { useCallback, useContext, useEffect, useState } from 'react'
+import React, { useCallback, useContext } from 'react'
import styled from 'styled-components'
-import { getMIDIDevices } from '../state'
+import { getCurrentMIDIDevice, getMIDIDevices } from '../state'
import { SynthInstrumentContext } from './Engine'
export const DeviceSelector = () => {
const [state, dispatch] = useContext(SynthInstrumentContext)
- const [devicesValues, setDevicesValues] = useState([])
const devices = getMIDIDevices(state)
+ const current = getCurrentMIDIDevice(state)
const onDeviceChange = useCallback(
event => {
- const device = state.midi.devices.find(device => device.id.toString() === event.target.value.toString())
-
- if (!device) {
- return
- }
-
- dispatch({ type: 'midi_switch_device', device })
+ dispatch({ type: 'midi_switch_device', device: event.target.value })
},
- [state.midi.devices, dispatch]
+ [dispatch]
)
- useEffect(() => {
- setDevicesValues(
- devices
- .filter(device => device.state === 'connected')
- .map(device => ({ label: device.name, value: device.id }) )
- )
- }, [devices])
-
return (
-
-
- {devicesValues.map(deviceValue =>
-
+
+ {devices.map(([id, name]) =>
+
)}
diff --git a/src/components/Engine.js b/src/components/Engine.js
index b0d7c90..b0688ba 100644
--- a/src/components/Engine.js
+++ b/src/components/Engine.js
@@ -1,27 +1,28 @@
import * as Tone from 'tone'
import styled from 'styled-components'
-import React, { createContext, useReducer, useEffect } from 'react'
-import { FLT_FREQ_MAX, FLT_FREQ_MIN, getParams, initialState, reducer } from '../state'
-
-let engine = {
- oscillator1: null,
- oscillator2: null,
- filter: {
- unit: null,
- envelope: null,
- },
- volume: null,
- distortion: null,
- delay: null,
- analyzer: null,
- reverb: null,
- shifter: null,
-}
+import React, { createContext, useReducer, useEffect, useRef } from 'react'
+import { config, FLT_FREQ_MAX, FLT_FREQ_MIN } from '../config'
+import { getParams, initialState, reducer } from '../state'
+import { mapRangeControl } from '../utils/math'
export const SynthInstrumentContext = createContext([initialState, () => null])
export const Engine = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
+ const engine = useRef({
+ oscillator1: null,
+ oscillator2: null,
+ filter: {
+ unit: null,
+ envelope: null,
+ },
+ volume: null,
+ distortion: null,
+ delay: null,
+ analyzer: null,
+ reverb: null,
+ shifter: null,
+ })
const { initialized } = state
const params = getParams(state)
@@ -51,7 +52,7 @@ export const Engine = ({ children }) => {
delay.chain(distortion, reverb, filter, shifter, analyzer, volume, Tone.Destination)
- engine = {
+ engine.current = {
oscillator1,
oscillator2,
filter: {
@@ -70,7 +71,28 @@ export const Engine = ({ children }) => {
dispatch({ type: 'init_engine' })
Tone.start()
- }, [])
+ }, [engine])
+
+ useEffect(() => {
+ if (!initialized) {
+ return
+ }
+
+ Object.entries(state.midi_map).forEach(([param, control]) => {
+ if (config[param].mappable) {
+ dispatch({
+ type: 'set_parameter',
+ name: param,
+ value: mapRangeControl(
+ state.cc[control] || config[param].default,
+ config[param].min,
+ config[param].max
+ )
+ })
+ }
+ })
+
+ }, [initialized, dispatch, state.cc, state.midi_map])
useEffect(() => {
if (!initialized) {
@@ -78,7 +100,7 @@ export const Engine = ({ children }) => {
}
if (state.analyzer.requesting) {
- dispatch({ type: 'set_analyzer', values: engine.analyzer.getValue() })
+ dispatch({ type: 'set_analyzer', values: engine.current.analyzer.getValue() })
}
}, [initialized, dispatch, state.analyzer])
@@ -95,19 +117,19 @@ export const Engine = ({ children }) => {
const parseFrequencies = notes => notes.map(({ note }) => Tone.Midi(note).toFrequency())
toPlays.forEach(note => {
- engine.oscillator1.triggerAttack(
+ engine.current.oscillator1.triggerAttack(
Tone.Midi(note.note).toFrequency(), Tone.now(), note.velocity
)})
- toPlays.forEach(note => engine.oscillator2.triggerAttack(
+ toPlays.forEach(note => engine.current.oscillator2.triggerAttack(
Tone.Midi(note.note).toFrequency(), Tone.now(), note.velocity
))
- engine.filter.envelope.triggerAttack()
+ engine.current.filter.envelope.triggerAttack()
- engine.oscillator1.triggerRelease(parseFrequencies(toRelease))
- engine.oscillator2.triggerRelease(parseFrequencies(toRelease))
- engine.filter.envelope.triggerRelease()
+ engine.current.oscillator1.triggerRelease(parseFrequencies(toRelease))
+ engine.current.oscillator2.triggerRelease(parseFrequencies(toRelease))
+ engine.current.filter.envelope.triggerRelease()
toPlays.map(({ note }) => dispatch({ type: 'note_triggered', note }))
}, [state.notes, dispatch, initialized])
@@ -118,7 +140,7 @@ export const Engine = ({ children }) => {
}
const decibels = Tone.gainToDb(params.master_vol)
- engine.volume.set({ volume: decibels })
+ engine.current.volume.set({ volume: decibels })
}, [params.master_vol, initialized])
useEffect(() => {
@@ -126,7 +148,7 @@ export const Engine = ({ children }) => {
return
}
- engine.oscillator1.set({
+ engine.current.oscillator1.set({
envelope: {
attack: params.osc1_env_atk,
decay: params.osc1_env_dec,
@@ -134,7 +156,7 @@ export const Engine = ({ children }) => {
release: params.osc1_env_rel,
},
})
- engine.oscillator2.set({
+ engine.current.oscillator2.set({
envelope: {
attack: params.osc2_env_atk,
decay: params.osc2_env_dec,
@@ -159,7 +181,7 @@ export const Engine = ({ children }) => {
return
}
- engine.filter.envelope.set({
+ engine.current.filter.envelope.set({
attack: params.flt_env_atk,
decay: params.flt_env_dec,
sustain: params.flt_env_sus,
@@ -173,8 +195,8 @@ export const Engine = ({ children }) => {
return
}
- engine.oscillator1.set({ oscillator: { type: params.osc1_type } })
- engine.oscillator2.set({ oscillator: { type: params.osc2_type } })
+ engine.current.oscillator1.set({ oscillator: { type: params.osc1_type } })
+ engine.current.oscillator2.set({ oscillator: { type: params.osc2_type } })
}, [params.osc1_type, params.osc2_type, initialized])
useEffect(() => {
@@ -182,7 +204,7 @@ export const Engine = ({ children }) => {
return
}
- engine.filter.unit.set({ type: params.flt_type })
+ engine.current.filter.unit.set({ type: params.flt_type })
}, [params.flt_type, params.flt_freq, params.flt_res, initialized])
useEffect(() => {
@@ -191,11 +213,11 @@ export const Engine = ({ children }) => {
}
if (params.flt_env_mix > 0) {
- engine.filter.scale.min = params.flt_freq
- engine.filter.scale.max = params.flt_freq + (FLT_FREQ_MAX - params.flt_freq) * params.flt_env_mix
+ engine.current.filter.scale.min = params.flt_freq
+ engine.current.filter.scale.max = params.flt_freq + (FLT_FREQ_MAX - params.flt_freq) * params.flt_env_mix
} else {
- engine.filter.scale.min = FLT_FREQ_MIN + (params.flt_freq - FLT_FREQ_MIN) * (1 - Math.abs(params.flt_env_mix))
- engine.filter.scale.max = params.flt_freq
+ engine.current.filter.scale.min = FLT_FREQ_MIN + (params.flt_freq - FLT_FREQ_MIN) * (1 - Math.abs(params.flt_env_mix))
+ engine.current.filter.scale.max = params.flt_freq
}
}, [params.flt_env_mix, params.flt_freq, initialized])
@@ -205,8 +227,8 @@ export const Engine = ({ children }) => {
return
}
- engine.oscillator1.set({ oscillator: { detune: params.osc1_detune + params.osc1_pitch * 100 } })
- engine.oscillator2.set({ oscillator: { detune: params.osc2_detune + params.osc2_pitch * 100 } })
+ engine.current.oscillator1.set({ oscillator: { detune: params.osc1_detune + params.osc1_pitch * 100 } })
+ engine.current.oscillator2.set({ oscillator: { detune: params.osc2_detune + params.osc2_pitch * 100 } })
}, [params.osc1_detune, params.osc1_pitch, params.osc2_detune, params.osc2_pitch, initialized])
useEffect(() => {
@@ -214,8 +236,8 @@ export const Engine = ({ children }) => {
return
}
- engine.oscillator1.set({ oscillator: { volume: Tone.gainToDb(params.osc1_vol) } })
- engine.oscillator2.set({ oscillator: { volume: Tone.gainToDb(params.osc2_vol) } })
+ engine.current.oscillator1.set({ oscillator: { volume: Tone.gainToDb(params.osc1_vol) } })
+ engine.current.oscillator2.set({ oscillator: { volume: Tone.gainToDb(params.osc2_vol) } })
}, [params.osc1_vol, params.osc2_vol, initialized])
useEffect(() => {
@@ -223,8 +245,8 @@ export const Engine = ({ children }) => {
return
}
- engine.oscillator1.set({ oscillator: { phase: params.osc1_phase } })
- engine.oscillator2.set({ oscillator: { phase: params.osc2_phase } })
+ engine.current.oscillator1.set({ oscillator: { phase: params.osc1_phase } })
+ engine.current.oscillator2.set({ oscillator: { phase: params.osc2_phase } })
}, [params.osc1_phase, params.osc2_phase, initialized])
useEffect(() => {
@@ -232,7 +254,7 @@ export const Engine = ({ children }) => {
return
}
- engine.distortion.set({ distortion: params.dist_amt })
+ engine.current.distortion.set({ distortion: params.dist_amt })
}, [params.dist_amt, initialized])
useEffect(() => {
@@ -240,9 +262,9 @@ export const Engine = ({ children }) => {
return
}
- engine.delay.set({ wet: params.delay_wet })
- engine.delay.set({ delayTime: params.delay_time })
- engine.delay.set({ feedback: params.delay_feed })
+ engine.current.delay.set({ wet: params.delay_wet })
+ engine.current.delay.set({ delayTime: params.delay_time })
+ engine.current.delay.set({ feedback: params.delay_feed })
}, [params.delay_wet, params.delay_time, params.delay_feed, initialized])
useEffect(() => {
@@ -250,8 +272,8 @@ export const Engine = ({ children }) => {
return
}
- engine.reverb.set({ wet: params.verb_wet })
- engine.reverb.set({ decay: params.verb_time })
+ engine.current.reverb.set({ wet: params.verb_wet })
+ engine.current.reverb.set({ decay: params.verb_time })
}, [params.verb_wet, params.verb_time, initialized])
useEffect(() => {
@@ -259,8 +281,8 @@ export const Engine = ({ children }) => {
return
}
- engine.shifter.set({ wet: params.shft_wet })
- engine.shifter.set({ frequency: params.shft_freq })
+ engine.current.shifter.set({ wet: params.shft_wet })
+ engine.current.shifter.set({ frequency: params.shft_freq })
}, [params.shft_wet, params.shft_freq, initialized])
return (
diff --git a/src/components/SynthController.js b/src/components/SynthController.js
index e775295..f0f43a3 100644
--- a/src/components/SynthController.js
+++ b/src/components/SynthController.js
@@ -1,73 +1,73 @@
import * as Tone from 'tone'
-import React, { useCallback, useContext, useEffect } from 'react'
+import React, { createRef, useCallback, useContext, useEffect } from 'react'
+import { mapRangeControl } from '../utils/math'
import { SynthInstrumentContext } from './Engine'
import styled from 'styled-components'
-import { getMIDIDevice } from '../state'
+import { getCurrentMIDIDevice } from '../state'
const STATUSBYTE_NOTEOFF = 0x8
const STATUSBYTE_NOTEON = 0x9
+const STATUSBYTE_CONTROLCHANGE = 0xB
-const isMessageStatus = (type, status) => Math.floor(type / 0x10) === status
+const hasStatusByte = (type, status) => Math.floor(type / 0x10) === status
export const SynthController = ({ displayControls = true }) => {
const [state, dispatch] = useContext(SynthInstrumentContext)
- const device = getMIDIDevice(state)
-
- const midiDevicesHandler = useCallback(
- event => {
- const type = event.port.state === 'connected' ? 'midi_device_connect' : 'midi_device_disconnect'
-
- if (state.midi.device === null) {
- dispatch({ type: 'midi_switch_device', device: event.port })
+ const current = getCurrentMIDIDevice(state)
+
+ const handleMidiAccess = useCallback((midiAccess) => {
+ midiAccess.onstatechange = (event) => {
+ if (event.port.type === 'input') {
+ dispatch({
+ type: event.port.state === 'connected' ? 'midi_device_connect' : 'midi_device_disconnect',
+ device: { id: event.port.id, name: event.port.name }
+ })
}
-
- dispatch({ type, device: event.port })
- },
- [device, dispatch]
- )
-
- useEffect(() => {
- if (!navigator.requestMIDIAccess) {
- return console.error('No midi access')
}
- navigator
- .requestMIDIAccess()
- .then(midiAccess => {
- midiAccess.onstatechange = midiDevicesHandler
+ midiAccess.inputs.forEach(input => {
+ input.onmidimessage = message => {
+ // dispatch({ type: 'midi_signal', status: true })
+ // setTimeout(() => {
+ // dispatch({ type: 'midi_signal', status: false })
+ // }, 50)
- if (!device) {
+ if (!message.data || message.target.id !== current) {
return
}
- device.onmidimessage = message => {
- dispatch({ type: 'midi_signal', status: true })
- setTimeout(() => {
- dispatch({ type: 'midi_signal', status: false })
- }, 50)
-
- if (!message.data || message.target.id !== device.id) {
- return
- }
+ const [type, val1, val2] = message.data
- const [type, note] = message.data
+ switch (true) {
+ case hasStatusByte(type, STATUSBYTE_NOTEON):
+ dispatch({ type: 'note_pressed', note: val1, velocity: mapRangeControl(val2, 0, 1) })
+ break
- switch (true) {
- case isMessageStatus(type, STATUSBYTE_NOTEON):
- dispatch({ type: 'note_pressed', note: note })
- break
+ case hasStatusByte(type, STATUSBYTE_NOTEOFF):
+ dispatch({ type: 'note_released', note: val1 })
+ break
- case isMessageStatus(type, STATUSBYTE_NOTEOFF):
- dispatch({ type: 'note_released', note: note })
- break
+ case hasStatusByte(type, STATUSBYTE_CONTROLCHANGE):
+ dispatch({ type: 'control_change', name: val1, value: val2 })
+ break
- default:
- break
- }
+ default:
+ break
}
- })
+ }
+ })
+ }, [current])
+
+ useEffect(() => {
+ if (!navigator.requestMIDIAccess) {
+ return console.error('No midi access')
+ }
+
+ navigator
+ .requestMIDIAccess()
+ .then(handleMidiAccess)
.catch(console.error)
- }, [dispatch, device])
+ }, [dispatch, handleMidiAccess])
return !displayControls
? null
diff --git a/src/components/ui/Knob.js b/src/components/ui/Knob.js
index bd3adce..17f8c81 100644
--- a/src/components/ui/Knob.js
+++ b/src/components/ui/Knob.js
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react'
import styled from 'styled-components'
-export const Knob = ({ label, min, max, value, onChange, step = null }) => {
+export const Knob = React.memo(({ label, min, max, value, onChange, step = null }) => {
const [id] = useState(Math.round(Math.random() * 100000))
const [active, setActive] = useState(false)
const [anchorX, setAnchorX] = useState(null)
@@ -66,7 +66,7 @@ export const Knob = ({ label, min, max, value, onChange, step = null }) => {
{label && {label}}
)
-}
+})
const calculateAngle = (value, min, max) => ((value - min) / (max - min)) * 270
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..1efeefa
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,52 @@
+export const FLT_FREQ_MIN = 20
+export const FLT_FREQ_MAX = 16000
+
+export const config = {
+ master_vol: { min: 0, max: 1, default: 0.7, mappable: true },
+
+ osc1_type: { default: 'sine', mappable: false }, // {sine,square,triangle,sawtooth}
+ osc1_vol: { min: 0, max: 1, default: 0.75, mappable: true }, // [0;1] gain
+ osc1_phase: { min: 0, max: 360, default: 0, mappable: true }, // [0;360] degress
+ osc1_pitch: { min: -24, max: 24, default: 0, mappable: true }, // [-24;24] semitones
+ osc1_detune: { min: -100, max: 100, default: 0, mappable: true }, // [-100;100] cents
+ osc1_env_atk: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+ osc1_env_dec: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+ osc1_env_sus: { min: 0, max: 1, default: 1, mappable: true }, // [0;1] unitless
+ osc1_env_rel: { min: 0, max: 1, default: 0.01, mappable: true }, // [0;1] sec
+
+ osc2_type: { default: 'sine', mappable: false }, // {sine,square,triangle,sawtooth}
+ osc2_vol: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] gain
+ osc2_phase: { min: 0, max: 360, default: 0, mappable: true }, // [0;360] degress
+ osc2_pitch: { min: -24, max: 24, default: 0, mappable: true }, // [-24;24] semitones
+ osc2_detune: { min: -100, max: 100, default: 0, mappable: true }, // [-100;100] cents
+ osc2_env_atk: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+ osc2_env_dec: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+ osc2_env_sus: { min: 0, max: 1, default: 1, mappable: true }, // [0;1] unitless
+ osc2_env_rel: { min: 0, max: 1, default: 0.01, mappable: true }, // [0;1] sec
+
+ flt_type: { default: 'lowpass', mappable: false }, // {highpass,lowpass,bandpass,notch}
+ flt_freq: { min: FLT_FREQ_MIN, max: FLT_FREQ_MAX, default: FLT_FREQ_MAX, mappable: true }, // [20;16000] Hz
+ flt_res: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] unitless
+ flt_env_mix: { min: -1, max: 1, default: 0, mappable: true }, // [-1;1] unitless
+ flt_env_atk: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+ flt_env_dec: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+ flt_env_sus: { min: 0, max: 1, default: 1, mappable: true }, // [0;1] unitless
+ flt_env_rel: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] sec
+
+ delay_wet: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] unitless
+ delay_time: { min: 0, max: 10, default: 0, mappable: true }, // [0;1] sec
+ delay_feed: { min: 0, max: 1, default: 0.5, mappable: true }, // [0;1] unitless
+
+ verb_wet: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] unitless
+ verb_time: { min: 0, max: 10, default: 4, mappable: true }, // [0;10] sec
+
+ shft_wet: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] unitless
+ shft_freq: { min: -500, max: 500, default: 0, mappable: true }, // [-500;500] Hz
+
+ dist_amt: { min: 0, max: 1, default: 0, mappable: true }, // [0;1] unitless
+}
+
+export const getParameterDefaults = () => Object.entries(config).reduce((obj, [name, param]) => ({
+ ...obj,
+ [name]: param.default
+}), {})
diff --git a/src/index.js b/src/index.js
index 01c1f35..194acdf 100644
--- a/src/index.js
+++ b/src/index.js
@@ -75,7 +75,7 @@ const Header = styled.header`
const Title = styled.h1`
margin: auto 0;
- text-shadow: 0 0 4px ${props => props.theme.colors.shadows.text}, inset 0 0 4px red;
+ text-shadow: 0 0 4px ${props => props.theme.colors.shadows.text};
text-transform: uppercase;
font-style: italic;
font-weight: bold;
diff --git a/src/state.js b/src/state.js
index e4e3250..0c63243 100644
--- a/src/state.js
+++ b/src/state.js
@@ -1,60 +1,20 @@
-export const FLT_FREQ_MIN = 20
-export const FLT_FREQ_MAX = 16000
+import { getParameterDefaults } from './config'
export const initialState = {
notes: [],
initialized: false,
midiSignal: false,
- parameters: {
- master_vol: 0.7, // [0;1] gain
-
- osc1_type: 'sine', // {sine,square,triangle,sawtooth}
- osc1_vol: 0.75, // [0;1] gain
- osc1_phase: 0, // [0;360] degress
- osc1_pitch: 0, // [-24;24] semitones
- osc1_detune: 0, // [-100;100] cents
- osc1_env_atk: 0.01, // [0;1] sec
- osc1_env_dec: 0, // [0;1] sec
- osc1_env_sus: 1, // [0;1] unitless
- osc1_env_rel: 0.01, // [0;1] sec
-
- osc2_type: 'sine', // {sine,square,triangle,sawtooth}
- osc2_vol: 0, // [0;1] gain
- osc2_phase: 0, // [0;360] degress
- osc2_pitch: 0, // [-24;24] semitones
- osc2_detune: 0, // [-100;100] cents
- osc2_env_atk: 0.01, // [0;1] sec
- osc2_env_dec: 0, // [0;1] sec
- osc2_env_sus: 1, // [0;1] unitless
- osc2_env_rel: 0.01, // [0;1] sec
-
- flt_type: 'lowpass', // {highpass,lowpass,bandpass,notch}
- flt_freq: FLT_FREQ_MAX, // [20;16000] Hz
- flt_res: 0, // [0;1] unitless
- flt_env_mix: 0, // [-1;1] unitless
- flt_env_atk: 0, // [0;1] sec
- flt_env_dec: 0, // [0;1] sec
- flt_env_sus: 1, // [0;1] unitless
- flt_env_rel: 0, // [0;1] sec
-
- delay_wet: 0, // [0;1] unitless
- delay_time: 0, // [O;10] sec
- delay_feed: 0.5, // [O;1] unitless
-
- verb_wet: 0, // [0;1] unitless
- verb_time: 4, // [0;10] sec
-
- shft_wet: 0, // [0;1] unitless
- shft_freq: 0, // [-500;500] Hz
-
- dist_amt: 0, // [0;1] unitless
+ parameters: getParameterDefaults(),
+ midi_map: {
+ osc1_env_rel: 15
},
+ cc: {},
analyzer: {
requesting: false,
values: [],
},
midi: {
- device: null,
+ current: null,
devices: [],
},
}
@@ -73,18 +33,20 @@ export const reducer = (state = initialState, action) => {
...state,
midi: {
...state.midi,
- devices: state.midi.devices.find(device => device.id.toString() === action.device.id.toString()) !== undefined
- ? state.midi.devices
- : [...state.midi.devices, action.device],
+ devices: {
+ ...state.midi.devices,
+ [action.device.id]: action.device.name
+ },
},
}
case 'midi_device_disconnect':
+ const { [action.device.id]: id, ...rest } = state.midi.devices
return {
...state,
midi: {
...state.midi,
- devices: state.midi.devices.filter(device => device.id !== action.device.id),
+ devices: rest,
},
}
@@ -93,7 +55,7 @@ export const reducer = (state = initialState, action) => {
...state,
midi: {
...state.midi,
- device: action.device,
+ current: action.device,
},
}
@@ -107,13 +69,13 @@ export const reducer = (state = initialState, action) => {
return {
...state,
notes:
- state.notes.find(note => note.note === action.note)
+ state.notes.find(note => note.note === action.note)
? state.notes.map(n => (
n.note === action.note
? { ...n, isPlaying: true, triggered: false, velocity: action.velocity }
: n
))
- : [ ...state.notes, { note: action.note, isPlaying: true, triggered: false, velocity: action.velocity }],
+ : [...state.notes, { note: action.note, isPlaying: true, triggered: false, velocity: action.velocity }],
}
case 'note_triggered':
@@ -156,6 +118,15 @@ export const reducer = (state = initialState, action) => {
parameters: action.parameters,
}
+ case 'control_change':
+ return {
+ ...state,
+ cc: {
+ ...state.cc,
+ [action.name]: action.value
+ }
+ }
+
default:
return state
}
@@ -164,8 +135,8 @@ export const reducer = (state = initialState, action) => {
/* Selectors */
export const getParams = state => state.parameters
export const getParam = (state, name) => getParams(state)[name] || null
-export const getMIDIDevices = state => state.midi.devices
-export const getMIDIDevice = state => state.midi.device
+export const getMIDIDevices = state => Object.entries(state.midi.devices)
+export const getCurrentMIDIDevice = state => state.midi.current
/* Dispatch helpers */
export const setParam = (dispatch, name, value) => dispatch({ type: 'set_parameter', name, value })
diff --git a/src/utils/math.js b/src/utils/math.js
new file mode 100644
index 0000000..880d5a2
--- /dev/null
+++ b/src/utils/math.js
@@ -0,0 +1,4 @@
+export const mapRange = (value, startMin, startMax, endMin, endMax) =>
+ ((Math.max(Math.min(value, startMax), startMin) - startMin) / startMax * (endMax - endMin)) + endMin
+
+export const mapRangeControl = (value, min, max) => mapRange(value, 0, 127, min, max)