Skip to content

Commit 584bf8d

Browse files
committed
Add tests for per-sample timestamp handling
1 parent 748cee7 commit 584bf8d

6 files changed

Lines changed: 222 additions & 8 deletions

File tree

Source/Processors/AudioNode/AudioNode.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#define __AUDIONODE_H_AF61F3C5__
2626

2727
#include "../../../JuceLibraryCode/JuceHeader.h"
28+
#include "../../TestableExport.h"
2829
#include <stdio.h>
2930

3031
#include "../Dsp/Dsp.h"
@@ -67,7 +68,7 @@ class Expander
6768
@see GenericProcessor, AudioEditor
6869
6970
*/
70-
class AudioNode : public GenericProcessor
71+
class TESTABLE AudioNode : public GenericProcessor
7172
{
7273
public:
7374
/** Constructor */

Tests/Processors/DataBufferTests.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,67 @@ TEST(DataBufferTest, CopyToAudioBuffer)
4141
for (int sample = 0; sample < audioBuffer.getNumSamples(); ++sample)
4242
EXPECT_EQ(audioBuffer.getSample(channel, sample), sample);
4343
}
44+
45+
for (int sample = 0; sample < numItems; ++sample)
46+
{
47+
EXPECT_EQ(timestamps[sample], sample);
48+
EXPECT_EQ(eventCodes[sample], (uint64) sample);
49+
}
50+
}
51+
52+
TEST(DataBufferTest, CopyToAudioBufferAcrossWrap)
53+
{
54+
constexpr int bufferSize = 11;
55+
constexpr int firstWriteSize = 8;
56+
constexpr int secondWriteSize = 6;
57+
constexpr int firstReadSize = 5;
58+
59+
DataBuffer dataBuffer (1, bufferSize);
60+
61+
{
62+
float data[firstWriteSize] = { 0, 1, 2, 3, 4, 5, 6, 7 };
63+
int64 sampleNumbers[firstWriteSize] = { 0, 1, 2, 3, 4, 5, 6, 7 };
64+
double timestamps[firstWriteSize] = { 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5 };
65+
uint64 eventCodes[firstWriteSize] = { 10, 11, 12, 13, 14, 15, 16, 17 };
66+
67+
EXPECT_EQ (dataBuffer.addToBuffer (data, sampleNumbers, timestamps, eventCodes, firstWriteSize), firstWriteSize);
68+
}
69+
70+
{
71+
AudioBuffer<float> discardBuffer (1, firstReadSize);
72+
int64 sampleNumber = -1;
73+
double timestamps[firstReadSize] = { -1, -1, -1, -1, -1 };
74+
uint64 eventCodes[firstReadSize] = { 0, 0, 0, 0, 0 };
75+
76+
EXPECT_EQ (dataBuffer.readAllFromBuffer (discardBuffer, &sampleNumber, timestamps, eventCodes, firstReadSize), firstReadSize);
77+
}
78+
79+
{
80+
float data[secondWriteSize] = { 100, 101, 102, 103, 104, 105 };
81+
int64 sampleNumbers[secondWriteSize] = { 8, 9, 10, 11, 12, 13 };
82+
double timestamps[secondWriteSize] = { 4.0, 4.5, 5.0, 5.5, 6.0, 6.5 };
83+
uint64 eventCodes[secondWriteSize] = { 18, 19, 20, 21, 22, 23 };
84+
85+
EXPECT_EQ (dataBuffer.addToBuffer (data, sampleNumbers, timestamps, eventCodes, secondWriteSize), secondWriteSize);
86+
}
87+
88+
constexpr int expectedReadSize = firstWriteSize - firstReadSize + secondWriteSize;
89+
AudioBuffer<float> audioBuffer (1, expectedReadSize);
90+
int64 startSampleNumber = -1;
91+
double timestamps[expectedReadSize] = {};
92+
uint64 eventCodes[expectedReadSize] = {};
93+
94+
EXPECT_EQ (dataBuffer.readAllFromBuffer (audioBuffer, &startSampleNumber, timestamps, eventCodes, expectedReadSize), expectedReadSize);
95+
EXPECT_EQ (startSampleNumber, 5);
96+
97+
const float expectedSamples[expectedReadSize] = { 5, 6, 7, 100, 101, 102, 103, 104, 105 };
98+
const double expectedTimestamps[expectedReadSize] = { 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5 };
99+
const uint64 expectedEventCodes[expectedReadSize] = { 15, 16, 17, 18, 19, 20, 21, 22, 23 };
100+
101+
for (int sample = 0; sample < expectedReadSize; ++sample)
102+
{
103+
EXPECT_EQ (audioBuffer.getSample (0, sample), expectedSamples[sample]);
104+
EXPECT_EQ (timestamps[sample], expectedTimestamps[sample]);
105+
EXPECT_EQ (eventCodes[sample], expectedEventCodes[sample]);
106+
}
44107
}

