Skip to content
Snippets Groups Projects
Commit 3db22011 authored by Philemon Prevot's avatar Philemon Prevot
Browse files

Merge branch 'QHBv3_version' into 'main'

HighBlueParser dev branch merged to empty main branch

See merge request !2
parents 4474e3f0 0348180f
No related branches found
No related tags found
1 merge request!2HighBlueParser dev branch merged to empty main branch
/build/
\ No newline at end of file
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)
LICENSE 0 → 100644
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.
README.md 0 → 100644
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.
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)
# 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/
/**
* 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();
}
/**
* 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
This diff is collapsed.
/**
* 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
#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
/**
* 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);
}
/**
* 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;
}
}
/**
* 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment