Переглянути джерело

Add demo, fix keyboard event handling

Add demo for GitHub Pages

Also disable keyboard event handling when one of Shift, Ctrl, Alt, or Cmd is pressed.
master
TheoryOfNekomata 4 роки тому
джерело
коміт
1ab5104bc4
10 змінених файлів з 5793 додано та 15 видалено
  1. +80
    -0
      docs/example.38f122ec.js
  2. +1
    -0
      docs/example.38f122ec.js.map
  3. +2
    -0
      docs/index.html
  4. +101
    -0
      example/index.html
  5. +399
    -0
      example/index.tsx
  6. +18
    -0
      example/package.json
  7. +5178
    -0
      example/yarn.lock
  8. +1
    -1
      package.json
  9. +2
    -12
      src/components/Keyboard/Keyboard.stories.tsx
  10. +11
    -2
      src/components/KeyboardMap/KeyboardMap.tsx

+ 80
- 0
docs/example.38f122ec.js
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 1
- 0
docs/example.38f122ec.js.map
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 2
- 0
docs/index.html Переглянути файл

@@ -0,0 +1,2 @@
<!DOCTYPE html><html lang="en-PH"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,shrink-to-fit=no"><title>Demo | React Musical Keyboard</title><meta name="description" content="@theoryofnekomata/react-musical-keyboard"><style>:root{--color-channel-0:#f55;--color-channel-1:#ff0;--color-channel-2:#0a0;--color-channel-3:#05a;--color-channel-4:#a0f;--color-channel-5:#a00;--color-channel-6:#a50;--color-channel-7:#fa0;--color-channel-8:#0f0;--color-channel-9:#0aa;--color-channel-10:#0ff;--color-channel-11:#f0a;--color-channel-12:#aa0;--color-channel-13:#550;--color-channel-14:#50a;--color-channel-15:#f5f}html{color:#fff;background-color:#000}body,html{width:100%;height:100%}body{margin:0;display:grid;grid-template-areas:"channel instrument instrument" "background background background" "keyboard keyboard keyboard";grid-template-columns:5rem auto auto;grid-template-rows:3rem auto 15rem}#channel{grid-area:channel}#channel,#instrument{-webkit-appearance:none;border:0;outline:0;background-color:initial;color:inherit;font:inherit;padding:0 1rem}#instrument{grid-area:instrument}#keyboard{grid-area:keyboard;width:100%;position:relative;overflow-x:auto;color:#000;background-color:#fff}#keyboard-scroll{width:750%;height:100%;top:0;left:0;position:absolute}@media (min-width:1080px){body{grid-template-rows:5rem auto 5rem}#keyboard-scroll{width:100%}}</style></head><body> <script src="example.38f122ec.js"></script>
</body></html>

+ 101
- 0
example/index.html Переглянути файл

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en-PH">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,shrink-to-fit=no">
<title>Demo | React Musical Keyboard</title>
<meta name="description" content="@theoryofnekomata/react-musical-keyboard">
<style>
:root {
--color-channel-0: #f55;
--color-channel-1: #ff0;
--color-channel-2: #0a0;
--color-channel-3: #05a;
--color-channel-4: #a0f;
--color-channel-5: #a00;
--color-channel-6: #a50;
--color-channel-7: #fa0;
--color-channel-8: #0f0;
--color-channel-9: #0aa;
--color-channel-10: #0ff;
--color-channel-11: #f0a;
--color-channel-12: #aa0;
--color-channel-13: #550;
--color-channel-14: #50a;
--color-channel-15: #f5f;
}

html {
width: 100%;
height: 100%;
color: white;
background-color: black;
}

body {
margin: 0;
width: 100%;
height: 100%;
display: grid;
grid-template-areas:
'channel instrument instrument'
'background background background'
'keyboard keyboard keyboard';
grid-template-columns: 5rem auto auto;
grid-template-rows: 3rem auto 15rem;
}

#channel {
grid-area: channel;
-webkit-appearance: none;
border: 0;
outline: 0;
background-color: transparent;
color: inherit;
font: inherit;
padding: 0 1rem;
}

#instrument {
grid-area: instrument;
-webkit-appearance: none;
border: 0;
outline: 0;
background-color: transparent;
color: inherit;
font: inherit;
padding: 0 1rem;
}

#keyboard {
grid-area: keyboard;
width: 100%;
position: relative;
overflow-x: auto;
color: black;
background-color: white;
}

