基于WebAudio API来创建合成音乐效果
看演示效果
废话不多说,直接上代码
第一步:创建一个蜂鸣器,就是喇叭,能响就行
var BEEP = {
VERSION: 3,
// Chrome, Opera, and Firefox already provide AudioContext()
// but Safari instead provides webkitAudioContext().
// Let’s just standardize this for our own sanity right here:
AudioContext: window.AudioContext ? window.AudioContext : window.webkitAudioContext,
// We’ll keep track of Voices, Triggers, and Instruments
// (but not Notes because we just throw those away left and right)
// so we can access and/or destroy them later even if unnamed.
voices: [],
triggers: [],
instruments: [],
// Once the “DOM Content Loaded” event fires we’ll come back
// and set this to either an existing DOM Element with a #beep ID
// or create a new element and append it to the Body.
domContainer: null,
// Destroy everything. EVERYTHING. DO IT. DO IT NOW.
destroy: function(){
while( this.voices.length ){
this.voices.pop().destroy()
}
while( this.triggers.length ){
this.triggers.pop().destroy()
}
while( this.instruments.length ){
this.instruments.pop().destroy()
}
},
// Create a new Instrument from the code
// currently in the code editor.
eval: function(){
var code = document.getElementById( 'editor' ).value
try {
eval( code )
}
catch( e ){
console.log( 'OMFG', e )
}
},
// Right now just runs BEEP.eval() but in the near future
// we might have some more tricks up our sleeve...
boot: function(){
this.eval()
},
// We need to tear everything down.
// Then build it right back up.
reset: function(){
this.destroy()
this.boot()
}
}
// We might as well create an instance of AudioContext
// that we can re-use over and over if necessary.
// Why? Because the number of AudioContext instance is
// limited by hardware. For example, my personal laptop
// can only handle 6 of them at once!
BEEP.audioContext = new BEEP.AudioContext()
// Once our DOM Content is ready for action
// it’s time to GIVE IT ACTION. W000000000000T !
document.addEventListener( 'DOMContentLoaded', function(){
BEEP.domContainer = document.getElementById( 'beep' )
BEEP.boot()
})
第二步:根据音调的不同,创建所需的音符
BEEP.Note = function( params ){
var that = this
if( typeof params === 'number' ) this.hertz = params
else if( typeof params === 'object' && params.hertz !== undefined ){
Object.keys( params ).forEach( function( key ){
that[ key ] = params[ key ]
})
}
else return BEEP.Note.EDO12( params )
}
// Common Western music has 12 notes per octave,
// lettered A through G with modifier symbols for sharps and flats.
// Let’s build a validator for Western music:
BEEP.Note.validateWestern = function( params ){
var
NAMES = [ 'A♭', 'A♮', 'B♭', 'B♮', 'C♮', 'C♯', 'D♮', 'E♭', 'E♮', 'F♮', 'F♯', 'G♮' ],
LETTERS = 'ABCDEFG',
SHARPS = 'CF',
FLATS = 'EAB',
temp
if( typeof params === 'undefined' ) params = {
}
else if( typeof params === 'string' ){
temp = params
params = {
}
temp.split( '' ).forEach( function( p, i ){
if( +p + '' !== 'NaN' ) params.octaveIndex = +p
else if( '♭♮♯#'.indexOf( p ) !== -1 ){
params.modifier = p
}
else if(( LETTERS + 'H' ).indexOf( p.toUpperCase() ) !== -1 ){
if( p.toUpperCase() === 'H' ) params.letter = 'B'
else if( p === 'b' && i > 0 ) params.modifier = '♭'
else params.letter = p.toUpperCase()
}
})
}
// What octave is this?
if( params.octaveIndex === undefined
|| params.octaveIndex === ''
|| +params.octaveIndex +'' === 'NaN' ) params.octaveIndex = 4
params.octaveIndex = +params.octaveIndex
if( params.octaveIndex < 0 ) params.octaveIndex = 0
else if( params.octaveIndex > 7 ) params.octaveIndex = 7
// What’s this Note’s name?
if( params.letter === undefined ) params.letter = 'A'
params.letterIndex = LETTERS.indexOf( params.letter )
if( params.modifier === undefined ) params.modifier = '♮'
if( params.A === undefined ) params.A = 440.00
// Force the correct accidental symbols.
if( params.modifier === 'b' ) params.modifier = '♭'
if( params.modifier === '#' ) params.modifier = '♯'
// Handy function for redefining the letter
// when the letterIndex may have shifted.
function setLetterByLetterIndex( params ){
if( params.letterIndex < 0 ){
params.letterIndex += LETTERS.length
params.octaveIndex --
}
if( params.letterIndex >= LETTERS.length ){
params.letterIndex -= LETTERS.length
// Next line commented out but left in as a reminder
// that it would cause G♯ conversion to A♭
// to jump up an entire octave for no good reason!
//params.octaveIndex ++
}
params.letter = LETTERS.substr( params.letterIndex, 1 )
return params
}
// Force the correct sharp / flat categorization.
// Why does the Equal Temperament scale consider certain letters flat or sharp
// when they are mathematically equal?!
// Has to do with the delta between Equal Temperament and the Just Scale.
// Where Equal Temperament comes in higher than Just we call it sharp,
// and where it comes in lower than Just we call it flat:
// http://www.phy.mtu.edu/~suits/scales.html
if( params.modifier === '♭' && FLATS.indexOf( params.letter ) === -1 ){
params.letterIndex = LETTERS.indexOf( params.letter ) - 1
params = setLetterByLetterIndex( params )
if( SHARPS.indexOf( params.letter ) > -1 ) params.modifier = '♯'
else params.modifier = '♮'
}
else if( params.modifier === '♯' && SHARPS.indexOf( params.letter ) === -1 ){
params.letterIndex = LETTERS.indexOf( params.letter ) + 1
params = setLetterByLetterIndex( params )
if( FLATS.indexOf( params.letter ) > -1 ) params.modifier = '♭'
else params.modifier = '♮'
}
// Now that we’re certain the modifier is correct
// we can set convenience booleans.
if( params.modifier === '♯' ) params.isSharp = true
else if( params.modifier === '♭' ) params.isFlat = true
else params.isNatural = true
// A final cleanse. Should test if this is still necessary...
params = setLetterByLetterIndex( params )
// Penultimate bits...
params.name = params.letter + params.modifier
params.nameSimple = params.letter
if( params.modifier !== '♮' ) params.nameSimple += params.modifier
params.nameIndex = NAMES.indexOf( params.name )
params.pianoKeyIndex = params.octaveIndex * 12 + params.nameIndex
if( params.nameIndex > 3 ) params.pianoKeyIndex -= 12
// What tuning method are we supposed to use?
if( params.tuning === undefined ) params.tuning = 'EDO12'
// We now have the majority of the Note ready for use.
// Everything except for ... the FREQUENCY of the Note!
// That will be decided based on the tuning method.
return params
}
// Does exactly what it says on the tin, man.
BEEP.Note.EDO12 = function( params ){
params = BEEP.Note.validateWestern( params )
params.hertz = params.A * Math.pow( Math.pow( 2, 1 / 12 ), params.pianoKeyIndex - 49 )
params.tuning = 'EDO12'
return new BEEP.Note( params )
}
// The most mathematically beautiful tuning,
// makes for sonically gorgeous experiences
// ... Until you change keys!
BEEP.Note.JustIntonation = function( params, key ){
var
that = this,
relationshipIndex
params = BEEP.Note.validateWestern( params )
params.tuning = 'JustIntonation'
params.key = new BEEP.Note.EDO12( key )
// This is Ptolemy’s “Intense Diatonic Scale” which is based on
// Pythagorean tuning. It is but one example of Just Intonation.
relationshipIndex = ( params.nameIndex - params.key.nameIndex ) % 12
if( relationshipIndex < 0 ) relationshipIndex += 12
params.hertz = [
params.key.hertz, // Do UNISON
params.key.hertz * 16 / 15,// minor 2nd
params.key.hertz * 9 / 8,// Re MAJOR 2nd
params.key.hertz * 6 / 5,// minor 3rd
params.key.hertz * 5 / 4,// Mi MAJOR 3rd
params.key.hertz * 4 / 3,// Fa PERFECT 4th
params.key.hertz * 45 / 32,// augmented 4th
params.key.hertz * 3 / 2,// So PERFECT 5th
params.key.hertz * 8 / 5,// minor 6th
params.key.hertz * 5 / 3,// La MAJOR 6th
params.key.hertz * 16 / 9,// minor 7th (HD, baby!)
params.key.hertz * 15 / 8,// Ti MAJOR 7th
params.key.hertz * 2 // Do OCTAVE
][ relationshipIndex ]
// If the key’s octave and our desired note’s octave were equal
// then we’d be done. Otherwise we’ve got to bump up or down our
// note by whole octaves.
params.hertz = params.hertz * Math.pow( 2, params.octaveIndex - params.key.octaveIndex )
return new BEEP.Note( params )
}
第三步:设置高低音
BEEP.Voice = function( a, b ){
// Remember the ? will be validated by Note()
// so it could be an Object, String, Number, etc.
//
// ( AudioContext, Note )
// ( AudioContext, ? )
// ( GainNode, Note )
// ( GainNode, ? )
if( a instanceof BEEP.AudioContext || a instanceof GainNode ){
if( a instanceof BEEP.AudioContext ){
this.audioContext = a
this.destination = a.destination
}
else if( a instanceof GainNode ){
this.audioContext = a.audioContext
this.destination = a
}
if( b instanceof BEEP.Note ) this.note = b
else this.note = new BEEP.Note( b )// Still ok if b === undefined.
}
// Again, the ? will be validated by Note()
// so it could be an Object, String, Number, etc.
//
// ( Note )
// ( Note, AudioContext )
// ( Note, GainNode )
// ( ? )
// ( ?, AudioContext )
// ( ?, GainNode )
else {
if( a instanceof BEEP.Note ) this.note = a
else this.note = new BEEP.Note( a )// Still ok if a === undefined.
if( b instanceof BEEP.AudioContext ){
this.audioContext = b
this.destination = b.destination
}
else if( b instanceof GainNode ){
this.audioContext = b.audioContext
this.destination = b
}
else {
this.audioContext = BEEP.audioContext
this.destination = this.audioContext.destination
}
}
// Create a Gain Node
// for turning this voice up and down.
this.gainNode = this.audioContext.createGain()
this.gainNode.gain.value = 0
this.gainNode.connect( this.destination )
this.gainHigh = this.note.gainHigh !== undefined ? this.note.gainHigh : 1
// Create an Oscillator
// for generating the sound.
this.oscillator = this.audioContext.createOscillator()
this.oscillator.connect( this.gainNode )
this.oscillator.type = 'sine'
this.oscillator.frequency.value = this.note.hertz
// Right now these do nothing; just here as a stand-in for the future.
this.duration = Infinity
this.attack = 0
this.decay = 0
this.sustain = 0
// Because of “iOS reasons” we cannot begin playing a sound
// until the user has tripped an event.
// So we’ll use this boolean to trip this.oscillator.start(0)
// on the first use of this Voice instance.
this.isPlaying = false
// Push a reference of this instance into BEEP’s library
// so we can access and/or destroy it later.
BEEP.voices.push( this )
}
// Voices are *always* emitting, so “playing” a Note
// is really a matter of turning its amplitude up.
BEEP.Voice.prototype.play = function( params ){
// Let’s create that Note.
// The params will specify a frequency assignment method to use
// otherwise Note() will pick a default.
if( params !== undefined ) this.note = new BEEP.Note( params )
this.oscillator.frequency.value = this.note.hertz
this.gainNode.gain.value = this.gainHigh || params.gainHigh || 1
// Oh, iOS. This “wait to play” shtick is for you.
if( this.isPlaying === false ){
this.isPlaying = true
this.oscillator.start( 0 )
}
return this
}
BEEP.Voice.prototype.pause