static files
This commit is contained in:
commit
1eda91a7f2
68
static/app/Main.js
Normal file
68
static/app/Main.js
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Keyboard} from 'keyboard/Keyboard'
|
||||
import domReady from 'domready'
|
||||
import 'style/main.css'
|
||||
import {AI} from 'ai/AI'
|
||||
import {Sound} from 'sound/Sound'
|
||||
import {Glow} from 'interface/Glow'
|
||||
import {Splash} from 'interface/Splash'
|
||||
|
||||
domReady(() => {
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.id = 'container'
|
||||
document.body.appendChild(container)
|
||||
|
||||
const ai = new AI()
|
||||
const glow = new Glow(container)
|
||||
const keyboard = new Keyboard(container)
|
||||
const sound = new Sound()
|
||||
|
||||
const splash = new Splash(document.body)
|
||||
splash.on('click', () => {
|
||||
container.classList.add('focus')
|
||||
keyboard.activate()
|
||||
})
|
||||
|
||||
sound.load()
|
||||
|
||||
keyboard.on('keyDown', (note) => {
|
||||
sound.keyDown(note)
|
||||
ai.keyDown(note)
|
||||
glow.user()
|
||||
})
|
||||
|
||||
keyboard.on('keyUp', (note) => {
|
||||
sound.keyUp(note)
|
||||
ai.keyUp(note)
|
||||
glow.user()
|
||||
})
|
||||
|
||||
ai.on('keyDown', (note, time) => {
|
||||
sound.keyDown(note, time, true)
|
||||
keyboard.keyDown(note, time, true)
|
||||
glow.ai(time)
|
||||
})
|
||||
|
||||
ai.on('keyUp', (note, time) => {
|
||||
sound.keyUp(note, time, true)
|
||||
keyboard.keyUp(note, time, true)
|
||||
glow.ai(time)
|
||||
})
|
||||
|
||||
})
|
108
static/app/ai/AI.js
Normal file
108
static/app/ai/AI.js
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Midi} from 'MidiConvert/src/Midi'
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import MidiConvert from 'MidiConvert/src/MidiConvert'
|
||||
import events from 'events'
|
||||
|
||||
window.generator = 'pop'
|
||||
|
||||
class AI extends events.EventEmitter{
|
||||
constructor(){
|
||||
super()
|
||||
|
||||
this._newTrack()
|
||||
|
||||
this._sendTimeout = -1
|
||||
|
||||
this._heldNotes = {}
|
||||
|
||||
this._lastPhrase = -1
|
||||
|
||||
/*setInterval(() => {
|
||||
//wait a max of 10 seconds before sending an event
|
||||
if (Date.now() - this._phraseStart > 5000){
|
||||
for (let note in this._heldNotes){
|
||||
this._track.noteOff(note, Tone.now())
|
||||
delete this._heldNotes[note]
|
||||
}
|
||||
this.send()
|
||||
}
|
||||
}, 200)*/
|
||||
|
||||
this._aiEndTime = 0
|
||||
}
|
||||
|
||||
_newTrack(){
|
||||
this._midi = new Midi()
|
||||
this._track = this._midi.track()
|
||||
}
|
||||
|
||||
send(){
|
||||
//trim the track to the first note
|
||||
if (this._track.length){
|
||||
let request = this._midi.slice(this._midi.startTime)
|
||||
this._newTrack()
|
||||
let endTime = request.duration
|
||||
//shorten the request if it's too long
|
||||
if (endTime > 10){
|
||||
request = request.slice(request.duration - 15)
|
||||
endTime = request.duration
|
||||
}
|
||||
let additional = endTime
|
||||
additional = Math.min(additional, 8)
|
||||
additional = Math.max(additional, 1)
|
||||
request.load(`/predict?duration=${endTime + additional}&generator=${generator}`, JSON.stringify(request.toArray()), 'POST').then((response) => {
|
||||
response.slice(endTime / 2).tracks[1].notes.forEach((note) => {
|
||||
const now = Tone.now()
|
||||
if (note.noteOn + now > this._aiEndTime){
|
||||
this._aiEndTime = note.noteOn + now
|
||||
this.emit('keyDown', note.midi, note.noteOn + now)
|
||||
note.duration = note.duration * 0.9
|
||||
this.emit('keyUp', note.midi, note.noteOff + now)
|
||||
}
|
||||
})
|
||||
})
|
||||
this._lastPhrase = -1
|
||||
}
|
||||
}
|
||||
|
||||
keyDown(note){
|
||||
if (this._track.length === 0 && this._lastPhrase === -1){
|
||||
this._lastPhrase = Date.now()
|
||||
}
|
||||
this._track.noteOn(note, Tone.now())
|
||||
clearTimeout(this._sendTimeout)
|
||||
this._heldNotes[note] = true
|
||||
}
|
||||
|
||||
keyUp(note){
|
||||
this._track.noteOff(note, Tone.now())
|
||||
delete this._heldNotes[note]
|
||||
// send something if there are no events for a moment
|
||||
if (Object.keys(this._heldNotes).length === 0){
|
||||
if (this._lastPhrase !== -1 && Date.now() - this._lastPhrase > 5000){
|
||||
//do it immediately
|
||||
this.send()
|
||||
} else {
|
||||
this._sendTimeout = setTimeout(this.send.bind(this), 600)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {AI}
|
65
static/app/interface/Glow.js
Normal file
65
static/app/interface/Glow.js
Normal file
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'style/glow.css'
|
||||
import Tone from 'Tone/core/Tone'
|
||||
|
||||
class Glow {
|
||||
constructor(container){
|
||||
this._element = document.createElement('div')
|
||||
this._element.id = 'glow'
|
||||
container.appendChild(this._element)
|
||||
|
||||
this._aiGlow = document.createElement('div')
|
||||
this._aiGlow.id = 'ai'
|
||||
this._element.appendChild(this._aiGlow)
|
||||
|
||||
this._userGlow = document.createElement('div')
|
||||
this._userGlow.id = 'user'
|
||||
this._element.appendChild(this._userGlow)
|
||||
|
||||
this._aiTime = -1
|
||||
|
||||
this._boundLoop = this._loop.bind(this)
|
||||
this._loop()
|
||||
|
||||
this._aiVisible = true
|
||||
}
|
||||
_loop(){
|
||||
requestAnimationFrame(this._boundLoop)
|
||||
if (this._aiTime < 0 || Tone.now() > this._aiTime){
|
||||
if (this._aiVisible){
|
||||
this._aiVisible = false
|
||||
this._aiGlow.classList.remove('visible')
|
||||
this._userGlow.classList.add('visible')
|
||||
}
|
||||
} else {
|
||||
if (!this._aiVisible){
|
||||
this._aiVisible = true
|
||||
this._aiGlow.classList.add('visible')
|
||||
this._userGlow.classList.remove('visible')
|
||||
}
|
||||
}
|
||||
}
|
||||
ai(time){
|
||||
this._aiTime = Math.max(this._aiTime, time + 0.3)
|
||||
}
|
||||
user(){
|
||||
this._aiTime = -1
|
||||
}
|
||||
}
|
||||
|
||||
export {Glow}
|
64
static/app/interface/Loader.js
Normal file
64
static/app/interface/Loader.js
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Buffer from 'Tone/core/Buffer'
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import events from 'events'
|
||||
const EventEmitter = events.EventEmitter
|
||||
import StartAudioContext from 'startaudiocontext'
|
||||
|
||||
export default class Loader extends EventEmitter{
|
||||
constructor(container){
|
||||
super()
|
||||
|
||||
const loader = document.createElement('div')
|
||||
loader.id = 'loader'
|
||||
container.appendChild(loader)
|
||||
|
||||
const loaderText = document.createElement('div')
|
||||
loaderText.id = 'loaderText'
|
||||
loaderText.textContent = 'loading'
|
||||
loader.appendChild(loaderText)
|
||||
|
||||
const fill = document.createElement('div')
|
||||
fill.id = 'fill'
|
||||
loader.appendChild(fill)
|
||||
|
||||
const fillText = document.createElement('div')
|
||||
fillText.id = 'fillText'
|
||||
fillText.textContent = 'loading'
|
||||
fill.appendChild(fillText)
|
||||
|
||||
StartAudioContext(Tone.context, loader)
|
||||
|
||||
Buffer.on('load', () => {
|
||||
|
||||
fillText.innerHTML = '<div id="piano"></div> <div id="play">PLAY</div>'
|
||||
loader.classList.add('clickable')
|
||||
|
||||
loader.addEventListener('click', () => {
|
||||
|
||||
this.emit('click')
|
||||
})
|
||||
})
|
||||
|
||||
Buffer.on('progress', (prog) => {
|
||||
|
||||
fill.style.width = `${(prog * 100).toFixed(2)}%`
|
||||
|
||||
})
|
||||
}
|
||||
}
|
74
static/app/interface/Splash.js
Normal file
74
static/app/interface/Splash.js
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Buffer from 'Tone/core/Buffer'
|
||||
import 'style/splash.css'
|
||||
import events from 'events'
|
||||
import Loader from 'interface/Loader'
|
||||
|
||||
class Splash extends events.EventEmitter{
|
||||
constructor(container){
|
||||
|
||||
super()
|
||||
const splash = document.createElement('div')
|
||||
splash.id = 'splash'
|
||||
container.appendChild(splash)
|
||||
|
||||
// the title
|
||||
const titleContainer = document.createElement('div')
|
||||
titleContainer.id = 'titleContainer'
|
||||
splash.appendChild(titleContainer)
|
||||
|
||||
const title = document.createElement('div')
|
||||
title.id = 'title'
|
||||
title.textContent = 'A.I. Duet'
|
||||
titleContainer.appendChild(title)
|
||||
|
||||
const subTitle = document.createElement('div')
|
||||
subTitle.id = 'subTitle'
|
||||
titleContainer.appendChild(subTitle)
|
||||
subTitle.textContent = 'Trade melodies with a neural network.'
|
||||
|
||||
const loader = new Loader(titleContainer)
|
||||
loader.on('click', () => {
|
||||
splash.classList.add('disappear')
|
||||
this.emit('click')
|
||||
})
|
||||
|
||||
const aiExperiments = document.createElement('a')
|
||||
aiExperiments.id = 'aiExperiments'
|
||||
aiExperiments.href = 'https://aiexperiments.withgoogle.com'
|
||||
aiExperiments.target = '_blank'
|
||||
splash.appendChild(aiExperiments)
|
||||
|
||||
// break
|
||||
const badgeBreak = document.createElement('div')
|
||||
badgeBreak.id = 'badgeBreak'
|
||||
splash.appendChild(badgeBreak)
|
||||
|
||||
const googleFriends = document.createElement('a')
|
||||
googleFriends.id = 'googleFriends'
|
||||
splash.appendChild(googleFriends)
|
||||
|
||||
const privacyAndTerms = document.createElement('div')
|
||||
privacyAndTerms.id = 'privacyAndTerms'
|
||||
privacyAndTerms.innerHTML = '<a target="_blank" href="https://www.google.com/intl/en/policies/privacy/">Privacy</a><span>&</span><a target="_blank" href="https://www.google.com/intl/en/policies/terms/">Terms</a>'
|
||||
splash.appendChild(privacyAndTerms)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export {Splash}
|
159
static/app/keyboard/Element.js
Normal file
159
static/app/keyboard/Element.js
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import events from 'events'
|
||||
import 'style/keyboard.css'
|
||||
import 'pepjs'
|
||||
import {Roll} from 'roll/Roll'
|
||||
|
||||
|
||||
const offsets = [0, 0.5, 1, 1.5, 2, 3, 3.5, 4, 4.5, 5, 5.5, 6]
|
||||
|
||||
class KeyboardElement extends events.EventEmitter {
|
||||
|
||||
constructor(container, lowest=36, octaves=4){
|
||||
super()
|
||||
this._container = document.createElement('div')
|
||||
this._container.id = 'keyboard'
|
||||
container.setAttribute('touch-action', 'none')
|
||||
container.addEventListener('pointerup', () => this._mousedown = false)
|
||||
container.appendChild(this._container)
|
||||
|
||||
this._keys = {}
|
||||
|
||||
this._mousedown = false
|
||||
|
||||
this.resize(lowest, octaves)
|
||||
|
||||
/**
|
||||
* The piano roll
|
||||
* @type {Roll}
|
||||
*/
|
||||
this._roll = new Roll(container)
|
||||
}
|
||||
|
||||
resize(lowest, octaves){
|
||||
this._keys = {}
|
||||
// clear the previous ones
|
||||
this._container.innerHTML = ''
|
||||
// each of the keys
|
||||
const keyWidth = (1 / 7) / octaves
|
||||
for (let i = lowest; i < lowest + octaves * 12; i++){
|
||||
let key = document.createElement('div')
|
||||
key.classList.add('key')
|
||||
let isSharp = ([1, 3, 6, 8, 10].indexOf(i % 12) !== -1)
|
||||
key.classList.add(isSharp ? 'black' : 'white')
|
||||
this._container.appendChild(key)
|
||||
// position the element
|
||||
|
||||
let noteOctave = Math.floor(i / 12) - Math.floor(lowest / 12)
|
||||
let offset = offsets[i % 12] + noteOctave * 7
|
||||
key.style.width = `${keyWidth * 100}%`
|
||||
key.style.left = `${offset * keyWidth * 100}%`
|
||||
key.id = i.toString()
|
||||
key.setAttribute('touch-action', 'none')
|
||||
|
||||
const fill = document.createElement('div')
|
||||
fill.id = 'fill'
|
||||
key.appendChild(fill)
|
||||
|
||||
this._bindKeyEvents(key)
|
||||
this._keys[i] = key
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_bindKeyEvents(key){
|
||||
|
||||
key.addEventListener('pointerover', (e) => {
|
||||
if (this._mousedown){
|
||||
const noteNum = parseInt(e.target.id)
|
||||
// this.keyDown(noteNum, false)
|
||||
this.emit('keyDown', noteNum)
|
||||
} else {
|
||||
key.classList.add('hover')
|
||||
}
|
||||
})
|
||||
key.addEventListener('pointerout', (e) => {
|
||||
if (this._mousedown){
|
||||
const noteNum = parseInt(e.target.id)
|
||||
// this.keyUp(noteNum, false)
|
||||
this.emit('keyUp', noteNum)
|
||||
} else {
|
||||
key.classList.remove('hover')
|
||||
}
|
||||
})
|
||||
key.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault()
|
||||
const noteNum = parseInt(e.target.id)
|
||||
// this.keyDown(noteNum, false)
|
||||
this.emit('keyDown', noteNum)
|
||||
this._mousedown = true
|
||||
})
|
||||
key.addEventListener('pointerup', (e) => {
|
||||
e.preventDefault()
|
||||
const noteNum = parseInt(e.target.id)
|
||||
// this.keyUp(noteNum, false)
|
||||
this.emit('keyUp', noteNum)
|
||||
this._mousedown = false
|
||||
})
|
||||
}
|
||||
|
||||
keyDown(noteNum, ai=false){
|
||||
// console.log('down', noteNum, ai)
|
||||
if (this._keys.hasOwnProperty(noteNum)){
|
||||
const key = this._keys[noteNum]
|
||||
key.classList.remove('hover')
|
||||
|
||||
const highlight = document.createElement('div')
|
||||
highlight.classList.add('highlight')
|
||||
highlight.classList.add('active')
|
||||
if (ai){
|
||||
highlight.classList.add('ai')
|
||||
}
|
||||
key.querySelector('#fill').appendChild(highlight)
|
||||
|
||||
this._roll.keyDown(noteNum, this._getNotePosition(noteNum), ai)
|
||||
}
|
||||
}
|
||||
|
||||
keyUp(noteNum, ai=false){
|
||||
// console.log('up', noteNum, ai)
|
||||
if (this._keys.hasOwnProperty(noteNum)){
|
||||
const query = ai ? '.highlight.active.ai' : '.highlight.active'
|
||||
const highlight = this._keys[noteNum].querySelector(query)
|
||||
if (highlight){
|
||||
highlight.classList.remove('active')
|
||||
setTimeout(() => highlight.remove(), 2000)
|
||||
//and up on the roll
|
||||
this._roll.keyUp(noteNum, ai)
|
||||
} else {
|
||||
// console.log(this._keys[noteNum].querySelector(query))
|
||||
//try again without ai
|
||||
this.keyUp(noteNum)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_getNotePosition(key){
|
||||
if (this._keys.hasOwnProperty(key)){
|
||||
return this._keys[key].querySelector('#fill').getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {KeyboardElement}
|
149
static/app/keyboard/Keyboard.js
Normal file
149
static/app/keyboard/Keyboard.js
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import AudioKeys from 'audiokeys'
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import events from 'events'
|
||||
import {KeyboardElement} from 'keyboard/Element'
|
||||
import buckets from 'buckets-js'
|
||||
import {Midi} from 'keyboard/Midi'
|
||||
import Buffer from 'Tone/core/Buffer'
|
||||
|
||||
class Keyboard extends events.EventEmitter{
|
||||
constructor(container){
|
||||
super()
|
||||
|
||||
this._active = false
|
||||
|
||||
/**
|
||||
* The audio key keyboard
|
||||
* @type {AudioKeys}
|
||||
*/
|
||||
this._keyboard = new AudioKeys({polyphony : 88, rows : 1, octaveControls : false})
|
||||
this._keyboard.down((e) => {
|
||||
this.keyDown(e.note)
|
||||
this._emitKeyDown(e.note)
|
||||
})
|
||||
this._keyboard.up((e) => {
|
||||
this.keyUp(e.note)
|
||||
this._emitKeyUp(e.note)
|
||||
})
|
||||
|
||||
/**
|
||||
* The piano interface
|
||||
*/
|
||||
this._keyboardInterface = new KeyboardElement(container, 48, 2)
|
||||
this._keyboardInterface.on('keyDown', (note) => {
|
||||
this.keyDown(note)
|
||||
this._emitKeyDown(note)
|
||||
})
|
||||
this._keyboardInterface.on('keyUp', (note) => {
|
||||
this.keyUp(note)
|
||||
this._emitKeyUp(note)
|
||||
})
|
||||
|
||||
window.addEventListener('resize', this._resize.bind(this))
|
||||
//size initially
|
||||
this._resize()
|
||||
|
||||
//make sure they don't get double clicked
|
||||
this._currentKeys = {}
|
||||
|
||||
//a queue of all of the events
|
||||
this._eventQueue = new buckets.PriorityQueue((a, b) => b.time - a.time)
|
||||
this._boundLoop = this._loop.bind(this)
|
||||
this._loop()
|
||||
|
||||
const bottom = document.createElement('div')
|
||||
bottom.id = 'bottom'
|
||||
container.appendChild(bottom)
|
||||
|
||||
//the midi input
|
||||
this._midi = new Midi()
|
||||
this._midi.on('keyDown', (note) => {
|
||||
this.keyDown(note)
|
||||
this._emitKeyDown(note)
|
||||
})
|
||||
this._midi.on('keyUp', (note) => {
|
||||
this.keyUp(note)
|
||||
this._emitKeyUp(note)
|
||||
})
|
||||
}
|
||||
|
||||
_loop(){
|
||||
requestAnimationFrame(this._boundLoop)
|
||||
const now = Tone.now()
|
||||
while(!this._eventQueue.isEmpty() && this._eventQueue.peek().time <= now){
|
||||
const event = this._eventQueue.dequeue()
|
||||
event.callback()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_emitKeyDown(note){
|
||||
if (this._active){
|
||||
this.emit('keyDown', note)
|
||||
}
|
||||
}
|
||||
|
||||
_emitKeyUp(note){
|
||||
if (this._active){
|
||||
this.emit('keyUp', note)
|
||||
}
|
||||
}
|
||||
|
||||
keyDown(note, time=Tone.now(), ai=false){
|
||||
if (!this._active){
|
||||
return
|
||||
}
|
||||
if (!this._currentKeys[note]){
|
||||
this._currentKeys[note] = 0
|
||||
}
|
||||
this._currentKeys[note] += 1
|
||||
this._eventQueue.add({
|
||||
time : time,
|
||||
callback : this._keyboardInterface.keyDown.bind(this._keyboardInterface, note, ai)
|
||||
})
|
||||
}
|
||||
|
||||
keyUp(note, time=Tone.now(), ai=false){
|
||||
if (!this._active){
|
||||
return
|
||||
}
|
||||
if (this._currentKeys[note]){
|
||||
this._currentKeys[note] -= 1
|
||||
this._currentKeys[note] = Math.max(this._currentKeys[note], 0)
|
||||
|
||||
this._eventQueue.add({
|
||||
time : time,
|
||||
callback : this._keyboardInterface.keyUp.bind(this._keyboardInterface, note, ai)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_resize(){
|
||||
const keyWidth = 24
|
||||
let octaves = Math.round((window.innerWidth / keyWidth) / 12)
|
||||
octaves = Math.max(octaves, 2)
|
||||
this._keyboardInterface.resize(48, octaves)
|
||||
}
|
||||
|
||||
activate(){
|
||||
this._active = true
|
||||
}
|
||||
}
|
||||
|
||||
export {Keyboard}
|
59
static/app/keyboard/Midi.js
Normal file
59
static/app/keyboard/Midi.js
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import events from 'events'
|
||||
import WebMidi from 'webmidi'
|
||||
|
||||
class Midi extends events.EventEmitter{
|
||||
constructor(){
|
||||
super()
|
||||
|
||||
this._isEnabled = false
|
||||
|
||||
WebMidi.enable((err) => {
|
||||
if (!err){
|
||||
this._isEnabled = true
|
||||
if (WebMidi.inputs){
|
||||
WebMidi.inputs.forEach((input) => this._bindInput(input))
|
||||
}
|
||||
WebMidi.addListener('connected', (device) => {
|
||||
if (device.input){
|
||||
this._bindInput(device.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_bindInput(inputDevice){
|
||||
if (this._isEnabled){
|
||||
WebMidi.addListener('disconnected', (device) => {
|
||||
if (device.input){
|
||||
device.input.removeListener('noteOn')
|
||||
device.input.removeListener('noteOff')
|
||||
}
|
||||
})
|
||||
inputDevice.addListener('noteon', 'all', (event) => {
|
||||
this.emit('keyDown', event.note.number)
|
||||
})
|
||||
inputDevice.addListener('noteoff', 'all', (event) => {
|
||||
this.emit('keyUp', event.note.number)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {Midi}
|
117
static/app/roll/Roll.js
Normal file
117
static/app/roll/Roll.js
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const THREE = require('three')
|
||||
|
||||
var geometry = new THREE.PlaneGeometry( 1, 1, 1 )
|
||||
var material = new THREE.MeshBasicMaterial( {color: 0x1FB7EC, side: THREE.DoubleSide} )
|
||||
var aiMaterial = new THREE.MeshBasicMaterial( {color: 0xFFB729, side: THREE.DoubleSide} )
|
||||
|
||||
window.zero = new THREE.Vector3(0, 0, 0)
|
||||
|
||||
function scale(value, inMin, inMax, min, max){
|
||||
return ((value - inMin) / (inMax - inMin)) * (max - min) + min
|
||||
}
|
||||
|
||||
class Roll {
|
||||
constructor(container){
|
||||
this._element = document.createElement('div')
|
||||
this._element.id = 'roll'
|
||||
container.appendChild(this._element)
|
||||
|
||||
this._camera = new THREE.OrthographicCamera(0, 1, 1, 0, 1, 1000 )
|
||||
this._camera.position.z = 1
|
||||
this._camera.lookAt(new THREE.Vector3(0, 0, 0))
|
||||
|
||||
this._scene = new THREE.Scene()
|
||||
|
||||
this._renderer = new THREE.WebGLRenderer({alpha: true})
|
||||
this._renderer.setClearColor(0x000000, 0)
|
||||
this._renderer.setPixelRatio( window.devicePixelRatio )
|
||||
this._renderer.sortObjects = false
|
||||
this._element.appendChild(this._renderer.domElement)
|
||||
|
||||
this._currentNotes = {}
|
||||
|
||||
window.camera = this._camera
|
||||
|
||||
//set the size initially
|
||||
this._resize()
|
||||
|
||||
//start the loop
|
||||
this._boundLoop = this._loop.bind(this)
|
||||
this._boundLoop()
|
||||
window.addEventListener('resize', this._resize.bind(this))
|
||||
|
||||
|
||||
}
|
||||
|
||||
keyDown(midi, box, ai=false){
|
||||
const selector = ai ? `ai${midi}` : midi
|
||||
if (midi && box && !this._currentNotes.hasOwnProperty(selector)){
|
||||
//translate the box coords to this space
|
||||
const initialScaling = 10000
|
||||
const plane = new THREE.Mesh( geometry, ai ? aiMaterial : material )
|
||||
const margin = 4
|
||||
const width = box.width - margin * 2
|
||||
plane.scale.set(width, initialScaling, 1)
|
||||
plane.position.z = 0
|
||||
plane.position.x = box.left + margin + width / 2
|
||||
plane.position.y = this._element.clientHeight + this._camera.position.y + initialScaling / 2
|
||||
this._scene.add(plane)
|
||||
|
||||
this._currentNotes[selector] = {
|
||||
plane : plane,
|
||||
position: this._camera.position.y
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
keyUp(midi, ai=false){
|
||||
const selector = ai ? `ai${midi}` : midi
|
||||
if (this._currentNotes[selector]){
|
||||
const plane = this._currentNotes[selector].plane
|
||||
const position = this._currentNotes[selector].position
|
||||
delete this._currentNotes[selector]
|
||||
// get the distance covered
|
||||
plane.scale.y = Math.max(this._camera.position.y - position, 5)
|
||||
plane.position.y = this._element.clientHeight + position + plane.scale.y / 2
|
||||
}
|
||||
}
|
||||
|
||||
_resize(){
|
||||
var frustumSize = 1000
|
||||
var aspect = this._element.clientWidth / this._element.clientHeight
|
||||
//make it match the screen pixesl
|
||||
this._camera.left = 0
|
||||
this._camera.bottom = this._element.clientHeight
|
||||
this._camera.right = this._element.clientWidth
|
||||
this._camera.top = 0
|
||||
|
||||
//update things
|
||||
this._camera.updateProjectionMatrix()
|
||||
this._renderer.setSize( this._element.clientWidth, this._element.clientHeight )
|
||||
}
|
||||
|
||||
_loop(){
|
||||
requestAnimationFrame(this._boundLoop)
|
||||
this._renderer.render( this._scene, this._camera )
|
||||
this._camera.position.y += 2
|
||||
}
|
||||
}
|
||||
|
||||
export {Roll}
|
101
static/app/sound/Sound.js
Normal file
101
static/app/sound/Sound.js
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Piano from 'Piano/src/Piano'
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import PolySynth from 'Tone/instrument/PolySynth'
|
||||
import Frequency from 'Tone/type/Frequency'
|
||||
import MonoSynth from 'Tone/instrument/MonoSynth'
|
||||
|
||||
class Sound {
|
||||
constructor(){
|
||||
|
||||
this._range = [36, 108]
|
||||
|
||||
/**
|
||||
* The piano audio
|
||||
* @type {Piano}
|
||||
*/
|
||||
this._piano = new Piano(this._range, 1, false).toMaster().setVolume('release', -Infinity)
|
||||
|
||||
/**
|
||||
* The piano audio
|
||||
* @type {Piano}
|
||||
*/
|
||||
this._aipiano = new Piano(this._range, 1, false).toMaster().setVolume('release', -Infinity)
|
||||
|
||||
this._synth = new PolySynth(8, MonoSynth).toMaster()
|
||||
this._synth.set({
|
||||
oscillator : {
|
||||
type : 'pwm',
|
||||
modulationFrequency : 3
|
||||
},
|
||||
envelope : {
|
||||
attackCurve : 'linear',
|
||||
attack : 0.05,
|
||||
decay : 0.3,
|
||||
sustain : 0.8,
|
||||
release : 3,
|
||||
},
|
||||
filter : {
|
||||
type : 'lowpass'
|
||||
},
|
||||
filterEnvelope : {
|
||||
baseFrequency : 800,
|
||||
octaves : 1,
|
||||
attack : 0.3,
|
||||
decay : 0.1,
|
||||
sustain : 1,
|
||||
release : 3,
|
||||
}
|
||||
})
|
||||
this._synth.volume.value = -36
|
||||
|
||||
window.synth = this._synth
|
||||
}
|
||||
|
||||
load(){
|
||||
const salamanderPath = 'audio/Salamander/'
|
||||
|
||||
return Promise.all([this._piano.load(salamanderPath), this._aipiano.load(salamanderPath)])
|
||||
}
|
||||
|
||||
keyDown(note, time=Tone.now(), ai=false){
|
||||
|
||||
if (note >= this._range[0] && note <= this._range[1]){
|
||||
if (ai){
|
||||
this._aipiano.keyDown(note, 1, time)
|
||||
this._synth.triggerAttack(Frequency(note, 'midi').toNote(), time)
|
||||
} else {
|
||||
this._piano.keyDown(note, 1, time)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
keyUp(note, time=Tone.now(), ai=false){
|
||||
if (note >= this._range[0] && note <= this._range[1]){
|
||||
if (ai){
|
||||
this._aipiano.keyUp(note, time)
|
||||
this._synth.triggerRelease(Frequency(note, 'midi').toNote(), time)
|
||||
} else {
|
||||
this._piano.keyUp(note, time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {Sound}
|
53
static/audio/README
Normal file
53
static/audio/README
Normal file
@ -0,0 +1,53 @@
|
||||
Salamander Grand Piano V2
|
||||
Yamaha C5
|
||||
|
||||
|
||||
Technical info
|
||||
|
||||
Recorded @ 48khz24bit
|
||||
16 Velocity layers Sampled in minor thirds from the lowest A.
|
||||
Hammer noise releases chromatically sampled in onle one layer.
|
||||
String resonance releases in minor thirds in three layers.
|
||||
Two AKG c414 disposed in an AB position ~12cm above the strings
|
||||
|
||||
|
||||
Some other general info:
|
||||
This piano has been optimized and only properly tested for linuxsampler.
|
||||
If you want to optimize the .sfz yourself, values of interest are:
|
||||
-amp_veltrack (dynamics, %)
|
||||
-ampeg_release (note release decay, seconds)
|
||||
-The volume(in dB) on the pedal noise is located on the bottom of the .sfz file under //pedalAction
|
||||
[!] I suggest you make a backup of the .sfz file before you start fiddling with it :)
|
||||
In the time of writing, sfz for linuxsampler has not come out of cvs. So, it's still a bit of a pain to get this paino going.
|
||||
|
||||
Changlog:
|
||||
V3
|
||||
* Removed rowchange after every opcode the .sfz file should be more human readable for people who'd like to customize things.
|
||||
* Re-export A5v7-15 that had noticeable delay in beginning of file.
|
||||
* Adjusted the velocity at wich notes are triggered.
|
||||
* Increased ampeg_release to 1.000
|
||||
* Decreased amp_veltrack to 75
|
||||
* Retuned version by Markus Fiedler
|
||||
* The old V2 .sfz should work so no need to delete it if you like it :)
|
||||
* fixed a missing note
|
||||
|
||||
V2:
|
||||
* Re-exported all notes with all lowcut filters removed and eased off some eq on some notes around C4
|
||||
* Replaced all pedal noise samples, there are now two down and two up samples.
|
||||
* Increased ampeg_veltrack on release harmonics from A0 to C2
|
||||
* Increased ampeg_release to 0.850
|
||||
* Introduced a 44.1khz16bit and an ogg vorbis version
|
||||
|
||||
|
||||
Licence:
|
||||
|
||||
CC-by
|
||||
http://creativecommons.org/licenses/by/3.0/
|
||||
|
||||
|
||||
Author: Alexander Holm
|
||||
Feel free to mail me with any questions, if you're lucky, I might just answer them ;)
|
||||
axeldenstore (at) gmail (dot) com
|
||||
|
||||
|
||||
|
BIN
static/audio/Salamander/A0.mp3
Normal file
BIN
static/audio/Salamander/A0.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A1.mp3
Normal file
BIN
static/audio/Salamander/A1.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A2.mp3
Normal file
BIN
static/audio/Salamander/A2.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A3.mp3
Normal file
BIN
static/audio/Salamander/A3.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A4.mp3
Normal file
BIN
static/audio/Salamander/A4.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A5.mp3
Normal file
BIN
static/audio/Salamander/A5.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A6.mp3
Normal file
BIN
static/audio/Salamander/A6.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/A7.mp3
Normal file
BIN
static/audio/Salamander/A7.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C1.mp3
Normal file
BIN
static/audio/Salamander/C1.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C2.mp3
Normal file
BIN
static/audio/Salamander/C2.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C3.mp3
Normal file
BIN
static/audio/Salamander/C3.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C4.mp3
Normal file
BIN
static/audio/Salamander/C4.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C5.mp3
Normal file
BIN
static/audio/Salamander/C5.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C6.mp3
Normal file
BIN
static/audio/Salamander/C6.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C7.mp3
Normal file
BIN
static/audio/Salamander/C7.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/C8.mp3
Normal file
BIN
static/audio/Salamander/C8.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds1.mp3
Normal file
BIN
static/audio/Salamander/Ds1.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds2.mp3
Normal file
BIN
static/audio/Salamander/Ds2.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds3.mp3
Normal file
BIN
static/audio/Salamander/Ds3.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds4.mp3
Normal file
BIN
static/audio/Salamander/Ds4.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds5.mp3
Normal file
BIN
static/audio/Salamander/Ds5.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds6.mp3
Normal file
BIN
static/audio/Salamander/Ds6.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Ds7.mp3
Normal file
BIN
static/audio/Salamander/Ds7.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs1.mp3
Normal file
BIN
static/audio/Salamander/Fs1.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs2.mp3
Normal file
BIN
static/audio/Salamander/Fs2.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs3.mp3
Normal file
BIN
static/audio/Salamander/Fs3.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs4.mp3
Normal file
BIN
static/audio/Salamander/Fs4.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs5.mp3
Normal file
BIN
static/audio/Salamander/Fs5.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs6.mp3
Normal file
BIN
static/audio/Salamander/Fs6.mp3
Normal file
Binary file not shown.
BIN
static/audio/Salamander/Fs7.mp3
Normal file
BIN
static/audio/Salamander/Fs7.mp3
Normal file
Binary file not shown.
58
static/images/badgeAI_master.svg
Normal file
58
static/images/badgeAI_master.svg
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="-861 1363 220 120" style="enable-background:new -861 1363 220 120;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M-841.5,1429l-1.3-3.9h-11.6l-1.3,3.9h-5.3l10.7-30.8h3.6l10.7,30.8H-841.5z M-848.5,1406.6l-4.3,13.9h8.5
|
||||
L-848.5,1406.6z"/>
|
||||
<path class="st0" d="M-829.8,1422.2c2,0,3.6,1.6,3.6,3.6c0,1.9-1.6,3.5-3.6,3.5c-1.9,0-3.5-1.6-3.5-3.5
|
||||
C-833.4,1423.8-831.8,1422.2-829.8,1422.2"/>
|
||||
<rect x="-821.3" y="1398.2" class="st0" width="5.2" height="30.8"/>
|
||||
<path class="st0" d="M-808,1422.2c2,0,3.6,1.6,3.6,3.6c0,1.9-1.6,3.5-3.6,3.5c-1.9,0-3.5-1.6-3.5-3.5
|
||||
C-811.5,1423.8-810,1422.2-808,1422.2"/>
|
||||
<polygon class="st0" points="-852.9,1444.4 -852.9,1453.1 -840.1,1453.1 -840.1,1457.6 -852.9,1457.6 -852.9,1466.1 -840.1,1466.1
|
||||
-840.1,1470.7 -857.9,1470.7 -857.9,1439.9 -840.1,1439.9 -840.1,1444.4 "/>
|
||||
<polygon class="st0" points="-818.1,1470.7 -823.6,1470.7 -827.6,1464.1 -831.5,1470.7 -837,1470.7 -830.3,1460.3 -836.9,1450.1
|
||||
-831.3,1450.1 -827.6,1456.6 -823.8,1450.1 -818.2,1450.1 -824.8,1460.3 "/>
|
||||
<path class="st0" d="M-810.4,1452.9c1.3-2.1,4-3.3,6.3-3.3c6.2,0,10.5,5,10.5,10.8c0,5.8-4.3,10.8-10.5,10.8c-2.4,0-5.1-1.2-6.3-3.3
|
||||
v13.1h-4.8v-30.8h4.8V1452.9z M-810.4,1460.4c0,3.5,2.4,6.5,6.1,6.5c3.6,0,6-3,6-6.5s-2.4-6.5-6-6.5
|
||||
C-808,1453.9-810.4,1456.9-810.4,1460.4"/>
|
||||
<path class="st0" d="M-786.6,1461.9c0.2,2.9,1.5,5.3,5.5,5.3c2.7,0,3.8-1.2,4.4-2.7h4.8c-0.6,3.5-4,6.7-9.2,6.7
|
||||
c-6.9,0-10.3-4.8-10.3-10.8c0-6.2,3.7-10.8,10.1-10.8c5.3,0,9.5,4,9.5,9.2c0,0.7,0,1.7-0.2,3H-786.6z M-786.6,1458.3h10.2
|
||||
c0-3.1-2.1-5.1-5-5.1C-784.3,1453.2-786.6,1454.9-786.6,1458.3"/>
|
||||
<path class="st0" d="M-755,1454.6c-0.7-0.2-1.1-0.2-1.9-0.2c-4.1,0-6.3,2.3-6.3,7.4v8.8h-4.8v-20.5h4.8v3.3c0.9-1.9,3.6-3.6,6.3-3.6
|
||||
c0.7,0,1.4,0.1,1.9,0.3V1454.6z"/>
|
||||
<path class="st0" d="M-749.1,1439.1c2,0,3.6,1.6,3.6,3.6c0,1.9-1.6,3.5-3.6,3.5c-1.9,0-3.5-1.6-3.5-3.5
|
||||
C-752.6,1440.7-751,1439.1-749.1,1439.1 M-751.4,1450.1h4.8v20.5h-4.8V1450.1z"/>
|
||||
<path class="st0" d="M-716.3,1470.7v-11.5c0-3.3-1.1-5.3-3.8-5.3c-2.4,0-4.3,1.5-4.3,6.4v10.4h-4.8v-11.5c0-3.3-1.1-5.3-3.7-5.3
|
||||
c-2.4,0-4.4,1.5-4.4,6.4v10.4h-4.8v-20.5h4.8v2.9c0.9-1.9,3-3.4,5.7-3.4c3.7,0,5.6,1.6,6.5,3.7c1-2.1,3.4-3.7,6.3-3.7
|
||||
c6.2,0,7.4,4.2,7.4,8.1v13H-716.3z"/>
|
||||
<path class="st0" d="M-703.7,1461.9c0.2,2.9,1.5,5.3,5.5,5.3c2.7,0,3.8-1.2,4.4-2.7h4.8c-0.6,3.5-4,6.7-9.2,6.7
|
||||
c-6.9,0-10.3-4.8-10.3-10.8c0-6.2,3.7-10.8,10.1-10.8c5.3,0,9.5,4,9.5,9.2c0,0.7,0,1.7-0.2,3H-703.7z M-703.7,1458.3h10.2
|
||||
c0-3.1-2.1-5.1-5-5.1C-701.4,1453.2-703.7,1454.9-703.7,1458.3"/>
|
||||
<path class="st0" d="M-685.1,1450.1h4.8v2.9c1.3-2.2,3.7-3.4,6.7-3.4c2.9,0,5,1.1,6.2,2.8c1,1.5,1.3,3.3,1.3,6.1v12.2h-4.8V1460
|
||||
c0-3.5-1-6.2-4.4-6.2c-3.3,0-4.9,2.7-4.9,6.2v10.6h-4.8V1450.1z"/>
|
||||
<path class="st0" d="M-650.1,1470.7c-0.7,0.1-2.2,0.3-3.6,0.3c-2.3,0-6.6-0.3-6.6-7.2v-9.5h-3.5v-4h3.5v-6.2h4.8v6.2h4.7v4h-4.7v8.4
|
||||
c0,3.6,1.1,4,3,4c0.7,0,1.9-0.1,2.4-0.2V1470.7z"/>
|
||||
<polygon class="st0" points="-857.1,1365.6 -860.9,1365.6 -860.9,1363.4 -850.7,1363.4 -850.7,1365.6 -854.6,1365.6 -854.6,1378.8
|
||||
-857.1,1378.8 "/>
|
||||
<path class="st0" d="M-849,1363.4h2.4v6.6c0.6-1.1,1.8-1.7,3.3-1.7c1.5,0,2.5,0.5,3.1,1.4c0.5,0.8,0.7,1.6,0.7,3v6.1h-2.4v-5.3
|
||||
c0-1.8-0.5-3.1-2.2-3.1c-1.7,0-2.5,1.3-2.5,3.1v5.3h-2.4V1363.4z"/>
|
||||
<path class="st0" d="M-835.4,1363c1,0,1.8,0.8,1.8,1.8c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8
|
||||
C-837.2,1363.8-836.4,1363-835.4,1363 M-836.6,1368.5h2.4v10.3h-2.4V1368.5z"/>
|
||||
<path class="st0" d="M-823.7,1375.9c0,2.1-1.8,3.1-4.1,3.1c-2.2,0-4.2-1.3-4.2-3.7h2.3c0,1.3,0.8,1.8,2,1.8c0.9,0,1.7-0.3,1.7-1.2
|
||||
c0-1-1-1-3.2-1.9c-1.4-0.5-2.4-1.1-2.4-2.9c0-1.9,1.8-3,3.9-3c2.2,0,3.9,1.5,3.9,3.5h-2.3c0-1-0.5-1.6-1.7-1.6
|
||||
c-0.8,0-1.5,0.4-1.5,1.1c0,0.9,0.9,1,2.8,1.7C-825,1373.5-823.7,1374-823.7,1375.9"/>
|
||||
<path class="st0" d="M-814.7,1363c1,0,1.8,0.8,1.8,1.8c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8
|
||||
C-816.4,1363.8-815.6,1363-814.7,1363 M-815.9,1368.5h2.4v10.3h-2.4V1368.5z"/>
|
||||
<path class="st0" d="M-802.9,1375.9c0,2.1-1.8,3.1-4.1,3.1c-2.2,0-4.2-1.3-4.2-3.7h2.3c0,1.3,0.8,1.8,2,1.8c0.9,0,1.7-0.3,1.7-1.2
|
||||
c0-1-1-1-3.2-1.9c-1.4-0.5-2.4-1.1-2.4-2.9c0-1.9,1.8-3,3.8-3c2.2,0,3.9,1.5,3.9,3.5h-2.3c0-1-0.5-1.6-1.7-1.6
|
||||
c-0.8,0-1.5,0.4-1.5,1.1c0,0.9,0.9,1,2.8,1.7C-804.3,1373.5-802.9,1374-802.9,1375.9"/>
|
||||
<path class="st0" d="M-785,1368.5v10.3h-2.4v-1.4c-0.6,1-2,1.7-3.2,1.7c-3.1,0-5.3-2.5-5.3-5.4c0-2.9,2.1-5.4,5.3-5.4
|
||||
c1.2,0,2.5,0.6,3.2,1.7v-1.4H-785z M-787.4,1373.6c0-1.8-1.2-3.3-3-3.3c-1.8,0-3,1.5-3,3.3s1.2,3.3,3,3.3
|
||||
C-788.6,1376.9-787.4,1375.4-787.4,1373.6"/>
|
||||
<path class="st0" d="M-781.9,1368.5h2.4v1.4c0.6-1.1,1.8-1.7,3.3-1.7c1.5,0,2.5,0.5,3.1,1.4c0.5,0.8,0.7,1.6,0.7,3v6.1h-2.4v-5.3
|
||||
c0-1.8-0.5-3.1-2.2-3.1c-1.7,0-2.5,1.3-2.5,3.1v5.3h-2.4V1368.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
83
static/images/badgeFriends_master.svg
Normal file
83
static/images/badgeFriends_master.svg
Normal file
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="-861 1363 220 120" style="enable-background:new -861 1363 220 120;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<polygon class="st0" points="-844.7,1378.8 -847.2,1378.8 -847.2,1369.2 -851.1,1378.8 -853,1378.8 -856.9,1369.2 -856.9,1378.8
|
||||
-859.4,1378.8 -859.4,1363.4 -857,1363.4 -852,1374.8 -847.1,1363.4 -844.7,1363.4 "/>
|
||||
<path class="st0" d="M-831.6,1368.5v10.3h-2.4v-1.4c-0.6,1-2,1.7-3.2,1.7c-3.1,0-5.3-2.5-5.3-5.4c0-2.9,2.1-5.4,5.3-5.4
|
||||
c1.2,0,2.5,0.6,3.2,1.7v-1.4H-831.6z M-834,1373.6c0-1.8-1.2-3.3-3-3.3c-1.8,0-3,1.5-3,3.3s1.2,3.3,3,3.3
|
||||
C-835.1,1376.9-834,1375.4-834,1373.6"/>
|
||||
<path class="st0" d="M-820.8,1377.4c-0.6,1-2,1.7-3.2,1.7c-3.1,0-5.3-2.5-5.3-5.4c0-2.9,2.1-5.4,5.3-5.4c1.2,0,2.5,0.6,3.2,1.7v-6.5
|
||||
h2.4v15.4h-2.4V1377.4z M-820.8,1373.6c0-1.8-1.2-3.3-3-3.3c-1.8,0-3,1.5-3,3.3s1.2,3.3,3,3.3
|
||||
C-822,1376.9-820.8,1375.4-820.8,1373.6"/>
|
||||
<path class="st0" d="M-813.7,1374.4c0.1,1.5,0.7,2.7,2.8,2.7c1.4,0,1.9-0.6,2.2-1.3h2.4c-0.3,1.8-2,3.3-4.6,3.3
|
||||
c-3.5,0-5.1-2.4-5.1-5.4c0-3.1,1.8-5.4,5.1-5.4c2.7,0,4.8,2,4.8,4.6c0,0.4,0,0.8-0.1,1.5H-813.7z M-813.7,1372.6h5.1
|
||||
c0-1.6-1-2.5-2.5-2.5C-812.5,1370.1-813.7,1370.9-813.7,1372.6"/>
|
||||
<polygon class="st0" points="-791.7,1371.1 -794.4,1378.8 -796,1378.8 -800,1368.5 -797.4,1368.5 -795.2,1375 -792.8,1368.5
|
||||
-790.5,1368.5 -788.2,1375 -786,1368.5 -783.4,1368.5 -787.4,1378.8 -789,1378.8 "/>
|
||||
<path class="st0" d="M-780.7,1363c1,0,1.8,0.8,1.8,1.8c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8
|
||||
C-782.5,1363.8-781.7,1363-780.7,1363 M-781.9,1368.5h2.4v10.3h-2.4V1368.5z"/>
|
||||
<path class="st0" d="M-770.7,1378.8c-0.4,0.1-1.1,0.1-1.8,0.1c-1.2,0-3.3-0.2-3.3-3.6v-4.8h-1.7v-2h1.7v-3.1h2.4v3.1h2.4v2h-2.4v4.2
|
||||
c0,1.8,0.6,2,1.5,2c0.4,0,1,0,1.2-0.1V1378.8z"/>
|
||||
<path class="st0" d="M-768.8,1363.4h2.4v6.6c0.6-1.1,1.8-1.7,3.3-1.7c1.5,0,2.5,0.5,3.1,1.4c0.5,0.8,0.7,1.6,0.7,3v6.1h-2.4v-5.3
|
||||
c0-1.8-0.5-3.1-2.2-3.1c-1.7,0-2.5,1.3-2.5,3.1v5.3h-2.4V1363.4z"/>
|
||||
<path class="st0" d="M-851.9,1398.9c0,2.1-1.8,3.1-4.1,3.1c-2.2,0-4.2-1.3-4.2-3.7h2.3c0,1.3,0.8,1.8,2,1.8c0.9,0,1.7-0.3,1.7-1.2
|
||||
c0-1-1-1-3.2-1.9c-1.4-0.5-2.4-1.1-2.4-2.9c0-1.9,1.8-3,3.8-3c2.2,0,3.9,1.5,3.9,3.5h-2.3c0-1-0.5-1.6-1.7-1.6
|
||||
c-0.8,0-1.5,0.4-1.5,1.1c0,0.9,0.9,1,2.8,1.7C-853.3,1396.6-851.9,1397.1-851.9,1398.9"/>
|
||||
<path class="st0" d="M-850.3,1396.7c0-2.9,2.1-5.4,5.4-5.4c3.3,0,5.4,2.5,5.4,5.4c0,2.9-2.1,5.4-5.4,5.4
|
||||
C-848.2,1402.1-850.3,1399.6-850.3,1396.7 M-841.9,1396.7c0-1.8-1.2-3.3-3-3.3c-1.8,0-3,1.5-3,3.3s1.2,3.3,3,3.3
|
||||
C-843,1399.9-841.9,1398.4-841.9,1396.7"/>
|
||||
<path class="st0" d="M-824.2,1401.8v-5.7c0-1.6-0.6-2.7-1.9-2.7c-1.2,0-2.2,0.7-2.2,3.2v5.2h-2.4v-5.8c0-1.6-0.5-2.6-1.9-2.6
|
||||
c-1.2,0-2.2,0.7-2.2,3.2v5.2h-2.4v-10.3h2.4v1.4c0.4-0.9,1.5-1.7,2.9-1.7c1.9,0,2.8,0.8,3.2,1.8c0.5-1,1.7-1.8,3.1-1.8
|
||||
c3.1,0,3.7,2.1,3.7,4v6.5H-824.2z"/>
|
||||
<path class="st0" d="M-817.2,1397.4c0.1,1.5,0.7,2.7,2.8,2.7c1.4,0,1.9-0.6,2.2-1.3h2.4c-0.3,1.8-2,3.3-4.6,3.3
|
||||
c-3.5,0-5.1-2.4-5.1-5.4c0-3.1,1.8-5.4,5.1-5.4c2.7,0,4.8,2,4.8,4.6c0,0.4,0,0.8-0.1,1.5H-817.2z M-817.2,1395.6h5.1
|
||||
c0-1.6-1-2.5-2.5-2.5C-816.1,1393.1-817.2,1393.9-817.2,1395.6"/>
|
||||
<path class="st0" d="M-795.9,1388.5c-0.3-0.1-0.7-0.2-1.1-0.2c-0.7,0-1.6,0-1.6,1.6v1.5h2.7v2h-2.7v8.3h-2.4v-8.3h-2.1v-2h2.1v-1.3
|
||||
c0-3.9,2.5-4,3.7-4c0.6,0,1.1,0.1,1.4,0.3V1388.5z"/>
|
||||
<path class="st0" d="M-787.6,1393.8c-0.4-0.1-0.6-0.1-0.9-0.1c-2,0-3.2,1.2-3.2,3.7v4.4h-2.4v-10.3h2.4v1.7c0.5-0.9,1.8-1.8,3.2-1.8
|
||||
c0.4,0,0.7,0,0.9,0.2V1393.8z"/>
|
||||
<path class="st0" d="M-784.7,1386c1,0,1.8,0.8,1.8,1.8c0,1-0.8,1.8-1.8,1.8c-1,0-1.8-0.8-1.8-1.8
|
||||
C-786.5,1386.8-785.7,1386-784.7,1386 M-785.9,1391.5h2.4v10.3h-2.4V1391.5z"/>
|
||||
<path class="st0" d="M-778.7,1397.4c0.1,1.5,0.7,2.7,2.8,2.7c1.4,0,1.9-0.6,2.2-1.3h2.4c-0.3,1.8-2,3.3-4.6,3.3
|
||||
c-3.5,0-5.1-2.4-5.1-5.4c0-3.1,1.8-5.4,5.1-5.4c2.7,0,4.8,2,4.8,4.6c0,0.4,0,0.8-0.1,1.5H-778.7z M-778.7,1395.6h5.1
|
||||
c0-1.6-1-2.5-2.5-2.5C-777.6,1393.1-778.7,1393.9-778.7,1395.6"/>
|
||||
<path class="st0" d="M-768.8,1391.5h2.4v1.4c0.6-1.1,1.8-1.7,3.3-1.7c1.5,0,2.5,0.5,3.1,1.4c0.5,0.8,0.7,1.6,0.7,3v6.1h-2.4v-5.3
|
||||
c0-1.8-0.5-3.1-2.2-3.1c-1.7,0-2.5,1.3-2.5,3.1v5.3h-2.4V1391.5z"/>
|
||||
<path class="st0" d="M-748.8,1400.4c-0.6,1-2,1.7-3.2,1.7c-3.1,0-5.3-2.5-5.3-5.4c0-2.9,2.1-5.4,5.3-5.4c1.2,0,2.5,0.6,3.2,1.7v-6.5
|
||||
h2.4v15.4h-2.4V1400.4z M-748.8,1396.7c0-1.8-1.2-3.3-3-3.3c-1.8,0-3,1.5-3,3.3s1.2,3.3,3,3.3
|
||||
C-749.9,1399.9-748.8,1398.4-748.8,1396.7"/>
|
||||
<path class="st0" d="M-735.8,1398.9c0,2.1-1.8,3.1-4.1,3.1c-2.2,0-4.2-1.3-4.2-3.7h2.3c0,1.3,0.8,1.8,2,1.8c0.9,0,1.7-0.3,1.7-1.2
|
||||
c0-1-1-1-3.2-1.9c-1.4-0.5-2.4-1.1-2.4-2.9c0-1.9,1.8-3,3.9-3c2.2,0,3.9,1.5,3.9,3.5h-2.3c0-1-0.5-1.6-1.7-1.6
|
||||
c-0.8,0-1.5,0.4-1.5,1.1c0,0.9,0.9,1,2.8,1.7C-737.2,1396.6-735.8,1397.1-735.8,1398.9"/>
|
||||
<path class="st0" d="M-722,1388.5c-0.3-0.1-0.7-0.2-1.1-0.2c-0.7,0-1.6,0-1.6,1.6v1.5h2.7v2h-2.7v8.3h-2.4v-8.3h-2.1v-2h2.1v-1.3
|
||||
c0-3.9,2.5-4,3.7-4c0.6,0,1.1,0.1,1.4,0.3V1388.5z"/>
|
||||
<path class="st0" d="M-713.7,1393.8c-0.4-0.1-0.6-0.1-0.9-0.1c-2,0-3.2,1.2-3.2,3.7v4.4h-2.4v-10.3h2.4v1.7c0.5-0.9,1.8-1.8,3.2-1.8
|
||||
c0.4,0,0.7,0,0.9,0.2V1393.8z"/>
|
||||
<path class="st0" d="M-713.3,1396.7c0-2.9,2.1-5.4,5.4-5.4c3.3,0,5.4,2.5,5.4,5.4c0,2.9-2.1,5.4-5.4,5.4
|
||||
C-711.1,1402.1-713.3,1399.6-713.3,1396.7 M-704.8,1396.7c0-1.8-1.2-3.3-3-3.3c-1.8,0-3,1.5-3,3.3s1.2,3.3,3,3.3
|
||||
C-706,1399.9-704.8,1398.4-704.8,1396.7"/>
|
||||
<path class="st0" d="M-687.2,1401.8v-5.7c0-1.6-0.6-2.7-1.9-2.7c-1.2,0-2.2,0.7-2.2,3.2v5.2h-2.4v-5.8c0-1.6-0.5-2.6-1.9-2.6
|
||||
c-1.2,0-2.2,0.7-2.2,3.2v5.2h-2.4v-10.3h2.4v1.4c0.4-0.9,1.5-1.7,2.9-1.7c1.9,0,2.8,0.8,3.2,1.8c0.5-1,1.7-1.8,3.1-1.8
|
||||
c3.1,0,3.7,2.1,3.7,4v6.5H-687.2z"/>
|
||||
<path class="st0" d="M-791.7,1458.8c0,7.7-6,13.4-13.5,13.4c-7.4,0-13.5-5.7-13.5-13.4c0-7.8,6-13.4,13.5-13.4
|
||||
C-797.7,1445.4-791.7,1451-791.7,1458.8 M-797.6,1458.8c0-4.8-3.5-8.1-7.6-8.1c-4.1,0-7.6,3.3-7.6,8.1c0,4.8,3.5,8.1,7.6,8.1
|
||||
C-801.1,1466.9-797.6,1463.6-797.6,1458.8"/>
|
||||
<path class="st0" d="M-762.4,1458.8c0,7.7-6,13.4-13.5,13.4c-7.4,0-13.5-5.7-13.5-13.4c0-7.8,6-13.4,13.5-13.4
|
||||
C-768.5,1445.4-762.4,1451-762.4,1458.8 M-768.3,1458.8c0-4.8-3.5-8.1-7.6-8.1c-4.1,0-7.6,3.3-7.6,8.1c0,4.8,3.5,8.1,7.6,8.1
|
||||
C-771.8,1466.9-768.3,1463.6-768.3,1458.8"/>
|
||||
<path class="st0" d="M-734.4,1446.2v24c0,9.9-5.8,14-12.7,14c-6.5,0-10.4-4.4-11.9-7.9l5.1-2.1c0.9,2.2,3.1,4.8,6.8,4.8
|
||||
c4.4,0,7.2-2.7,7.2-7.9v-1.9h-0.2c-1.3,1.6-3.9,3-7.1,3c-6.7,0-12.8-5.8-12.8-13.4c0-7.6,6.1-13.5,12.8-13.5c3.2,0,5.7,1.4,7.1,3
|
||||
h0.2v-2.2H-734.4z M-739.6,1458.8c0-4.7-3.1-8.2-7.2-8.2c-4.1,0-7.5,3.5-7.5,8.2c0,4.7,3.4,8.1,7.5,8.1
|
||||
C-742.7,1466.9-739.6,1463.5-739.6,1458.8"/>
|
||||
<rect x="-730.5" y="1432" class="st0" width="5.9" height="39.4"/>
|
||||
<path class="st0" d="M-702.2,1463.2l4.6,3c-1.5,2.2-5,5.9-11.2,5.9c-7.6,0-13.1-5.9-13.1-13.4c0-8,5.6-13.4,12.5-13.4
|
||||
c7,0,10.4,5.5,11.5,8.5l0.6,1.5l-17.9,7.4c1.4,2.7,3.5,4.1,6.5,4.1C-705.8,1466.9-703.7,1465.4-702.2,1463.2 M-716.3,1458.4l12-5
|
||||
c-0.7-1.7-2.6-2.8-5-2.8C-712.2,1450.6-716.4,1453.2-716.3,1458.4"/>
|
||||
<path class="st0" d="M-839.9,1472.2c-11.5,0-21.1-9.3-21.1-20.8s9.6-20.8,21.1-20.8c6.3,0,10.9,2.5,14.3,5.7l-4,4
|
||||
c-2.4-2.3-5.7-4.1-10.3-4.1c-8.4,0-14.9,6.8-14.9,15.1c0,8.4,6.5,15.1,14.9,15.1c5.4,0,8.5-2.2,10.5-4.2c1.6-1.6,2.7-4,3.1-7.2
|
||||
h-13.6v-5.7h19.1c0.2,1,0.3,2.2,0.3,3.6c0,4.3-1.2,9.5-4.9,13.3C-829,1470.2-833.7,1472.2-839.9,1472.2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 7.4 KiB |
9
static/images/keyboard_icon.svg
Normal file
9
static/images/keyboard_icon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 131.7 119.8" enable-background="new 0 0 131.7 119.8" xml:space="preserve">
|
||||
<polyline fill="#FBBC05" points="52.6,87.1 52.6,61 46.3,61 46.3,25.5 31.8,25.5 31.8,87.1 "/>
|
||||
<polyline fill="#FBBC05" points="77.5,87.1 77.5,61 71.3,61 71.3,25.5 63,25.5 63,61 56.7,61 56.7,87.1 "/>
|
||||
<polyline fill="#FBBC05" points="102.5,87.1 102.5,61 102.5,25.5 96.2,25.5 87.9,25.5 87.9,61 81.7,61 81.7,87.1 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 777 B |
16
static/index.html
Normal file
16
static/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>AI DUET</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
|
||||
<meta name="description" content="AI DUET">
|
||||
<link href="https://fonts.googleapis.com/css?family=Quicksand:400,700" rel="stylesheet">
|
||||
<script type="text/javascript" src="build/Main.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
33
static/package.json
Normal file
33
static/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "ai-duet",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "build/Main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://creativelab-internal.googlesource.com/cl-mlx-piano/"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer-loader": "^3.2.0",
|
||||
"babel-core": "^6.17.0",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-preset-es2015": "^6.16.0",
|
||||
"buckets-js": "^1.98.1",
|
||||
"css-loader": "^0.23.1",
|
||||
"domready": "^1.0.8",
|
||||
"events": "^1.1.0",
|
||||
"file-loader": "^0.9.0",
|
||||
"jsmidgen": "^0.1.5",
|
||||
"midi-file-parser": "^1.0.0",
|
||||
"node-sass": "^3.4.2",
|
||||
"pepjs": "^0.4.2",
|
||||
"sass-loader": "^3.2.0",
|
||||
"startaudiocontext": "^1.2.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"three": "^0.81.2",
|
||||
"tone": "^0.8.0",
|
||||
"url-loader": "^0.5.7",
|
||||
"webmidi": "^2.0.0-beta.1",
|
||||
"webpack": "^1.12.14"
|
||||
}
|
||||
}
|
2
static/style/common.scss
Normal file
2
static/style/common.scss
Normal file
@ -0,0 +1,2 @@
|
||||
$blue : rgb(30, 183, 235);
|
||||
$orange : rgb(249, 187, 45);
|
29
static/style/glow.css
Normal file
29
static/style/glow.css
Normal file
@ -0,0 +1,29 @@
|
||||
@import 'common.scss';
|
||||
|
||||
#glow {
|
||||
|
||||
#ai, #user {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
transition: opacity 1.2s;
|
||||
|
||||
&.visible {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
$glowReach : 60%;
|
||||
|
||||
#ai {
|
||||
opacity: 0;
|
||||
background-image: radial-gradient(ellipse farthest-corner at 50% 0px, $orange 0%, black $glowReach);
|
||||
}
|
||||
|
||||
#user {
|
||||
opacity: 0;
|
||||
background-image: radial-gradient(ellipse farthest-corner at 50% 0px, $blue 0%, black $glowReach);
|
||||
}
|
||||
}
|
86
static/style/keyboard.css
Normal file
86
static/style/keyboard.css
Normal file
@ -0,0 +1,86 @@
|
||||
@import 'common.scss';
|
||||
|
||||
$borderWidth : 2px;
|
||||
$borderColor: rgb(215, 215, 215);
|
||||
$blackColor: black;
|
||||
$whiteColor: rgb(34, 34, 34);
|
||||
$blackHover : #aaa;
|
||||
$whiteHover : #aaa;
|
||||
|
||||
$blackKeyMargin : $borderWidth + 2px;
|
||||
|
||||
|
||||
#keyboard {
|
||||
position: absolute;
|
||||
width: calc(100% - 2 * #{$borderWidth});
|
||||
user-select: none;
|
||||
|
||||
.key {
|
||||
position: absolute;
|
||||
height: calc(100% - 2 * #{$borderWidth});
|
||||
width: 10px;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
|
||||
&.black {
|
||||
z-index: 1;
|
||||
height: 50%;
|
||||
|
||||
#fill {
|
||||
background-color: $blackColor;
|
||||
width: calc(100% - #{$blackKeyMargin * 2});
|
||||
left: $blackKeyMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&.white {
|
||||
z-index: 0;
|
||||
|
||||
#fill {
|
||||
background-color: $whiteColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.white, &.black {
|
||||
|
||||
&.hover #fill{
|
||||
border-color: white;
|
||||
background-color: $blackHover;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#fill {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: $borderWidth solid $borderColor;
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
|
||||
.highlight {
|
||||
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background-color: $blue;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.ai {
|
||||
background-color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
58
static/style/main.css
Normal file
58
static/style/main.css
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
$keyboardHeight : 100px;
|
||||
$bottomHeight : 30px;
|
||||
|
||||
body, #container{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
margin: 0px;
|
||||
background-color: black;
|
||||
|
||||
#keyboard {
|
||||
position: absolute;
|
||||
bottom: $bottomHeight;
|
||||
left: 0px;
|
||||
height: $keyboardHeight;
|
||||
}
|
||||
|
||||
#roll {
|
||||
width : 100%;
|
||||
height: calc(100% - #{$keyboardHeight + $bottomHeight});
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#glow {
|
||||
width : 100%;
|
||||
height: calc(100% - #{$keyboardHeight + $bottomHeight});
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#bottom {
|
||||
width: 100%;
|
||||
height: $bottomHeight;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
background-color: rgb(34, 34, 34);
|
||||
box-shadow: inset 0px 12px 30px -5px rgba(0,0,0,0.75);
|
||||
}
|
||||
}
|
||||
|
||||
#container {
|
||||
opacity: 0.4;
|
||||
filter: blur(4px);
|
||||
$transitionTime : 0.2s;
|
||||
transition: filter $transitionTime, opacity $transitionTime;
|
||||
|
||||
&.focus {
|
||||
filter: blur(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
11
static/style/piano.scss
Normal file
11
static/style/piano.scss
Normal file
@ -0,0 +1,11 @@
|
||||
@import "common.scss";
|
||||
|
||||
$keyboardWidth: 100%;
|
||||
|
||||
#Keyboard {
|
||||
width: $keyboardWidth;
|
||||
height: $keyboardHeight;
|
||||
position: absolute;
|
||||
bottom: $bottombarHeight;
|
||||
left: 0px;
|
||||
}
|
9
static/style/roll.scss
Normal file
9
static/style/roll.scss
Normal file
@ -0,0 +1,9 @@
|
||||
@import "common.scss";
|
||||
|
||||
#Roll {
|
||||
$bottomSize : $keyboardHeight + $bottombarHeight;
|
||||
width: 100%;
|
||||
height: calc(100% - #{$bottomSize});
|
||||
bottom: $bottomSize;
|
||||
left: 0px;
|
||||
}
|
222
static/style/splash.css
Normal file
222
static/style/splash.css
Normal file
@ -0,0 +1,222 @@
|
||||
@import 'common.scss';
|
||||
|
||||
$topMargin: 50px;
|
||||
|
||||
#splash {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 100;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&.disappear {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#learnMore {
|
||||
margin-top: $topMargin;
|
||||
font-size: 18px;
|
||||
display: block;
|
||||
// @mixin yellowLink;
|
||||
}
|
||||
|
||||
#titleContainer {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 80%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
min-width: 300px;
|
||||
|
||||
#title {
|
||||
line-height: 60px;
|
||||
font-size: 65px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#subTitle {
|
||||
margin-top: $topMargin;
|
||||
letter-spacing: 0.8px;
|
||||
line-height: 30px;
|
||||
font-size: 20px;
|
||||
width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
$loaderWidth: 200px;
|
||||
$loaderHeight: 60px;
|
||||
|
||||
#loader {
|
||||
position: relative;
|
||||
margin-top: $topMargin;
|
||||
background-color: black;
|
||||
border: 2px solid $orange;
|
||||
width: $loaderWidth;
|
||||
height: $loaderHeight;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
transition: transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#fillText:active {
|
||||
color: black!important;
|
||||
background-color: $orange;
|
||||
|
||||
#piano {
|
||||
filter: brightness(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#loaderText {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: black;
|
||||
background-color: $orange;
|
||||
}
|
||||
|
||||
#fill {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
overflow: hidden;
|
||||
background-color: black;
|
||||
|
||||
|
||||
#fillText {
|
||||
width: $loaderWidth;
|
||||
height: 100%;
|
||||
color: $orange;
|
||||
|
||||
$imgWidth: 40px;
|
||||
$margin : 52px;
|
||||
|
||||
* {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
#play {
|
||||
right: $margin;
|
||||
}
|
||||
|
||||
#piano {
|
||||
left: $margin;
|
||||
width: $imgWidth;
|
||||
height: 100%;
|
||||
background-image : url(../images/keyboard_icon.svg);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#loaderText, #fillText {
|
||||
line-height: $loaderHeight;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#buildWith {
|
||||
margin-top: $topMargin;
|
||||
}
|
||||
|
||||
$badgeWidth : 80px;
|
||||
$badgeHeight: 50px;
|
||||
$badgeMargin : 20px;
|
||||
$badegOpacity: 0.7;
|
||||
|
||||
#aiExperiments, #googleFriends {
|
||||
width: $badgeWidth;
|
||||
height: $badgeHeight;
|
||||
position: absolute;
|
||||
bottom: $badgeMargin;
|
||||
opacity: $badegOpacity;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
#aiExperiments {
|
||||
left: $badgeMargin;
|
||||
background-image: url(../images/badgeAI_master.svg);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
&:active {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#googleFriends {
|
||||
cursor: initial;
|
||||
left: $badgeWidth + $badgeMargin * 3;
|
||||
background-image: url(../images/badgeFriends_master.svg);
|
||||
}
|
||||
|
||||
#badgeBreak{
|
||||
$breakHeight: $badgeHeight * 0.8;
|
||||
height: $breakHeight;
|
||||
left: $badgeWidth + $badgeMargin * 1.8;
|
||||
background-color: white;
|
||||
opacity: $badegOpacity / 2;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
bottom: $badgeMargin + ($badgeHeight - $breakHeight) / 2;
|
||||
}
|
||||
|
||||
#privacyAndTerms {
|
||||
position: absolute;
|
||||
bottom: $badgeMargin;
|
||||
right: $badgeMargin;
|
||||
width: auto;
|
||||
font-weight: normal;
|
||||
|
||||
* {
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
display: inline;
|
||||
opacity: $badegOpacity;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
&:active {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
static/third_party/MidiConvert/LICENSE.md
vendored
Normal file
21
static/third_party/MidiConvert/LICENSE.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
[The MIT License](http://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright © 2016 Yotam Mann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
112
static/third_party/MidiConvert/README.md
vendored
Normal file
112
static/third_party/MidiConvert/README.md
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
## [DEMO](https://tonejs.github.io/MidiConvert/)
|
||||
|
||||
MidiConvert makes it straightforward to work with MIDI files in Javascript. It uses [midi-file-parser](https://github.com/NHQ/midi-file-parser) to decode MIDI files and [jsmidgen](https://github.com/dingram/jsmidgen) to encode MIDI files.
|
||||
|
||||
|
||||
```javascript
|
||||
//load a midi file
|
||||
MidiConvert.load("path/to/midi.mid", function(midi){
|
||||
console.log(midi)
|
||||
})
|
||||
```
|
||||
|
||||
### Format
|
||||
|
||||
The data parsed from the midi file looks like this:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// the transport and timing data
|
||||
header : {
|
||||
bpm : Number, // the tempo, e.g. 120
|
||||
timeSignature : [Number, Number], // the time signature, e.g. [4, 4],
|
||||
PPQ : Number // the Pulses Per Quarter of the midi file
|
||||
},
|
||||
// an array of midi tracks
|
||||
tracks : [
|
||||
{
|
||||
name : String, // the track name if one was given
|
||||
notes : [
|
||||
{
|
||||
midi : Number, // midi number, e.g. 60
|
||||
time : Number, // time in seconds
|
||||
note : String, // note name, e.g. "C4"
|
||||
velocity : Number, // normalized 0-1 velocity
|
||||
duration : String // duration between noteOn and noteOff
|
||||
}
|
||||
],
|
||||
//midi control changes
|
||||
controlChanges : {
|
||||
//if there are control changes in the midi file
|
||||
'91' : [
|
||||
{
|
||||
number : Number // the cc number
|
||||
time : Number, // time in seconds
|
||||
value : Number // normalized 0-1
|
||||
}
|
||||
],
|
||||
},
|
||||
instrument : String //the instrument if one is given
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Raw Midi Parsing
|
||||
|
||||
If you are using Node.js or have the raw binary string from the midi file, just use the `parse` method:
|
||||
|
||||
```javascript
|
||||
fs.readFile("test.mid", "binary", function(err, midiBlob){
|
||||
if (!err){
|
||||
var midi = MidiConvert.parse(midiBlob)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Encoding Midi
|
||||
|
||||
You can also create midi files from scratch of by modifying an existing file.
|
||||
|
||||
```javascript
|
||||
//create a new midi file
|
||||
var midi = MidiConvert.create()
|
||||
//add a track
|
||||
midi.track()
|
||||
//chain note events: note, time, duration
|
||||
.note(60, 0, 2)
|
||||
.note(63, 1, 2)
|
||||
.note(60, 2, 2)
|
||||
|
||||
//write the output
|
||||
fs.writeFileSync("output.mid", midi.encode(), "binary")
|
||||
```
|
||||
|
||||
### Tone.Part
|
||||
|
||||
The note data can be easily passed into [Tone.Part](http://tonejs.github.io/docs/#Part)
|
||||
|
||||
```javascript
|
||||
var synth = new Tone.PolySynth(8).toMaster()
|
||||
|
||||
MidiConvert.load("path/to/midi.mid", function(midi){
|
||||
|
||||
//make sure you set the tempo before you schedule the events
|
||||
Tone.Transport.bpm.value = midi.bpm
|
||||
|
||||
//pass in the note events from one of the tracks as the second argument to Tone.Part
|
||||
var midiPart = new Tone.Part(function(time, note){
|
||||
|
||||
//use the events to play the synth
|
||||
synth.triggerAttackRelease(note.name, note.duration, time, note.velocity)
|
||||
|
||||
}, midi.tracks[0].notes).start()
|
||||
|
||||
//start the transport to hear the events
|
||||
Tone.Transport.start()
|
||||
})
|
||||
```
|
||||
|
||||
### Acknowledgment
|
||||
|
||||
MidiConvert uses [midi-file-parser](https://github.com/NHQ/midi-file-parser) which is ported from [jasmid](https://github.com/gasman/jasmid) for decoding MIDI data and and [jsmidgen](https://github.com/dingram/jsmidgen) for encoding MIDI data.
|
54
static/third_party/MidiConvert/src/BinaryInsert.js
vendored
Normal file
54
static/third_party/MidiConvert/src/BinaryInsert.js
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Return the index of the element at or before the given time
|
||||
*/
|
||||
function findElement(array, time) {
|
||||
let beginning = 0
|
||||
const len = array.length
|
||||
let end = len
|
||||
if (len > 0 && array[len - 1].time <= time){
|
||||
return len - 1
|
||||
}
|
||||
while (beginning < end){
|
||||
// calculate the midpoint for roughly equal partition
|
||||
let midPoint = Math.floor(beginning + (end - beginning) / 2)
|
||||
const event = array[midPoint]
|
||||
const nextEvent = array[midPoint + 1]
|
||||
if (event.time === time){
|
||||
//choose the last one that has the same time
|
||||
for (let i = midPoint; i < array.length; i++){
|
||||
let testEvent = array[i]
|
||||
if (testEvent.time === time){
|
||||
midPoint = i
|
||||
}
|
||||
}
|
||||
return midPoint
|
||||
} else if (event.time < time && nextEvent.time > time){
|
||||
return midPoint
|
||||
} else if (event.time > time){
|
||||
//search lower
|
||||
end = midPoint
|
||||
} else if (event.time < time){
|
||||
//search upper
|
||||
beginning = midPoint + 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a binary search to insert the note
|
||||
* in the correct spot in the array
|
||||
* @param {Array} array
|
||||
* @param {Object} event
|
||||
* @param {Number=} offset
|
||||
*/
|
||||
function BinaryInsert(array, event){
|
||||
if (array.length){
|
||||
const index = findElement(array, event.time)
|
||||
array.splice(index + 1, 0, event)
|
||||
} else {
|
||||
array.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
export {BinaryInsert}
|
39
static/third_party/MidiConvert/src/Control.js
vendored
Normal file
39
static/third_party/MidiConvert/src/Control.js
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
const channelNames = {
|
||||
"1" : "modulationWheel",
|
||||
"2" : "breath",
|
||||
"4" : "footController",
|
||||
"5" : "portamentoTime",
|
||||
"7" : "volume",
|
||||
"8" : "balance",
|
||||
"10" : "pan",
|
||||
"64" : "sustain",
|
||||
"65" : "portamentoTime",
|
||||
"66" : "sostenuto",
|
||||
"67" : "softPedal",
|
||||
"68" : "legatoFootswitch",
|
||||
"84" : "portamentoContro"
|
||||
}
|
||||
|
||||
class Control{
|
||||
constructor(number, time, value){
|
||||
|
||||
this.number = number
|
||||
|
||||
this.time = time
|
||||
|
||||
this.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* The common name of the control change event
|
||||
* @type {String}
|
||||
* @readOnly
|
||||
*/
|
||||
get name(){
|
||||
if (channelNames.hasOwnProperty(this.number)){
|
||||
return channelNames[this.number]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {Control}
|
29
static/third_party/MidiConvert/src/Header.js
vendored
Normal file
29
static/third_party/MidiConvert/src/Header.js
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Parse tempo and time signature from the midiJson
|
||||
* @param {Object} midiJson
|
||||
* @return {Object}
|
||||
*/
|
||||
function parseHeader(midiJson){
|
||||
var ret = {
|
||||
PPQ : midiJson.header.ticksPerBeat
|
||||
}
|
||||
for (var i = 0; i < midiJson.tracks.length; i++){
|
||||
var track = midiJson.tracks[i]
|
||||
for (var j = 0; j < track.length; j++){
|
||||
var datum = track[j]
|
||||
if (datum.type === "meta"){
|
||||
if (datum.subtype === "timeSignature"){
|
||||
ret.timeSignature = [datum.numerator, datum.denominator]
|
||||
} else if (datum.subtype === "setTempo"){
|
||||
if (!ret.bpm){
|
||||
ret.bpm = 60000000 / datum.microsecondsPerBeat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.bpm = ret.bpm || 120
|
||||
return ret
|
||||
}
|
||||
|
||||
export {parseHeader}
|
44
static/third_party/MidiConvert/src/Merge.js
vendored
Normal file
44
static/third_party/MidiConvert/src/Merge.js
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
function hasMoreValues(arrays, positions){
|
||||
for (let i = 0; i < arrays.length; i++){
|
||||
let arr = arrays[i]
|
||||
let pos = positions[i]
|
||||
if (arr.length > pos){
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getLowestAtPosition(arrays, positions, encoders){
|
||||
let lowestIndex = 0
|
||||
let lowestValue = Infinity
|
||||
for (let i = 0; i < arrays.length; i++){
|
||||
let arr = arrays[i]
|
||||
let pos = positions[i]
|
||||
if (arr[pos] && (arr[pos].time < lowestValue)){
|
||||
lowestIndex = i
|
||||
lowestValue = arr[pos].time
|
||||
}
|
||||
}
|
||||
encoders[lowestIndex](arrays[lowestIndex][positions[lowestIndex]])
|
||||
// increment array
|
||||
positions[lowestIndex] += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine multiple arrays keeping the timing in order
|
||||
* The arguments should alternate between the array and the encoder callback
|
||||
* @param {...Array|Function} args
|
||||
*/
|
||||
function Merge(...args){
|
||||
const arrays = args.filter((v, i) => (i % 2) === 0)
|
||||
const positions = new Uint32Array(arrays.length)
|
||||
const encoders = args.filter((v, i) => (i % 2) === 1)
|
||||
const output = []
|
||||
while(hasMoreValues(arrays, positions)){
|
||||
getLowestAtPosition(arrays, positions, encoders)
|
||||
}
|
||||
}
|
||||
|
||||
export {Merge}
|
205
static/third_party/MidiConvert/src/Midi.js
vendored
Normal file
205
static/third_party/MidiConvert/src/Midi.js
vendored
Normal file
@ -0,0 +1,205 @@
|
||||
import Decoder from 'midi-file-parser'
|
||||
import Encoder from 'jsmidgen'
|
||||
import Util from './Util'
|
||||
import {Track} from './Track'
|
||||
import {parseHeader} from './Header'
|
||||
|
||||
/**
|
||||
* @class The Midi object. Contains tracks and the header info.
|
||||
*/
|
||||
class Midi {
|
||||
constructor(){
|
||||
|
||||
this.header = {
|
||||
//defaults
|
||||
bpm : 120,
|
||||
timeSignature : [4, 4],
|
||||
PPQ : 480
|
||||
}
|
||||
|
||||
this.tracks = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the given url and parse the midi at that url
|
||||
* @param {String} url
|
||||
* @param {*} data Anything that should be sent in the XHR
|
||||
* @param {String} method Either GET or POST
|
||||
* @return {Promise}
|
||||
*/
|
||||
load(url, data=null, method='GET'){
|
||||
return new Promise((success, fail) => {
|
||||
var request = new XMLHttpRequest()
|
||||
request.open(method, url)
|
||||
request.responseType = 'arraybuffer'
|
||||
// decode asynchronously
|
||||
request.addEventListener('load', () => {
|
||||
if (request.readyState === 4 && request.status === 200){
|
||||
success(this.decode(request.response))
|
||||
} else {
|
||||
fail(request.status)
|
||||
}
|
||||
})
|
||||
request.addEventListener('error', fail)
|
||||
request.send(data)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the bytes
|
||||
* @param {String|ArrayBuffer} bytes The midi file encoded as a string or ArrayBuffer
|
||||
* @return {Midi} this
|
||||
*/
|
||||
decode(bytes){
|
||||
|
||||
if (bytes instanceof ArrayBuffer){
|
||||
var byteArray = new Uint8Array(bytes)
|
||||
bytes = String.fromCharCode.apply(null, byteArray)
|
||||
}
|
||||
|
||||
const midiData = Decoder(bytes)
|
||||
|
||||
this.header = parseHeader(midiData)
|
||||
|
||||
//replace the previous tracks
|
||||
this.tracks = []
|
||||
|
||||
midiData.tracks.forEach((trackData) => {
|
||||
|
||||
const track = new Track()
|
||||
this.tracks.push(track)
|
||||
|
||||
let absoluteTime = 0
|
||||
trackData.forEach((event) => {
|
||||
absoluteTime += Util.ticksToSeconds(event.deltaTime, this.header)
|
||||
if (event.type === 'meta' && event.subtype === 'trackName'){
|
||||
track.name = Util.cleanName(event.text)
|
||||
} else if (event.subtype === 'noteOn'){
|
||||
track.noteOn(event.noteNumber, absoluteTime, event.velocity / 127)
|
||||
} else if (event.subtype === 'noteOff'){
|
||||
track.noteOff(event.noteNumber, absoluteTime)
|
||||
} else if (event.subtype === 'controller' && event.controllerType){
|
||||
track.cc(event.controllerType, absoluteTime, event.value / 127)
|
||||
} else if (event.type === 'meta' && event.subtype === 'instrumentName'){
|
||||
track.instrument = event.text
|
||||
}
|
||||
})
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the Midi object as a Buffer String
|
||||
* @returns {String}
|
||||
*/
|
||||
encode(){
|
||||
const output = new Encoder.File({
|
||||
ticks : this.header.PPQ
|
||||
})
|
||||
|
||||
this.tracks.forEach((track, i) => {
|
||||
const trackEncoder = output.addTrack()
|
||||
trackEncoder.setTempo(this.bpm)
|
||||
track.encode(trackEncoder, this.header)
|
||||
})
|
||||
return output.toBytes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the output encoding into an Array
|
||||
* @return {Array}
|
||||
*/
|
||||
toArray(){
|
||||
const encodedStr = this.encode()
|
||||
const buffer = new Array(encodedStr.length)
|
||||
for (let i = 0; i < encodedStr.length; i++){
|
||||
buffer[i] = encodedStr.charCodeAt(i)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new track.
|
||||
* @param {String=} name Optionally include the name of the track
|
||||
* @returns {Track}
|
||||
*/
|
||||
track(name){
|
||||
const track = new Track(name)
|
||||
this.tracks.push(track)
|
||||
return track
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a track either by it's name or track index
|
||||
* @param {Number|String} trackName
|
||||
* @return {Track}
|
||||
*/
|
||||
get(trackName){
|
||||
if (Util.isNumber(trackName)){
|
||||
return this.tracks[trackName]
|
||||
} else {
|
||||
return this.tracks.find((t) => t.name === trackName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice the midi file between the startTime and endTime. Returns a copy of the
|
||||
* midi
|
||||
* @param {Number} startTime
|
||||
* @param {Number} endTime
|
||||
* @returns {Midi} this
|
||||
*/
|
||||
slice(startTime=0, endTime=this.duration){
|
||||
const midi = new Midi()
|
||||
midi.header = this.header
|
||||
midi.tracks = this.tracks.map((t) => t.slice(startTime, endTime))
|
||||
return midi
|
||||
}
|
||||
|
||||
/**
|
||||
* the time of the first event
|
||||
* @type {Number}
|
||||
*/
|
||||
get startTime(){
|
||||
const startTimes = this.tracks.map((t) => t.startTime)
|
||||
return Math.min.apply(Math, startTimes)
|
||||
}
|
||||
|
||||
/**
|
||||
* The bpm of the midi file in beats per minute
|
||||
* @type {Number}
|
||||
*/
|
||||
get bpm(){
|
||||
return this.header.bpm
|
||||
}
|
||||
set bpm(bpm){
|
||||
const prevTempo = this.header.bpm
|
||||
this.header.bpm = bpm
|
||||
//adjust the timing of all the notes
|
||||
const ratio = prevTempo / bpm
|
||||
this.tracks.forEach((track) => track.scale(ratio))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The timeSignature of the midi file
|
||||
* @type {Array}
|
||||
*/
|
||||
get timeSignature(){
|
||||
return this.header.timeSignature
|
||||
}
|
||||
set timeSignature(timeSig){
|
||||
this.header.timeSignature = timeSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration is the end time of the longest track
|
||||
* @type {Number}
|
||||
*/
|
||||
get duration(){
|
||||
const durations = this.tracks.map((t) => t.duration)
|
||||
return Math.max.apply(Math, durations)
|
||||
}
|
||||
}
|
||||
|
||||
export {Midi}
|
69
static/third_party/MidiConvert/src/MidiConvert.js
vendored
Normal file
69
static/third_party/MidiConvert/src/MidiConvert.js
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
import {Midi} from './Midi'
|
||||
|
||||
const MidiConvert = {
|
||||
/**
|
||||
* Parse all the data from the Midi file into this format:
|
||||
* {
|
||||
* // the transport and timing data
|
||||
* header : {
|
||||
* bpm : Number, // tempo, e.g. 120
|
||||
* timeSignature : [Number, Number], // time signature, e.g. [4, 4],
|
||||
* PPQ : Number // PPQ of the midi file
|
||||
* },
|
||||
* // an array for each of the midi tracks
|
||||
* tracks : [
|
||||
* {
|
||||
* name : String, // the track name if one was given
|
||||
* notes : [
|
||||
* {
|
||||
* time : Number, // time in seconds
|
||||
* name : String, // note name, e.g. 'C4'
|
||||
* midi : Number, // midi number, e.g. 60
|
||||
* velocity : Number, // normalized velocity
|
||||
* duration : Number // duration between noteOn and noteOff
|
||||
* }
|
||||
* ],
|
||||
* controlChanges : { //all of the control changes
|
||||
* 64 : [ //array for each cc value
|
||||
* {
|
||||
* number : Number, //the cc number
|
||||
* time : Number, //the time of the event in seconds
|
||||
* name : String, // if the cc value has a common name (e.g. 'sustain')
|
||||
* value : Number, //the normalized value
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* @param {Binary String} fileBlob The output from fs.readFile or FileReader
|
||||
* @returns {Object} All of the options parsed from the midi file.
|
||||
*/
|
||||
parse : function(fileBlob){
|
||||
return new Midi().decode(fileBlob)
|
||||
},
|
||||
/**
|
||||
* Load and parse a midi file. See `parse` for what the results look like.
|
||||
* @param {String} url
|
||||
* @param {Function=} callback
|
||||
* @returns {Promise} A promise which is invoked with the returned Midi object
|
||||
*/
|
||||
load : function(url, callback){
|
||||
const promise = new Midi().load(url)
|
||||
if (callback){
|
||||
promise.then(callback)
|
||||
}
|
||||
return promise
|
||||
},
|
||||
/**
|
||||
* Create an empty midi file
|
||||
* @return {Midi}
|
||||
*/
|
||||
create : function(){
|
||||
return new Midi()
|
||||
}
|
||||
}
|
||||
|
||||
export default MidiConvert
|
||||
|
||||
module.exports = MidiConvert
|
100
static/third_party/MidiConvert/src/Note.js
vendored
Normal file
100
static/third_party/MidiConvert/src/Note.js
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
import Util from './Util'
|
||||
|
||||
class Note{
|
||||
constructor(midi, time, duration=0, velocity=1){
|
||||
|
||||
/**
|
||||
* The MIDI note number
|
||||
* @type {Number}
|
||||
*/
|
||||
this.midi;
|
||||
|
||||
if (Util.isNumber(midi)){
|
||||
this.midi = midi
|
||||
} else if (Util.isPitch(midi)){
|
||||
this.name = midi
|
||||
} else {
|
||||
throw new Error('the midi value must either be in Pitch Notation (e.g. C#4) or a midi value')
|
||||
}
|
||||
|
||||
/**
|
||||
* The note on time in seconds
|
||||
* @type {Number}
|
||||
*/
|
||||
this.time = time
|
||||
|
||||
/**
|
||||
* The duration in seconds
|
||||
* @type {Number}
|
||||
*/
|
||||
this.duration = duration
|
||||
|
||||
/**
|
||||
* The velocity 0-1
|
||||
* @type {Number}
|
||||
*/
|
||||
this.velocity = velocity
|
||||
}
|
||||
|
||||
/**
|
||||
* If the note is the same as the given note
|
||||
* @param {String|Number} note
|
||||
* @return {Boolean}
|
||||
*/
|
||||
match(note){
|
||||
if (Util.isNumber(note)){
|
||||
return this.midi === note
|
||||
} else if (Util.isPitch(note)){
|
||||
return this.name.toLowerCase() === note.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The note in Scientific Pitch Notation
|
||||
* @type {String}
|
||||
*/
|
||||
get name(){
|
||||
return Util.midiToPitch(this.midi)
|
||||
}
|
||||
set name(name){
|
||||
this.midi = Util.pitchToMidi(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for time
|
||||
* @type {Number}
|
||||
*/
|
||||
get noteOn(){
|
||||
return this.time
|
||||
}
|
||||
set noteOn(t){
|
||||
this.time = t
|
||||
}
|
||||
|
||||
/**
|
||||
* The note off time
|
||||
* @type {Number}
|
||||
*/
|
||||
get noteOff(){
|
||||
return this.time + this.duration
|
||||
}
|
||||
set noteOff(time){
|
||||
this.duration = time - this.time
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the note to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON(){
|
||||
return {
|
||||
name : this.name,
|
||||
midi : this.midi,
|
||||
time : this.time,
|
||||
velocity : this.velocity,
|
||||
duration : this.duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {Note}
|
213
static/third_party/MidiConvert/src/Track.js
vendored
Normal file
213
static/third_party/MidiConvert/src/Track.js
vendored
Normal file
@ -0,0 +1,213 @@
|
||||
import {Note} from './Note'
|
||||
import {Control} from './Control'
|
||||
import {Merge} from './Merge'
|
||||
import {BinaryInsert} from './BinaryInsert'
|
||||
|
||||
class Track {
|
||||
constructor(name='', instrument=''){
|
||||
|
||||
/**
|
||||
* The name of the track
|
||||
* @type {String}
|
||||
*/
|
||||
this.name = name
|
||||
|
||||
/**
|
||||
* The note events
|
||||
* @type {Array}
|
||||
*/
|
||||
this.notes = []
|
||||
|
||||
/**
|
||||
* The control changes
|
||||
* @type {Object}
|
||||
*/
|
||||
this.controlChanges = {}
|
||||
|
||||
/**
|
||||
* The tracks insturment if one exists
|
||||
* @type {String}
|
||||
*/
|
||||
this.instrument = ''
|
||||
}
|
||||
|
||||
note(midi, time, duration=0, velocity=1){
|
||||
const note = new Note(midi, time, duration, velocity)
|
||||
BinaryInsert(this.notes, note)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note on event
|
||||
* @param {Number|String} midi The midi note as either a midi number or
|
||||
* Pitch Notation like ('C#4')
|
||||
* @param {Number} time The time in seconds
|
||||
* @param {Number} velocity The velocity value 0-1
|
||||
* @return {Track} this
|
||||
*/
|
||||
noteOn(midi, time, velocity=1){
|
||||
const note = new Note(midi, time, 0, velocity)
|
||||
BinaryInsert(this.notes, note)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note off event. Go through and find an unresolved
|
||||
* noteOn event with the same pitch.
|
||||
* @param {String|Number} midi The midi number or note name.
|
||||
* @param {Number} time The time of the event in seconds
|
||||
* @return {Track} this
|
||||
*/
|
||||
noteOff(midi, time){
|
||||
for (let i = 0; i < this.notes.length; i++){
|
||||
let note = this.notes[i]
|
||||
if (note.match(midi) && note.duration === 0){
|
||||
note.noteOff = time
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a CC event
|
||||
* @param {Number} num The CC number
|
||||
* @param {Number} time The time of the event in seconds
|
||||
* @param {Number} value The value of the CC
|
||||
* @return {Track} this
|
||||
*/
|
||||
cc(num, time, value){
|
||||
if (!this.controlChanges.hasOwnProperty(num)){
|
||||
this.controlChanges[num] = []
|
||||
}
|
||||
const cc = new Control(num, time, value)
|
||||
BinaryInsert(this.controlChanges[num], cc)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of all the note on events
|
||||
* @type {Array<Object>}
|
||||
* @readOnly
|
||||
*/
|
||||
get noteOns(){
|
||||
const noteOns = []
|
||||
this.notes.forEach((note) => {
|
||||
noteOns.push({
|
||||
time : note.noteOn,
|
||||
midi : note.midi,
|
||||
name : note.name,
|
||||
velocity : note.velocity
|
||||
})
|
||||
})
|
||||
return noteOns
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of all the noteOff events
|
||||
* @type {Array<Object>}
|
||||
* @readOnly
|
||||
*/
|
||||
get noteOffs(){
|
||||
const noteOffs = []
|
||||
this.notes.forEach((note) => {
|
||||
noteOffs.push({
|
||||
time : note.noteOff,
|
||||
midi : note.midi,
|
||||
name : note.name
|
||||
})
|
||||
})
|
||||
return noteOffs
|
||||
}
|
||||
|
||||
/**
|
||||
* The length in seconds of the track
|
||||
* @type {Number}
|
||||
*/
|
||||
get length() {
|
||||
return this.notes.length
|
||||
}
|
||||
|
||||
/**
|
||||
* The time of the first event in seconds
|
||||
* @type {Number}
|
||||
*/
|
||||
get startTime() {
|
||||
if (this.notes.length){
|
||||
let firstNote = this.notes[0]
|
||||
return firstNote.noteOn
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The time of the last event in seconds
|
||||
* @type {Number}
|
||||
*/
|
||||
get duration() {
|
||||
if (this.notes.length){
|
||||
let lastNote = this.notes[this.notes.length - 1]
|
||||
return lastNote.noteOff
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale the timing of all the events in the track
|
||||
* @param {Number} amount The amount to scale all the values
|
||||
*/
|
||||
scale(amount){
|
||||
this.notes.forEach((note) => {
|
||||
note.time *= amount
|
||||
note.duration *= amount
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice returns a new track with only events that occured between startTime and endTime.
|
||||
* Modifies this track.
|
||||
* @param {Number} startTime
|
||||
* @param {Number} endTime
|
||||
* @returns {Track}
|
||||
*/
|
||||
slice(startTime=0, endTime=this.duration){
|
||||
// get the index before the startTime
|
||||
const noteStartIndex = Math.max(this.notes.findIndex((note) => note.time >= startTime), 0)
|
||||
const noteEndIndex = this.notes.findIndex((note) => note.noteOff >= endTime) + 1
|
||||
const track = new Track(this.name)
|
||||
track.notes = this.notes.slice(noteStartIndex, noteEndIndex)
|
||||
//shift the start time
|
||||
track.notes.forEach((note) => note.time = note.time - startTime)
|
||||
return track
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the output to the stream
|
||||
*/
|
||||
encode(trackEncoder, header){
|
||||
|
||||
const ticksPerSecond = header.PPQ / (60 / header.bpm)
|
||||
let lastEventTime = 0
|
||||
|
||||
const CHANNEL = 0
|
||||
|
||||
function getDeltaTime(time){
|
||||
const ticks = Math.floor(ticksPerSecond * time)
|
||||
const delta = Math.max(ticks - lastEventTime, 0)
|
||||
lastEventTime = ticks
|
||||
return delta
|
||||
}
|
||||
|
||||
Merge(this.noteOns, (noteOn) => {
|
||||
trackEncoder.addNoteOn(CHANNEL, noteOn.name, getDeltaTime(noteOn.time), Math.floor(noteOn.velocity * 127))
|
||||
}, this.noteOffs, (noteOff) => {
|
||||
trackEncoder.addNoteOff(CHANNEL, noteOff.name, getDeltaTime(noteOff.time))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export {Track}
|
53
static/third_party/MidiConvert/src/Util.js
vendored
Normal file
53
static/third_party/MidiConvert/src/Util.js
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
function cleanName(str){
|
||||
//ableton adds some weird stuff to the track
|
||||
return str.replace(/\u0000/g, '')
|
||||
}
|
||||
|
||||
function ticksToSeconds(ticks, header){
|
||||
return (60 / header.bpm) * (ticks / header.PPQ);
|
||||
}
|
||||
|
||||
function isNumber(val){
|
||||
return typeof val === 'number'
|
||||
}
|
||||
|
||||
function isString(val){
|
||||
return typeof val === 'string'
|
||||
}
|
||||
|
||||
const isPitch = (function(){
|
||||
const regexp = /^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i
|
||||
return (val) => {
|
||||
return isString(val) && regexp.test(val)
|
||||
}
|
||||
}())
|
||||
|
||||
|
||||
function midiToPitch(midi){
|
||||
const scaleIndexToNote = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
||||
const octave = Math.floor(midi / 12) - 1;
|
||||
const note = midi % 12;
|
||||
return scaleIndexToNote[note] + octave;
|
||||
}
|
||||
|
||||
const pitchToMidi = (function(){
|
||||
const regexp = /^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i
|
||||
const noteToScaleIndex = {
|
||||
"cbb" : -2, "cb" : -1, "c" : 0, "c#" : 1, "cx" : 2,
|
||||
"dbb" : 0, "db" : 1, "d" : 2, "d#" : 3, "dx" : 4,
|
||||
"ebb" : 2, "eb" : 3, "e" : 4, "e#" : 5, "ex" : 6,
|
||||
"fbb" : 3, "fb" : 4, "f" : 5, "f#" : 6, "fx" : 7,
|
||||
"gbb" : 5, "gb" : 6, "g" : 7, "g#" : 8, "gx" : 9,
|
||||
"abb" : 7, "ab" : 8, "a" : 9, "a#" : 10, "ax" : 11,
|
||||
"bbb" : 9, "bb" : 10, "b" : 11, "b#" : 12, "bx" : 13,
|
||||
}
|
||||
return (note) => {
|
||||
const split = regexp.exec(note)
|
||||
const pitch = split[1]
|
||||
const octave = split[2]
|
||||
const index = noteToScaleIndex[pitch.toLowerCase()]
|
||||
return index + (parseInt(octave) + 1) * 12
|
||||
}
|
||||
}())
|
||||
|
||||
module.exports = {cleanName, ticksToSeconds, isString, isNumber, isPitch, midiToPitch, pitchToMidi}
|
21
static/third_party/Piano/LICENSE.md
vendored
Normal file
21
static/third_party/Piano/LICENSE.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
[The MIT License](http://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright © 2016 Yotam Mann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
3
static/third_party/Piano/README.md
vendored
Normal file
3
static/third_party/Piano/README.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
A [Multisampled](https://en.wikipedia.org/wiki/Sample-based_synthesis#Multisampling) Piano at 5 velocity levels across 88 keys (sampled every third note) of a Yamaha C5. The sounds are from [Salamander Grand Piano](https://archive.org/details/SalamanderGrandPianoV3).
|
||||
|
||||
See [Main.js](https://github.com/tambien/Piano/blob/master/Main.js) for an example of how to use the API with either a MIDI file or MIDI keyboard.
|
41
static/third_party/Piano/src/Harmonics.js
vendored
Normal file
41
static/third_party/Piano/src/Harmonics.js
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
import Salamander from './Salamander'
|
||||
import PianoBase from './PianoBase'
|
||||
import {noteToMidi, createSource, midiToFrequencyRatio} from './Util'
|
||||
import Buffers from 'Tone/core/Buffers'
|
||||
|
||||
// the harmonics notes that Salamander has
|
||||
const harmonics = [21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87]
|
||||
|
||||
export default class Harmonics extends PianoBase {
|
||||
|
||||
constructor(range=[21, 108]){
|
||||
super()
|
||||
|
||||
const lowerIndex = harmonics.findIndex((note) => note >= range[0])
|
||||
let upperIndex = harmonics.findIndex((note) => note >= range[1])
|
||||
upperIndex = upperIndex === -1 ? upperIndex = harmonics.length : upperIndex
|
||||
|
||||
const notes = harmonics.slice(lowerIndex, upperIndex)
|
||||
|
||||
this._buffers = {}
|
||||
|
||||
for (let n of notes){
|
||||
this._buffers[n] = Salamander.getHarmonicsUrl(n)
|
||||
}
|
||||
}
|
||||
|
||||
start(note, gain, time){
|
||||
let [midi, ratio] = midiToFrequencyRatio(note)
|
||||
if (this._buffers.has(midi)){
|
||||
const source = createSource(this._buffers.get(midi)).connect(this.output)
|
||||
source.playbackRate.value = ratio
|
||||
source.start(time, 0, undefined, gain, 0)
|
||||
}
|
||||
}
|
||||
|
||||
load(baseUrl){
|
||||
return new Promise((success, fail) => {
|
||||
this._buffers = new Buffers(this._buffers, success, baseUrl)
|
||||
})
|
||||
}
|
||||
}
|
124
static/third_party/Piano/src/Note.js
vendored
Normal file
124
static/third_party/Piano/src/Note.js
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import Salamander from './Salamander'
|
||||
import PianoBase from './PianoBase'
|
||||
import {noteToMidi, createSource, midiToFrequencyRatio} from './Util'
|
||||
import Buffers from 'Tone/core/Buffers'
|
||||
|
||||
/**
|
||||
* Internal class
|
||||
*/
|
||||
class Note extends Tone{
|
||||
constructor(time, source, velocity, gain){
|
||||
super()
|
||||
//round the velocity
|
||||
this._velocity = velocity
|
||||
this._startTime = time
|
||||
|
||||
this.output = source
|
||||
this.output.start(time, 0, undefined, gain, 0)
|
||||
}
|
||||
|
||||
stop(time){
|
||||
if (this.output.buffer){
|
||||
|
||||
// return the amplitude of the damper playback
|
||||
let progress = (time - this._startTime) / this.output.buffer.duration
|
||||
progress = (1 - progress) * this._velocity
|
||||
// stop the buffer
|
||||
this.output.stop(time, 0.2)
|
||||
|
||||
return Math.pow(progress, 0.5)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps velocity depths to Salamander velocities
|
||||
*/
|
||||
const velocitiesMap = {
|
||||
1 : [8],
|
||||
2 : [6, 12],
|
||||
3 : [1, 8, 15],
|
||||
4 : [1, 5, 10, 15],
|
||||
5 : [1, 4, 8, 12, 16],
|
||||
6 : [1, 3, 7, 10, 13, 16],
|
||||
7 : [1, 3, 6, 9, 11, 13, 16],
|
||||
8 : [1, 3, 5, 7, 9, 11, 13, 15],
|
||||
9 : [1, 3, 5, 7, 9, 11, 13, 15, 16],
|
||||
10 : [1, 2, 3, 5, 7, 9, 11, 13, 15, 16],
|
||||
11 : [1, 2, 3, 5, 7, 9, 11, 13, 14, 15, 16],
|
||||
12 : [1, 2, 3, 4, 5, 7, 9, 11, 13, 14, 15, 16],
|
||||
13 : [1, 2, 3, 4, 5, 7, 9, 11, 12, 13, 14, 15, 16],
|
||||
14 : [1, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15, 16],
|
||||
15 : [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
16 : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
}
|
||||
|
||||
const notes = [21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108]
|
||||
|
||||
/**
|
||||
* Manages all of the hammered string sounds
|
||||
*/
|
||||
export default class Strings extends PianoBase {
|
||||
|
||||
constructor(range=[21, 108], velocities=1){
|
||||
super()
|
||||
|
||||
const lowerIndex = notes.findIndex((note) => note >= range[0])
|
||||
let upperIndex = notes.findIndex((note) => note >= range[1])
|
||||
upperIndex = upperIndex === -1 ? upperIndex = notes.length : upperIndex + 1
|
||||
|
||||
const slicedNotes = notes.slice(lowerIndex, upperIndex)
|
||||
|
||||
this._buffers = velocitiesMap[velocities].slice()
|
||||
|
||||
this._buffers.forEach((vel, i) => {
|
||||
this._buffers[i] = {}
|
||||
slicedNotes.forEach((note) => {
|
||||
this._buffers[i][note] = Salamander.getNotesUrl(note, vel)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_hasNote(note, velocity){
|
||||
return this._buffers.hasOwnProperty(velocity) && this._buffers[velocity].has(note)
|
||||
}
|
||||
|
||||
_getNote(note, velocity){
|
||||
return this._buffers[velocity].get(note)
|
||||
}
|
||||
|
||||
start(note, velocity, time){
|
||||
|
||||
let velPos = velocity * (this._buffers.length - 1)
|
||||
let roundedVel = Math.round(velPos)
|
||||
let diff = roundedVel - velPos
|
||||
let gain = 1 - diff * 0.5
|
||||
|
||||
let [midi, ratio] = midiToFrequencyRatio(note)
|
||||
|
||||
if (this._hasNote(midi, roundedVel)){
|
||||
let source = createSource(this._getNote(midi, roundedVel))
|
||||
source.playbackRate.value = ratio
|
||||
|
||||
let retNote = new Note(time, source, velocity, gain).connect(this.output)
|
||||
|
||||
return retNote
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
load(baseUrl){
|
||||
const promises = []
|
||||
this._buffers.forEach((obj, i) => {
|
||||
let prom = new Promise((success) => {
|
||||
this._buffers[i] = new Buffers(obj, success, baseUrl)
|
||||
})
|
||||
promises.push(prom)
|
||||
})
|
||||
return Promise.all(promises)
|
||||
}
|
||||
}
|
64
static/third_party/Piano/src/Pedal.js
vendored
Normal file
64
static/third_party/Piano/src/Pedal.js
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
import PianoBase from './PianoBase'
|
||||
import Salamander from './Salamander'
|
||||
import {createSource} from './Util'
|
||||
import Buffers from 'Tone/core/Buffers'
|
||||
|
||||
export default class Pedal extends PianoBase {
|
||||
constructor(load=true){
|
||||
super()
|
||||
|
||||
this._downTime = Infinity
|
||||
|
||||
this._currentSound = null
|
||||
|
||||
this._buffers = null
|
||||
|
||||
this._loadPedalSounds = load
|
||||
}
|
||||
|
||||
load(baseUrl){
|
||||
if (this._loadPedalSounds){
|
||||
return new Promise((success) => {
|
||||
this._buffers = new Buffers({
|
||||
up : 'pedalU1.mp3',
|
||||
down : 'pedalD1.mp3'
|
||||
}, success, baseUrl)
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Squash the current playing sound
|
||||
*/
|
||||
_squash(time){
|
||||
if (this._currentSound){
|
||||
this._currentSound.stop(time, 0.1)
|
||||
}
|
||||
this._currentSound = null
|
||||
}
|
||||
|
||||
_playSample(time, dir){
|
||||
if (this._loadPedalSounds){
|
||||
this._currentSound = createSource(this._buffers.get(dir))
|
||||
this._currentSound.connect(this.output).start(time, 0, undefined, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
down(time){
|
||||
this._squash(time)
|
||||
this._downTime = time
|
||||
this._playSample(time, 'down')
|
||||
}
|
||||
|
||||
up(time){
|
||||
this._squash(time)
|
||||
this._downTime = Infinity
|
||||
this._playSample(time, 'up')
|
||||
}
|
||||
|
||||
isDown(time){
|
||||
return time > this._downTime
|
||||
}
|
||||
}
|
189
static/third_party/Piano/src/Piano.js
vendored
Normal file
189
static/third_party/Piano/src/Piano.js
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
import Gain from 'Tone/core/Gain'
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import Frequency from 'Tone/type/Frequency'
|
||||
import Pedal from './Pedal'
|
||||
import Note from './Note'
|
||||
import Harmonics from './Harmonics'
|
||||
import Release from './Release'
|
||||
import Salamander from './Salamander'
|
||||
|
||||
/**
|
||||
* @class Multisampled Grand Piano using [Salamander Piano Samples](https://archive.org/details/SalamanderGrandPianoV3)
|
||||
* @extends {Tone}
|
||||
*/
|
||||
export default class Piano extends Tone{
|
||||
|
||||
constructor(range=[21, 108], velocities=1, release=true){
|
||||
|
||||
super(0, 1)
|
||||
|
||||
this._loaded = false
|
||||
|
||||
this._heldNotes = new Map()
|
||||
|
||||
this._sustainedNotes = new Map()
|
||||
|
||||
this._notes = new Note(range, velocities).connect(this.output)
|
||||
|
||||
this._pedal = new Pedal(release).connect(this.output)
|
||||
|
||||
if (release){
|
||||
this._harmonics = new Harmonics(range).connect(this.output)
|
||||
|
||||
this._release = new Release(range).connect(this.output)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all the samples
|
||||
* @param {String} baseUrl The url for the Salamander base folder
|
||||
* @return {Promise}
|
||||
*/
|
||||
load(url){
|
||||
const promises = [this._notes.load(url), this._pedal.load(url)]
|
||||
if (this._harmonics){
|
||||
promises.push(this._harmonics.load(url))
|
||||
}
|
||||
if (this._release){
|
||||
promises.push(this._release.load(url))
|
||||
}
|
||||
return Promise.all(promises).then(() => {
|
||||
this._loaded = true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the pedal down at the given time. Causes subsequent
|
||||
* notes and currently held notes to sustain.
|
||||
* @param {Time} time The time the pedal should go down
|
||||
* @returns {Piano} this
|
||||
*/
|
||||
pedalDown(time){
|
||||
if (this._loaded){
|
||||
time = this.toSeconds(time)
|
||||
if (!this._pedal.isDown(time)){
|
||||
this._pedal.down(time)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the pedal up. Dampens sustained notes
|
||||
* @param {Time} time The time the pedal should go up
|
||||
* @returns {Piano} this
|
||||
*/
|
||||
pedalUp(time){
|
||||
if (this._loaded){
|
||||
time = this.toSeconds(time)
|
||||
if (this._pedal.isDown(time)){
|
||||
this._pedal.up(time)
|
||||
// dampen each of the notes
|
||||
this._sustainedNotes.forEach((notes) => {
|
||||
notes.forEach((note) => {
|
||||
note.stop(time)
|
||||
})
|
||||
})
|
||||
this._sustainedNotes.clear()
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a note.
|
||||
* @param {String|Number} note The note to play
|
||||
* @param {Number} velocity The velocity to play the note
|
||||
* @param {Time} time The time of the event
|
||||
* @return {Piano} this
|
||||
*/
|
||||
keyDown(note, velocity=0.8, time=Tone.now()){
|
||||
if (this._loaded){
|
||||
time = this.toSeconds(time)
|
||||
|
||||
if (this.isString(note)){
|
||||
note = Math.round(Frequency(note).toMidi())
|
||||
}
|
||||
|
||||
if (!this._heldNotes.has(note)){
|
||||
let key = this._notes.start(note, velocity, time)
|
||||
if (key){
|
||||
this._heldNotes.set(note, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a held note.
|
||||
* @param {String|Number} note The note to stop
|
||||
* @param {Time} time The time of the event
|
||||
* @return {Piano} this
|
||||
*/
|
||||
keyUp(note, time=Tone.now()){
|
||||
if (this._loaded){
|
||||
time = this.toSeconds(time)
|
||||
|
||||
if (this.isString(note)){
|
||||
note = Math.round(Frequency(note).toMidi())
|
||||
}
|
||||
|
||||
if (this._heldNotes.has(note)){
|
||||
|
||||
let key = this._heldNotes.get(note)
|
||||
this._heldNotes.delete(note)
|
||||
|
||||
if (this._release){
|
||||
this._release.start(note, time)
|
||||
}
|
||||
|
||||
if (this._pedal.isDown(time)){
|
||||
let notes = []
|
||||
if (this._sustainedNotes.has(note)){
|
||||
notes = this._sustainedNotes.get(note)
|
||||
}
|
||||
notes.push(key)
|
||||
this._sustainedNotes.set(note, notes)
|
||||
} else {
|
||||
let dampenGain = key.stop(time)
|
||||
if (this._harmonics){
|
||||
this._harmonics.start(note, dampenGain, time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the volumes of each of the components
|
||||
* @param {String} param
|
||||
* @param {Decibels} vol
|
||||
* @return {Piano} this
|
||||
* @example
|
||||
* //either as an string
|
||||
* piano.setVolume('release', -10)
|
||||
*/
|
||||
setVolume(param, vol){
|
||||
switch(param){
|
||||
case 'note':
|
||||
this._notes.volume = vol
|
||||
break
|
||||
case 'pedal':
|
||||
this._pedal.volume = vol
|
||||
break
|
||||
case 'release':
|
||||
if (this._release){
|
||||
this._release.volume = vol
|
||||
}
|
||||
break
|
||||
case 'harmonics':
|
||||
if (this._harmonics){
|
||||
this._harmonics.volume = vol
|
||||
}
|
||||
break
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
16
static/third_party/Piano/src/PianoBase.js
vendored
Normal file
16
static/third_party/Piano/src/PianoBase.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import Master from 'Tone/core/Master'
|
||||
|
||||
export default class PianoBase extends Tone {
|
||||
constructor(vol=0){
|
||||
super(0, 1)
|
||||
|
||||
this.volume = vol
|
||||
}
|
||||
get volume(){
|
||||
return this.gainToDb(this.output.gain.value)
|
||||
}
|
||||
set volume(vol){
|
||||
this.output.gain.value = this.dbToGain(vol)
|
||||
}
|
||||
}
|
29
static/third_party/Piano/src/Release.js
vendored
Normal file
29
static/third_party/Piano/src/Release.js
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
import Salamander from './Salamander'
|
||||
import PianoBase from './PianoBase'
|
||||
import {createSource} from './Util'
|
||||
import Buffers from 'Tone/core/Buffers'
|
||||
|
||||
export default class Release extends PianoBase {
|
||||
|
||||
constructor(range){
|
||||
super()
|
||||
|
||||
this._buffers = {}
|
||||
for (let i = range[0]; i <= range[1]; i++){
|
||||
this._buffers[i] = Salamander.getReleasesUrl(i)
|
||||
}
|
||||
}
|
||||
|
||||
load(baseUrl){
|
||||
return new Promise((success) => {
|
||||
this._buffers = new Buffers(this._buffers, success, baseUrl)
|
||||
})
|
||||
}
|
||||
|
||||
start(note, time){
|
||||
if (this._buffers.has(note)){
|
||||
let source = createSource(this._buffers.get(note)).connect(this.output)
|
||||
source.start(time, 0, undefined, 0.01, 0)
|
||||
}
|
||||
}
|
||||
}
|
16
static/third_party/Piano/src/Salamander.js
vendored
Normal file
16
static/third_party/Piano/src/Salamander.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
import {noteToMidi, midiToNote} from './Util'
|
||||
|
||||
export default {
|
||||
|
||||
getReleasesUrl(midi){
|
||||
return `rel${midi - 20}.mp3`
|
||||
},
|
||||
|
||||
getHarmonicsUrl(midi){
|
||||
return `harmL${encodeURIComponent(midiToNote(midi))}.mp3`
|
||||
},
|
||||
|
||||
getNotesUrl(midi, vel){
|
||||
return `${encodeURIComponent(midiToNote(midi))}.mp3`
|
||||
}
|
||||
}
|
28
static/third_party/Piano/src/Util.js
vendored
Normal file
28
static/third_party/Piano/src/Util.js
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
import Tone from 'Tone/core/Tone'
|
||||
import Frequency from 'Tone/type/Frequency'
|
||||
import BufferSource from 'Tone/source/BufferSource'
|
||||
|
||||
function noteToMidi(note){
|
||||
return Frequency(note).toMidi()
|
||||
}
|
||||
|
||||
function midiToNote(midi){
|
||||
return Frequency(midi, 'midi').toNote().replace('#', 's')
|
||||
}
|
||||
|
||||
function midiToFrequencyRatio(midi){
|
||||
let mod = midi % 3
|
||||
if (mod === 1){
|
||||
return [midi - 1, Tone.prototype.intervalToFrequencyRatio(1)]
|
||||
} else if (mod === 2){
|
||||
return [midi + 1, Tone.prototype.intervalToFrequencyRatio(-1)]
|
||||
} else {
|
||||
return [midi, 1]
|
||||
}
|
||||
}
|
||||
|
||||
function createSource(buffer){
|
||||
return new BufferSource(buffer)
|
||||
}
|
||||
|
||||
export {midiToNote, noteToMidi, createSource, midiToFrequencyRatio}
|
557
static/third_party/audiokeys.js
vendored
Normal file
557
static/third_party/audiokeys.js
vendored
Normal file
File diff suppressed because one or more lines are too long
67
static/webpack.config.js
Normal file
67
static/webpack.config.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var webpack = require('webpack');
|
||||
|
||||
var PROD = process.argv.indexOf('-p') !== -1
|
||||
|
||||
module.exports = {
|
||||
'context': __dirname,
|
||||
entry: {
|
||||
'Main': 'app/Main',
|
||||
},
|
||||
output: {
|
||||
filename: './build/[name].js',
|
||||
chunkFilename: './build/[id].js',
|
||||
sourceMapFilename : '[file].map',
|
||||
},
|
||||
resolve: {
|
||||
root: __dirname,
|
||||
modulesDirectories : ['node_modules', 'app', 'third_party', 'node_modules/tone', 'style'],
|
||||
},
|
||||
plugins: PROD ? [
|
||||
new webpack.optimize.UglifyJsPlugin({minimize: true})
|
||||
] : [],
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules)/,
|
||||
loader: 'babel', // 'babel-loader' is also a valid name to reference
|
||||
query: {
|
||||
presets: ['es2015']
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: 'style!css!autoprefixer!sass'
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|gif|svg)$/,
|
||||
loader: 'url-loader',
|
||||
},
|
||||
{
|
||||
test : /\.(ttf|eot|woff(2)?)(\?[a-z0-9]+)?$/,
|
||||
loader : 'file-loader?name=images/font/[hash].[ext]'
|
||||
}
|
||||
]
|
||||
},
|
||||
devtool: PROD ? '' : '#eval-source-map'
|
||||
};
|
Loading…
Reference in New Issue
Block a user