#keyboard-scroll {
width: 750%;
height: 100%;
top: 0;
left: 0;
position: absolute;
}

@media (min-width: 1080px) {
body {
grid-template-rows: 5rem auto 5rem;
}

#keyboard-scroll {
width: 100%;
}
}
</style>
</head>
<body>
<script src="index.tsx"></script>
</body>
</html>

+ 399
- 0
example/index.tsx Переглянути файл

@@ -0,0 +1,399 @@
import * as React from 'react'
import ReactDOM from 'react-dom'

import Keyboard, { KeyboardMap } from '../src'

interface SoundGenerator {
changeInstrument(channel: number, patch: number): void,
noteOn(channel: number, key: number, velocity: number): void,
noteOff(channel: number, key: number, velocity: number): void,
getInstrumentNames(): string[],
}

type MIDIMessage = [number, number, number?]

interface MIDIOutput {
send(message: MIDIMessage): void
}

class MidiGenerator implements SoundGenerator {
constructor(private output: MIDIOutput) {
}

noteOn(channel: number, key: number, velocity: number) {
this.output.send([0b10010000 + channel, key, velocity])
}

noteOff(channel: number, key: number, velocity: number) {
this.output.send([0b10000000 + channel, key, velocity])
}

changeInstrument(channel: number, patch: number) {
this.output.send([0b11000000 + channel, patch])
}

getInstrumentNames(): string[] {
return [
'Acoustic Grand Piano',
'Bright Acoustic Piano',
'Electric Grand Piano',
'Honky-tonk Piano',
'Electric Piano 1',
'Electric Piano 2',
'Harpsichord',
'Clavi',
'Celesta',
'Glockenspiel',
'Music Box',
'Vibraphone',
'Marimba',
'Xylophone',
'Tubular Bells',
'Dulcimer',
'Drawbar Organ',
'Percussive Organ',
'Rock Organ',
'Church Organ',
'Reed Organ',
'Accordion',
'Harmonica',
'Tango Accordion',
'Acoustic Guitar (nylon)',
'Acoustic Guitar (steel)',
'Electric Guitar (jazz)',
'Electric Guitar (clean)',
'Electric Guitar (muted)',
'Overdriven Guitar',
'Distortion Guitar',
'Guitar harmonics',
'Acoustic Bass',
'Electric Bass (finger)',
'Electric Bass (pick)',
'Fretless Bass',
'Slap Bass 1',
'Slap Bass 2',
'Synth Bass 1',
'Synth Bass 2',
'Violin',
'Viola',
'Cello',
'Contrabass',
'Tremolo Strings',
'Pizzicato Strings',
'Orchestral Harp',
'Timpani',
'String Ensemble 1',
'String Ensemble 2',
'SynthStrings 1',
'SynthStrings 2',
'Choir Aahs',
'Voice Oohs',
'Synth Voice',
'Orchestra Hit',
'Trumpet',
'Trombone',
'Tuba',
'Muted Trumpet',
'French Horn',
'Brass Section',
'SynthBrass 1',
'SynthBrass 2',
'Soprano Sax',
'Alto Sax',
'Tenor Sax',
'Baritone Sax',
'Oboe',
'English Horn',
'Bassoon',
'Clarinet',
'Piccolo',
'Flute',
'Recorder',
'Pan Flute',
'Blown Bottle',
'Shakuhachi',
'Whistle',
'Ocarina',
'Lead 1 (square)',
'Lead 2 (sawtooth)',
'Lead 3 (calliope)',
'Lead 4 (chiff)',
'Lead 5 (charang)',
'Lead 6 (voice)',
'Lead 7 (fifths)',
'Lead 8 (bass + lead)',
'Pad 1 (new age)',
'Pad 2 (warm)',
'Pad 3 (polysynth)',
'Pad 4 (choir)',
'Pad 5 (bowed)',
'Pad 6 (metallic)',
'Pad 7 (halo)',
'Pad 8 (sweep)',
'FX 1 (rain)',
'FX 2 (soundtrack)',
'FX 3 (crystal)',
'FX 4 (atmosphere)',
'FX 5 (brightness)',
'FX 6 (goblins)',
'FX 7 (echoes)',
'FX 8 (sci-fi)',
'Sitar',
'Banjo',
'Shamisen',
'Koto',
'Kalimba',
'Bag pipe',
'Fiddle',
'Shanai',
'Tinkle Bell',
'Agogo',
'Steel Drums',
'Woodblock',
'Taiko Drum',
'Melodic Tom',
'Synth Drum',
'Reverse Cymbal',
'Guitar Fret Noise',
'Breath Noise',
'Seashore',
'Bird Tweet',
'Telephone Ring',
'Helicopter',
'Applause',
'Gunshot',
]
}
}

