瀏覽代碼

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}


Loading…
取消
儲存