git.s-ol.nu isomorphic-kb-explorer / 582452e
add scale highlighting s-ol 7 months ago
2 changed file(s) with 94 addition(s) and 20 deletion(s). Raw diff Collapse all Expand all
22 import * as notes from './notes';
33
44 const statbyte = (stat, chan) => (stat << 4) | chan;
5 const CHANNEL = 0b1011;
6 const ALL_NOTES_OFF = 0x7B;
7 const ALL_SOUND_OFF = 0x78;
58 const NOTE_ON = 0b1001;
69 const NOTE_OFF = 0b1000;
710 const CCHANGE = 0b1011;
2326 val: message.data[2] / 127,
2427 });
2528
26 const Hexagon = ({ x, y, children, state, note, send, major }) => {
29 const Hexagon = ({ x, y, children, state, note, send, major, scale }) => {
2730 if (major == 'row') {
2831 if (y % 2 == 0)
2932 x += 0.5;
3437
3538 return (
3639 <div
37 className={'hexagon' + (state ? ' active' : '')}
40 className={`hexagon ${state ? ' active' : ''} ${scale}`}
3841 style={pos([x, y], major)}
3942 >
4043 <div
7578 },
7679 };
7780
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 };
7890
7991 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 }) => (
81110 <div
82111 className={`keyboard ${major}-major`}
83112 style={{
88117 {range(w).map((_, x) => (
89118 range(h).map((_, y) => {
90119 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;
91122 return (
92123 <Hexagon
93124 key={x + ',' + y}
96127 state={state[note]}
97128 send={send}
98129 major={major}
130 scale={onCore ? "core" : (onScale ? "" : "disabled")}
99131 >
100132 {showMidi ? note : notes.music[note % 12]}
101133 </Hexagon>
132164 export default class App extends React.Component {
133165 state = {
134166 layout: 'wicki_hayden',
135 notes: {},
167 scale: 'major',
168 offset: 60,
169 state: {},
136170 };
137171
138172 componentDidMount() {
146180 }
147181
148182 componentDidUpdate(prevProps) {
183 this.send(CHANNEL, ALL_SOUND_OFF, 0);
184 this.send(CHANNEL, ALL_NOTES_OFF, 0);
185
149186 if (prevProps.midiin)
150187 prevProps.midiin.onmidimessage = null;
151188
158195 setTimeout(() => this.onmessage({ cmd: NOTE_OFF, note }), 250);
159196 }
160197
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
162202 if (!this.props.midiout)
163203 return;
164204
165 const msg = [statbyte(command, 0), note, 127];
205 const msg = [statbyte(command, 0), note, vel];
166206 this.props.midiout.send(msg);
167207 }
168208
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 });
172213
173214 onmessage = (e) => {
174215 const message = parse(e);
181222
182223 switch (message.cmd) {
183224 case NOTE_ON:
184 this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: true } }));
225 this.setState(({ state }) => ({ state: { ...state, [message.note]: true } }));
185226 break;
186227
187228 case NOTE_OFF:
188 this.setState(({ notes }) => ({ notes: { ...notes, [message.note]: false } }));
229 this.setState(({ state }) => ({ state: { ...state, [message.note]: false } }));
189230 break;
190231 }
191232 }
196237 const note = notes.key2midi[e.code];
197238 if (!note) return;
198239 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 } }));
200245 this.send(NOTE_ON, note);
201246 }
202247
206251 const note = notes.key2midi[e.code];
207252 if (!note) return;
208253 e.preventDefault();
209 this.setState(({ notes }) => ({ notes: { ...notes, [note]: false } }));
254
255 this.setState(({ state }) => ({ state: { ...state, [note]: false } }));
210256 this.send(NOTE_OFF, note);
211257 }
212258
213259 render() {
214260 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);
218264 chord.sort();
219265
220266 return (
221267 <div className="app">
222268 <button onClick={configure}>settings</button>
223 <label>layout:
269 <label>layout:{' '}
224270 <select name="layout" onChange={this.onlayout} value={layout}>
225271 <option value="wicki_hayden">Wicki-Hayden</option>
226272 <option value="harmonic">Harmonic Table</option>
227273 <option value="gerhard">Gerhard</option>
228274 </select>
229275 </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>
230289
231290 <div className="main">
232291 <Keyboard
233292 w={12}
234293 h={4}
235294 send={this.send}
295 showMidi={showMidi}
236296 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}
239300 />
240301 <ChordView
241302 chord={chord}
8181 font-size: ${rem(longSize * 0.5)};
8282 text-align: center;
8383 background: #696969;
84 border: 3px solid #696969;
8485 color: #eeeeee;
8586 cursor: pointer;
8687
87 transition: background 300ms;
88 transition: background 300ms, border 300ms;
8889 }
90
91 .hexagon.disabled > .inner {
92 background: #363636 !important;
93 border-color: #363636;
94 color: #848484;
95 }
96 .hexagon.core> .inner {
97 border-color: #eeeeee;
98 }
99
89100 .hexagon > .inner:hover {
90101 background: #848484;
102 border-color: #848484;
91103 }
92104 .hexagon > .inner:active,
93105 .hexagon.active > .inner {
94106 background: #eeeeee;
107 border-color: #eeeeee;
95108 color: #696969;
96109 }
97110