Ver código fonte

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 anos atrás
pai
commit
1ab5104bc4
10 arquivos alterados com 5793 adições e 15 exclusões
  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
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 0
docs/example.38f122ec.js.map
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 2
- 0
docs/index.html Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 1
package.json Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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}


Carregando…
Cancelar
Salvar