8000 Feature/midi note select by funwithtriangles · Pull Request #189 · nudibranchrecords/hedron · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Feature/midi note select #189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
"standard-react"
],
"env": {
"browser" : true
"browser" : true,
"jest/globals": true
},
"plugins": [
"jest"
],
"rules": {
"key-spacing" : 0,
"jsx-quotes" : [2, "prefer-single"],
Expand Down
111 changes: 111 additions & 0 deletions mockTests/inputLinks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/** * SETUP ***/

import proxyquire from 'proxyquire'
import listen from 'redux-action-listeners'
import { createStore, applyMiddleware, combineReducers } from 'redux'

import inputsReducer from '../src/store/inputs/reducer'
import nodesReducer from '../src/store/nodes/reducer'
import inputLinksReducer from '../src/store/inputLinks/reducer'

import { constructMidiId } from '../src/utils/midiMessage'

import { uInputLinkUpdateMidiInput } from '../src/store/inputLinks/actions'

const rootReducer = combineReducers(
{
nodes: nodesReducer,
inputs: inputsReducer,
inputLinks: inputLinksReducer,
}
)

let uniqueId
const uid = () => {
uniqueId++
return 'id_' + uniqueId
}

const inputLinksListener = proxyquire('../src/store/inputLinks/listener', {
'uid': uid,
}).default

const rootListener = {
types: 'all',

handleAction (action, dispatched, store) {
inputLinksListener(action, store)
},
}

/** * TEST ***/

test('(mock) Input Links - Update link midi input', () => {
uniqueId = 0
const messageType = 'controlChange'
const noteNum = 100
const channel = 13
const deviceId = 'Akai Foo'

// State starts assuming some dropdown value have changed
const startState = {
nodes: {
option_a: {
key: 'channel',
value: channel,
},
option_b: {
key: 'messageType',
value: messageType,
},
option_c: {
key: 'noteNum',
value: noteNum,
},
option_x: {
key: 'foo',
value: 1,
},
},
inputs: {
midi_0_0: {
deviceId: deviceId,
assignedLinkIds: [
'link_a',
],
},
},
inputLinks: {
link_a: {
id: 'link_a',
deviceId: deviceId,
midiOptionIds: [
'option_a', 'option_b', 'option_c',
],
input: {
id: 'midi_0_0',
type: 'midi',
},
},
},
}

const store = createStore(rootReducer, startState, applyMiddleware(listen(rootListener)))

let state

// The dropdown value has changed and so this action would be dispatched
store.dispatch(uInputLinkUpdateMidiInput('link_a'))
state = store.getState()

const oldInput = state.inputs.midi_0_0
expect(oldInput.assignedLinkIds.length).toBe(0)

const newInputId = constructMidiId(messageType, noteNum, channel)
const newInput = state.inputs[newInputId]
expect(newInput.assignedLinkIds[0]).toBe('link_a')
expect(newInput.deviceId).toBe(deviceId)

const link = state.inputLinks.link_a
expect(link.input.id).toBe(newInputId)
})
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"lint": "eslint src config example-projects mockTests",
"lint:css": "stylelint \"src/components/**/*.js\"",
"lint:fix": "yarn lint -- --fix",
"test": "yarn lint && cross-env NODE_ENV=test tape-watch --once -r @babel/register src/**/*.spec.js mockTests/**/*.spec.js -p tap-diff",
"test": "yarn lint && yarn jest && cross-env NODE_ENV=test tape-watch --once -r @babel/register src/**/*.spec.js mockTests/**/*.spec.js -p tap-diff",
"test:dev": "cross-env NODE_ENV=test tape-watch -r @babel/register src/**/*.spec.js mockTests/**/*.spec.js -p tap-diff",
"test-jest": "yarn jest --watch",
"compile": "electron-webpack",
"dist": "yarn compile && electron-builder",
"dist:dev": "yarn dist -- --dir -c.compression=store -c.mac.identity=null && open dist/mac/hedron.app/ --args --distDev",
Expand All @@ -35,6 +36,11 @@
"glslify-import"
]
},
"jest": {
"testMatch": [
"**/?(*.)+(test).[jt]s?(x)"
]
},
"dependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
Expand Down Expand Up @@ -110,9 +116,11 @@
"eslint-config-standard": "^6.2.1",
"eslint-config-standard-react": "^4.2.0",
"eslint-plugin-babel": "^4.0.1",
"eslint-plugin-jest": "^22.3.0",
"eslint-plugin-promise": "^3.4.1",
"eslint-plugin-react": "^6.10.0",
"eslint-plugin-standard": "^2.0.1",
"jest": "^24.1.0",
"jsdom": "^11.5.1",
"jsdom-global": "^3.0.2",
"minimist": "^1.2.0",
Expand Down
8 changes: 6 additions & 2 deletions src/components/Control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'
import Modifier from '../../containers/Modifier'
import Select from '../../containers/Select'