Tests/Processors/RecordNodeTests.cpp

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ class RecordNodeTests : public testing::Test {
5656
return inputBuffer;
5757
}
5858

59-
void writeBlock(AudioBuffer<float> &buffer, TTLEvent* maybeTtlEvent = nullptr) {
60-
auto outBuffer = tester->processBlock(processor, buffer, maybeTtlEvent);
59+
void writeBlock(AudioBuffer<float> &buffer, TTLEvent* maybeTtlEvent = nullptr, const double* sampleTimestamps = nullptr) {
60+
auto outBuffer = tester->processBlock(processor, buffer, maybeTtlEvent, sampleTimestamps);
6161
// Assert the buffer hasn't changed after process()
6262
ASSERT_EQ(outBuffer.getNumSamples(), buffer.getNumSamples());
6363
ASSERT_EQ(outBuffer.getNumChannels(), buffer.getNumChannels());
@@ -179,6 +179,55 @@ class RecordNodeTests : public testing::Test {
179179
*output = loadNpyFileBinaryFullpath(npyFilePath.string());
180180
}
181181

182+
template <typename T>
183+
std::vector<T> parseNpyPayload(const std::vector<char>& binary) {
184+
if (binary.size() < static_cast<size_t>(10)) {
185+
ADD_FAILURE() << "NPY payload is too small to contain a valid header";
186+
return {};
187+
}
188+
189+
const uint16_t headerLength = static_cast<uint8_t>(binary[8])
190+
| (static_cast<uint16_t>(static_cast<uint8_t>(binary[9])) << 8);
191+
const size_t payloadOffset = 10 + headerLength;
192+
193+
if (payloadOffset > binary.size()) {
194+
ADD_FAILURE() << "NPY payload header exceeds file size";
195+
return {};
196+
}
197+
198+
if (((binary.size() - payloadOffset) % sizeof(T)) != static_cast<size_t>(0)) {
199+
ADD_FAILURE() << "NPY payload size is not aligned to element size";
200+
return {};
201+
}
202+
203+
std::vector<T> values((binary.size() - payloadOffset) / sizeof(T));
204+
205+
if (!values.empty()) {
206+
memcpy(values.data(), binary.data() + payloadOffset, values.size() * sizeof(T));
207+
}
208+
209+
return values;
210+
}
211+
212+
template <typename T>
213+
std::vector<T> loadNpyData(const std::string& basename) {
214+
bool success = false;
215+
std::vector<char> binary;
216+
loadNpyFileBinary(basename, &binary, &success);
217+
218+
if (!success) {
219+
ADD_FAILURE() << "Failed to load NPY file: " << basename;
220+
return {};
221+
}
222+
223+
return parseNpyPayload<T>(binary);
224+
}
225+
226+
template <typename T>
227+
std::vector<T> loadNpyDataFullpath(const std::filesystem::path& path) {
228+
return parseNpyPayload<T>(loadNpyFileBinaryFullpath(path.string()));
229+
}
230+
182231

183232
void compareBinaryFilesHex(const std::string& filename, const std::vector<char> binData, const std::string& expectedBinDataHex) {
184233
std::vector<char> expectedBinData;
@@ -223,6 +272,32 @@ class RecordNodeTests : public testing::Test {
223272
float sampleRate = 1.0;
224273
};
225274

275+
class HardwareSynced_RecordNodeTests : public RecordNodeTests {
276+
protected:
277+
void SetUp() override {
278+
sampleRate = 100.0f;
279+
numChannels = 8;
280+
tester = std::make_unique<ProcessorTester>(TestSourceNodeBuilder
281+
(FakeSourceNodeParams{
282+
numChannels,
283+
sampleRate,
284+
bitVolts,
285+
1,
286+
0,
287+
true
288+
}));
289+
290+
parentRecordingDir = std::filesystem::temp_directory_path() / "record_node_hardware_sync_tests";
291+
if (std::filesystem::exists(parentRecordingDir)) {
292+
std::filesystem::remove_all(parentRecordingDir);
293+
}
294+
std::filesystem::create_directory(parentRecordingDir);
295+
296+
tester->setRecordingParentDirectory(parentRecordingDir.string());
297+
processor = tester->createProcessor<RecordNode>(Plugin::Processor::RECORD_NODE);
298+
}
299+
};
300+
226301
TEST_F(RecordNodeTests, TestInputOutput_Continuous_Single) {
227302
int numSamples = 100;
228303
tester->startAcquisition(true);
@@ -421,6 +496,32 @@ TEST_F(RecordNodeTests, Test_PersistsSampleNumbersAndTimestamps) {
421496
compareBinaryFilesHex("timestamps.npy", timeStampsBin, expectedTimeStampsHex);
422497
}
423498

499+
TEST_F(HardwareSynced_RecordNodeTests, Test_PersistsPerSampleHardwareTimestamps) {
500+
tester->startAcquisition(true);
501+
502+
const int numSamples = 5;
503+
auto firstBuffer = createBuffer(1000.0f, 20.0f, numChannels, numSamples);
504+
std::vector<double> firstTimestamps { 10.000, 10.011, 10.021, 10.034, 10.048 };
505+
writeBlock(firstBuffer, nullptr, firstTimestamps.data());
506+
507+
auto secondBuffer = createBuffer(2000.0f, 20.0f, numChannels, numSamples);
508+
std::vector<double> secondTimestamps { 10.061, 10.073, 10.084, 10.098, 10.113 };
509+
writeBlock(secondBuffer, nullptr, secondTimestamps.data());
510+
511+
tester->stopAcquisition();
512+
513+
auto persistedTimestamps = loadNpyData<double>("timestamps.npy");
514+
515+
std::vector<double> expectedTimestamps = firstTimestamps;
516+
expectedTimestamps.insert(expectedTimestamps.end(), secondTimestamps.begin(), secondTimestamps.end());
517+
518+
ASSERT_EQ(persistedTimestamps.size(), expectedTimestamps.size());
519+
520+
for (size_t index = 0; index < expectedTimestamps.size(); ++index) {
521+
EXPECT_DOUBLE_EQ(persistedTimestamps[index], expectedTimestamps[index]);
522+
}
523+
}
524+
424525
TEST_F(RecordNodeTests, Test_PersistsStructureOeBin) {
425526
tester->startAcquisition(true);
426527

@@ -530,6 +631,37 @@ TEST_F(RecordNodeTests, Test_PersistsEvents) {
530631
compareBinaryFilesHex("full_words.npy", fullWordsBin, expectedFullWordsHex);
531632
}
532633

634+
TEST_F(HardwareSynced_RecordNodeTests, Test_PersistsHardwareEventTimestampFromBlockArray) {
635+
processor->setRecordEvents(true);
636+
processor->updateSettings();
637+
638+
tester->startAcquisition(true);
639+
640+
const int numSamples = 5;
641+
auto streamId = processor->getDataStreams()[0]->getStreamId();
642+
auto eventChannels = tester->getSourceNodeDataStream(streamId)->getEventChannels();
643+
ASSERT_GE(eventChannels.size(), 1);
644+
645+
TTLEventPtr eventPtr = TTLEvent::createTTLEvent(
646+
eventChannels[0],
647+
1,
648+
2,
649+
true);
650+
651+
auto inputBuffer = createBuffer(1000.0f, 20.0f, numChannels, numSamples);
652+
std::vector<double> blockTimestamps { 20.000, 20.031, 20.047, 20.062, 20.081 };
653+
writeBlock(inputBuffer, eventPtr.get(), blockTimestamps.data());
654+
655+
tester->stopAcquisition();
656+
657+
std::filesystem::path timestampPath;
658+
ASSERT_TRUE(eventsPathFor("timestamps.npy", &timestampPath));
659+
auto eventTimestamps = loadNpyDataFullpath<double>(timestampPath);
660+
661+
ASSERT_EQ(eventTimestamps.size(), static_cast<size_t>(1));
662+
EXPECT_DOUBLE_EQ(eventTimestamps[0], blockTimestamps[1]);
663+
}
664+
533665
// ============================================================================
534666
// SEQUENTIAL BLOCK FILE BATCH WRITE TESTS
535667
// ============================================================================

Tests/TestHelpers/Processors/FakeSourceNode.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ void FakeSourceNode::updateSettings()
2020
"FakeSourceNode" + String (i),
2121
"description",
2222
"identifier",
23-
params.sampleRate
23+
params.sampleRate,
24+
params.generatesTimestamps
2425
};
2526

2627
cachedDataStreams.add (new DataStream (settings));

Tests/TestHelpers/Processors/FakeSourceNode.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct FakeSourceNodeParams
1212
float bitVolts = 1.0f;
1313
int streams = 1;
1414
uint32_t metadataSizeBytes = 0;
15+
bool generatesTimestamps = false;
1516
};
1617

1718
class TESTABLE FakeSourceNode : public GenericProcessor

Tests/TestHelpers/include/TestFixtures.h

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,27 +230,43 @@ class ProcessorTester
230230
AudioBuffer<float> processBlock (
231231
GenericProcessor* processor,
232232
const AudioBuffer<float>& buffer,
233-
TTLEvent* maybeTtlEvent = nullptr)
233+
TTLEvent* maybeTtlEvent = nullptr,
234+
const double* sampleTimestamps = nullptr)
234235
{
235236
auto audioProcessor = (AudioProcessor*) processor;
236237
auto dataStreams = processor->getDataStreams();
238+
auto* sourceProcessor = getSourceNode();
237239

238240
MidiBuffer eventBuffer;
239241
for (const auto* datastream : dataStreams)
240242
{
241243
HeapBlock<char> data;
242244
auto streamId = datastream->getStreamId();
245+
double startTimestamp = sampleTimestamps != nullptr ? sampleTimestamps[0] : 0.0;
243246
size_t dataSize = SystemEvent::fillTimestampAndSamplesData (
244247
data,
245-
processor,
248+
sourceProcessor,
246249
streamId,
247250
currentSampleIndex,
248-
// NOTE: this timestamp is actually ignored in the current implementation?
249-
0,
251+
startTimestamp,
250252
buffer.getNumSamples(),
251253
0);
252254
eventBuffer.addEvent (data, dataSize, 0);
253255

256+
if (sampleTimestamps != nullptr)
257+
{
258+
HeapBlock<char> timestampData;
259+
size_t timestampDataSize = SystemEvent::fillTimestampArrayData (
260+
timestampData,
261+
sourceProcessor,
262+
streamId,
263+
currentSampleIndex,
264+
sampleTimestamps,
265+
buffer.getNumSamples(),
266+
0);
267+
eventBuffer.addEvent (timestampData, timestampDataSize, 0);
268+
}
269+
254270
if (maybeTtlEvent != nullptr)
255271
{
256272
size_t ttlSize = maybeTtlEvent->getChannelInfo()->getDataSize() + maybeTtlEvent->getChannelInfo()->getTotalEventMetadataSize() + EVENT_BASE_SIZE;

0 commit comments

Comments
 (0)