static files

This commit is contained in:
Yotam Mann 2016-11-11 13:52:19 -05:00
commit 1eda91a7f2
76 changed files with 3727 additions and 0 deletions

68
static/app/Main.js Normal file
View 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
View 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}

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

View 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)}%`
})
}
}

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

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

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

View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
$blue : rgb(30, 183, 235);
$orange : rgb(249, 187, 45);

29
static/style/glow.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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;
}
}
}
}
}

View 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
View 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.

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

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

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

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

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

View 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

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

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

View 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
View 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
View 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.

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

View 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
View 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)
}
}
}

View 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
View 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

File diff suppressed because one or more lines are too long

67
static/webpack.config.js Normal file
View 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'
};