// 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); } }