class WaveGenerator implements SoundGenerator {
private output: AudioContext
private sounds = 'sine triangle sawtooth square'.split(' ')
private oscillators = new Array(16).fill({})
private channels = new Array(16).fill(0)
private baseFrequency = 440

constructor() {
const tryWindow = window as any
const AudioContext = tryWindow.AudioContext || tryWindow['webkitAudioContext']
this.output = new AudioContext()
}

private getKeyFrequency = (keyNumber: number, baseKeyNumber: number, baseKeyFrequency: number) => (
baseKeyFrequency * Math.pow(
Math.pow(2, 1 / 12),
(keyNumber - baseKeyNumber),
)
)

noteOn(channel: number, key: number, velocity: number) {
if (this.oscillators[channel][key]) {
this.oscillators[channel][key].stop()
delete this.oscillators[channel][key]
}

this.oscillators[channel][key] = this.output.createOscillator()
const gainNode = this.output.createGain()

this.oscillators[channel][key].type = this.sounds[this.channels[channel]]
this.oscillators[channel][key].connect(gainNode)
gainNode.connect(this.output.destination)
gainNode.gain.value = velocity * 0.001

this.oscillators[channel][key].frequency.value = this.getKeyFrequency(key, 69, this.baseFrequency)
this.oscillators[channel][key].start()
}

noteOff(channel: number, key: number, _velocity: number) {
if (this.oscillators[channel][key]) {
try {
this.oscillators[channel][key].stop()
} catch (err) {
}
delete this.oscillators[channel][key]
}
}

changeInstrument(channel: number, patch: number) {
this.channels[channel] = patch
}

getInstrumentNames(): string[] {
return this.sounds
}
}

const App = () => {
const [channel, setChannel] = React.useState(0)
const [keyChannels, setKeyChannels] = React.useState<{ key: number; velocity: number; channel: number }[]>([])
const [instruments, setInstruments, ] = React.useState<string[]>([])
const [instrument, setInstrument] = React.useState(0)
const generator = React.useRef<SoundGenerator | undefined>(undefined)
const scrollRef = React.useRef<HTMLDivElement>(null)

const handleKeyOn = (newKeys: { key: number; velocity: number; channel: number; id: number }[]) => {
setKeyChannels((oldKeys) => {
const oldKeysKeys = oldKeys.map((k) => k.key)
const newKeysKeys = newKeys.map((k) => k.key)
const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key))
const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key))

keysOn.forEach((k) => {
if (!generator.current) {
return
}
generator.current.noteOn(k.channel, k.key, Math.floor(k.velocity * 127))
})

keysOff.forEach((k) => {
if (!generator.current) {
return
}
generator.current.noteOff(k.channel, k.key, Math.floor(k.velocity * 127))
})

return newKeys
})
}

const handleChangeInstrument: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
const { value: rawValue } = e.target
const value = Number(rawValue)
setInstrument(value)
}

const handleChangeChannel: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const { value: rawValue } = e.target
const value = Number(rawValue)
setChannel(value)
}

React.useEffect(() => {
if (!generator.current) {
return
}
generator.current.changeInstrument(channel, instrument)
}, [channel, instrument])

React.useEffect(() => {
const { navigator: maybeNavigator } = window
const navigator = maybeNavigator as Navigator & {
requestMIDIAccess: () => Promise<{ outputs: Map<string, unknown> }>
}
if ('requestMIDIAccess' in navigator) {
navigator.requestMIDIAccess().then((m) => {
generator.current = new MidiGenerator(Array.from(m.outputs.values())[0] as MIDIOutput)
setInstruments(generator.current!.getInstrumentNames())
generator.current.changeInstrument(0, 0)
})
} else {
generator.current = new WaveGenerator()
setInstruments(generator.current!.getInstrumentNames())
generator.current.changeInstrument(0, 0)
}

}, [])

React.useEffect(() => {
const { current } = scrollRef
if (current) {
current.scrollLeft = current.scrollWidth * 0.4668
}
}, [scrollRef])

