diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f3d6549d836622181a9d050a803b132fb753c32b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ccd70e9a9f1cc3ea46e4b4223602374bf6c0bb44 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.1) +if(POLICY CMP0063) + cmake_policy(SET CMP0063 NEW) +endif() + +project(jasonrec) +set(JASONREC_VERSION "2.0") +add_definitions(-DJASONREC_VERSION="${JASONREC_VERSION}") + +find_path(LIBUSB_INCLUDE_DIR + NAMES libusb.h + PATH_SUFFIXES "include" "libusb" "libusb-1.0") +find_library(LIBUSB_LIBRARY + NAMES usb usb-1.0 + PATH_SUFFIXES "lib" "lib32" "lib64") + +add_subdirectory(src) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..82994a1745c76a1fa435a0d2dbbc3b3b1fa8e7c4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Université de Toulon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..93dfcc2864ccde39e62f6dad09956e55549f2890 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +SMIoT JASON Qualilife sound recorder +==================================== + +This repository provides `jasonrec`, a command line application to record audio +samples from the JASON Qualilife sound card developed by SMIoT. + +Prerequisites +------------- +To build and run the application, you will need: + +* a C++ compiler supporting C++11 (e.g., g++ 4.8.1 and above) +* the `libusb` library and header +* CMake 3.1 or above (optional) + +On a debian-based Linux system, these can be installed with: +``` +sudo apt install g++ libusb-1.0-0-dev cmake +``` + +Compilation / Installation +-------------------------- +Clone the repository somewhere or download and extract it. + +### Using CMake +If cmake is available, create a build directory, compile, and install: +``` +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make +sudo make install/strip +``` +This will install globally; pass `-DCMAKE_INSTALL_PREFIX=/some/directory` to +the `cmake` call to install to `/some/directory` instead. It is also possible +to run the compiled application from the `src` subdirectory of the `build` +directory, skipping installation altogether. + +### Without CMake +If cmake is not available, you can still try to compile it manually. Just make +sure to link against `libusb`. For `g++`, an example `Makefile` is included in +the `src` directory, so the following may work: +``` +cd src +make +``` + +### Under Windows +As for any other platform, there are multiple options on Windows. The following +has been tested successfully: Install CMake using the MSI installer from +https://cmake.org, install the Microsoft Visual Studio Build Tools from +https://aka.ms/buildtools (specifically, the C++ compiler), download and extract +the precompiled Windows binaries from https://libusb.info. Open the x64 Native +Tools Command Prompt and navigate to the source directory. Run the following: +``` +mkdir build +mkdir install +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DLIBUSB_INCLUDE_DIR=<libusb_dir>/include -DLIBUSB_LIBRARY=<libusb_dir>/MS64/dll/libusb-1.0.lib -DCMAKE_INSTALL_PREFIX=../install +nmake +nmake install +``` +Replace `<libusb_dir>` with the path you extracted libusb to. If compilation and +installation succeeded, you will find a `jasonrec.exe` in `install/bin`. Copy +the `MS64/dll/libusb-1.0.dll` file from the libusb directory into `install/bin`. +You can now run `jasonrec.exe` from a command prompt, or by creating a shortcut +to it that includes suitable command line options. + + +Usage +----- +Running `jasonrec` without any arguments (or with any unsupported number of +arguments, in fact) will display information on its usage: +``` +SMIoT JASON Qualilife sound recorder v1.3 +Usage:jasonrec channels rate filename [--help, -h] [--chunk_len, -c CHUNK_LEN] [--total_len, -t TOTAL_LEN] [--device, -d DEVICE] [--bit_depth, -b BIT_DEPTH] [--imu, -i IMU] [--filter, -f FILTER] [--verbose, -v] +Positional arguments: + CHANNELS: number of channels to record (1 to 5) + RATE: sample rate in Hz to record at (integral number) + FILENAME: output file name. should include strftime() format specifiers + if CHUNK_LEN is specified. For miliseconds, use %z. Example: location/recording_%Y%m%d_%H%M%S_%z.wav +Optional arguments: +-h, --help show this help message and exit + --bit_depth, -b BIT_DEPTH: Size of each samples in bits. Must be a multiple of 8. (Default: 16) + --imu, -i IMU: IMU file name. Similar to FILENAME. Disable by default. + --filter, -f FILTER: Number of the filter to use. Must be between 0 and 2. (Default: 0) + --chunk_len, -c CHUNK_LEN: length per output file in seconds; will start a new file whenever + this length is reached. If not given or zero, will record a single file. + --total_len, -t TOTAL_LEN: Total recording length; will stop when this length is reached. + If not given or zero, will record continuously until killed. + --device, -d DEVICE: Which device to use in case multiple JASON cards are connected, + where 0 is the first, 1 is the second card found (and so on). + --verbose, -v Enable the printing of status message +``` + +As an example, to record a single 30-minute file of 2 channels at 16 kHz, run: +``` +jasonrec 2 16000 recording.wav -t 1800 +``` + +To record 4 channels at 128 kHz sample rate in 5-minute chunks with filenames +based on time stamps, without stopping, run: +``` +jasonrec 4 128000 %Y-%m-%d_%H-%M-%S.wav -c 300 +``` +To record the same 4 channels at 128 kHz sample rate in 5-minute chunks with filenames +based on time stamps, without stopping, but with the saving of the imu data run: +``` +jasonrec 4 128000 %Y-%m-%d_%H-%M-%S.wav -c 300 -i %Y-%m-%d_%H-%M-%S.csv +``` +File names may also include directory names based on time stamps, but the +directories have to be created in advance. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..3a9ed88ff9ef8b948129bd0d5613341499e14236 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(jasonrec + recorder.cpp + filewriter.cpp + cleanexit.cpp + main.cpp) +# compile as C++11 +set_property(TARGET jasonrec + PROPERTY CXX_STANDARD 11) +# do not export any symbols by default +set_property(TARGET jasonrec + PROPERTY CXX_VISIBILITY_PRESET hidden) +set_property(TARGET jasonrec + PROPERTY VISIBILITY_INLINES_HIDDEN true) + +target_include_directories(jasonrec + PRIVATE ${LIBUSB_INCLUDE_DIR}) +target_link_libraries(jasonrec + ${LIBUSB_LIBRARY}) + +install(TARGETS jasonrec DESTINATION bin) diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4ddfea9fa111a0fb4f45ee01323cefacf6c14076 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,3 @@ +# If you cannot build with cmake, this file should work for debian-based Linux. +all: + g++ -std=c++11 main.cpp recorder.cpp filewriter.cpp -o jasonrec -lusb-1.0 -I/usr/include/libusb-1.0/ diff --git a/src/cleanexit.cpp b/src/cleanexit.cpp new file mode 100644 index 0000000000000000000000000000000000000000..bd977c8327f21ed2dfce4625bb2d89edffabcce8 --- /dev/null +++ b/src/cleanexit.cpp @@ -0,0 +1,35 @@ +/** + * Simple signal handler for clean termination of an application. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + */ + +#include <atomic> +#include <csignal> +#ifdef __unix__ +#include <unistd.h> +#endif + +std::atomic<bool> _exit_requested(false); + +void caught_signal(int) +{ + _exit_requested.store(true); +} + +void allow_clean_exit() { +#ifdef __unix__ + struct sigaction sa = {0}; + sa.sa_handler = caught_signal; + sigfillset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); +#else + signal(SIGINT, caught_signal); + signal(SIGTERM, caught_signal); +#endif +} + +bool exit_requested() { + return _exit_requested.load(); +} diff --git a/src/cleanexit.h b/src/cleanexit.h new file mode 100644 index 0000000000000000000000000000000000000000..24a88dd473897e30a69d9f3010128315095de43b --- /dev/null +++ b/src/cleanexit.h @@ -0,0 +1,21 @@ +/** + * Simple signal handler for clean termination of an application. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + */ + +#ifndef CLEANEXIT_H +#define CLEANEXIT_H + +/** + * Set up signal handlers to make exit_requested() work. + */ +void allow_clean_exit(); + +/** + * Check if the application was requested to terminate. + * \returns whether the application was requested to terminate. + */ +bool exit_requested(); + +#endif diff --git a/src/filewriter.cpp b/src/filewriter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..01009d01be8de7a453518546a3110b63ff266229 --- /dev/null +++ b/src/filewriter.cpp @@ -0,0 +1,602 @@ +/** + * Wave file writing, with or without splitting by length. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + * Author: Maxence Ferrari <maxence.ferrari@lis-lab.fr> + */ + +#include "filewriter.h" +#include "macros.h" +#include <stdexcept> +#include <vector> +#include <iostream> +#include <chrono> +#include <sstream> +#include <iomanip> +#include <cstring> +#include <cmath> + +FileWriter::FileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth) : + filename_template(filename_template), qhb_version(qhb_version), num_channels(num_channels), sample_rate(sample_rate), depth(depth) { + // nothing +} + +FileWriter::~FileWriter() { + // nothing +} + +std::string FileWriter::generate_filename() { + // this has of course nothing to do with file writing and could be pulled + // out, but I doubt anybody will ever care + using namespace std::chrono; + auto miliseconds = duration_cast<milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count(); + long seconds = miliseconds/1000; + struct tm *timeinfo = localtime(&seconds); + + size_t found = filename_template.find("%z"); + while(found != std::string::npos){ + std::stringstream ss; + ss << std::setw(3) << std::setfill('0') << miliseconds%1000; + std::string s = ss.str(); + filename_template.replace(found, 2, s); + found = filename_template.find("%z", found+3); + } + + size_t length = 0; + size_t space = 0; + std::vector<char> buffer; + while (!length) { + space += 100; + buffer.resize(filename_template.size() + space); + length = strftime(buffer.data(), buffer.size(), + filename_template.c_str(), timeinfo); + } + return std::string(buffer.begin(), buffer.begin() + length); +} + +void FileWriter::write(std::vector<uint8_t> &samples, std::vector<uint8_t> &imu_data) { + write(samples.data(), samples.size(), imu_data.data()); +} + + +void store_little_endian(uint8_t (&target)[2], uint16_t value) { + target[0] = value & 0xFF; + target[1] = (value >> 8) & 0xFF; +} + +void store_little_endian(uint8_t (&target)[4], uint32_t value) { + target[0] = value & 0xFF; + target[1] = (value >> 8) & 0xFF; + target[2] = (value >> 16) & 0xFF; + target[3] = (value >> 24) & 0xFF; +} + +uint32_t read_little_endian(uint8_t (&source)[4]) { + return (source[0] + source[1] << 8 + source[2] << 16 + source[3] << 24); +} + +WavFileWriter::WavFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth) : + WavFileWriter(filename_template, qhb_version, num_channels, sample_rate, depth, 0) { + // nothing +} + +WavFileWriter::WavFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth, + size_t expected_num_samples) : + FileWriter(filename_template, qhb_version, num_channels, sample_rate, depth), + outfile(generate_filename(), std::ios::out | std::ios::binary | std::ios::trunc), + samples_written(0) { + // check if we could open the file + if (!outfile.is_open()) { + throw std::runtime_error("could not create output file"); + } + // write header + store_little_endian(header.fmt_channels, num_channels); + store_little_endian(header.fmt_sample_rate, sample_rate); + store_little_endian(header.fmt_bits_per_sample, 8 * depth); + store_little_endian(header.fmt_byte_rate, num_channels * sample_rate * depth); + store_little_endian(header.fmt_frame_size, num_channels * depth); + if (expected_num_samples) { + size_t expected_data_size = expected_num_samples * num_channels * depth; + store_little_endian(header.data_size, expected_data_size); + store_little_endian(header.chunk_size, expected_data_size + 36); + // TODO: on linux, we could use fallocate to reserve the final size + } + // TODO: on posix, we could use posix_fadvice to indicate sequential access + outfile.seekp(0); + outfile.write((char*) &header, sizeof(header)); +} + +WavFileWriter::~WavFileWriter() { + // finalize header, if needed + size_t data_size = samples_written * num_channels * depth; + if (data_size != read_little_endian(header.data_size)) { + store_little_endian(header.data_size, data_size); + store_little_endian(header.chunk_size, data_size + 36); + outfile.seekp(0); + outfile.write((char*) &header, sizeof(header)); + } +} + +void WavFileWriter::write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) { + outfile.write((char*) samples, num_samples * sizeof(*samples)); + samples_written += num_samples /(num_channels * depth); +} + +IMUFileWriter::IMUFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate,size_t depth, size_t timestamp) : + FileWriter(filename_template, qhb_version, num_channels, sample_rate, depth), + outfile(generate_filename(), + std::ios::out | std::ios::trunc), + last_timestamp(0), + rcvState(StateReception::Waiting), + msgDecodedFunction(0), + msgDecodedPayloadLength(0), + msgDecodedPayload(nullptr), + msgDecodedPayloadIndex(0), + msgDecoded(0) { + outfile << header; +} + +inline float le16tof(uint8_t *array){ + return static_cast<float>(static_cast<int16_t>(__builtin_bswap16(*reinterpret_cast<uint16_t*>(array)))); +} + +float IMUFileWriter::GetFloatSafe(const unsigned char *p, int index) { + unsigned char tmp[4]; + std::memcpy(tmp, p + index, 4); // Copy 4 bytes from p + index into tmp + + float result; + std::memcpy(&result, tmp, sizeof(result)); // Copy bytes from tmp into result + + return result; +} + +void IMUFileWriter::DecodeMessage(unsigned char c) { + switch (rcvState) { + case StateReception::Waiting: + if (c == 0xFE) + rcvState = StateReception::FunctionMSB; + break; + case StateReception::FunctionMSB: + msgDecodedFunction = static_cast<int16_t>(c << 8); + rcvState = StateReception::FunctionLSB; + break; + case StateReception::FunctionLSB: + msgDecodedFunction += static_cast<int16_t>(c << 0); + rcvState = StateReception::PayloadLengthMSB; + break; + case StateReception::PayloadLengthMSB: + msgDecodedPayloadLength = static_cast<uint16_t>(c << 8); + rcvState = StateReception::PayloadLengthLSB; + break; + case StateReception::PayloadLengthLSB: + msgDecodedPayloadLength += static_cast<uint16_t>(c << 0); + if (msgDecodedPayloadLength > 0) { + if (msgDecodedPayloadLength < 1024) { + msgDecodedPayloadIndex = 0; + msgDecodedPayload = static_cast<unsigned char*>(malloc(msgDecodedPayloadLength)); + if (msgDecodedPayload == nullptr) { + throw std::bad_alloc(); // Handle memory allocation failure + } + rcvState = StateReception::Payload; + } else { + rcvState = StateReception::Waiting; + } + } else + rcvState = StateReception::Decode; + break; + case StateReception::Payload: + if (msgDecodedPayloadIndex > msgDecodedPayloadLength) + { + //Erreur + msgDecodedPayloadIndex = 0; + rcvState = StateReception::Waiting; + } + msgDecodedPayload[msgDecodedPayloadIndex++] = c; + if (msgDecodedPayloadIndex >= msgDecodedPayloadLength) + { + rcvState = StateReception::Decode; + msgDecodedPayloadIndex = 0; + } + break; + case StateReception::Decode: + { + //Lance l'event de fin de decodage + ProcessDecodedMessage(msgDecodedFunction, msgDecodedPayloadLength, msgDecodedPayload); + msgDecoded++; + rcvState = StateReception::Waiting; + } + break; + default: + rcvState = StateReception::Waiting; + break; + } +} + +void IMUFileWriter::ProcessDecodedMessage(int msgFunction, int msgPayloadLength, const unsigned char* msgPayload) { + unsigned int timeStamp = 0; + switch(static_cast<short>(msgFunction)) { + case static_cast<short>(HS_DATA_PACKET_FULL_TIMESTAMP): { + IMUFileWriter::SensorType sensorType = static_cast<SensorType>(msgPayload[0]); + unsigned char id = msgPayload[1]; + unsigned char nbChannels = msgPayload[2]; + unsigned char range = msgPayload[3]; + unsigned char resolutionBits = msgPayload[4]; + unsigned short samplingFrequency = BUILD_UINT16(msgPayload[6], msgPayload[5]); + unsigned short nbSamples = BUILD_UINT16(msgPayload[8], msgPayload[7]); + + int lengthPerSample = nbChannels * resolutionBits / 8 + 4; + double accelMaxValue = pow(2, resolutionBits)/2; + double gyroMaxValue = pow(2, resolutionBits) / 2; + double gyroRange = 250.0; //Hardcode pour le moment + double magRange = 4900.0; //Fixe + + for(int i=0; i < nbSamples && msgPayloadLength >= lengthPerSample * i + 9; i++) { + timeStamp = BUILD_UINT32(msgPayload[9 + i * lengthPerSample+3], msgPayload[9 + i * lengthPerSample+2], msgPayload[9 + i * lengthPerSample+1], msgPayload[9 + i * lengthPerSample]); + + if(timeStamp > lastTimeStamp) { + lastTimeStamp = timeStamp; + switch(sensorType) { + case IMUFileWriter::SensorType::IMU: { + outfile << timeStamp; + outfile << ", " << BUILD_UINT16(msgPayload[13 + i * lengthPerSample],msgPayload[13 + i * lengthPerSample+1]); // AccelX + outfile << ", " << BUILD_UINT16(msgPayload[15 + i * lengthPerSample],msgPayload[15 + i * lengthPerSample+1]); // AccelY + outfile << ", " << BUILD_UINT16(msgPayload[17 + i * lengthPerSample],msgPayload[17 + i * lengthPerSample+1]); // AccelZ + outfile << ", " << BUILD_UINT16(msgPayload[19 + i * lengthPerSample],msgPayload[19 + i * lengthPerSample+1]); // GyroX + outfile << ", " << BUILD_UINT16(msgPayload[21 + i * lengthPerSample],msgPayload[21 + i * lengthPerSample+1]); // GyroY + outfile << ", " << BUILD_UINT16(msgPayload[23 + i * lengthPerSample],msgPayload[23 + i * lengthPerSample+1]); // GyroZ + outfile << ", " << BUILD_UINT16(msgPayload[25 + i * lengthPerSample],msgPayload[25 + i * lengthPerSample+1]); // MagX + outfile << ", " << BUILD_UINT16(msgPayload[27 + i * lengthPerSample],msgPayload[27 + i * lengthPerSample+1]); // MagY + outfile << ", " << BUILD_UINT16(msgPayload[29 + i * lengthPerSample],msgPayload[29 + i * lengthPerSample+1]); // MagZ + outfile << std::endl; + } + break; + case IMUFileWriter::SensorType::Accel: + break; + case IMUFileWriter::SensorType::Gyro: + break; + case IMUFileWriter::SensorType::Mag: + break; + case IMUFileWriter::SensorType::Temperature: + break; + case IMUFileWriter::SensorType::Pressure: + break; + case IMUFileWriter::SensorType::Light: + break; + default: + break; + } + } else { + outfile << "TS IMU Error" << std::endl; + } + } + } + break; + case static_cast<short>(HS_DATA_PACKET_FULL_TIMESTAMP_V2): { + IMUFileWriter::SensorType sensorType = static_cast<SensorType>(msgPayload[0]); + unsigned char id = msgPayload[1]; + unsigned char nbChannels = msgPayload[2]; + float rangeScale = GetFloatSafe(msgPayload, 3); + unsigned char resolutionBits = msgPayload[7]; + float samplingFrequency = GetFloatSafe(msgPayload, 8); + unsigned short nbSamples = msgPayload[12]; + unsigned char dataSize = (resolutionBits / 8); + + int lengthPerSample = nbChannels * resolutionBits / 8 + 4; + double dataMaxValue = std::pow(2, resolutionBits) / 2.0; + + for(int i = 0; i < nbSamples && msgPayloadLength >= static_cast<size_t>(lengthPerSample * i + 13); i++) { + uint32_t timeStamp = BUILD_UINT32(msgPayload[13 + i * lengthPerSample+3],msgPayload[13 + i * lengthPerSample+2],msgPayload[13 + i * lengthPerSample+1],msgPayload[13 + i * lengthPerSample]); + switch(sensorType) { + case IMUFileWriter::SensorType::Accel: + if (lastAccelTimeStamp >= 500000000) + lastAccelTimeStamp = 0; + if (timeStamp > lastAccelTimeStamp) { + lastAccelTimeStamp = timeStamp; + outfile << "ACCEL, " << timeStamp; + outfile << ", " << BUILD_INT16(msgPayload[17 + i * lengthPerSample],msgPayload[17 + i * lengthPerSample+1]) * ( rangeScale / dataMaxValue ); + outfile << ", " << BUILD_INT16(msgPayload[17 + dataSize + i * lengthPerSample],msgPayload[17 +dataSize + i * lengthPerSample+1]) * ( rangeScale / dataMaxValue ); + outfile << ", " << BUILD_INT16(msgPayload[17 + 2*dataSize + i * lengthPerSample],msgPayload[17 +2*dataSize + i * lengthPerSample+1]) * ( rangeScale / dataMaxValue ); + outfile << std::endl; + } + else { + //printf("TS Accel Error\n"); + } + break; + case IMUFileWriter::SensorType::Gyro: + if (lastGyroTimeStamp >= 500000000) + lastGyroTimeStamp = 0; + if (timeStamp > lastGyroTimeStamp) { + lastGyroTimeStamp = timeStamp; + outfile << "GYRO, " << timeStamp; + outfile << ", " << BUILD_INT16(msgPayload[17 + i * lengthPerSample],msgPayload[17 + i * lengthPerSample+1]) * ( rangeScale / dataMaxValue); + outfile << ", " << BUILD_INT16(msgPayload[17 + dataSize + i * lengthPerSample],msgPayload[17 +dataSize + i * lengthPerSample+1]) * ( rangeScale / dataMaxValue); + outfile << ", " << BUILD_INT16(msgPayload[17 + 2*dataSize + i * lengthPerSample],msgPayload[17 +2*dataSize + i * lengthPerSample+1]) * ( rangeScale / dataMaxValue); + outfile << std::endl; + } + else { + //printf("TS Gyro Error\n"); + } + break; + case IMUFileWriter::SensorType::Mag: + if (lastMagTimeStamp >= 500000000) + lastMagTimeStamp = 0; + if (timeStamp > lastMagTimeStamp) { + lastMagTimeStamp = timeStamp; + outfile << "MAG, " << timeStamp; + outfile << ", " << BUILD_INT16(msgPayload[17 + i * lengthPerSample+1],msgPayload[17 + i * lengthPerSample]) * ( rangeScale / dataMaxValue); + outfile << ", " << BUILD_INT16(msgPayload[17 + dataSize + i * lengthPerSample+1],msgPayload[17 +dataSize + i * lengthPerSample]) * ( rangeScale / dataMaxValue); + outfile << ", " << BUILD_INT16(msgPayload[17 + 2*dataSize + i * lengthPerSample+1],msgPayload[17 +2*dataSize + i * lengthPerSample]) * ( rangeScale / dataMaxValue); + outfile << std::endl; + } + else { + //printf("TS Mag Error\n"); + } + break; + case IMUFileWriter::SensorType::Temperature: + if (lastTemperatureTimeStamp >= 500000000) + lastTemperatureTimeStamp = 0; + if (timeStamp > lastTemperatureTimeStamp) { + lastTemperatureTimeStamp = timeStamp; + outfile << "TEMP, " << timeStamp; + outfile << ", " << GetFloatSafe(msgPayload,17 + i * lengthPerSample); + outfile << std::endl; + } + else { + //printf("TS Temperature Error\n"); + } + break; + case IMUFileWriter::SensorType::Pressure: + if (lastPressureTimeStamp >= 500000000) + lastPressureTimeStamp = 0; + if (timeStamp > lastPressureTimeStamp) { + lastPressureTimeStamp = timeStamp; + outfile << "PRESSURE, " << timeStamp; + outfile << ", " << IMUFileWriter::GetFloatSafe(msgPayload,17 + i * lengthPerSample); + outfile << std::endl; + } + else { + //printf("TS Pressure Error\n"); + } + break; + case IMUFileWriter::SensorType::Light: + if (lastLightTimeStamp >= 500000000) + lastLightTimeStamp = 0; + if (timeStamp > lastLightTimeStamp) { + lastLightTimeStamp = timeStamp; + outfile << "LIGHT, " << timeStamp; + outfile << ", " << BUILD_UINT16(msgPayload[17 + i * lengthPerSample],msgPayload[17 + i * lengthPerSample+1]); + outfile << ", " << BUILD_UINT16(msgPayload[17 + dataSize+i * lengthPerSample],msgPayload[17 +dataSize+ i * lengthPerSample+1]); + outfile << std::endl; + } + else { + //printf("TS Light Error\n"); + } + break; + case IMUFileWriter::SensorType::Unknown: + outfile << "UNKNOWN, " << timeStamp; + outfile << std::endl; + default: + break; + } + } + } + break; + case static_cast<short>(GPS_DATA_PACKET): { + IMUFileWriter::GPSDatas gpsDatas; + unsigned short ms = BUILD_UINT16(msgPayload[3], msgPayload[4]); + if(ms > 999) { + ms = 0; + } + + gpsDatas.dateOfFix.year = msgPayload[7]; + gpsDatas.dateOfFix.month = msgPayload[6]; + gpsDatas.dateOfFix.day = msgPayload[5]; + gpsDatas.dateOfFix.hour = msgPayload[0]; + gpsDatas.dateOfFix.minute = msgPayload[1]; + gpsDatas.dateOfFix.second = msgPayload[2]; + + gpsDatas.fix = msgPayload[8] != 0; + gpsDatas.fixQuality = msgPayload[9]; + gpsDatas.latitude = GetFloatSafe(msgPayload, 10); + gpsDatas.latitudeDirection = static_cast<char>(msgPayload[14]); + gpsDatas.longitude = GetFloatSafe(msgPayload, 15); + gpsDatas.longitudeDirection = static_cast<char>(msgPayload[19]); + gpsDatas.speed = GetFloatSafe(msgPayload, 20); + gpsDatas.angle = GetFloatSafe(msgPayload, 24); + gpsDatas.altitude = GetFloatSafe(msgPayload, 28); + gpsDatas.satellites = msgPayload[32]; + gpsDatas.antenna = msgPayload[33]; + + if (lastGPSDate.year != gpsDatas.dateOfFix.year || lastGPSDate.month != gpsDatas.dateOfFix.month ||lastGPSDate.day != gpsDatas.dateOfFix.day || + lastGPSDate.hour!=gpsDatas.dateOfFix.hour || lastGPSDate.minute!=gpsDatas.dateOfFix.minute || lastGPSDate.second!=gpsDatas.dateOfFix.second) { + lastGPSDate = gpsDatas.dateOfFix; + outfile << "GPS, " << static_cast<int>(gpsDatas.dateOfFix.year); + outfile << "/" << static_cast<int>(gpsDatas.dateOfFix.month); + outfile << "/" << static_cast<int>(gpsDatas.dateOfFix.day); + outfile << " " << static_cast<int>(gpsDatas.dateOfFix.hour); + outfile << ":" << static_cast<int>(gpsDatas.dateOfFix.minute); + outfile << ":" << static_cast<int>(gpsDatas.dateOfFix.second); + outfile << " fix:" << gpsDatas.fix; + outfile << ", fixQual:" << static_cast<int>(gpsDatas.fixQuality); + outfile << ", Lat:" << gpsDatas.latitude; + outfile << " " << gpsDatas.latitudeDirection; + outfile << ", Lon:" << gpsDatas.longitude; + outfile << " " << gpsDatas.longitudeDirection; + outfile << ", speed:" << gpsDatas.speed; + outfile << ", ang:" << gpsDatas.angle; + outfile << ", alt:" << gpsDatas.altitude; + outfile << ", sat:" << static_cast<int>(gpsDatas.satellites); + outfile << std::endl; + } + } + break; + case static_cast<short>(GPS_PPS_PACKET): { + uint64_t PPSTimeStamp = BUILD_UINT64(msgPayload[7], msgPayload[6], msgPayload[5], msgPayload[4], + msgPayload[3], msgPayload[2], msgPayload[1], msgPayload[0]); + + PPSTimeStamp *= 10; // Convert to nanoseconds (assuming internal clock frequency is 100MHz) + + if(PPSTimeStamp>lastPPSTimeStampNS) { + lastPPSTimeStampNS = PPSTimeStamp; + outfile << "PPS: " << PPSTimeStamp; + outfile << std::endl; + } + } + break; + default: break; + } +} + +void IMUFileWriter::write(uint8_t *sample, size_t size, uint8_t *imu_data) { + uint8_t *imu_data_cur(imu_data); + + if(this->qhb_version == 3) { + for(int i=1; i<size-1; i++) + { + DecodeMessage(imu_data[i]); + } + } + else { + uint8_t *imu_data_cur(imu_data); + while(imu_data_cur + frame_size + 5 < imu_data + additional_data_size){ + if(!(imu_data_cur[0]==0xFE && imu_data_cur[1]==0x0A && imu_data_cur[2]==0x0A && imu_data_cur[5]==0x08)) { + // skip trame if header is incorrect + imu_data_cur += frame_size + 5; + continue; + } + imu_data_cur += 5; // skip frame header + auto timestamp = static_cast<int32_t>(__builtin_bswap32(*reinterpret_cast<uint32_t*>(imu_data_cur + 9))); + if (timestamp > last_timestamp) { + last_timestamp = timestamp; + outfile << timestamp; + outfile << "," << le16tof(imu_data_cur + 13) / 32756 * 19.62; // ax resolution +- 2g + outfile << "," << le16tof(imu_data_cur + 15) / 32756 * 19.62; // ay resolution +- 2g + outfile << "," << le16tof(imu_data_cur + 17) / 32756 * 19.62; // az resolution +- 2g + outfile << "," << le16tof(imu_data_cur + 19) / 32756 * 250; // gx resolution +- 255deg/sec + outfile << "," << le16tof(imu_data_cur + 21) / 32756 * 250; // gy resolution +- 255deg/sec + outfile << "," << le16tof(imu_data_cur + 23) / 32756 * 250; // gz resolution +- 255deg/sec + outfile << "," << le16tof(imu_data_cur + 25) / 32756 * 4900.; // mx +- 4900µTesla + outfile << "," << le16tof(imu_data_cur + 27) / 32756 * (-4900.); // my +- 4900µTesla + outfile << "," << le16tof(imu_data_cur + 29) / 32756 * (-4900.); // mz +- 4900µTesla + outfile << std::endl; + } + imu_data_cur += frame_size; + } + } +} + +size_t IMUFileWriter::get_last_timestamp(){ + return last_timestamp; +} + + +SplitWavFileWriter::SplitWavFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, + size_t depth, size_t samples_per_file) : + FileWriter(filename_template, qhb_version, num_channels, sample_rate, depth), + samples_per_file(samples_per_file), + current_file(nullptr), + current_file_samples_written(0) { + // nothing +} + +SplitWavFileWriter::~SplitWavFileWriter() { + if (current_file) { + delete current_file; + } +} + +void SplitWavFileWriter::write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) { + size_t available = num_samples /(num_channels * depth); + // start as many new files as required to write out the samples + while (current_file_samples_written + available >= samples_per_file) { + if (!current_file) { + current_file = new WavFileWriter(filename_template, qhb_version, num_channels, sample_rate, depth, samples_per_file); + } + // write out as much as fits into the current file and move the pointer + size_t missing = samples_per_file - current_file_samples_written; + current_file->write(samples, missing * num_channels * depth, imu_data); + samples += missing * num_channels * depth; + available -= missing; + // start a new file + delete current_file; + current_file = nullptr; + current_file_samples_written = 0; + } + // if there are samples left, write them to the current file + if (available) { + if (!current_file) { + current_file = new WavFileWriter(filename_template, qhb_version, num_channels, sample_rate, depth, samples_per_file); + } + current_file->write(samples, available * num_channels * depth, imu_data); + current_file_samples_written += available; + } +} + + +SplitIMUWavFileWriter::SplitIMUWavFileWriter(std::string &filename_template, std::string &imu_name_template, size_t qhb_version, + size_t num_channels, size_t sample_rate, size_t depth, size_t samples_per_file): + FileWriter(filename_template, qhb_version, num_channels, sample_rate, depth), + imu_name_template(imu_name_template), + samples_per_file(samples_per_file), + current_file(nullptr), + imu_file(nullptr), + current_file_samples_written(0), + max_timestamp(0) { + // nothing +} + +SplitIMUWavFileWriter::~SplitIMUWavFileWriter() { + if (current_file) { + delete current_file; + } + if (imu_file) { + delete imu_file; + } +} + +void SplitIMUWavFileWriter::write(uint8_t *samples, size_t num_samples, uint8_t* imu_data) { + size_t available = num_samples /(num_channels * depth); + // start as many new files as required to write out the samples + while (current_file_samples_written + available >= samples_per_file) { + if (!current_file) { + current_file = new WavFileWriter(filename_template, qhb_version, num_channels, sample_rate, depth, samples_per_file); + if (imu_file) { + max_timestamp = imu_file->get_last_timestamp(); + delete imu_file; + imu_file = new IMUFileWriter(imu_name_template, qhb_version, num_channels, sample_rate, depth, max_timestamp); + } + } + if (!imu_file) imu_file = new IMUFileWriter(imu_name_template, qhb_version, num_channels, sample_rate, depth, max_timestamp); + // write out as much as fits into the current file and move the pointer + size_t missing = samples_per_file - current_file_samples_written; + current_file->write(samples, missing * num_channels * depth, imu_data); + if (imu_data) imu_file->write(samples, missing * num_channels * depth, imu_data); + imu_data = nullptr; // prevent multiple writing + samples += missing * num_channels * depth; + available -= missing; + // start a new file + delete current_file; + max_timestamp = imu_file->get_last_timestamp(); + delete imu_file; + current_file = nullptr; + imu_file = nullptr; + current_file_samples_written = 0; + } + // if there are samples left, write them to the current file + if (available) { + if (!current_file) { + current_file = new WavFileWriter(filename_template, qhb_version, num_channels, sample_rate, depth, samples_per_file); + if (imu_file) { + max_timestamp = imu_file->get_last_timestamp(); + delete imu_file; + imu_file = new IMUFileWriter(imu_name_template, qhb_version, num_channels, sample_rate, depth, max_timestamp); + } + } + if (!imu_file) imu_file = new IMUFileWriter(imu_name_template, qhb_version, num_channels, sample_rate, depth, max_timestamp); + current_file->write(samples, available * num_channels * depth, imu_data); + if (imu_data) imu_file->write(samples, available * num_channels * depth, imu_data); + imu_data = nullptr; + current_file_samples_written += available; + } + +} diff --git a/src/filewriter.h b/src/filewriter.h new file mode 100644 index 0000000000000000000000000000000000000000..8cbf897a80334dd503d4be767a8081ecab3c1950 --- /dev/null +++ b/src/filewriter.h @@ -0,0 +1,280 @@ +/** + * Wave file writing, with or without splitting by length. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + * Author: Maxence Ferrari <maxence.ferrari@lis-lab.fr> + */ + +#ifndef FILEWRITER_H +#define FILEWRITER_H + +#include <cstdint> +#include <string> +#include <vector> +#include <fstream> +#include <iostream> +#include <cstdio> +#define HS_DATA_PACKET_FULL_TIMESTAMP 0x0A0A +#define HS_DATA_PACKET_FULL_TIMESTAMP_V2 0x0A0C +#define GPS_DATA_PACKET 0x0A0D +#define GPS_PPS_PACKET 0x0A0E + + +/** Abstract base class for writing sample data to files. + */ +class FileWriter { +protected: + // output file name template + std::string filename_template; + std::string generate_filename(); + + // sample format options + size_t qhb_version; + size_t num_channels; + size_t sample_rate; + size_t depth; +public: + /** Abstract constructor to be used by subclasses. + */ + FileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth); + virtual ~FileWriter(); + + /** Writes out the given vector of 8-bit samples. + * \param[in] samples The samples to write, with interleaved channels. + */ + void write(std::vector<uint8_t> &samples, std::vector<uint8_t> &imu_data); + virtual void write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) = 0; +}; + + +/** The header of a PCM WAVE file as stored on disk. + */ +typedef struct { + char chunk_id[4] = {'R', 'I', 'F', 'F'}; + std::uint8_t chunk_size[4] = {0, 0, 0, 0}; // sample data size + 36 = file size - 8, little endian + char riff_type[4] = {'W', 'A', 'V', 'E'}; + char fmt_id[4] = {'f', 'm', 't', ' '}; + std::uint8_t fmt_length[4] = {16, 0, 0, 0}; // 16, little endian + std::uint8_t fmt_tag[2] = {1, 0}; // 0x0001 (= PCM), little endian + std::uint8_t fmt_channels[2] = {0, 0}; // number of channels, little endian + std::uint8_t fmt_sample_rate[4] = {0, 0, 0, 0}; // samples per second, little endian + std::uint8_t fmt_byte_rate[4] = {0, 0, 0, 0}; // bytes per second (per channel), little endian + std::uint8_t fmt_frame_size[2] = {0, 0}; // channels * 16 bit, little endian + std::uint8_t fmt_bits_per_sample[2] = {16, 0}; // 16, little endian + char data_id[4] = {'d', 'a', 't', 'a'}; + std::uint8_t data_size[4] = {0, 0, 0, 0}; // sample data size = file size - 44, little endian +} WavFileHeader; +// TODO: possibly use WAVEFORMATEXTENSIBLE for more than two channels: +// https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/extensible-wave-format-descriptors + + +/** Class for writing sample data to a single PCM WAVE file. + */ +class WavFileWriter: public FileWriter { +private: + std::ofstream outfile; + WavFileHeader header; + size_t samples_written; +public: + /** Instantiates a wave file writer. + * \param[in] filename_template The name of the file to write to. Will be + * created or opened immediately, truncating any existing content. May + * contain format specifiers as understood by the strftime() function, + * filled in based on the current system date and local time. + * \param[in] num_channels The number of channels the sample data to be + * written will contain. + * \param[in] sample_rate The number of samples per second (per channel) the + * sample data to be written will contain. + * \param[in] depth The number of bytes per samples the + * sample data to be written will contain. + */ + WavFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth); + /** Instantiates a wave file writer. + * \param[in] filename_template The name of the file to write to. Will be + * created or opened immediately, truncating any existing content. May + * contain format specifiers as understood by the strftime() function, + * filled in based on the current system date and local time. + * \param[in] num_channels The number of channels the sample data to be + * written will contain. + * \param[in] sample_rate The number of samples per second (per channel) the + * sample data to be written will contain. + * \param[in] depth The number of bytes per samples the + * sample data to be written will contain. + * \param[in] expected_num_samples The expected total number of samples (per + * channel) that will be written. The file header will be written + * accordingly and not rewritten on closing the file if the number matches. + */ + WavFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth, + size_t expected_num_samples); + ~WavFileWriter() override; + + void write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) override; +}; + + +/** Class for writing sample data to a sequence of PCM WAVE files, split up to + * reach a given target length per file. + */ +class SplitWavFileWriter: public FileWriter { +private: + size_t samples_per_file; + WavFileWriter *current_file; + size_t current_file_samples_written; +public: + /** Instantiates a splitted wave file writer. + * \param[in] filename_template The name of the file to write to. Will be + * created or opened when required, truncating any existing content. Should + * contain format specifiers as understood by the strftime() function, + * filled in based on the current system date and local time, otherwise it + * will be repeatedly overwritten. + * \param[in] num_channels The number of channels the sample data to be + * written will contain. + * \param[in] sample_rate The number of samples per second (per channel) the + * sample data to be written will contain. + * \param[in] depth The number of bytes per samples the + * sample data to be written will contain. + * \param[in] samples_per_file The target number of samples (per channel) + * that will be written to a file before starting the next one. + */ + SplitWavFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, + size_t depth, size_t samples_per_file); + ~SplitWavFileWriter() override; + + void write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) override; +}; + + +class IMUFileWriter: public FileWriter { + // const std::string header = "Timestamp,ax,ay,az,gx,gy,gz,mx,my,mz\n"; + const std::string header = "Sensor Type,TimeStamp(ms) or Time, val0,val1,val2,val3,val4,val5,val6,val7\n"; + const size_t frame_size = 32; + const size_t additional_data_size = 736; +private: + enum class SensorType { + Unknown = 0, + Accel = 1, + Gyro = 2, + Mag = 3, + Temperature = 4, + Pressure = 5, + Light = 6, + Piezo = 7, + IMU = 8 + }; + struct DateTime { + unsigned short year; + unsigned char month; + unsigned char day; + unsigned char weekDay; + unsigned char hour; + unsigned char minute; + unsigned char second; + }; + struct GPSDatas { + DateTime dateOfFix; + bool fix; + unsigned char fixQuality; + double latitude; + char latitudeDirection; + double longitude; + char longitudeDirection; + double speed; + double angle; + double altitude; + unsigned char satellites; + unsigned char antenna; + }; + + std::ofstream outfile; + size_t last_timestamp = 0; + unsigned int lastAccelTimeStamp = 0; + unsigned int lastGyroTimeStamp = 0; + unsigned int lastMagTimeStamp = 0; + unsigned int lastLightTimeStamp = 0; + unsigned int lastPressureTimeStamp = 0; + unsigned int lastTemperatureTimeStamp = 0; + unsigned int lastTimeStamp = 0; + DateTime lastGPSDate; + double lastPPSTimeStampNS; + + enum class StateReception { + Waiting, + FunctionMSB, + FunctionLSB, + PayloadLengthMSB, + PayloadLengthLSB, + Payload, + Decode + }; + + StateReception rcvState; + int msgDecodedFunction; + int msgDecodedPayloadLength; + unsigned char *msgDecodedPayload; + int msgDecodedPayloadIndex; + unsigned int msgDecoded; + + void ProcessDecodedMessage(int msgFunction, int msgPayloadLength, + const unsigned char* msgPayload); + float GetFloatSafe(const unsigned char *p, int index); +public: + /** Instantiates a splitted wave file writer. + * \param[in] filename_template The name of the file to write to. Will be + * created or opened when required, truncating any existing content. Should + * contain format specifiers as understood by the strftime() function, + * filled in based on the current system date and local time, otherwise it + * will be repeatedly overwritten. + * \param[in] num_channels The number of channels the sample data to be + * written will contain. + * \param[in] sample_rate The number of samples per second (per channel) the + * sample data to be written will contain. + * \param[in] depth The number of bytes per samples the + * sample data to be written will contain. + * \param[in] samples_per_file The target number of samples (per channel) + * that will be written to a file before starting the next one. + */ + unsigned char CalculateChecksum(int msgFunction, + int msgPayloadLength, const unsigned char msgPayload[]); + void DecodeMessage(unsigned char c); + + IMUFileWriter(std::string &filename_template, size_t qhb_version, size_t num_channels, size_t sample_rate, size_t depth, size_t timestamp); + void write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) override; + size_t get_last_timestamp(); +}; + +/** Class for writing sample data to a sequence of PCM WAVE files, split up to + * reach a given target length per file. + */ +class SplitIMUWavFileWriter: public FileWriter { +private: + size_t samples_per_file; + WavFileWriter *current_file; + size_t current_file_samples_written; + std::string &imu_name_template; + IMUFileWriter *imu_file; + size_t max_timestamp; +public: + /** Instantiates a splitted wave file writer. + * \param[in] filename_template The name of the file to write to. Will be + * created or opened when required, truncating any existing content. Should + * contain format specifiers as understood by the strftime() function, + * filled in based on the current system date and local time, otherwise it + * will be repeatedly overwritten. + * \param[in] num_channels The number of channels the sample data to be + * written will contain. + * \param[in] sample_rate The number of samples per second (per channel) the + * sample data to be written will contain. + * \param[in] depth The number of bytes per samples the + * sample data to be written will contain. + * \param[in] samples_per_file The target number of samples (per channel) + * that will be written to a file before starting the next one. + */ + SplitIMUWavFileWriter(std::string &filename_template, std::string &imu_name_template, size_t qhb_version, size_t num_channels, + size_t sample_rate, size_t depth, size_t samples_per_file); + ~SplitIMUWavFileWriter() override; + + void write(uint8_t *samples, size_t num_samples, uint8_t *imu_data) override; +}; + + +#endif // FILEWRITER_H diff --git a/src/macros.h b/src/macros.h new file mode 100644 index 0000000000000000000000000000000000000000..eaeff1277efd9b8b6a10d04b576b82e055a198b3 --- /dev/null +++ b/src/macros.h @@ -0,0 +1,28 @@ +#include <cstdint> // For standard integer types like uint8_t, uint16_t, etc. + +inline uint16_t BUILD_UINT16(uint8_t loByte, uint8_t hiByte) { + return static_cast<uint16_t>((loByte & 0x00FF) + ((hiByte & 0x00FF) << 8)); +} + +inline int16_t BUILD_INT16(uint8_t hiByte, uint8_t loByte) { + return static_cast<int16_t>((loByte & 0x00FF) + ((hiByte & 0x00FF) << 8)); +} + +inline uint32_t BUILD_UINT32(uint8_t Byte0, uint8_t Byte1, uint8_t Byte2, uint8_t Byte3) { + return static_cast<uint32_t>(static_cast<uint32_t>(Byte0 & 0x00FF) + + (static_cast<uint32_t>(Byte1 & 0x00FF) << 8) + + (static_cast<uint32_t>(Byte2 & 0x00FF) << 16) + + (static_cast<uint32_t>(Byte3 & 0x00FF) << 24)); +} + +inline uint64_t BUILD_UINT64(uint8_t Byte0, uint8_t Byte1, uint8_t Byte2, uint8_t Byte3, + uint8_t Byte4, uint8_t Byte5, uint8_t Byte6, uint8_t Byte7) { + return static_cast<uint64_t>(static_cast<uint64_t>(Byte0 & 0x00FF) + + (static_cast<uint64_t>(Byte1 & 0x00FF) << 8) + + (static_cast<uint64_t>(Byte2 & 0x00FF) << 16) + + (static_cast<uint64_t>(Byte3 & 0x00FF) << 24) + + (static_cast<uint64_t>(Byte4 & 0x00FF) << 32) + + (static_cast<uint64_t>(Byte5 & 0x00FF) << 40) + + (static_cast<uint64_t>(Byte6 & 0x00FF) << 48) + + (static_cast<uint64_t>(Byte7 & 0x00FF) << 56)); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e8a6f59da449241e2ced2a5d35c275fd4d2bd3c9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,230 @@ +/** + * SMIoT JASON Qualilife sound recorder command line program. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + * Author: Maxence Ferrari <maxence.ferrari@lis-lab.fr> + */ +#include <iostream> +#include <string> +#include <memory> +#include <cstring> +#include "recorder.h" +#include "filewriter.h" +#include "cleanexit.h" + +void print_usage(char *name) { + std::cout << "SMIoT JASON Qualilife sound recorder"; +#ifdef JASONREC_VERSION + std::cout << " v" << JASONREC_VERSION; +#endif + std::cout << std::endl; + std::cout << "Usage: " << name << " qhbversion channels rate filename [--help, -h] [--chunk_len, -c CHUNK_LEN] " + << "[--total_len, -t TOTAL_LEN] [--device, -d DEVICE] [--bit_depth, -b BIT_DEPTH] " + << "[--imu, -i IMU] [--filter, -f FILTER] [--verbose, -v]" << std::endl; + std::cout << "Positional arguments:" << std::endl; + std::cout << " QHB VERSION:\tversion of the QHB audio card (2 or 3)" << std::endl; + std::cout << " CHANNELS:\tnumber of channels to record (1 to 5)" << std::endl; + std::cout << " RATE:\tsample rate in Hz to record at (integral number)" << std::endl; + std::cout << " FILENAME:\toutput file name. should include strftime() format specifiers" << std::endl; + std::cout << " if CHUNK_LEN is specified. For miliseconds, use %z. Example: location/recording_%Y%m%d_%H%M%S_%z.wav" << std::endl; + std::cout << "Optional arguments:" << std::endl; + std::cout << "-h, --help\t\tshow this help message and exit" << std::endl; + + std::cout << " --bit_depth, -b\tBIT_DEPTH:\tSize of each samples in bits. Must be a multiple of 8. (Default: 16)" << std::endl; + std::cout << " --imu, -i\tIMU:\tIMU file name. Similar to FILENAME. Disable by default." << std::endl; + std::cout << " --filter, -f\tFILTER:\tNumber of the filter to use. Must be between 0 and 2. (Default: 0)" << std::endl; + std::cout << " --chunk_len, -c\tCHUNK_LEN:\tlength per output file in seconds; will start a new file whenever" << std::endl; + std::cout << " this length is reached. If not given or zero, will record a single file." << std::endl; + std::cout << " --total_len, -t\tTOTAL_LEN:\tTotal recording length; will stop when this length is reached." << std::endl; + std::cout << " If not given or zero, will record continuously until killed." << std::endl; + std::cout << " --device, -d\tDEVICE:\tWhich device to use in case multiple JASON cards are connected," << std::endl; + std::cout << " where 0 is the first, 1 is the second card found (and so on)." << std::endl; + std::cout << " --verbose, -v\t\tEnable the printing of status message " << std::endl; +} + +int record(size_t qhb_version, size_t channels, size_t rate, size_t depth, size_t filter, std::string &filename, std::string &imu_name, + float chunklen, float totallen, size_t device, bool verbose, size_t accelSamplingFrequency, size_t gyroSamplingFrequency, size_t magSamplingFrequency, size_t accelRangeScale, size_t gyroRangeScale, size_t magRangeScale) { + JasonRecorder recorder = JasonRecorder(verbose); + std::cout << "Found " << recorder.get_device_count() << " JASON card(s)." << std::endl; + if (recorder.get_device_count() == 0) { + std::cout << "Aborting." << std::endl; + return 2; + } + try { + // prepare the device + std::cout << "Selecting device number " << device << "..." << std::endl; + recorder.set_device(device); + // prepare the file writer + std::unique_ptr<FileWriter> filewriter; + if (chunklen > 0 && imu_name.empty()) { + // implementation note: in C++14 we would use std::make_unique<SplitWavFileWriter>(...) + filewriter.reset(new SplitWavFileWriter(filename, qhb_version, channels, rate, depth, chunklen * rate)); + } + else if (chunklen > 0) { + // implementation note: in C++14 we would use std::make_unique<SplitWavFileWriter>(...) + filewriter.reset(new SplitIMUWavFileWriter(filename, imu_name, qhb_version, channels, rate, depth, chunklen * rate)); + } + else if (imu_name.empty()){ + filewriter.reset(new WavFileWriter(filename, qhb_version, channels, rate, depth, totallen * rate)); + } + else{ + filewriter.reset(new SplitIMUWavFileWriter(filename, imu_name, qhb_version, channels, rate, depth, totallen * rate)); + } + // start the recording loop + std::cout << "Starting to record..." << std::endl; + allow_clean_exit(); + size_t total_samples_wanted = totallen * rate; + size_t total_samples_read = 0; + size_t failed_attempts = 0; + size_t sample_size = channels * depth; + std::vector<std::uint8_t> samples; + std::vector<std::uint8_t> imu_data; + try { + std::cout << "Setting recording format to " << channels << " channels at " << rate << " Hz " << (8 * depth) << " bits" << std::endl; + recorder.start_recording(qhb_version, channels, rate, depth, filter, accelSamplingFrequency, gyroSamplingFrequency, magSamplingFrequency, accelRangeScale, gyroRangeScale, magRangeScale); + // we will record until we have enough (or forever, if totallen == 0) + while ((total_samples_wanted == 0) || (total_samples_read < total_samples_wanted)) { + if (exit_requested()) { + std::cout << "Termination requested." << std::endl; + break; + } + recorder.get_samples(samples, imu_data, false, 500); + if (!samples.empty()) { + total_samples_read += samples.size() / sample_size; + // if we have too much now, crop the last packet + if ((total_samples_wanted > 0) && (total_samples_read > total_samples_wanted)) { + samples.resize(samples.size() - (total_samples_read - total_samples_wanted) * sample_size); + } + // pass it on to the file writer + filewriter->write(samples, imu_data); + failed_attempts = 0; + } + else { + // if we received no message or no audio data 20x in a row, abort + failed_attempts += 1; + if (failed_attempts >= 20) { + throw std::runtime_error("Device does not send audio data."); + } + } + } + recorder.stop_recording(); + std::cout << "Stopped recording." << std::endl; + } + catch (const std::exception& e) { + recorder.stop_recording(); + throw; + } + } + catch (const std::exception& e) { + std::cout << "Error: " << e.what() << std::endl; + return 2; + } + return 0; +} + +int main(int argc, char *argv[]) { + if (argc < 5) { + print_usage(argv[0]); + return 2; + } + + // parse command line options + int qhb_version; + try { + qhb_version = std::stoi(argv[1]); + if ((qhb_version < 2) || (qhb_version > 3)) { + std::cout << "Error: QHBVERSION must be 2..3, got " << qhb_version << " instead. Version unsupported." << std::endl; + return 2;} + } + catch (const std::exception& e) { + std::cout << "Error: Could not interpret " << argv[1] << " as an integer." << std::endl; + return 2; + } + int num_channels; + try { + num_channels = std::stoi(argv[2]); + if ((num_channels < 1) || (num_channels > MAX_CHANNELS)) { + std::cout << "Error: CHANNELS must be in 1.." << MAX_CHANNELS << ", got " << num_channels << " instead" << std::endl; + return 2;} + } + catch (const std::exception& e) { + std::cout << "Error: Could not interpret " << argv[2] << " as an integer." << std::endl; + return 2; + } + int rate; + try { + rate = std::stoi(argv[3]); + if (rate!= 32000 && rate!=64000 && rate!=128000 && rate!=256000 && rate!=512000) { + std::cout << "Error: RATE must be a power 2 times 32kHz, got " << rate << " instead" << std::endl; + return 2; + } + } + catch (const std::exception& e) { + std::cout << "Error: Could not interpret " << argv[3] << " as an integer." << std::endl; + return 2; + } + std::string filename = argv[4]; + + int i=5; + + int device(0), bit_depth(16), filter(0); + float chunklen(0), totallen(0); + float accelSamplingFrequency(25), gyroSamplingFrequency(25), magSamplingFrequency(20); + float accelRangeScale(8), gyroRangeScale(250), magRangeScale(12); + std::string imu_name; + bool verbose(false); + while (i < argc){ + try{ + if (strcmp(argv[i], "--chunk_len") == 0 || strcmp(argv[i], "-c") == 0) { + chunklen = atof(argv[++i]); + if ((chunklen < 0)) { + std::cout << "Error: CHUNKLEN must be positive or zero, got " << chunklen << " instead" << std::endl; + return 2; + }} + else if (strcmp(argv[i], "--imu") == 0 || strcmp(argv[i], "-i") == 0) { + imu_name = argv[++i]; + } + else if (strcmp(argv[i], "--device") == 0 || strcmp(argv[i], "-d") == 0) { + device = atoi(argv[++i]); + if ((device < 0)) { + std::cout << "Error: DEVICE must be nonnegative, got " << device << " instead" << std::endl; + return 2; + }} + else if (strcmp(argv[i], "--total_len") == 0 || strcmp(argv[i], "-t") == 0) { + totallen = atof(argv[++i]); + if ((totallen < 0)) { + std::cout << "Error: TOTALLEN must be positive or zero, got " << totallen << " instead" << std::endl; + return 2; + }} + else if (strcmp(argv[i], "--bit_depth") == 0 || strcmp(argv[i], "-b") == 0) { + bit_depth = atoi(argv[++i]); + if (bit_depth % 8) { + std::cout << "Error: DEPTH must be a multiple of 8, got " << bit_depth << " instead" << std::endl; + return 2; + }} + else if (strcmp(argv[i], "--filter") == 0 || strcmp(argv[i], "-f") == 0) { + filter = atoi(argv[++i]); + if (filter < 0 || filter > 2 ) { + std::cout << "Error: filter must be between 0 and 2, got " << filter << " instead" << std::endl; + return 2; + }} + else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0){ + print_usage(argv[0]); + return 1; + } + else if (strcmp(argv[i], "--verbose") == 0 || strcmp(argv[i], "-v") == 0){ + verbose = true; + } + else { + std::cout << "Unrecognized argument " << argv[i] << std::endl; + return 2; + }} + catch (const std::exception& e) { + std::cout << "Error: Could not interpret " << argv[i] << " ( " << argv[i-1] << " ) " " as a number." << std::endl; + return 2; + } + i++; + } + // hand over to the recording function + return record(qhb_version, num_channels, rate, bit_depth/8, filter, filename, imu_name, chunklen, totallen, device, verbose, accelSamplingFrequency, gyroSamplingFrequency, magSamplingFrequency, accelRangeScale, gyroRangeScale, magRangeScale); +} diff --git a/src/recorder.cpp b/src/recorder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..efb1d7291977fc2490ab7d6296a4b358c30791b8 --- /dev/null +++ b/src/recorder.cpp @@ -0,0 +1,321 @@ +/** + * SMIoT JASON Qualilife sound recording class. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + * Author: Maxence Ferrari <maxence.ferrari@lis-lab.fr> + */ + +#include "recorder.h" +#include <stdexcept> +#include <iostream> +#include <vector> +#include <array> +#include <algorithm> + +JasonRecorder::JasonRecorder(bool verbose) : verbose(verbose) { + // create libusb context + if (libusb_init(&ctx) < 0) { + throw std::runtime_error("libusb initialization failed"); + } + + // set debug level + libusb_set_debug(ctx, 3); + + // discover JASON sound cards + libusb_device** all_devices; + if (libusb_get_device_list(ctx, &all_devices) < 0) { + throw std::runtime_error("libusb device enumeration failed"); + } + libusb_device** device = all_devices; + while (*device != NULL) { + struct libusb_device_descriptor desc; + if (libusb_get_device_descriptor(*device, &desc) < 0) { + continue; + } + if ((desc.idVendor == VENDOR_ID) && (desc.idProduct == PRODUCT_ID)) { + devices.push_back(*device); + } + else { + libusb_unref_device(*device); + } + device++; + } + libusb_free_device_list(all_devices, 0); +} + +JasonRecorder::~JasonRecorder() { + // free handle + if (handle) { + libusb_release_interface(handle, 0); + libusb_close(handle); + } + // free devices + for (auto& device : devices) { + libusb_unref_device(device); + } + // free libusb libusb context + libusb_exit(ctx); +} + +size_t JasonRecorder::get_device_count() { + return devices.size(); +} + +void JasonRecorder::set_device(size_t number) { + if (handle) { + libusb_release_interface(handle, 0); + libusb_close(handle); + handle = NULL; + } + if (number >= devices.size()) { + throw std::out_of_range("device number too large"); + } + if (libusb_open(devices[number], &handle) < 0) { + throw std::runtime_error("could not open USB device (try again as root)"); + } + if (libusb_claim_interface(handle, 0) < 0) { + throw std::runtime_error("could not claim USB interface"); + } +} + +void JasonRecorder::send_message(std::uint16_t cmd) { + send_message(cmd, NULL, 0); +} + +void JasonRecorder::send_message(std::uint16_t cmd, std::vector<std::uint8_t> &payload) { + send_message(cmd, payload.data(), payload.size()); +} + +void JasonRecorder::send_message(std::uint16_t cmd, std::uint8_t *payload, size_t length) { + if (!handle) { + throw std::logic_error("must call set_device() first"); + } + // message format: 0xfe + payload size (2 byte) + command (1 byte) + payload + std::vector<std::uint8_t> data; + data.reserve(6 + length); + data.push_back(FRAME_START); + data.push_back((std::uint8_t) ((cmd >> 8) & 0xFF)); + data.push_back((std::uint8_t) (cmd & 0xFF)); + data.push_back((std::uint8_t) ((length >> 8) & 0xFF)); + data.push_back((std::uint8_t) (length & 0xFF)); + if (length) { + data.insert(data.end(), payload, payload + length); + } + // compute the checksum + data.push_back(FRAME_START); + for (int i=1; i < 5+length; data[5 + length] ^= data[i++]); + // send message, allow a maximum of 10 seconds for it to go through + int sent; + if (libusb_bulk_transfer(handle, ENDPOINT_SEND, data.data(), data.size(), &sent, 10000) < 0) { + throw std::runtime_error("could not send message to device"); + } + else if (sent != data.size()) { + throw std::runtime_error("could not send complete message to device"); + }; +} + +void JasonRecorder::getBytesFromFloat(std::array<unsigned char, 4> &p, float f) { + unsigned char *f_ptr = reinterpret_cast<unsigned char*>(&f); + for (int i = 0; i < 4; i++) { + p[i] = f_ptr[i]; + } +} + +void JasonRecorder::start_recording(int qhb_version, std::uint8_t num_channels, size_t sample_rate, std::uint8_t depth, std::uint8_t num_filter, size_t accelSamplingRate, size_t gyroSamplingRate, size_t magSamplingRate, size_t accelRangeScale, size_t gyroRangeScale, size_t magRangeScale) { + if (qhb_version == 2) { + std::vector<std::uint8_t> payload1 = { + START, + (std::uint8_t) ((sample_rate >> 24) & 0xFF), + (std::uint8_t) ((sample_rate >> 16) & 0xFF), + (std::uint8_t) ((sample_rate >> 8) & 0xFF), + (std::uint8_t) (sample_rate & 0xFF), + num_channels, + (std::uint8_t) (8 * depth), + num_filter}; + send_message(START_ID, payload1); + } else if (qhb_version == 3) + { + std::array<unsigned char, 4> accelSamplingRateBytes = {0}; + std::array<unsigned char, 4> accelRangeScaleBytes = {0}; + getBytesFromFloat(accelSamplingRateBytes, accelSamplingRate); + getBytesFromFloat(accelRangeScaleBytes, accelRangeScale); + std::vector<std::uint8_t> payload2 = { + (std::uint8_t) (0x01), + (std::uint8_t) (0x00), + (std::uint8_t) (accelRangeScaleBytes[0]), + (std::uint8_t) (accelRangeScaleBytes[1]), + (std::uint8_t) (accelRangeScaleBytes[2]), + (std::uint8_t) (accelRangeScaleBytes[3]), + (std::uint8_t) (accelSamplingRateBytes[0]), + (std::uint8_t) (accelSamplingRateBytes[1]), + (std::uint8_t) (accelSamplingRateBytes[2]), + (std::uint8_t) (accelSamplingRateBytes[3]) + }; + send_message(SET_SENSOR, payload2); + + std::array<unsigned char, 4> gyroSamplingRateBytes = {0}; + std::array<unsigned char, 4> gyroRangeScaleBytes = {0}; + getBytesFromFloat(gyroSamplingRateBytes, gyroSamplingRate); + getBytesFromFloat(gyroRangeScaleBytes, gyroRangeScale); + std::vector<std::uint8_t> payload3 = { + (std::uint8_t) (0x02), + (std::uint8_t) (0x00), + (std::uint8_t) (gyroRangeScaleBytes[0]), + (std::uint8_t) (gyroRangeScaleBytes[1]), + (std::uint8_t) (gyroRangeScaleBytes[2]), + (std::uint8_t) (gyroRangeScaleBytes[3]), + (std::uint8_t) (gyroSamplingRateBytes[0]), + (std::uint8_t) (gyroSamplingRateBytes[1]), + (std::uint8_t) (gyroSamplingRateBytes[2]), + (std::uint8_t) (gyroSamplingRateBytes[3]) + }; + send_message(SET_SENSOR, payload3); + + std::array<unsigned char, 4> magSamplingRateBytes = {0}; + std::array<unsigned char, 4> magRangeScaleBytes = {0}; + getBytesFromFloat(magSamplingRateBytes, magSamplingRate); + getBytesFromFloat(magRangeScaleBytes, magRangeScale); + std::vector<uint8_t> payload4 { + (std::uint8_t) (0x03), + (std::uint8_t) (0x00), + (std::uint8_t) (magRangeScaleBytes[0]), + (std::uint8_t) (magRangeScaleBytes[1]), + (std::uint8_t) (magRangeScaleBytes[2]), + (std::uint8_t) (magRangeScaleBytes[3]), + (std::uint8_t) (magSamplingRateBytes[0]), + (std::uint8_t) (magSamplingRateBytes[1]), + (std::uint8_t) (magSamplingRateBytes[2]), + (std::uint8_t) (magSamplingRateBytes[3]) + }; + send_message(SET_SENSOR, payload4); + + std::vector<std::uint8_t> payload1 = { + START, + (std::uint8_t) ((sample_rate >> 24) & 0xFF), + (std::uint8_t) ((sample_rate >> 16) & 0xFF), + (std::uint8_t) ((sample_rate >> 8) & 0xFF), + (std::uint8_t) (sample_rate & 0xFF), + num_channels, + (std::uint8_t) (8 * depth), + num_filter}; + send_message(START_ID, payload1); + } + + this->num_channels = num_channels; + this->sample_rate = sample_rate; + this->depth = depth; + this->num_filter = num_filter; + recording = true; +} + + +void JasonRecorder::stop_recording() { + std::vector<std::uint8_t> payload1 = { + STOP, + (std::uint8_t) ((this->sample_rate >> 24) & 0xFF), + (std::uint8_t) ((this->sample_rate >> 16) & 0xFF), + (std::uint8_t) ((this->sample_rate >> 8) & 0xFF), + (std::uint8_t) (this->sample_rate & 0xFF), + this->num_channels, + (std::uint8_t) (8 * this->depth), + this->num_filter}; + send_message(START_ID, payload1); + recording = false; +} + +size_t JasonRecorder::receive_message(uint8_t *buffer, size_t max_wait) { + if (!handle) { + throw std::logic_error("must call set_device() first"); + } + int received; + int status = libusb_bulk_transfer(handle, ENDPOINT_RECEIVE, buffer, MAX_MSG_LENGTH, &received, max_wait); + if (status == LIBUSB_ERROR_OVERFLOW) { + throw std::runtime_error("buffer too small to receive message from device"); + } + else if ((status < 0) && (status != LIBUSB_ERROR_TIMEOUT)) { + throw std::runtime_error("could not receive message from device"); + } + return received; +} + +void JasonRecorder::get_samples(std::vector<std::uint8_t> &samples, std::vector<std::uint8_t> &imu_data, bool planar, size_t max_wait) { + if (!num_channels || !sample_rate) { + throw std::logic_error("must call set_format() first"); + } + std::array<std::uint8_t, MAX_MSG_LENGTH> buffer{}; + while (true) { + size_t received = receive_message(buffer.data(), max_wait); + if (received) { + // we could read the payload length, but it is wrong for sample data + //size_t length = buffer[1] << 8 + buffer[2]; + if (buffer[0] != FRAME_START); // invalid message + else if ((((std::uint16_t) buffer[1] << 8 )|(buffer[2])) == DATA_ID) { + // find the beginning and length of the samples in the buffer + size_t start = this->additional_data_size + 6; + imu_data.resize(0); + imu_data.reserve(this->additional_data_size); + imu_data.insert(imu_data.begin(), &buffer[6], &buffer[start]); + size_t num_samples = (received - start); + num_samples = (num_samples / (num_channels * this->depth)) * num_channels * this->depth; + // copy data to provided vector + if (planar || (num_channels == 1)) { + // copy out directly + samples.resize(0); + samples.reserve(num_samples); + samples.insert(samples.end(), &buffer[start], &buffer[start] + num_samples); + } + else { + // convert from blocked channels to interleaved channels + samples.resize(num_samples); + JasonRecorder::interleave_channels(&buffer[start], + samples.data(), num_samples, + this->num_channels, this->depth); + } + break; + } + else if (this->verbose && (((std::uint16_t) buffer[1] << 8 )|(buffer[2])) == STATUS_ID) { + samples.resize(0); + std::uint8_t cks=FRAME_START; //buffer[0] == FRAME_START already check + for (int i=1; i < 31; cks ^= buffer[i++]); + std::cout << " Sr: " << ( ((size_t) buffer[5] << 24) | ((size_t) buffer[6] << 16) + | ((size_t) buffer[7] << 8) | ((size_t) buffer[8])) + << " #Ch: " << (size_t) buffer[9] << " D: " << (size_t) buffer[10] << " Time: " + << 2000 + buffer[11] <<'-'<< (size_t) buffer[12] <<'-'<< (size_t) buffer[13] <<' ' + << (size_t) buffer[14] <<':'<< (size_t) buffer[15] <<':'<< (size_t) buffer[16] + << " UUID: " << std::hex << (size_t) buffer[17] << (size_t) buffer[18] << (size_t) buffer[19] << (size_t) buffer[20] + << (size_t) buffer[21] << (size_t) buffer[22] << (size_t) buffer[23] << (size_t) buffer[24] << std::dec + << " Rec: " << (buffer[25] !=0) + << " SPI: " << (size_t) buffer[26] << (size_t) buffer[27] << (size_t) buffer[28] << (size_t) buffer[29] + << " CKS: " << (cks == 0?"True":"False") << std::endl; + break; + } + } + else if (max_wait > 0) { + // we timed out, we do not want to wait again + samples.resize(0); + break; + } + } +} + +void JasonRecorder::interleave_channels(std::uint8_t *input, std::uint8_t *output, size_t num_bytes, + size_t num_channels, size_t depth) { + // the input comes in num_channels blocks of num_samples_per_channel little-endian 16-bit samples each + // we write these to the output in a round-robin manner, interleaving the channels + // we use a pattern that accesses the output strictly sequentially, so it can be used to write to a mem-mapped file + if ((num_channels < 1) || (num_channels > MAX_CHANNELS)) { + throw std::out_of_range("num_channels must be in [1, 6]"); + } + // prepare one input pointer per channel + std::uint8_t *inputs[num_channels]; + for (size_t c = 0; c < num_channels; c++) { + inputs[c] = input + c * (num_bytes/num_channels); + } + // iterate over the samples, copying in interleaved fashion + size_t c = 0; + for (size_t b=0; b < num_bytes;) { + *(output++) = *(inputs[c]++); + if (++b % depth == 0) + c = (c + 1) % num_channels; + } +} diff --git a/src/recorder.h b/src/recorder.h new file mode 100644 index 0000000000000000000000000000000000000000..4bcf71f3c3786e87b517fae464fcd910987787e2 --- /dev/null +++ b/src/recorder.h @@ -0,0 +1,130 @@ +/** + * SMIoT JASON Qualilife sound recording class. + * + * Author: Jan Schlüter <jan.schluter@lis-lab.fr> + * Author: Maxence Ferrari <maxence.ferrari@lis-lab.fr> + */ + +#ifndef RECORDER_H +#define RECORDER_H + +#include <libusb.h> +#include <cstdint> +#include <vector> +#include <array> + + +#define MAX_MSG_LENGTH 65536 +#define MAX_CHANNELS 6 + +/** Class for retrieving sample data from a JASON Qualilife sound card. + */ +class JasonRecorder { + const std::int16_t VENDOR_ID = 0x04D8; + const std::int16_t PRODUCT_ID = 0x0053; + const std::uint8_t ENDPOINT_SEND = 0x01; + const std::uint8_t ENDPOINT_RECEIVE = 0x81; + + // device control messages + const std::uint8_t FRAME_START = 0xFE; + const std::uint16_t START_ID = 0x0C01; + const std::uint16_t SET_SENSOR = 0x0C09; + const std::uint16_t SET_CLOCK_ID = 0x0C06; + const std::uint16_t DATA_ID = 0x0B01; + const std::uint16_t STATUS_ID = 0x0B02; + + const std::uint8_t START = 1; + const std::uint8_t STOP = 0; +private: + // libusb handles + struct libusb_context *ctx; + std::vector<struct libusb_device*> devices; + struct libusb_device_handle *handle = NULL; + /** Sends a message to the device, without payload. + * \param[in] cmd The command identifier. + */ + void send_message(std::uint16_t cmd); + /** Sends a message with payload to the device. + * \param[in] cmd The command identifier. + * \param[in] payload The payload data to include. + */ + void send_message(std::uint16_t cmd, std::vector<std::uint8_t> &payload); + /** Sends a message with payload to the device. + * \param[in] cmd The command identifier. + * \param[in] payload Pointer to the payload data to include. + * \param[in] length The size of the payload data in bytes. + */ + void send_message(std::uint16_t cmd, std::uint8_t *payload, size_t length); + /** Transform a float32 into an array of bytes. + * \param[in] p A pointer to the array where the bytes of the float will be stored. + * \param[in] index The starting position in the array `p` where the first byte will be stored. + * \param[in] f The float value from which the bytes will be extracted. + */ + void getBytesFromFloat(std::array<unsigned char, 4> &p, float f); + + // device messages sent back + /** Reads a message from the device. Waits for a message if necessary. + * \param[out] buffer Pointer to memory to write the message to. + * \param[out] length Length of the buffer. + * \param[in] max_wait Waiting time in milliseconds. Set to zero to wait + * indefinitely. If no message could be read within this time, returns zero. + * \returns the number of bytes received and written to the buffer. + */ + size_t receive_message(uint8_t *buffer, size_t max_wait = 0); + + // device state, as far as known + size_t additional_data_size = 730; + std::uint8_t num_channels = 0; + std::uint8_t depth = 0; + std::uint8_t num_filter = 0; + size_t sample_rate = 0; + bool recording = false; + bool verbose = false; +public: + JasonRecorder(bool verbose); + ~JasonRecorder(); + + /** Searches for JASON Qualilife sound cards attached via USB. + * \returns the number of devices found. May be zero. + */ + size_t get_device_count(); + /** Selects the given device and opens it. + * \param[in] number The device, must be smaller than get_device_count(). + */ + void set_device(size_t number); + /** Starts recording from the device chosen with set_device(). Requires set_device() to be called before. + * \param[in] num_channels The number of channels, between 1 and 5. + * \param[in] sample_rate The number of samples per second (per channel). + * \param[in] depth The number of bytes of each sample. + * \param[in] num_filter The filter number (between 0 and 2). + */ + void start_recording(int qhb_version, std::uint8_t num_channels, size_t sample_rate, std::uint8_t depth, std::uint8_t num_filter, size_t accelSamplingRate, size_t gyroSamplingRate, size_t magSamplingRate, size_t accelRangeScale, size_t gyroRangeScale, size_t magRangeScale); + /** Stops recording from the device chosen with set_device(). */ + void stop_recording(); + /** Fetches a messages from the device chosen with set_device(). + * Requires the format to have been set before using set_format(). + * If the messsages contains samples, they will be put in samples. + * \param[out] samples A vector the samples will be written to, replacing + * existing content if any. + * \param[in] planar If true, return the samples in planar form, i.e., one + * channel after the other, the format sent by the device. If false (the + * default), interleave the channels as done in PCM WAVE files. + * \param[in] max_wait Maximum time in milliseconds to block waiting for a + * packet from the device. If the time elapses before receiving any packets, + * or if a different type of packet was received, returns with an empty + * samples vector. Set to zero to wait indefinitely for a sample packet. + */ + void get_samples(std::vector<std::uint8_t> &samples, std::vector<std::uint8_t> &imu_data, bool planar=false, size_t max_wait=0); + /** Converts samples from planar to interleaved form. + * \param[in] input A pointer to the planar samples to read. + * \param[out] output A pointer to write the interleaved samples to. + * \param[in] num_bytes The total number of bytes + * to convert. + * \param[in] num_channels The number of channels. + * \param[in] depth The number of bytes per sample. + */ + static void interleave_channels(std::uint8_t *input, std::uint8_t *output, size_t num_bytes, + size_t num_channels, size_t depth); +}; + +#endif // RECORDER_H