From 261a6a6ada8912b694a4d6429d1d7a37ab3a5d43 Mon Sep 17 00:00:00 2001 From: Neghmurken Date: Tue, 5 Oct 2021 10:43:43 +0200 Subject: [PATCH] WIP : Midi learn --- src/components/DeviceSelector.js | 31 ++------ src/components/Engine.js | 124 ++++++++++++++++++------------ src/components/SynthController.js | 94 +++++++++++----------- src/components/ui/Knob.js | 4 +- src/config.js | 52 +++++++++++++ src/index.js | 2 +- src/state.js | 81 +++++++------------ src/utils/math.js | 4 + 8 files changed, 213 insertions(+), 179 deletions(-) create mode 100644 src/config.js create mode 100644 src/utils/math.js 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)