-
jsiegle authored
Some caveats: - If a source doesn't generate its own timestamps, all timestamps will be zero. This is obviously bad, so we need a way to protect this from happening. - If there are multiple timestamps from different sources, they might conflict. Again, something needs to be done about this in the near future. - If only a 32-bit timestamp is given, it needs to be shifted by 8 bits to fit in our 64-bit timestamp slot
jsiegle authoredSome caveats: - If a source doesn't generate its own timestamps, all timestamps will be zero. This is obviously bad, so we need a way to protect this from happening. - If there are multiple timestamps from different sources, they might conflict. Again, something needs to be done about this in the near future. - If only a 32-bit timestamp is given, it needs to be shifted by 8 bits to fit in our 64-bit timestamp slot
RecordNode.cpp 15.04 KiB
/*
------------------------------------------------------------------
This file is part of the Open Ephys GUI
Copyright (C) 2013 Open Ephys
------------------------------------------------------------------
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "RecordNode.h"
#include "ProcessorGraph.h"
#include "Channel.h"
RecordNode::RecordNode()
: GenericProcessor("Record Node"),
isRecording(false), isProcessing(false), signalFilesShouldClose(false),
timestamp(0)
{
continuousDataIntegerBuffer = new int16[10000];
continuousDataFloatBuffer = new float[10000];
signalFilesShouldClose = false;
settings.numInputs = 256;
settings.numOutputs = 0;
eventChannel = new Channel(this, 0);
eventChannel->isEventChannel = true;
recordMarker = new char[10];
for (int i = 0; i < 9; i++)
{
recordMarker[i] = 0;
}
recordMarker[9] = 255;
// 128 inputs, 0 outputs
setPlayConfigDetails(getNumInputs(),getNumOutputs(),44100.0,128);
}
RecordNode::~RecordNode()
{
}
void RecordNode::setChannel(Channel* ch)
{
int channelNum = channelPointers.indexOf(ch);
std::cout << "Record node setting channel to " << channelNum << std::endl;
setCurrentChannel(channelNum);
// for (int i = 0; i < con.size(); i++)
// {
// if (continuousChannels[i].nodeId == id &&
// continuousChannels[i].chan == chan)
// {
// std::cout << "Found channel " << i << std::endl;
// setCurrentChannel(i);
// break;
// }
// }
}
void RecordNode::setChannelStatus(Channel* ch, bool status)
{
//std::cout << "Setting channel status!" << std::endl;
setChannel(ch);
if (status)
setParameter(2, 1.0f);
else
setParameter(2, 0.0f);
}
// void RecordNode::enableCurrentChannel(bool state)
// {
// continuousChannels[nextAvailableChannel].isRecording = state;
// }
void RecordNode::resetConnections()
{
//std::cout << "Resetting connections" << std::endl;
nextAvailableChannel = 0;
wasConnected = false;
channelPointers.clear();
eventChannelPointers.clear();
}
void RecordNode::filenameComponentChanged(FilenameComponent* fnc)
{
std::cout << "Got a new file" << std::endl;
dataDirectory = fnc->getCurrentFile();
std::cout << "File name: " << dataDirectory.getFullPathName();
if (dataDirectory.isDirectory())
std::cout << " is a directory." << std::endl;
else
std::cout << " is NOT a directory." << std::endl;
createNewDirectory();
}
void RecordNode::addInputChannel(GenericProcessor* sourceNode, int chan)
{
if (chan != getProcessorGraph()->midiChannelIndex)
{
int channelIndex = getNextChannel(false);
setPlayConfigDetails(channelIndex+1,0,44100.0,128);
channelPointers.add(sourceNode->channels[chan]);
// std::cout << channelIndex << std::endl;
updateFileName(channelPointers[channelIndex]);
//if (channelPointers[channelIndex]->isRecording)
// std::cout << " This channel will be recorded." << std::endl;
//else
// std::cout << " This channel will NOT be recorded." << std::endl;
//std::cout << "adding channel " << getNextChannel(false) << std::endl;
//std::pair<int, Channel> newPair (getNextChannel(false), newChannel);
//std::cout << "adding channel " << getNextChannel(false) << std::endl;
//continuouschannelPointers.insert(newPair);
}
else
{
for (int n = 0; n < sourceNode->eventChannels.size(); n++)
{
eventChannelPointers.add(sourceNode->eventChannels[n]);
}
}
}
void RecordNode::updateFileName(Channel* ch)
{
String filename = rootFolder.getFullPathName();
filename += rootFolder.separatorString;
if (!ch->isEventChannel)
{
filename += ch->nodeId;
filename += "_";
filename += ch->name;
filename += ".continuous";
}
else
{
filename += "all_channels.events";
}
ch->filename = filename;
ch->file = 0;
//std::cout << "Updating " << filename << std::endl;
}
void RecordNode::createNewDirectory()
{
std::cout << "Creating new directory." << std::endl;
rootFolder = File(dataDirectory.getFullPathName() + File::separator + generateDirectoryName());
updateFileName(eventChannel);
for (int i = 0; i < channelPointers.size(); i++)
{
updateFileName(channelPointers[i]);
}
}
String RecordNode::generateDirectoryName()
{
Time calendar = Time::getCurrentTime();
Array<int> t;
t.add(calendar.getYear()-2000);
t.add(calendar.getMonth()+1); // January = 0
t.add(calendar.getDayOfMonth());
t.add(calendar.getHours());
t.add(calendar.getMinutes());
t.add(calendar.getSeconds());
String filename = "";
for (int n = 0; n < t.size(); n++)
{
if (t[n] < 10)
filename += "0";
filename += t[n];
if (n == 2)
filename += "_";
else if (n < 5)
filename += "-";
}
return filename;
}
String RecordNode::generateDateString()
{
Time calendar = Time::getCurrentTime();
String datestring;
datestring += String(calendar.getDayOfMonth());
datestring += "-";
datestring += calendar.getMonthName(true);
datestring += "-";
datestring += String(calendar.getYear());
datestring += " ";
datestring += calendar.getHours();
datestring += ":";
datestring += calendar.getMinutes();
datestring += ":";
datestring += calendar.getSeconds();
return datestring;
}
void RecordNode::setParameter(int parameterIndex, float newValue)
{
// 0 = stop recording
// 1 = start recording
// 2 = toggle individual channel (0.0f = OFF, anything else = ON)
if (parameterIndex == 1)
{
isRecording = true;
std::cout << "START RECORDING." << std::endl;
if (!rootFolder.exists())
rootFolder.createDirectory();
openFile(eventChannel);
// create / open necessary files
for (int i = 0; i < channelPointers.size(); i++)
{
if (channelPointers[i]->isRecording)
{
openFile(channelPointers[i]);
}
}
}
else if (parameterIndex == 0)
{
std::cout << "STOP RECORDING." << std::endl;
if (isRecording)
{
// close necessary files
signalFilesShouldClose = true;
}
isRecording = false;
}
else if (parameterIndex == 2)
{
if (isProcessing)
{
std::cout << "Toggling channel " << currentChannel << std::endl;
if (newValue == 0.0f)
{
channelPointers[currentChannel]->isRecording = false;
if (isRecording)
{
closeFile(channelPointers[currentChannel]);
}
}
else
{
channelPointers[currentChannel]->isRecording = true;
if (isRecording)
{
openFile(channelPointers[currentChannel]);
}
}
}
}
}
void RecordNode::openFile(Channel* ch)
{
std::cout << "OPENING FILE: " << ch->filename << std::endl;
File f = File(ch->filename);
FILE* chFile;
bool fileExists = f.exists();
chFile = fopen(ch->filename.toUTF8(), "ab");
if (!fileExists)
{
// create and write header
std::cout << "Writing header." << std::endl;
String header = generateHeader(ch);
//std::cout << header << std::endl;
std::cout << "File ID: " << chFile << ", number of bytes: " << header.getNumBytesAsUTF8() << std::endl;
fwrite(header.toUTF8(), 1, header.getNumBytesAsUTF8(), chFile);
std::cout << "Wrote header." << std::endl;
}
else
{
std::cout << "File already exists, just opening." << std::endl;
}
//To avoid a race condition resulting on data written before the header,
//do not assign the channel pointer until the header has been written
ch->file = chFile;
}
void RecordNode::closeFile(Channel* ch)
{
std::cout << "CLOSING FILE: " << ch->filename << std::endl;
if (ch->file != NULL)
fclose(ch->file);
}
String RecordNode::generateHeader(Channel* ch)
{
String header = "header.format = 'OPEN EPHYS DATA FORMAT v0.0'; \n";
header += "header.header_bytes = ";
header += String(HEADER_SIZE);
header += ";\n";
if (ch->isEventChannel)
{
header += "header.description = 'each record contains one 64-bit timestamp, one 16-bit sample position, one uint8 event type, one uint8 processor ID, one uint8 event ID, and one uint8 event channel'; \n";
}
else
{
header += "header.description = 'each record contains one 64-bit timestamp, one 16-bit sample count (N), N 16-bit samples, and one 10-byte record marker (0 0 0 0 0 0 0 0 0 255)'; \n";
}
header += "header.date_created = '";
header += generateDateString();
header += "';\n";
header += "header.channel = '";
header += ch->name;
header += "';\n";
if (ch->isEventChannel)
{
header += "header.channelType = 'Event';\n";
}
else
{
header += "header.channelType = 'Continuous';\n";
header += "header.sampleRate = ";
header += String(ch->sampleRate);
header += ";\n";
}
header += "header.bitVolts = ";
header += String(ch->bitVolts);
header += ";\n";
header = header.paddedRight(' ', HEADER_SIZE);
//std::cout << header << std::endl;
return header;
}
void RecordNode::closeAllFiles()
{
for (int i = 0; i < channelPointers.size(); i++)
{
if (channelPointers[i]->isRecording)
{
closeFile(channelPointers[i]);
}
}
closeFile(eventChannel);
}
bool RecordNode::enable()
{
//updateFileName(eventChannel);
isProcessing = true;
return true;
}
bool RecordNode::disable()
{
// close files if necessary
setParameter(0, 10.0f);
isProcessing = false;
return true;
}
float RecordNode::getFreeSpace()
{
return 1.0f - float(dataDirectory.getBytesFreeOnVolume())/float(dataDirectory.getVolumeTotalSize());
}
void RecordNode::writeContinuousBuffer(float* data, int nSamples, int channel)
{
if (channelPointers[channel]->file == NULL)
return;
float scaleFactor = float(0x7fff) * channelPointers[channel]->bitVolts;
// scale the data appropriately -- currently just getting it into the right
// range; actually need to take into account the gain of each channel
for (int n = 0; n < nSamples; n++)
{
*(continuousDataFloatBuffer+n) = *(data+n) / scaleFactor; // 10000.0f;
}
// find file and write samples to disk
//if (nSamples < 1000) // this is temporary, but there seems to be an error reading in the data if too many samples are written
// in the first few blocks
//{
AudioDataConverters::convertFloatToInt16BE(continuousDataFloatBuffer, continuousDataIntegerBuffer, nSamples);
int16 samps = (int16) nSamples;
//std::cout << samps << std::endl;
fwrite(×tamp, // ptr
8, // size of each element
1, // count
channelPointers[channel]->file); // ptr to FILE object
fwrite(&samps, // ptr
2, // size of each element
1, // count
channelPointers[channel]->file); // ptr to FILE object
fwrite(continuousDataIntegerBuffer, // ptr
2, // size of each element
nSamples, // count
channelPointers[channel]->file); // ptr to FILE object
// FIXME: ensure fwrite returns equal "count"; otherwise,
// there was an error.
// write a 10-byte marker indicating the end of a record
fwrite(recordMarker, // ptr
1, // size of each element
10, // count
channelPointers[channel]->file); // ptr to FILE object
//}
}
void RecordNode::writeEventBuffer(MidiMessage& event, int samplePosition) //, int node, int channel)
{
// find file and write samples to disk
//std::cout << "Received event!" << std::endl;
const uint8* dataptr = event.getRawData();
int16 samplePos = (int16) samplePosition;
// write timestamp (for buffer only, not the actual event timestamp!!!!!)
fwrite(×tamp, // ptr
8, // size of each element
1, // count
eventChannel->file); // ptr to FILE object
fwrite(&samplePos, // ptr
2, // size of each element
1, // count
eventChannel->file); // ptr to FILE object
// write 1st four bytes of event (type, nodeId, eventId, eventChannel)
fwrite(dataptr, 1, 4, eventChannel->file);
}
void RecordNode::handleEvent(int eventType, MidiMessage& event, int samplePosition)
{
if (eventType == TTL)
{
writeEventBuffer(event, samplePosition);
}
else if (eventType == TIMESTAMP)
{
const uint8* dataptr = event.getRawData();
memcpy(×tamp, dataptr, 8);
}
}
void RecordNode::process(AudioSampleBuffer& buffer,
MidiBuffer& events,
int& nSamples)
{
//std::cout << "Record node processing block." << std::endl;
//std::cout << "Num channels: " << buffer.getNumChannels() << std::endl;
if (isRecording)
{
//timestamp = timer.getHighResolutionTicks();
// WHY IS THIS AFFECTING THE LFP DISPLAY?
//buffer.applyGain(0, nSamples, 5.2438f);
// cycle through events -- extract the TTLs and the timestamps
checkForEvents(events);
// cycle through buffer channels
if (channelPointers.size() > 0)
{
for (int i = 0; i < buffer.getNumChannels(); i++)
{
if (channelPointers[i]->isRecording)
{
// write buffer to disk!
writeContinuousBuffer(buffer.getSampleData(i),
nSamples,
i);
//std::cout << "Record channel " << i << std::endl;
}
}
}
return;
}
// this is intended to prevent parameter changes from closing files
// before recording stops
if (signalFilesShouldClose)
{
closeAllFiles();
signalFilesShouldClose = false;
}
}