return (
<React.Fragment>
<input
type="number"
id="channel"
min={0}
max={15}
onChange={handleChangeChannel}
defaultValue={0}
/>
<select
id="instrument"
onChange={handleChangeInstrument}
defaultValue={0}
>
{Array.isArray(instruments) && instruments.map((name, i) => (
<option
key={i}
value={i}
>
{name}
</option>
))}
</select>
<div
id="keyboard"
ref={scrollRef}
>
<div
id="keyboard-scroll"
>
<Keyboard
hasMap
startKey={0}
endKey={127}
keyChannels={keyChannels}
height="100%"
>
<KeyboardMap
channel={channel}
onChange={handleKeyOn}
keyboardMapping={{
KeyQ: 60,
Digit2: 61,
KeyW: 62,
Digit3: 63,
KeyE: 64,
KeyR: 65,
Digit5: 66,
KeyT: 67,
Digit6: 68,
KeyY: 69,
Digit7: 70,
KeyU: 71,
KeyI: 72,
Digit9: 73,
KeyO: 74,
Digit0: 75,
KeyP: 76,
BracketLeft: 77,
Equal: 78,
BracketRight: 79,

KeyZ: 48,
KeyS: 49,
KeyX: 50,
KeyD: 51,
KeyC: 52,
KeyV: 53,
KeyG: 54,
KeyB: 55,
KeyH: 56,
KeyN: 57,
KeyJ: 58,
KeyM: 59,
Comma: 60,
KeyL: 61,
Period: 62,
Semicolon: 63,
Slash: 64,
}}
/>
</Keyboard>
</div>
</div>
</React.Fragment>
)
}

const container = window.document.createElement('div')

container.style.display = 'contents'

window.document.body.appendChild(container)

ReactDOM.render(<App />, container)

+ 18
- 0
example/package.json Переглянути файл

@@ -0,0 +1,18 @@
{
"private": true,
"dependencies": {
"parcel": "^1.12.4"
},
"scripts": {
"start": "parcel index.html",
"build": "parcel build index.html -d ../docs --public-url ./"
},
"alias": {
"react": "../node_modules/react",
"react-dom": "../node_modules/react-dom/profiling",
"scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
},
"browserslist": [
"last 2 Chrome versions"
]
}

+ 5178
- 0
example/yarn.lock
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 1
- 1
package.json Переглянути файл

@@ -1,5 +1,5 @@
{
"version": "1.0.12",
"version": "1.0.13",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",


+ 2
- 12
src/components/Keyboard/Keyboard.stories.tsx Переглянути файл

@@ -154,18 +154,8 @@ const HasMapComponent = () => {
setKeyChannels((oldKeys) => {
const oldKeysKeys = oldKeys.map((k) => k.key)
const newKeysKeys = newKeys.map((k) => k.key)
const keysOff = oldKeys
.filter((ok) => !newKeysKeys.includes(ok.key))
.map((k) => ({
...k,
velocity: k.velocity > 1 ? 1 : k.velocity < 0 ? 0 : k.velocity,
}))
const keysOn = newKeys
.filter((nk) => !oldKeysKeys.includes(nk.key))
.map((k) => ({
...k,
velocity: k.velocity > 1 ? 1 : k.velocity < 0 ? 0 : k.velocity,
}))
const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key))
const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key))

keysOn.forEach((k) => {
midiAccess.current?.send([0b10010000 + k.channel, k.key, Math.floor(k.velocity * 127)])


+ 11
- 2
src/components/KeyboardMap/KeyboardMap.tsx Переглянути файл

@@ -60,7 +60,7 @@ const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onCha

if (e.buttons === 1) {
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
}
keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, channel, id: -1 }]
if (typeof onChange! === 'function') {
@@ -87,7 +87,7 @@ const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onCha
return
}
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
}
keysOnRef.current = [
...keysOnRef.current,
@@ -254,6 +254,10 @@ const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onCha
return
}

if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
return
}

const { [e.code]: key = null } = keyboardMapping as Record<string, number>

if (key === null) {
@@ -285,6 +289,10 @@ const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onCha
return
}

if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
return
}

const { [e.code]: key = null } = keyboardMapping as Record<string, number>

if (key === null) {
@@ -313,6 +321,7 @@ const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onCha
width: '100%',
height: '100%',
zIndex: 4,
outline: 0,
}}
onContextMenu={handleContextMenu}
onDragStart={handleDragStart}


Завантаження…
Відмінити
Зберегти