const Control = ({ nodeId, type }) => {
const Control = ({ nodeId, type, onChangeAction }) => {
switch (type) {
case 'select':
return <Select nodeId={nodeId} />
return <Select nodeId={nodeId} />
case 'slider':
return <Modifier nodeId={nodeId} />
default:
Expand All @@ -17,6 +17,10 @@ const Control = ({ nodeId, type }) => {
Control.propTypes = {
nodeId: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
onChangeAction: PropTypes.shape({
type: PropTypes.string.isRequired,
payload: PropTypes.object,
}),
}

export default Control
1 change: 1 addition & 0 deletions src/containers/Control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const mapStateToProps = (state, ownProps) => {
const node = getNode(state, ownProps.nodeId)
return {
type: node.type || 'slider',
onChangeAction: node.onChangeAction,
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/containers/Select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const mapDispatchToProps = (dispatch, ownProps) => {
dispatch(nodeValueUpdate(ownProps.nodeId, value.value, {
dontMutate: true,
}))

if (ownProps.onChangeAction) {
dispatch(ownProps.onChangeAction)
}
},
onAssignClick: () => {
dispatch(uInputLinkCreate(ownProps.nodeId, 'midi', 'midi'))
Expand Down
23 changes: 16 additions & 7 deletions src/inputs/MidiInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { midiStopLearning, midiUpdateDevices, midiMessage } from '../store/midi/
import { uInputLinkCreate } from '../store/inputLinks/actions'
import { clockPulse } from '../store/clock/actions'
import { newData as teachMidi } from '../utils/getMidiMode'
import processMidiMessage from '../utils/processMidiMessage'
import { processMidiData } from '../utils/midiMessage'

export default (store) => {
const => {
const state = store.getState()
const m = processMidiMessage(rawMessage)
const m = processMidiData(rawMessage.data)

if (m.type !== 'timingClock' && m.type !== 'noteOff') {
if (m.messageType !== 'timingClock' && m.messageType !== 'noteOff') {
store.dispatch(midiMessage(rawMessage.target.name, {
data: rawMessage.data,
timeStamp: rawMessage.timeStamp,
Expand All @@ -20,7 +20,7 @@ export default (store) => {

if (learning) {
let controlType
const mode = teachMidi(rawMessage.data, m.type)
const mode = teachMidi(rawMessage.data, m.messageType)

if (mode !== 'learning') {
if (mode === 'ignore') {
Expand All @@ -30,18 +30,27 @@ export default (store) => {
controlType = mode
}
store.dispatch(uInputLinkCreate(
learning.id, m.id, learning.type, rawMessage.target.name, controlType
learning.id,
m.id,
learning.type,
{
deviceId: rawMessage.target.name,
controlType,
channel: m.channel,
messageType: m.messageType,
noteNum: m.noteNum,
}
))
store.dispatch(midiStopLearning())
}
} else {
store.dispatch(inputFired(m.id, m.value, {
noteOn: m.type === 'noteOn',
noteOn: m.messageType === 'noteOn',
type: 'midi',
}))
}
// If no note data, treat as clock
} else if (m.type === 'timingClock') {
} else if (m.messageType === 'timingClock') {
// Only dispatch clock pulse if no generated clock
if (!state.clock.isGenerated) {
store.dispatch(clockPulse())
Expand Down
1 change: 1 addition & 0 deletions src/renderer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ if (process.env.NODE_ENV !== 'development') {
'NODE_VALUE_UPDATE', 'NODE_RANGE_UPDATE', 'NODE_SHOT_ARM', 'NODE_SHOT_DISARM', 'NODE_SHOT_FIRED',
'NODE_VALUES_BATCH_UPDATE',
],
maxAge: 10,
})
}

Expand Down
3 changes: 2 additions & 1 deletion src/selectors/_test/getInputLinkLfoOptionIds.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ test('(Selector) getInputLinkLfoOptionIds (input is not "lfo")', (t) => {
input: {
id: 'BAR',
},
lfoOptionIds: [],
},
},
}

const actual = getInputLinkLfoOptionIds(state, 'aaa')

t.equal(actual, undefined, 'Returns undefined')
t.deepEqual(actual, [], 'Returns empty array')
t.end()
})

Expand Down
4 changes: 2 additions & 2 deletions src/selectors/getInputLinkLfoOptionIds.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import getNodes from './getNodes'

export default (state, linkId) => {
const link = state.inputLinks[linkId]
const optionIds = link.lfoOptionIds

if (link.input && link.input.id === 'lfo') {
const optionIds = link.lfoOptionIds
const optionNodes = getNodes(state, optionIds)
const shapeOpt = optionNodes.find(node => node.key === 'shape')
const isNoise = shapeOpt.value === 'noise'
Expand Down Expand Up @@ -32,5 +32,5 @@ export default (state, linkId) => {
}
}

return undefined
return optionIds
}
27 changes: 12 additions & 15 deletions src/store/inputLinks/actions.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
export function uInputLinkCreate (nodeId, inputId, inputType, deviceId, controlType) {
export function uInputLinkCreate (nodeId, inputId, inputType, meta) {
return {
type: 'U_INPUT_LINK_CREATE',
payload: {
nodeId,
inputId,
inputType,
deviceId,
controlType,
meta,
},
}
}
Expand Down Expand Up @@ -39,32 +38,30 @@ export function rInputLinkDelete (id) {
}
}

export function uInputLinkUpdate (linkId, inputId, inputType) {
export function rInputLinkUpdate (id, newProperties) {
return {
type: 'U_INPUT_LINK_UPDATE',
type: 'R_INPUT_LINK_UPDATE',
payload: {
linkId,
inputId,
inputType,
id,
newProperties,
},
}
}

export function inputLinksReplaceAll (links) {
export function uInputLinkUpdateMidiInput (linkId) {
return {
type: 'INPUT_LINKS_REPLACE_ALL',
type: 'U_INPUT_LINK_UPDATE_MIDI_INPUT',
payload: {
links,
linkId,
},
}
}

export function rInputLinkUpdate (linkId, input) {
export function inputLinksReplaceAll (links) {
return {
type: 'R_INPUT_LINK_UPDATE',
type: 'INPUT_LINKS_REPLACE_ALL',
payload: {
linkId,
input,
links,
},
}
}
Expand Down
34 changes: 34 additions & 0 deletions src/store/inputLinks/listener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import getInputLink from '../../selectors/getInputLink'
import getNodes from '../../selectors/getNodes'
import { constructMidiId } from '../../utils/midiMessage'
import { inputAssignedLinkCreate, inputAssignedLinkDelete } from '../inputs/actions'
import { rInputLinkUpdate } from '../inputLinks/actions'

const handleUpdateMidiInput = (action, store) => {
const state = store.getState()
const link = getInputLink(state, action.payload.linkId)
const opts = getNodes(state, link.midiOptionIds)

let messageType, noteNum, channel

opts.forEach(opt => {
if (opt.key === 'messageType') messageType = opt.value
if (opt.key === 'noteNum') noteNum = opt.value
if (opt.key === 'channel') channel = opt.value
})

const newInputId = constructMidiId(messageType, noteNum, channel)

store.dispatch(inputAssignedLinkDelete(link.input.id, link.id))
store.dispatch(inputAssignedLinkCreate(newInputId, link.id, link.deviceId))
store.dispatch(rInputLinkUpdate(link.id, { input: { id: newInputId, type: 'midi' } }))
}

export default (action, store) => {
switch (action.type) {
case 'U_INPUT_LINK_UPDATE_MIDI_INPUT':
handleUpdateMidiInput(action, store)
break
}
}

Loading
0