File size: 4,699 Bytes
af6912c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
var soundsCache = require('./sounds-cache');
var pitchToNoteName = require('./pitch-to-note-name');
var centsToFactor = require("./cents-to-factor");

function placeNote(outputAudioBuffer, sampleRate, sound, startArray, volumeMultiplier, ofsMs, fadeTimeSec, noteEndSec, debugCallback) {
	// sound contains { instrument, pitch, volume, len, pan, tempoMultiplier
	// len is in whole notes. Multiply by tempoMultiplier to get seconds.
	// ofsMs is an offset to subtract from the note to line up programs that have different length onsets.
	var OfflineAC = window.OfflineAudioContext ||
		window.webkitOfflineAudioContext;

	var len = sound.len * sound.tempoMultiplier;
	if (ofsMs)
		len +=ofsMs/1000;
	len -= noteEndSec;
	if (len < 0)
		len = 0.005; // Have some small audible length no matter how short the note is.
	var offlineCtx = new OfflineAC(2,Math.floor((len+fadeTimeSec)*sampleRate),sampleRate);
	var noteName = pitchToNoteName[sound.pitch];
	if (!soundsCache[sound.instrument]) {
		// It shouldn't happen that the entire instrument cache wasn't created, but this has been seen in practice, so guard against it.
		if (debugCallback)
			debugCallback('placeNote skipped (instrument empty): '+sound.instrument+':'+noteName)
		return Promise.resolve();
	}
	var noteBufferPromise = soundsCache[sound.instrument][noteName];

	if (!noteBufferPromise) {
		// if the note isn't present then just skip it - it will leave a blank spot in the audio.
		if (debugCallback)
			debugCallback('placeNote skipped: '+sound.instrument+':'+noteName)
		return Promise.resolve();
	}

	return noteBufferPromise
		.then(function (response) {
			// create audio buffer
			var source = offlineCtx.createBufferSource();
			source.buffer = response.audioBuffer;

			// add gain
			// volume can be between 1 to 127. This translation to gain is just trial and error.
			// The smaller the first number, the more dynamic range between the quietest to loudest.
			// The larger the second number, the louder it will be in general.
			var volume = (sound.volume / 96) * volumeMultiplier;
			source.gainNode = offlineCtx.createGain();

			// add pan if supported and present
			if (sound.pan && offlineCtx.createStereoPanner) {
				source.panNode = offlineCtx.createStereoPanner();
				source.panNode.pan.setValueAtTime(sound.pan, 0);
			}
			source.gainNode.gain.value = volume; // Math.min(2, Math.max(0, volume));
			source.gainNode.gain.linearRampToValueAtTime(source.gainNode.gain.value, len);
			source.gainNode.gain.linearRampToValueAtTime(0.0, len + fadeTimeSec);

			if (sound.cents) {
				source.playbackRate.value = centsToFactor(sound.cents);
			}

			// connect all the nodes
			if (source.panNode) {
				source.panNode.connect(offlineCtx.destination);
				source.gainNode.connect(source.panNode);
			} else {
				source.gainNode.connect(offlineCtx.destination);
			}
			source.connect(source.gainNode);

			// Do the process of creating the sound and placing it in the buffer
			source.start(0);

			if (source.noteOff) {
				source.noteOff(len + fadeTimeSec);
			} else {
				source.stop(len + fadeTimeSec);
			}
			var fnResolve;
			offlineCtx.oncomplete = function(e) {
				if (e.renderedBuffer && e.renderedBuffer.getChannelData) { // If the system gets overloaded or there are network problems then this can start failing. Just drop the note if so.
					for (var i = 0; i < startArray.length; i++) {
						//Math.floor(startArray[i] * sound.tempoMultiplier * sampleRate)
						var start = startArray[i] * sound.tempoMultiplier;
						if (ofsMs)
							start -=ofsMs/1000;
						if (start < 0)
							start = 0; // If the item that is moved back is at the very beginning of the buffer then don't move it back. To do that would be to push everything else forward. TODO-PER: this should probably be done at some point but then it would change timing in existing apps.
						start = Math.floor(start*sampleRate);
						copyToChannel(outputAudioBuffer, e.renderedBuffer, start);
					}
				}
				if (debugCallback)
					debugCallback('placeNote: '+sound.instrument+':'+noteName)
				fnResolve();
			};
			offlineCtx.startRendering();
			return new Promise(function(resolve) {
				fnResolve = resolve;
			});
		})
		.catch(function (error) {
			if (debugCallback)
				debugCallback('placeNote catch: '+error.message)
			return Promise.resolve()
		});
}

var copyToChannel = function(toBuffer, fromBuffer, start) {
	for (var ch = 0; ch < 2; ch++) {
		var fromData = fromBuffer.getChannelData(ch);
		var toData = toBuffer.getChannelData(ch);

		// Mix the current note into the existing track
		for (var n = 0; n < fromData.length; n++) {
			toData[n + start] += fromData[n];
		}
	}
};

module.exports = placeNote;