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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
// Use a power of two to eliminate round-off converting from frames to time.
let sampleRate = 32768;
// How many grains to play.
let numberOfTests = 100;
// Duration of each grain to be played. Make a whole number of frames
let duration = Math.floor(0.01 * sampleRate) / sampleRate;
// A little extra bit of silence between grain boundaries. Must be a whole
// number of frames.
let grainGap = Math.floor(0.005 * sampleRate) / sampleRate;
// Time step between the start of each grain. We need to add a little
// bit of silence so we can detect grain boundaries
let timeStep = duration + grainGap;
// Time step between the start for each grain. Must be a whole number of
// frames.
let grainOffsetStep = Math.floor(0.001 * sampleRate) / sampleRate;
// How long to render to cover all of the grains.
let renderTime = (numberOfTests + 1) * timeStep;
let context;
let renderedData;
// Create a buffer containing the data that we want. The function f
// returns the desired value at sample frame k.
function createSignalBuffer(context, f) {
// Make sure the buffer has enough data for all of the possible
// grain offsets and durations. The additional 1 is for any
// round-off errors.
let signalLength =
Math.floor(1 + sampleRate * (numberOfTests * grainOffsetStep + duration));
let buffer = context.createBuffer(2, signalLength, sampleRate);
let data = buffer.getChannelData(0);
for (let k = 0; k < signalLength; ++k) {
data[k] = f(k);
}
return buffer;
}
// From the data array, find the start and end sample frame for each
// grain. This depends on the data having 0's between grain, and
// that the grain is always strictly non-zero.
function findStartAndEndSamples(data) {
let nSamples = data.length;
let startTime = [];
let endTime = [];
let lookForStart = true;
// Look through the rendered data to find the start and stop
// times of each grain.
for (let k = 0; k < nSamples; ++k) {
if (lookForStart) {
// Find a non-zero point and record the start. We're not
// concerned with the value in this test, only that the
// grain started here.
if (renderedData[k]) {
startTime.push(k);
lookForStart = false;
}
} else {
// Find a zero and record the end of the grain.
if (!renderedData[k]) {
endTime.push(k);
lookForStart = true;
}
}
}
return {start: startTime, end: endTime};
}
function playGrain(context, source, time, offset, duration) {
let bufferSource = context.createBufferSource();
bufferSource.buffer = source;
bufferSource.connect(context.destination);
bufferSource.start(time, offset, duration);
}
// Play out all grains. Returns a object containing two arrays, one
// for the start time and one for the grain offset time.
function playAllGrains(context, source, numberOfNotes) {
let startTimes = new Array(numberOfNotes);
let offsets = new Array(numberOfNotes);
for (let k = 0; k < numberOfNotes; ++k) {
let timeOffset = k * timeStep;
let grainOffset = k * grainOffsetStep;
playGrain(context, source, timeOffset, grainOffset, duration);
startTimes[k] = timeOffset;
offsets[k] = grainOffset;
}
return {startTimes: startTimes, grainOffsetTimes: offsets};
}
// Verify that the start and end frames for each grain match our
// expected start and end frames.
function verifyStartAndEndFrames(startEndFrames, should) {
let startFrames = startEndFrames.start;
let endFrames = startEndFrames.end;
// Count of how many grains started at the incorrect time.
let errorCountStart = 0;
// Count of how many grains ended at the incorrect time.
let errorCountEnd = 0;
should(
startFrames.length == endFrames.length, 'Found all grain starts and ends')
.beTrue();
should(startFrames.length, 'Number of start frames').beEqualTo(numberOfTests);
should(endFrames.length, 'Number of end frames').beEqualTo(numberOfTests);
// Examine the start and stop times to see if they match our
// expectations.
for (let k = 0; k < startFrames.length; ++k) {
let expectedStart = timeToSampleFrame(k * timeStep, sampleRate);
// The end point is the duration.
let expectedEnd = expectedStart +
grainLengthInSampleFrames(k * grainOffsetStep, duration, sampleRate);
if (startFrames[k] != expectedStart)
++errorCountStart;
if (endFrames[k] != expectedEnd)
++errorCountEnd;
should([startFrames[k], endFrames[k]], 'Pulse ' + k + ' boundary')
.beEqualToArray([expectedStart, expectedEnd]);
}
// Check that all the grains started or ended at the correct time.
if (!errorCountStart) {
should(
startFrames.length, 'Number of grains that started at the correct time')
.beEqualTo(numberOfTests);
} else {
should(
errorCountStart,
'Number of grains out of ' + numberOfTests +
'that started at the wrong time')
.beEqualTo(0);
}
if (!errorCountEnd) {
should(endFrames.length, 'Number of grains that ended at the correct time')
.beEqualTo(numberOfTests);
} else {
should(
errorCountEnd,
'Number of grains out of ' + numberOfTests +
' that ended at the wrong time')
.beEqualTo(0);
}
}
|