/*****************************************************************************************
                                    Guitar Synth Script
Version 1.0.0
Written Mark "Tarquin" Wilton-Jones, 1-18/06/2026
******************************************************************************************

Please see http://www.howtocreate.co.uk/jslibs/script-linechart for details
Please see http://www.howtocreate.co.uk/tutorials/jsexamples/guitarsynth.html for details and API documentation
Please see http://www.howtocreate.co.uk/jslibs/termsOfUse.html for terms and conditions of use

Simulates guitar sounds dynamically using the Web Audio API.
_______________________________________________________________________________________*/

//frequencies in A440 standard pitch
var A4 = 440;
var strings = {
	dropD: -31,
	lowE: -29,
	A: -24,
	D: -19,
	G: -14,
	B: -10,
	highE: -5
};
var audioContext;
function getHz(semitonesFromA4) {
	return A4 * Math.pow( 2, semitonesFromA4 / 12 ) || 0;
}
var chords;
function recalculateChords() {
	chords = {
		A:    [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz( strings.G + 2 ),  getHz( strings.B + 2 ),  getHz(strings.highE)        ],
		A2:   [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz( strings.G + 2 ),  getHz(strings.B),        getHz(strings.highE)        ],
		A4:   [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz(strings.highE)        ],
		A6:   [                             getHz(strings.A),        getHz( strings.D + 4 ),  getHz( strings.G + 2 ),  getHz( strings.B + 2 ),  getHz(strings.highE)        ],
		A7:   [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz(strings.G),        getHz( strings.B + 2 ),  getHz(strings.highE)        ],
		A9:   [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz( strings.G + 4 ),  getHz( strings.B + 2 ),  getHz(strings.highE)        ],
		A11:  [ getHz( strings.lowE + 5 ),  getHz( strings.A + 5 ),  getHz( strings.D + 7 ),  getHz( strings.G + 6 ),  getHz( strings.B + 5 ),  getHz( strings.highE + 5 )  ],
		Am:   [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz(strings.highE)        ],
		Am7:  [                             getHz(strings.A),        getHz( strings.D + 2 ),  getHz(strings.G),        getHz( strings.B + 1 ),  getHz(strings.highE)        ],
		As:   [                             getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 3 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 1 )  ],
		As2:  [                             getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 3 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		As4:  [                             getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 3 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 1 )  ],
		As6:  [ getHz( strings.lowE + 6 ),  getHz( strings.A + 5 ),  getHz(strings.D),        getHz(strings.G),        getHz( strings.B + 6 ),  getHz( strings.highE + 6 )  ],
		As7:  [                             getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 1 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 1 )  ],
		As9:  [                                                      getHz( strings.D + 8 ),  getHz( strings.G + 7 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 8 )  ],
		As11: [ getHz( strings.lowE + 6 ),  getHz( strings.A + 6 ),  getHz( strings.D + 8 ),  getHz( strings.G + 7 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 6 )  ],
		Asm:  [                             getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 1 )  ],
		Asm7: [                             getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 1 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 1 )  ],
		B:    [                             getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 2 )  ],
		B2:   [                             getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		B4:   [                             getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz( strings.B + 5 ),  getHz( strings.highE + 2 )  ],
		B6:   [                             getHz( strings.A + 2 ),  getHz( strings.D + 1 ),  getHz( strings.G + 1 ),  getHz(strings.B),        getHz( strings.highE + 2 )  ],
		B7:   [                             getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 2 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 2 )  ],
		B9:   [                                                      getHz( strings.D + 9 ),  getHz( strings.G + 8 ),  getHz( strings.B + 7 ),  getHz( strings.highE + 9 )  ],
		B11:  [ getHz( strings.lowE + 7 ),  getHz( strings.A + 7 ),  getHz( strings.D + 9 ),  getHz( strings.G + 8 ),  getHz( strings.B + 7 ),  getHz( strings.highE + 7 )  ],
		Bm:   [                             getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 2 )  ],
		Bm7:  [                             getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 2 )  ],
		C:    [                             getHz( strings.A + 3 ),  getHz( strings.D + 2 ),  getHz(strings.G),        getHz( strings.B + 1 ),  getHz(strings.highE)        ],
		C2:   [                             getHz( strings.A + 3 ),  getHz(strings.D),        getHz(strings.G),        getHz( strings.B + 1 )                               ],
		C4:   [                             getHz( strings.A + 3 ),  getHz( strings.D + 3 ),  getHz(strings.G),        getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		C6:   [ getHz( strings.lowE + 8 ),  getHz(strings.A),        getHz( strings.D + 7 ),  getHz(strings.G),        getHz( strings.B + 8 ),  getHz(strings.highE)        ],
		C7:   [                             getHz( strings.A + 3 ),  getHz( strings.D + 2 ),  getHz( strings.G + 3 ),  getHz( strings.B + 1 ),  getHz(strings.highE)        ],
		C9:   [                             getHz( strings.A + 3 ),  getHz( strings.D + 2 ),  getHz(strings.G),        getHz( strings.B + 3 ),  getHz(strings.highE)        ],
		C11:  [                             getHz( strings.A + 3 ),  getHz( strings.D + 2 ),  getHz(strings.G),        getHz( strings.B + 3 ),  getHz( strings.highE + 1 )  ],
		Cm:   [                             getHz( strings.A + 3 ),  getHz( strings.D + 5 ),  getHz( strings.G + 5 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 3 )  ],
		Cm7:  [                             getHz( strings.A + 3 ),  getHz( strings.D + 5 ),  getHz( strings.G + 3 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 3 )  ],
		Cs:   [                             getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 6 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 4 )  ],
		Cs2:  [                             getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 6 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ],
		Cs4:  [                             getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 6 ),  getHz( strings.B + 7 ),  getHz( strings.highE + 4 )  ],
		Cs6:  [ getHz( strings.lowE + 9 ),  getHz( strings.A + 8 ),  getHz( strings.D + 6 ),  getHz( strings.G + 6 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 6 )  ],
		Cs7:  [                             getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 4 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 4 )  ],
		Cs9:  [                                                      getHz( strings.D + 1 ),  getHz( strings.G + 1 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 1 )  ],
		Cs11: [ getHz( strings.lowE + 9 ),  getHz( strings.A + 9 ),  getHz( strings.D + 11 ), getHz( strings.G + 10 ), getHz( strings.B + 9 ),  getHz( strings.highE + 9 )  ],
		Csm:  [                             getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 6 ),  getHz( strings.B + 5 ),  getHz( strings.highE + 4 )  ],
		Csm7: [                             getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 4 ),  getHz( strings.B + 5 ),  getHz( strings.highE + 4 )  ],
		D:    [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 2 )  ],
		D2:   [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz(strings.highE)        ],
		D4:   [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 3 )  ],
		D6:   [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz(strings.B),        getHz( strings.highE + 2 )  ],
		D7:   [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 2 )  ],
		D9:   [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 5 ),  getHz( strings.highE + 2 )  ],
		Dm:   [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 1 )  ],
		Dm7:  [                                                      getHz(strings.D),        getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		D11:  [                             getHz(strings.A),        getHz(strings.D),        getHz(strings.G),        getHz( strings.B + 3 ),  getHz( strings.highE + 2 )  ],
		Ds:   [                             getHz( strings.A + 6 ),  getHz( strings.D + 8 ),  getHz( strings.G + 8 ),  getHz( strings.B + 8 ),  getHz( strings.highE + 6 )  ],
		Ds2:  [                             getHz( strings.A + 6 ),  getHz( strings.D + 8 ),  getHz( strings.G + 8 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 6 )  ],
		Ds4:  [                             getHz( strings.A + 6 ),  getHz( strings.D + 8 ),  getHz( strings.G + 8 ),  getHz( strings.B + 9 ),  getHz( strings.highE + 6 )  ],
		Ds6:  [                                                      getHz( strings.D + 1 ),  getHz( strings.G + 3 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 3 )  ],
		Ds7:  [                             getHz( strings.A + 6 ),  getHz( strings.D + 8 ),  getHz( strings.G + 6 ),  getHz( strings.B + 8 ),  getHz( strings.highE + 6 )  ],
		Ds11: [ getHz( strings.lowE + 11 ), getHz( strings.A + 11 ), getHz( strings.D + 13 ), getHz( strings.G + 12 ), getHz( strings.B + 11 ), getHz( strings.highE + 11 ) ],
		Ds9:  [                             getHz( strings.A + 6 ),  getHz( strings.D + 5 ),  getHz(strings.G),        getHz( strings.B + 6 ),  getHz( strings.highE + 6 )  ],
		Dsm:  [                             getHz( strings.A + 6 ),  getHz( strings.D + 8 ),  getHz( strings.G + 8 ),  getHz( strings.B + 7 ),  getHz( strings.highE + 6 )  ],
		Dsm7: [                                                      getHz( strings.D + 1 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		E:    [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz( strings.G + 1 ),  getHz(strings.B),        getHz(strings.highE)        ],
		E2:   [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz(strings.B),        getHz(strings.highE)        ],
		E4:   [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz( strings.G + 2 ),  getHz(strings.B),        getHz(strings.highE)        ],
		E6:   [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz( strings.G + 1 ),  getHz( strings.B + 2 ),  getHz(strings.highE)        ],
		E7:   [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz( strings.G + 1 ),  getHz( strings.B + 3 ),  getHz(strings.highE)        ],
		E9:   [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz( strings.G + 1 ),  getHz(strings.B),        getHz( strings.highE + 2 )  ],
		E11:  [ getHz(strings.lowE),        getHz(strings.A),        getHz( strings.D + 2 ),  getHz( strings.G + 1 ),  getHz(strings.B),        getHz(strings.highE)        ],
		Em:   [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz(strings.G),        getHz(strings.B),        getHz(strings.highE)        ],
		Em7:  [ getHz(strings.lowE),        getHz( strings.A + 2 ),  getHz( strings.D + 2 ),  getHz(strings.G),        getHz( strings.B + 3 ),  getHz(strings.highE)        ],
		F:    [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 3 ),  getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		F2:   [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 3 ),  getHz( strings.G + 0 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		F4:   [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 3 ),  getHz( strings.G + 3 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		F6:   [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 3 ),  getHz( strings.G + 2 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 1 )  ],
		F7:   [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 1 ),  getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		F9:   [                                                      getHz( strings.D + 3 ),  getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 3 )  ],
		F11:  [ getHz( strings.lowE + 1 ),  getHz( strings.A + 1 ),  getHz( strings.D + 3 ),  getHz( strings.G + 2 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		Fm:   [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 3 ),  getHz( strings.G + 1 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		Fm7:  [ getHz( strings.lowE + 1 ),  getHz( strings.A + 3 ),  getHz( strings.D + 1 ),  getHz( strings.G + 1 ),  getHz( strings.B + 1 ),  getHz( strings.highE + 1 )  ],
		Fs:   [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 4 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		Fs2:  [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 4 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		Fs4:  [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		Fs6:  [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 4 ),  getHz( strings.G + 3 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 2 )  ],
		Fs7:  [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 2 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		Fs9:  [                                                      getHz( strings.D + 4 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 4 )  ],
		Fs11: [ getHz( strings.lowE + 2 ),  getHz( strings.A + 2 ),  getHz( strings.D + 4 ),  getHz( strings.G + 3 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		Fsm:  [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 4 ),  getHz( strings.G + 2 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		Fsm7: [ getHz( strings.lowE + 2 ),  getHz( strings.A + 4 ),  getHz( strings.D + 2 ),  getHz( strings.G + 2 ),  getHz( strings.B + 2 ),  getHz( strings.highE + 2 )  ],
		G:    [ getHz( strings.lowE + 3 ),  getHz( strings.A + 2 ),  getHz(strings.D),        getHz(strings.G),        getHz(strings.B),        getHz( strings.highE + 3 )  ],
		G2:   [ getHz( strings.lowE + 3 ),  getHz( strings.A + 0 ),  getHz(strings.D),        getHz(strings.G),        getHz( strings.B + 3 ),  getHz( strings.highE + 3 )  ],
		G4:   [ getHz( strings.lowE + 3 ),  getHz( strings.A + 5 ),  getHz( strings.D + 5 ),  getHz( strings.G + 5 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 3 )  ],
		G6:   [ getHz( strings.lowE + 3 ),  getHz( strings.A + 2 ),  getHz(strings.D),        getHz(strings.G),        getHz(strings.B),        getHz(strings.highE)        ],
		G7:   [ getHz( strings.lowE + 3 ),  getHz( strings.A + 2 ),  getHz(strings.D),        getHz(strings.G),        getHz(strings.B),        getHz( strings.highE + 1 )  ],
		G9:   [ getHz( strings.lowE + 3 ),  getHz( strings.A + 0 ),  getHz(strings.D),        getHz(strings.G),        getHz(strings.B),        getHz( strings.highE + 3 )  ],
		G11:  [ getHz( strings.lowE + 3 ),  getHz( strings.A + 2 ),  getHz(strings.D),        getHz(strings.G),        getHz( strings.B + 1 ),  getHz( strings.highE + 3 )  ],
		Gm:   [ getHz( strings.lowE + 3 ),  getHz( strings.A + 5 ),  getHz( strings.D + 5 ),  getHz( strings.G + 3 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 3 )  ],
		Gm7:  [ getHz( strings.lowE + 3 ),  getHz( strings.A + 5 ),  getHz( strings.D + 3 ),  getHz( strings.G + 3 ),  getHz( strings.B + 3 ),  getHz( strings.highE + 3 )  ],
		Gs:   [ getHz( strings.lowE + 4 ),  getHz( strings.A + 6 ),  getHz( strings.D + 6 ),  getHz( strings.G + 5 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ],
		Gs2:  [                                                      getHz( strings.D + 6 ),  getHz( strings.G + 8 ),  getHz( strings.B + 9 ),  getHz( strings.highE + 6 )  ],
		Gs4:  [ getHz( strings.lowE + 4 ),  getHz( strings.A + 6 ),  getHz( strings.D + 6 ),  getHz( strings.G + 6 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ],
		Gs6:  [ getHz( strings.lowE + 4 ),  getHz( strings.A + 6 ),  getHz( strings.D + 6 ),  getHz( strings.G + 5 ),  getHz( strings.B + 6 ),  getHz( strings.highE + 4 )  ],
		Gs7:  [ getHz( strings.lowE + 4 ),  getHz( strings.A + 6 ),  getHz( strings.D + 4 ),  getHz( strings.G + 5 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ],
		Gs9:  [                                                      getHz( strings.D + 6 ),  getHz( strings.G + 5 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 6 )  ],
		Gsm:  [ getHz( strings.lowE + 4 ),  getHz( strings.A + 6 ),  getHz( strings.D + 6 ),  getHz( strings.G + 4 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ],
		Gsm7: [ getHz( strings.lowE + 4 ),  getHz( strings.A + 6 ),  getHz( strings.D + 4 ),  getHz( strings.G + 4 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ],
		Gs11: [ getHz( strings.lowE + 4 ),  getHz( strings.A + 4 ),  getHz( strings.D + 6 ),  getHz( strings.G + 5 ),  getHz( strings.B + 4 ),  getHz( strings.highE + 4 )  ]
	};
}
recalculateChords();
function createReverbConfiguration(reverbParams) {
	var duration = Math.max( Number(reverbParams.duration) || Number.MIN_VALUE, Number.MIN_VALUE );
	var exponentialDecayFactor = Number(reverbParams.exponentialDecayFactor) || 0;
	var reverbVolume = ( typeof(reverbParams.reverbVolume) == 'number' ) ? Math.max( Number(reverbParams.reverbVolume) || 0, 0 ) : 1;
	var originalVolume = ( typeof(reverbParams.originalVolume) == 'number') ? Math.max( Number(reverbParams.originalVolume) || 0, 0 ) : 1;

	if( !audioContext ) {
		if( window.AudioContext ) {
			audioContext = new AudioContext();
		} else if( window.webkitAudioContext ) {
			audioContext = new webkitAudioContext();
		} else {
			return null;
		}
	}

  var audioSampleRate = audioContext.sampleRate;
  var totalSamples = Math.ceil( audioSampleRate * duration );
  var stereo = 2;
  var echoPattern = audioContext.createBuffer( stereo, totalSamples, audioSampleRate );
  var channelDataArray;
  for( var leftOrRight = 0, i; leftOrRight < stereo; leftOrRight++ ) {
    channelDataArray = echoPattern.getChannelData(leftOrRight);
    for( i = 0; i < totalSamples; i++ ) {
    	//each array entry is random white noise (-1 to 1), with the amplitude decayed exponentially
    	//so each sample (normally 44100 Hz) gets replayed a random amount, sometimes phase inverted, in time with the next samples
      channelDataArray[i] = ( ( Math.random() * 2 ) - 1 ) * Math.pow( 1 - ( i / totalSamples ), exponentialDecayFactor );
    }
  }

	//convolver is an echo unit, that continually replays samples of a sound for as long as needed, at a specified amplitude per replay
	var reverb = audioContext.createConvolver();
	reverb.buffer = echoPattern;

	var reverbInput = audioContext.createGain();
	var reverbDry = audioContext.createGain();
	var reverbWet = audioContext.createGain();
	//input gain does nothing, it just exists to allow the input to be split into a wet/dry signal line
	reverbInput.gain.value = 1;
	reverbDry.gain.value = originalVolume;
	reverbWet.gain.value = reverbVolume;
	reverbInput.connect(reverb);
	reverb.connect(reverbWet);
	reverbInput.connect(reverbDry);
	reverbWet.connect(audioContext.destination);
	reverbDry.connect(audioContext.destination);

	return {
		reverbState: 'ready',
		input: reverbInput,
		disconnect: function (immediate) {
			if( !echoPattern ) {
				return;
			}
			var thisObj = this;
			if( !immediate ) {
				this.reverbState = 'countdown';
				setTimeout( function () {
					thisObj.disconnect(true);
				}, duration * 1000 + 150 );
				return;
			}
			this.input = null;
			this.reverbState = 'stopping';
			reverbParams = duration = exponentialDecayFactor = reverbVolume = originalVolume = audioSampleRate = totalSamples = stereo = echoPattern = channelDataArray = null;
			var now = audioContext.currentTime;
			reverbInput.gain.linearRampToValueAtTime( 0, now + 0.05 );
			reverbWet.gain.linearRampToValueAtTime( 0, now + 0.05 );
			reverbDry.gain.linearRampToValueAtTime( 0, now + 0.05 );
			setTimeout( function () {
				reverbInput.disconnect();
				reverb.disconnect();
				reverbWet.disconnect();
				reverbDry.disconnect();
				reverb = reverbInput = reverbWet = reverbDry = null;
				thisObj.reverbState = 'disconnected';
			}, 200 );
		}
	};
}
function getSlide(slideParams) {
	if( !slideParams || !slideParams.toTime || !slideParams.steps ) {
		return [];
	}
	var fromTime = Math.min( Math.max( Number(slideParams.fromTime) || 0, 0 ), Number.MAX_SAFE_INTEGER );
	var toTime = Math.min( Math.max( Number(slideParams.toTime) || Number.MIN_VALUE, Number.MIN_VALUE ), Number.MAX_SAFE_INTEGER );
	var fromHz = Math.max( Number(slideParams.fromHz) || 0, 0 );
	var toHz = Math.max( Number(slideParams.toHz) || 0, 0 );
	var steps = Math.min( Math.max( Math.floor( Number(slideParams.steps) || 1 ), 1 ), Number.MAX_SAFE_INTEGER );
	var timeDiff = toTime - fromTime;
	if( timeDiff <= 0 ) {
		return [];
	}
	if( steps == 1 ) {
		return [ { time: fromTime, hz: fromHz }, { time: toTime, hz: toHz } ];
	}
	var hzDiff = toHz - fromHz;
	var rampTime = Math.min( 0.005, timeDiff / ( 4 * steps ) );
	var remainingTime = timeDiff - ( rampTime * steps );
	var slideArray = [];
	for( var i = 0; i < steps; i++ ) {
		slideArray[slideArray.length] = { time: fromTime + ( remainingTime * i / ( steps - 1 ) ) + ( rampTime * i ), hz: fromHz + i * hzDiff / steps };
		if( i == steps - 1 ) {
			//avoid rounding errors in the calculations
			slideArray[slideArray.length] = { time: toTime, hz: toHz };
		} else {
			slideArray[slideArray.length] = { time: fromTime + ( remainingTime * i / ( steps - 1 ) ) + ( rampTime * ( i + 1 ) ), hz: fromHz + ( i + 1 ) * hzDiff / steps };
		}
	}
	return slideArray;
}
function playNotes(notesParams) {
	function makeClippingCurve( clipThreshold, clipStrength ) {
		//the midpoint of the specified indexes will be taken as 0, with the wave on either side
		//0 will be the lowest possible negative value of the audio wave, 1 will be the highest positive value
		//it does not need a full number of bits covering the full span of audio bit values, since it will interpolate in between
		var interpolationPoints = 5000;
		var mapping = new Float32Array(interpolationPoints);
		var x;
		for( var i = 0; i < interpolationPoints; i++ ) {
			//an array of -1 to 1 values
			x = ( 2 * i / interpolationPoints ) - 1;
			if( x > clipThreshold ) {
				x = ( x + ( clipThreshold * clipStrength ) ) / ( clipStrength + 1 );
			} else if( x < -clipThreshold ) {
				x = ( x - ( clipThreshold * clipStrength ) ) / ( clipStrength + 1 );
			}
			mapping[i] = x;
		}
		return mapping;
	}
	function clampFrequency( frequency, note, harmonic, timed, vibrato ) {
		var warning, property;
		if( frequency > maxFrequencyAllowed ) {
			if( window.console && console.warn ) {
				warning = '\u26A0 Frequency ' + frequency + ' Hz for playNotes call ' +
				          playNotes.callCount + ' notes[' + note + ']' +
				          ( ( vibrato >= 0 ) ? ( '.timedVibrato[' + vibrato + ']' ) : '' ) +
				          ( ( timed >= 0 ) ? ( '.timedHz[' + timed + ']' ) : '' ) +
				          ( harmonic ? ( ' tone ' + ( harmonic + 1 ) ) : '' ) +
				          " is above the computer's maximum of " + maxFrequencyAllowed +
				          ' Hz, so it was clamped to ' + maxFrequencyAllowed + ' Hz. ' +
				          ( ( vibrato < 0 ) ? ( harmonic ? 'Perhaps numTones is set too high.' : 'That note is too high to be heard.' ) : 'This is excessive for a vibrato.' );
				if( debugNotes ) {
					property = ( timed < 0 ) ? ( vibrato < 0 ) ? 'note' : ( 'vibrato' + vibrato ) : ( 'timed' + timed );
					if( !errors[property] ) {
						errors[property] = true;
						console.groupCollapsed( '%c' + warning, 'color: #000; font-weight: normal;' );
						console.log( 'Parameters:', notesParams );
						console.trace('Stack trace:');
						console.groupEnd();
					}
				} else if( !playNotes.warnings_shown_once ) {
					playNotes.warnings_shown_once = true;
					console.log(warning);
					console.log('No further frequency warnings will be shown. To see all frequency warnings and stack traces, enable the debugNotes option.');
				}
			}
			frequency = maxFrequencyAllowed;
		}
		return frequency;
	}
	//get the data variables into a usable format, within constraints
	if( !notesParams ) {
		notesParams = {};
	}
	var stopPrevious = Array.isArray(notesParams.stopPrevious) ? notesParams.stopPrevious.slice() : [];
	var notes = Array.isArray(notesParams.notes) ? notesParams.notes.slice() : [];
	var sound = notesParams.sound || 'clean';
	var numTones = Math.max( Number(notesParams.numTones) || 10, 1 );
	var ringtimeFactor = Math.min( Math.max( Number(notesParams.ringtimeFactor) || 1, 0.21 ), Number.MAX_SAFE_INTEGER );
	var masterVolume = ( typeof(notesParams.masterVolume) == 'number' ) ? Math.max( Number(notesParams.masterVolume) || 0, 0 ) : 1;
	var balance = Math.min( Math.max( Number(notesParams.balance) || 0, -1 ), 1 );
	var delayBetweenNotes = Math.min( Math.max( Number(notesParams.delayBetweenNotes) || 0, Number.MIN_SAFE_INTEGER ), Number.MAX_SAFE_INTEGER );
	var imperfection = Math.max( Number(notesParams.imperfection) || 0, 0 );
	var reverb = ( notesParams.reverb && window.GainNode && ( notesParams.reverb.input instanceof GainNode ) && typeof(notesParams.reverb.disconnect) == 'function' ) ? notesParams.reverb : window.undefined;
	var debugNotes = notesParams.debugNotes;
	var callback = ( typeof(notesParams.onstop) == 'function' ) ? notesParams.onstop : null;
	playNotes.callCount++;
	if( delayBetweenNotes < 0 ) {
		notes.reverse();
		delayBetweenNotes *= -1;
	}
	if( !audioContext ) {
		if( window.AudioContext ) {
			audioContext = new AudioContext();
		} else if( window.webkitAudioContext ) {
			audioContext = new webkitAudioContext();
		} else {
			return null;
		}
	}
	var i, j, n;
	if( stopPrevious ) {
		for( i = 0; i < stopPrevious.length; i++ ) {
			if( stopPrevious[i] && typeof(stopPrevious[i].stopPlaying) == 'function' && stopPrevious[i].stopPlaying != playNotes.stopPlaying_replacer ) {
				stopPrevious[i].stopPlaying();
			}
		}
	}
	var store;
	if( !notes.length ) {
		store = { playingState: 'disconnected', stopPlaying: playNotes.stopPlaying_replacer };
		if( callback ) {
			setTimeout( function () {
				callback( { type: 'stop', target: store } );
			}, 1 );
		}
		return store;
	}
	var oscillators = [];
	var gains = [];
	var disconnectors = [];
	store = {
		playingState: 'playing',
		stopPlaying: function () {
			//free up the local scope
			this.stopPlaying = playNotes.stopPlaying_replacer;
			this.playingState = 'stopping';
			var now = audioContext.currentTime;
			var currentValue;
			//audible clicks if they are instantly disconnected, since the wave snaps from wherever it was to 0
			//so need to ramp the gain down nicely first
			for( var i = 0; i < gains.length; i++ ) {
				//cancelAndHoldAtTime is not supported in Firefox, have to workaround
				//store the value as it is right now
				currentValue = gains[i].gain.value;
				//stop it from processing any existing gain changes
				gains[i].gain.cancelScheduledValues(now);
				//cancelling seems to snap it back to either the before or after value of the currently processing scheduled ramp,
				//producing a click, so set the value to what it just was, to remove the click
				gains[i].gain.setValueAtTime( currentValue, now );
				//now slowly lower the gain nicely, make it take long enough to minimise clicks
				//any longer, and the overlapping notes become audible instead
				gains[i].gain.linearRampToValueAtTime( 0, now + 0.05 );
			}
			setTimeout( function () {
				//give plenty of time for the gain to drop to 0, because scheduling is not perfect, then disconnect everything
				for( i = 0; i < oscillators.length; i++ ) {
					oscillators[i].stop();
					oscillators[i].disconnect();
				}
				for( i = 0; i < gains.length; i++ ) {
					gains[i].disconnect();
				}
				for( i = 0; i < disconnectors.length; i++ ) {
					disconnectors[i].disconnect();
				}
				oscillators.length = 0;
				disconnectors.length = 0;
				store.playingState = 'disconnected';
				if( callback ) {
					callback( { type: 'stop', target: store } );
				}
			}, 200 );
		}
	};
	var now = audioContext.currentTime;
	var preamp;
	if( sound == 'overdrive' || sound == 'mutedoverdrive' ) {
		gains[gains.length] = preamp = audioContext.createGain();
	}
	var panner = disconnectors[disconnectors.length] = audioContext.createStereoPanner();
	panner.pan.value = balance;
	panner.connect( reverb ? reverb.input : audioContext.destination );
	var offset, ringtime, oscillator, frequencyGain, stopAfter, lastOscillator, startHz, timedHz, timedVibrato, goodVibrato, volume, errors, vibratoOscillator, vibratoGain;
	var maxOscillator = 0;
	var maxFrequencyAllowed = audioContext.sampleRate / 2;
	for( i = 0; i < notes.length; i++ ) {
		//normalise the note data
		if( notes[i] && typeof(notes[i]) == 'object' ) {
			startHz = Math.max( Number(notes[i].startHz) || 0, 0 );
			timedHz = Array.isArray(notes[i].timedHz) ? notes[i].timedHz.slice() : [];
			timedVibrato = Array.isArray(notes[i].timedVibrato) ? notes[i].timedVibrato.slice() : [];
			volume = Math.max( ( typeof(notes[i].volume) == 'number' ) ? ( notes[i].volume || 0 ) : 1, 0 );
		} else {
			startHz = Math.max( Number(notes[i]) || 0, 0 );
			timedHz = [];
			timedVibrato = [];
			volume = 1;
		}
		errors = {};
		//calculate imperfection and duration
		offset = ( Math.random() - 0.5 ) * imperfection * 200;
		if( sound == 'muted' || sound == 'mutedoverdrive' ) {
			ringtime = 2;
		} else {
			//higher notes ring for less time, but within reason
			ringtime = ringtimeFactor * Math.max( 10, Math.min( 30, 10 + 20 * ( A4 - startHz ) / ( A4 - getHz(strings.lowE) ) ) );
		}
		stopAfter = ringtime + i * delayBetweenNotes;
		//normalise timed frequency
		for( j = 0; j < timedHz.length; j++ ) {
			timedHz[j] = ( timedHz[j] && typeof(timedHz[j]) == 'object' && 'time' in timedHz[j] && 'hz' in timedHz[j] ) ? {
				hz: Math.max( Number(timedHz[j].hz) || 0, 0 ),
				time: Math.max( Number(timedHz[j].time) || 0, 0 )
			} : null;
		}
		//normalise vibrato
		goodVibrato = [];
		for( j = 0; j < timedVibrato.length; j++ ) {
			if( timedVibrato[j] && typeof(timedVibrato[j]) == 'object' && 'time' in timedVibrato[j] && 'hz' in timedVibrato[j] && 'strength' in timedVibrato[j] ) {
				goodVibrato[goodVibrato.length] = {
					hz: Math.max( Number(timedVibrato[j].hz) || 0, 0 ),
					time: Math.max( Number(timedVibrato[j].time) || 0, 0 ),
					strength: Math.max( Number(timedVibrato[j].strength) || 0, 0 )
				};
			}
		}
		//prepare vibrato, since the same one is used for every frequency of the note
		if( goodVibrato.length ) {
			oscillators[oscillators.length] = vibratoOscillator = audioContext.createOscillator();
			gains[gains.length] = vibratoGain = audioContext.createGain();
			vibratoOscillator.frequency.setValueAtTime( 0, now );
			vibratoGain.gain.setValueAtTime( 0, now );
			vibratoOscillator.connect(vibratoGain);
			for( j = 0; j < goodVibrato.length; j++ ) {
				//hard jumps, not linear shifts, since that sounds more like real vibrato
				vibratoOscillator.frequency.setValueAtTime( clampFrequency( goodVibrato[j].hz, i, 0, -1, j ), now + goodVibrato[j].time );
				//rapid exponential, to avoid sudden pitch jumps if the vibratoOscillator is high
				vibratoGain.gain.setTargetAtTime( goodVibrato[j].strength * 100, now + goodVibrato[j].time, 0.05 );
			}
			vibratoOscillator.start();
			vibratoOscillator.stop( now + stopAfter );
		}
		for( j = 0; j < numTones; j++ ) {
			oscillators[oscillators.length] = oscillator = audioContext.createOscillator();
			gains[gains.length] = frequencyGain = audioContext.createGain();
			frequencyGain.gain.setValueAtTime( 0, now );
			frequencyGain.gain.setValueAtTime( 0, now + ( i * delayBetweenNotes ) );
			if( sound == 'muted' || sound == 'mutedoverdrive' ) {
				oscillator.type = 'sine';
				//reduced harmonics means less overall volume, so boost gain a little above clean
				frequencyGain.gain.linearRampToValueAtTime( masterVolume * volume * 0.3 / Math.pow( j + 1, 3 ), now + 0.002 + ( i * delayBetweenNotes ) );
				frequencyGain.gain.exponentialRampToValueAtTime( masterVolume * 0.0000001 / Math.pow( j + 1, 3 ), now + ringtime + ( i * delayBetweenNotes ) );
			} else if( sound == 'harmonic' ) {
				oscillator.type = 'sine';
				if( j == 1 ) {
					//12 fret harmonic dominates at full strength
					frequencyGain.gain.linearRampToValueAtTime( masterVolume * volume * 0.2, now + 0.0005 + ( i * delayBetweenNotes ) );
					frequencyGain.gain.exponentialRampToValueAtTime( masterVolume * 0.0000001, now + ringtime + ( i * delayBetweenNotes ) );
				} else if( !( ( j - 1 ) % 4 ) ) {
					//bell chime happens when even multiples of 2 are louder
					frequencyGain.gain.linearRampToValueAtTime( masterVolume * volume * 0.2 / Math.pow( j + 1, 2.2 ), now + 0.2 + ( i * delayBetweenNotes ) );
					frequencyGain.gain.exponentialRampToValueAtTime( masterVolume * 0.0000001 / Math.pow( j + 1, 2.2 ), now + ringtime + ( i * delayBetweenNotes ) );
				} else if( !( ( j + 1 ) % 4 ) ) {
					//odd harmonics of 2 are quieter, to make the bell chime
					frequencyGain.gain.linearRampToValueAtTime( masterVolume * volume * 0.05 / Math.pow( j + 1, 2.2 ), now + 0.2 + ( i * delayBetweenNotes ) );
					frequencyGain.gain.exponentialRampToValueAtTime( masterVolume * 0.0000001 / Math.pow( j + 1, 2.2 ), now + ringtime + ( i * delayBetweenNotes ) );
				} else {
					//fundamental and its harmonics that are not harmonics of the fretted note, slowly appear quietly
					frequencyGain.gain.linearRampToValueAtTime( masterVolume * volume * 0.01 / Math.pow( j + 1, 2.5 ), now + 2 + ( i * delayBetweenNotes ) );
					frequencyGain.gain.exponentialRampToValueAtTime( masterVolume * 0.0000001 / Math.pow( j + 1, 2.5 ), now + ringtime + ( i * delayBetweenNotes ) );
				}
			} else {
				oscillator.type = 'sine';
				frequencyGain.gain.linearRampToValueAtTime( masterVolume * volume * 0.22 / Math.pow( j + 1, 1.3 ), now + 0.002 + ( i * delayBetweenNotes ) );
				frequencyGain.gain.exponentialRampToValueAtTime( masterVolume * 0.0000001 / Math.pow( j + 1, 1.3 ), now + ringtime + ( i * delayBetweenNotes ) );
			}
			if( sound == 'overdrive' || sound == 'mutedoverdrive' ) {
				oscillator.connect(frequencyGain).connect(preamp);
			} else {
				oscillator.connect(frequencyGain).connect(panner);
			}
			oscillator.frequency.setValueAtTime( clampFrequency( ( j + 1 ) * startHz, i, j, -1, -1 ), now );
			oscillator.detune.value = offset;
			for( n = 0; n < timedHz.length; n++ ) {
				if( timedHz[n] ) {
					oscillator.frequency.linearRampToValueAtTime( clampFrequency( ( j + 1 ) * timedHz[n].hz, i, j, n, -1 ), now + timedHz[n].time );
				}
			}
			if( stopAfter > maxOscillator ) {
				maxOscillator = stopAfter;
				lastOscillator = oscillator;
			}
			oscillator.start();
			oscillator.stop( now + stopAfter );
			if( goodVibrato.length ) {
				//this gets added to the .value, so it oscillates around the offset value (it takes both vibrato and imperfection into account)
				vibratoOscillator.connect(vibratoGain).connect(oscillator.detune);
			}
		}
	}
	var waveShaper;
	if( sound == 'overdrive' || sound == 'mutedoverdrive' ) {
		//preamp boosts the clean combined wave, rather than each potentially being clipped separately by hitting the digital amplitude limit,
		//since the sum of clipped waves is different from the clipped sum of the combined wave
		//separate waves could be clip( amplitude_limit( 0.3 * 4, 1 ) + amplitude_limit( -0.28 * 4, 1 ), 0.2 ) = 1 + -1 = 0
		//combined waves could be clip( amplitude_limit( ( 0.3 + -0.28 ) * 4, 1 ), 0.2 ) = 0.2
		//most of the time this makes very little audible difference, but it removes random crackles
		preamp.gain.setValueAtTime( 0.8 / 0.22, now );
		disconnectors[disconnectors.length] = waveShaper = audioContext.createWaveShaper();
		//clipping starts at 0.2, and the curve is reasonably harsh - higher harshness (50) gives a square wave
		waveShaper.curve = makeClippingCurve( masterVolume * 0.2, 2.5 );
		waveShaper.oversample = '2x';
		preamp.connect(waveShaper).connect(panner);
	}
	lastOscillator.onended = function () {
		//avoid re-calling the stopPlaying method if that is what caused this event to fire
		if( store.stopPlaying != playNotes.stopPlaying_replacer ) {
			store.stopPlaying();
		}
		//just in case the engine thinks it has to keep this scope alive while it has an event handler
		this.onended = null;
	};
	return store;
}
//avoid polluting global scope, without preserving local scope
playNotes.stopPlaying_replacer = function () {};
playNotes.callCount = 0;