updating sample player, does not rely on Piano.js and using FluidR3 soundfont for AI sound

This commit is contained in:
Yotam Mann 2017-01-13 16:44:27 -05:00
parent 7249b3eac7
commit a0d4af1092
44 changed files with 155 additions and 531 deletions

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,5 @@
- Fluid-Soundfont
- Generated from [FluidR3_GM.sf2](http://www.musescore.org/download/fluid-soundfont.tar.gz) (141 MB uncompressed)
- Released under [Creative Commons Attribution 3.0 license](http://creativecommons.org/licenses/by/3.0/us/)
- Instrument names as .json file [here](http://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/names.json)
- URL prefix to fetch files: http://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/

View File

@ -0,0 +1,88 @@
/**
* 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 Frequency from 'Tone/type/Frequency'
import Buffers from 'Tone/core/Buffers'
import MultiPlayer from 'Tone/source/MultiPlayer'
import Tone from 'Tone/core/Tone'
import AudioBuffer from 'Tone/core/Buffer'
class Sampler{
constructor(baseUrl='', range=[21, 108]){
//all the notes of the piano sampled every 3rd note
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]
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._urls = {}
slicedNotes.forEach(note => {
this._urls[note - 1] = baseUrl + Frequency(note, 'midi').toNote().replace('#', 's') + '.mp3'
this._urls[note] = baseUrl + Frequency(note, 'midi').toNote().replace('#', 's') + '.mp3'
this._urls[note + 1] = baseUrl + Frequency(note, 'midi').toNote().replace('#', 's') + '.mp3'
})
this._player = null
this._loaded = false
AudioBuffer.on('load', () => {
this._loaded = true
})
}
load(){
return new Promise(done => {
this._player = new MultiPlayer(this._urls, done).toMaster()
this._player.fadeOut = 0.2
})
}
set volume(vol){
if (this._loaded){
this._player.volume.value = vol
}
}
keyDown(note, time){
if (this._loaded){
let pitch = this._midiToFrequencyPitch(note)
const duration = this._player.buffers.get(note).duration * 0.95
this._player.start(note, time, 0, duration - this._player.fadeOut, pitch)
}
}
keyUp(note, time){
if (this._loaded){
this._player.stop(note, time)
}
}
_midiToFrequencyPitch(midi){
let mod = midi % 3
if (mod === 1){
return 1
} else if (mod === 2){
return -1
} else {
return 0
}
}
}
export {Sampler}

62
static/src/sound/Sound.js Normal file
View File

@ -0,0 +1,62 @@
/**
* 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 Tone from 'Tone/core/Tone'
import PolySynth from 'Tone/instrument/PolySynth'
import Frequency from 'Tone/type/Frequency'
import MonoSynth from 'Tone/instrument/MonoSynth'
import {Sampler} from 'sound/Sampler'
class Sound {
constructor(){
this._range = [24, 108]
this._piano = new Sampler('audio/Salamander/', this._range)
this._synth = new Sampler('audio/string_ensemble/', this._range)
}
load(){
return Promise.all([this._piano.load(), this._synth.load()])
}
keyDown(note, time=Tone.now(), ai=false){
if (note >= this._range[0] && note <= this._range[1]){
this._piano.keyDown(note, time)
if (ai){
this._synth.volume = -8
this._synth.keyDown(note, time)
}
}
}
keyUp(note, time=Tone.now(), ai=false){
if (note >= this._range[0] && note <= this._range[1]){
time += 0.05
this._piano.keyUp(note, time)
if (ai){
this._synth.keyUp(note, time)
}
}
}
}
export {Sound}

View File

@ -1,21 +0,0 @@
[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.

View File

@ -1,3 +0,0 @@
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

@ -1,41 +0,0 @@
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)
})
}
}

View File

@ -1,124 +0,0 @@
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)
}
}

View File

@ -1,64 +0,0 @@
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
}
}

View File

@ -1,189 +0,0 @@
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

@ -1,16 +0,0 @@
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)
}
}

View File

@ -1,29 +0,0 @@
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

@ -1,16 +0,0 @@
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`
}
}

View File

@ -1,28 +0,0 @@
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}