Parcourir la source

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 il y a 4 ans
Parent
révision
1ab5104bc4
10 fichiers modifiés avec 5793 ajouts et 15 suppressions
  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
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 1
- 0
docs/example.38f122ec.js.map
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 2
- 0
docs/index.html Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 1
- 1
package.json Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

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


Chargement…
Annuler
Enregistrer