2 | 2 |
import * as notes from './notes';
|
3 | 3 |
|
4 | 4 |
const statbyte = (stat, chan) => (stat << 4) | chan;
|
|
5 |
const CHANNEL = 0b1011;
|
|
6 |
const ALL_NOTES_OFF = 0x7B;
|
|
7 |
const ALL_SOUND_OFF = 0x78;
|
5 | 8 |
const NOTE_ON = 0b1001;
|
6 | 9 |
const NOTE_OFF = 0b1000;
|
7 | 10 |
const CCHANGE = 0b1011;
|
|
23 | 26 |
val: message.data[2] / 127,
|
24 | 27 |
});
|
25 | 28 |
|
26 | |
const Hexagon = ({ x, y, children, state, note, send, major }) => {
|
|
29 |
const Hexagon = ({ x, y, children, state, note, send, major, scale }) => {
|
27 | 30 |
if (major == 'row') {
|
28 | 31 |
if (y % 2 == 0)
|
29 | 32 |
x += 0.5;
|
|
34 | 37 |
|
35 | 38 |
return (
|
36 | 39 |
<div
|
37 | |
className={'hexagon' + (state ? ' active' : '')}
|
|
40 |
className={`hexagon ${state ? ' active' : ''} ${scale}`}
|
38 | 41 |
style={pos([x, y], major)}
|
39 | 42 |
>
|
40 | 43 |
<div
|
|
75 | 78 |
},
|
76 | 79 |
};
|
77 | 80 |
|
|
81 |
const scales = {
|
|
82 |
major: [2, 2, 1, 2, 2, 2, 1],
|
|
83 |
minor_nat: [2, 1, 2, 2, 1, 2, 2],
|
|
84 |
minor_harm: [2, 1, 2, 2, 1, 3, 1],
|
|
85 |
minor_mel: [2, 1, 2, 2, 2, 2, 1],
|
|
86 |
minor_hung: [2, 1, 3, 1, 1, 3, 1],
|
|
87 |
whole: [2, 2, 2, 2, 2, 2],
|
|
88 |
penta: [2, 2, 3, 2, 3],
|
|
89 |
};
|
78 | 90 |
|
79 | 91 |
const range = i => new Array(i).fill(true);
|
80 | |
const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state }) => (
|
|
92 |
const tallyup = (steps, offset=0) => {
|
|
93 |
const notes = [];
|
|
94 |
|
|
95 |
let sum = 0;
|
|
96 |
for (let i = 0; i < steps.length; i++) {
|
|
97 |
notes[i] = offset + sum;
|
|
98 |
sum += steps[i];
|
|
99 |
}
|
|
100 |
|
|
101 |
if (sum != 12) throw new Error(`scale doesn't tally up: ${steps} / ${notes}`);
|
|
102 |
|
|
103 |
return {
|
|
104 |
notes,
|
|
105 |
modulo: notes.map(n => n % 12),
|
|
106 |
};
|
|
107 |
};
|
|
108 |
|
|
109 |
const Keyboard = ({ w, h, send, layout: { major, map }, showMidi, state, scale }) => (
|
81 | 110 |
<div
|
82 | 111 |
className={`keyboard ${major}-major`}
|
83 | 112 |
style={{
|
|
88 | 117 |
{range(w).map((_, x) => (
|
89 | 118 |
range(h).map((_, y) => {
|
90 | 119 |
const note = map([x, y], [w, h]);
|
|
120 |
const onCore = scale && scale.notes.indexOf(note) >= 0;
|
|
121 |
const onScale = scale && scale.modulo.indexOf(note % 12) >= 0;
|
91 | 122 |
return (
|
92 | 123 |
<Hexagon
|
93 | 124 |
key={x + ',' + y}
|
|
96 | 127 |
state={state[note]}
|
97 | 128 |
send={send}
|
98 | 129 |
major={major}
|
|
130 |
scale={onCore ? "core" : (onScale ? "" : "disabled")}
|
99 | 131 |
>
|
100 | 132 |
{showMidi ? note : notes.music[note % 12]}
|
101 | 133 |
</Hexagon>
|
|
132 | 164 |
export default class App extends React.Component {
|
133 | 165 |
state = {
|
134 | 166 |
layout: 'wicki_hayden',
|
135 | |
notes: {},
|
|
167 |
scale: 'major',
|
|
168 |
offset: 60,
|
|
169 |
state: {},
|
136 | 170 |
};
|
137 | 171 |
|
138 | 172 |
componentDidMount() {
|
|
146 | 180 |
}
|
147 | 181 |
|
148 | 182 |
componentDidUpdate(prevProps) {
|
|
183 |
this.send(CHANNEL, ALL_SOUND_OFF, 0);
|
|
184 |
this.send(CHANNEL, ALL_NOTES_OFF, 0);
|
|
185 |
|
149 | 186 |
if (prevProps.midiin)
|
150 | 187 |
prevProps.midiin.onmidimessage = null;
|
151 | 188 |
|
|
158 | 195 |
setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250);
|
159 | 196 |
}
|
160 | 197 |
|
161 | |
send = (command, note) => {
|
|
198 |
send = (command, note, vel=127) => {
|
|
199 |
if (command === NOTE_ON && this.state.offset === null)
|
|
200 |
this.setState({ offset: note });
|
|
201 |
|
162 | 202 |
if (!this.props.midiout)
|
163 | 203 |
return;
|
164 | 204 |
|
165 | |
const msg = [statbyte(command, 0), note, 127];
|
|
205 |
const msg = [statbyte(command, 0), note, vel];
|
166 | 206 |
this.props.midiout.send(msg);
|
167 | 207 |
}
|
168 | 208 |
|
169 | |
onlayout = (e) => {
|
170 | |
this.setState({ layout: e.target.value });
|
171 | |
}
|
|
209 |
onlayout = (e) => this.setState({ layout: e.target.value })
|
|
210 |
onscale = (e) => this.setState({ scale: e.target.value })
|
|
211 |
|
|
212 |
onoffset = (e) => this.setState({ offset: null });
|
172 | 213 |
|
173 | 214 |
onmessage = (e) => {
|
174 | 215 |
const message = parse(e);
|
|
181 | 222 |
|
182 | 223 |
switch (message.cmd) {
|
183 | 224 |
case NOTE_ON:
|
184 | |
this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } }));
|
|
225 |
this.setState(({ state }) => ({ state: { ...state, [message.note]: true } }));
|
185 | 226 |
break;
|
186 | 227 |
|
187 | 228 |
case NOTE_OFF:
|
188 | |
this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } }));
|
|
229 |
this.setState(({ state }) => ({ state: { ...state, [message.note]: false } }));
|
189 | 230 |
break;
|
190 | 231 |
}
|
191 | 232 |
}
|
|
196 | 237 |
const note = notes.key2midi[e.code];
|
197 | 238 |
if (!note) return;
|
198 | 239 |
e.preventDefault();
|
199 | |
this.setState(({ notes }) => ({ notes: { ...notes, [note]: true } }));
|
|
240 |
|
|
241 |
if (this.state.offset === null)
|
|
242 |
this.setState({ offset: note });
|
|
243 |
|
|
244 |
this.setState(({ state }) => ({ state: { ...state, [note]: true } }));
|
200 | 245 |
this.send(NOTE_ON, note);
|
201 | 246 |
}
|
202 | 247 |
|
|
206 | 251 |
const note = notes.key2midi[e.code];
|
207 | 252 |
if (!note) return;
|
208 | 253 |
e.preventDefault();
|
209 | |
this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } }));
|
|
254 |
|
|
255 |
this.setState(({ state }) => ({ state: { ...state, [note]: false } }));
|
210 | 256 |
this.send(NOTE_OFF, note);
|
211 | 257 |
}
|
212 | 258 |
|
213 | 259 |
render() {
|
214 | 260 |
const { configure, showMidi } = this.props;
|
215 | |
const { layout, notes } = this.state;
|
216 | |
|
217 | |
const chord = Object.entries(notes).filter(([note, on]) => on).map(([k, _]) => k);
|
|
261 |
const { scale, offset, layout, state } = this.state;
|
|
262 |
|
|
263 |
const chord = Object.entries(state).filter(([note, on]) => on).map(([k, _]) => k);
|
218 | 264 |
chord.sort();
|
219 | 265 |
|
220 | 266 |
return (
|
221 | 267 |
<div className="app">
|
222 | 268 |
<button onClick={configure}>settings</button>
|
223 | |
<label>layout:
|
|
269 |
<label>layout:{' '}
|
224 | 270 |
<select name="layout" onChange={this.onlayout} value={layout}>
|
225 | 271 |
<option value="wicki_hayden">Wicki-Hayden</option>
|
226 | 272 |
<option value="harmonic">Harmonic Table</option>
|
227 | 273 |
<option value="gerhard">Gerhard</option>
|
228 | 274 |
</select>
|
229 | 275 |
</label>
|
|
276 |
<label>scale:{' '}
|
|
277 |
<select name="layout" onChange={this.onscale} value={scale}>
|
|
278 |
<option value="none">None</option>
|
|
279 |
<option value="major">Major</option>
|
|
280 |
<option value="minor_nat">Natural Minor</option>
|
|
281 |
<option value="minor_harm">Harmonic Minor</option>
|
|
282 |
<option value="minor_mel">Melodic Minor</option>
|
|
283 |
<option value="minor_hung">Hungarian Minor</option>
|
|
284 |
<option value="whole">Whole-Tone</option>
|
|
285 |
<option value="penta">Pentatonic</option>
|
|
286 |
</select>
|
|
287 |
</label>
|
|
288 |
<button onClick={this.onoffset}>set offset</button>
|
230 | 289 |
|
231 | 290 |
<div className="main">
|
232 | 291 |
<Keyboard
|
233 | 292 |
w={12}
|
234 | 293 |
h={4}
|
235 | 294 |
send={this.send}
|
|
295 |
showMidi={showMidi}
|
236 | 296 |
layout={layouts[layout]}
|
237 | |
showMidi={showMidi}
|
238 | |
state={notes}
|
|
297 |
scale={scale !== "none" && offset !== null && tallyup(scales[scale], offset)}
|
|
298 |
offset={offset}
|
|
299 |
state={state}
|
239 | 300 |
/>
|
240 | 301 |
<ChordView
|
241 | 302 |
chord={chord}
|