Skip to content
Snippets Groups Projects
Commit 13ad7cd0 authored by Jan Schlüter's avatar Jan Schlüter
Browse files

Initial import

parents
Branches
No related tags found
2 merge requests!2HighBlueParser dev branch merged to empty main branch,!1High blue rec
cmake_minimum_required(VERSION 3.1)
if(POLICY CMP0063)
cmake_policy(SET CMP0063 NEW)
endif()
project(jasonrec)
set(JASONREC_VERSION "1.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.
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 ..
make
sudo make install
```
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
```
Usage
-----
Running `jasonrec` without any arguments (or with any unsupported number of
arguments, in fact) will display information on its usage:
```
Usage: jasonrec CHANNELS RATE FILENAME [CHUNKLEN [TOTALLEN [DEVICE]]]
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 CHUNKLEN is specified. Example: location/recording_%Y%m%dT%H%M%S.wav
CHUNKLEN: 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.
TOTALLEN: total recording length; will stop when this length is reached.
If not given or zero, will record continuously until killed.
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).
```
As an example, to record a single 30-minute file of 2 channels at 16 kHz, run:
```
jasonrec 2 16000 recording.wav 0 30
```
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 300
```
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
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/
/**
* Wave file writing, with or without splitting by length.
*
* Author: Jan Schlüter <jan.schluter@lis-lab.fr>
*/
#include "filewriter.h"
#include <stdexcept>
#include <vector>
#include <iostream>
#include <ctime>
FileWriter::FileWriter(std::string &filename_template, size_t num_channels, size_t sample_rate) :
filename_template(filename_template), num_channels(num_channels), sample_rate(sample_rate) {
// 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
time_t timer;
time(&timer);
struct tm *timeinfo = localtime(&timer);
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<int16_t> &samples) {
write(samples.data(), samples.size());
}
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 num_channels, size_t sample_rate) :
WavFileWriter(filename_template, num_channels, sample_rate, 0) {
// nothing
}
WavFileWriter::WavFileWriter(std::string &filename_template, size_t num_channels, size_t sample_rate, size_t expected_num_samples) :
FileWriter(filename_template, num_channels, sample_rate),
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_byte_rate, num_channels * sample_rate * 2);
store_little_endian(header.fmt_frame_size, num_channels * 2);
if (expected_num_samples) {
size_t expected_data_size = expected_num_samples * num_channels * 2;
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 * 2;
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(int16_t *samples, size_t num_samples) {
outfile.write((char*) samples, num_samples * sizeof(*samples));
samples_written += num_samples / num_channels;
}
SplitWavFileWriter::SplitWavFileWriter(std::string &filename_template, size_t num_channels, size_t sample_rate, size_t samples_per_file) :
FileWriter(filename_template, num_channels, sample_rate),
samples_per_file(samples_per_file),
current_file(NULL),
current_file_samples_written(0) {
// nothing
}
SplitWavFileWriter::~SplitWavFileWriter() {
if (current_file) {
delete current_file;
}
}
void SplitWavFileWriter::write(int16_t *samples, size_t num_samples) {
size_t available = num_samples / num_channels;
// 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, num_channels, sample_rate, 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);
samples += missing * num_channels;
available -= missing;
// start a new file
delete current_file;
current_file = NULL;
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, num_channels, sample_rate, samples_per_file);
}
current_file->write(samples, available * num_channels);
current_file_samples_written += available;
}
}
/**
* Wave file writing, with or without splitting by length.
*
* Author: Jan Schlüter <jan.schluter@lis-lab.fr>
*/
#ifndef FILEWRITER_H
#define FILEWRITER_H
#include <cstdint>
#include <string>
#include <vector>
#include <fstream>
/** 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 num_channels;
size_t sample_rate;
public:
/** Abstract constructor to be used by subclasses.
*/
FileWriter(std::string &filename_template, size_t num_channels, size_t sample_rate);
virtual ~FileWriter();
/** Writes out the given vector of 16-bit samples.
* \param[in] samples The samples to write, with interleaved channels.
*/
void write(std::vector<int16_t> &samples);
/** Writes out the given array of 16-bit samples.
* \param[in] samples Pointer to the samples to write, with interleaved
* channels.
* \param[in] num_samples The number of samples to write, adding up the
* counts per channel (must be divisible by the number of channels).
*/
virtual void write(int16_t *samples, size_t num_samples) = 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.
*/
WavFileWriter(std::string &filename_template, size_t num_channels, size_t sample_rate);
/** 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] 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 num_channels, size_t sample_rate, size_t expected_num_samples);
~WavFileWriter();
void write(int16_t *samples, size_t num_samples);
};
/** 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] 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 num_channels, size_t sample_rate, size_t samples_per_file);
~SplitWavFileWriter();
void write(int16_t *samples, size_t num_samples);
};
#endif // FILEWRITER_H
/**
* SMIoT JASON Qualilife sound recorder command line program.
*
* Author: Jan Schlüter <jan.schluter@lis-lab.fr>
*/
#include <iostream>
#include <string>
#include <memory>
#include "recorder.h"
#include "filewriter.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 << " CHANNELS RATE FILENAME [CHUNKLEN [TOTALLEN [DEVICE]]]" << std::endl;
std::cout << " CHANNELS: number of channels to record (1 to 5)" << std::endl;
std::cout << " RATE: sample rate in Hz to record at (integral number)" << std::endl;
std::cout << " FILENAME: output file name; should include strftime() format specifiers" << std::endl;
std::cout << " if CHUNKLEN is specified. Example: location/recording_%Y%m%dT%H%M%S.wav" << std::endl;
std::cout << " CHUNKLEN: length 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 << " TOTALLEN: total 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: which 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;
}
int record(size_t channels, size_t rate, std::string &filename, float chunklen, float totallen, size_t device) {
JasonRecorder recorder = JasonRecorder();
std::cout << "Found " << recorder.get_device_count() << " JASON card(s)." << std::endl;
if (recorder.get_device_count() == 0) {
std::cout << "Aborting." << std::endl;
return 1;
}
try {
// prepare the device
std::cout << "Selecting device number " << device << "..." << std::endl;
recorder.set_device(device);
std::cout << "Setting recording format to " << channels << " channels at " << rate << " Hz..." << std::endl;
recorder.set_format(channels, rate);
// prepare the file writer
std::unique_ptr<FileWriter> filewriter;
if (chunklen > 0) {
// implementation note: in C++14 we would use std::make_unique<SplitWavFileWriter>(...)
filewriter.reset(new SplitWavFileWriter(filename, channels, rate, chunklen * rate));
}
else {
filewriter.reset(new WavFileWriter(filename, channels, rate, totallen * rate));
}
// start the recording loop
std::cout << "Starting to record..." << std::endl;
size_t total_samples_wanted = totallen * rate;
size_t total_samples_read = 0;
std::vector<std::int16_t> samples;
try {
recorder.start_recording();
// we will record until we have enough (or forever, if totallen == 0)
while ((total_samples_wanted == 0) || (total_samples_read < total_samples_wanted)) {
recorder.get_samples(samples);
total_samples_read += samples.size() / channels;
// 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) * channels);
}
// pass it on to the file writer
filewriter->write(samples);
}
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 1;
}
return 0;
}
int main(int argc, char *argv[]) {
if ((argc < 4) || (argc > 7)) {
print_usage(argv[0]);
return 1;
}
// parse command line options
int num_channels;
try {
num_channels = std::stoi(argv[1]);
}
catch (const std::exception& e) {
std::cout << "Error: Could not interpret " << argv[1] << " as an integer." << std::endl;
return 1;
}
int rate;
try {
rate = std::stoi(argv[2]);
}
catch (const std::exception& e) {
std::cout << "Error: Could not interpret " << argv[2] << " as an integer." << std::endl;
return 1;
}
std::string filename = argv[3];
float chunklen;
try {
chunklen = (argc > 4) ? std::stof(argv[4]) : 0;
}
catch (const std::exception& e) {
std::cout << "Error: Could not interpret " << argv[4] << " as a float." << std::endl;
return 1;
}
float totallen;
try {
totallen = (argc > 5) ? std::stof(argv[5]) : 0;
}
catch (const std::exception& e) {
std::cout << "Error: Could not interpret " << argv[5] << " as a float." << std::endl;
return 1;
}
int device;
try {
device = (argc > 6) ? std::stoi(argv[6]) : 0;
}
catch (const std::exception& e) {
std::cout << "Error: Could not interpret " << argv[6] << " as an integer." << std::endl;
return 1;
}
// check command line options for validity
if ((num_channels < 1) || (num_channels > 5)) {
std::cout << "Error: CHANNELS must be in 1..5, got " << num_channels << " instead" << std::endl;
return 1;
}
if ((rate <= 0)) {
std::cout << "Error: RATE must be positive, got " << rate << " instead" << std::endl;
return 1;
}
if ((chunklen < 0)) {
std::cout << "Error: CHUNKLEN must be positive or zero, got " << chunklen << " instead" << std::endl;
return 1;
}
if ((totallen < 0)) {
std::cout << "Error: TOTALLEN must be positive or zero, got " << totallen << " instead" << std::endl;
return 1;
}
if ((device < 0)) {
std::cout << "Error: DEVICE must be nonnegative, got " << device << " instead" << std::endl;
return 1;
}
// hand over to the recording function
return record(num_channels, rate, filename, chunklen, totallen, device);
}
/**
* SMIoT JASON Qualilife sound recording class.
*
* Author: Jan Schlüter <jan.schluter@lis-lab.fr>
*/
#include "recorder.h"
#include <stdexcept>
#include <vector>
#include <array>
#include <algorithm>
JasonRecorder::JasonRecorder() {
// 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_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_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)");
}
}
void JasonRecorder::send_message(std::uint8_t cmd) {
send_message(cmd, NULL, 0);
}
void JasonRecorder::send_message(std::uint8_t cmd, std::vector<std::uint8_t> &payload) {
send_message(cmd, payload.data(), payload.size());
}
void JasonRecorder::send_message(std::uint8_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(4 + length);
data.push_back(0xfe);
data.push_back((std::uint8_t) ((length >> 8) & 0xFF));
data.push_back((std::uint8_t) (length & 0xFF));
data.push_back(cmd);
if (length) {
data.insert(data.end(), payload, payload + length);
}
// 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::set_format(size_t num_channels, size_t sample_rate) {
// set channels
std::vector<std::uint8_t> payload1 = {(std::uint8_t) num_channels};
send_message(CMD_SET_CHANNELS, payload1);
this->num_channels = num_channels;
// set sample rate (needs to be sent in Big-endian order, MSB first)
std::vector<std::uint8_t> payload2 = {0x00,
(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)
};
send_message(CMD_SET_SAMPLE_RATE, payload2);
this->sample_rate = sample_rate;
// set resolution to 16 bits
std::vector<std::uint8_t> payload3 = {16};
send_message(CMD_SET_RESOLUTION, payload3);
}
void JasonRecorder::start_recording() {
if (!num_channels || !sample_rate) {
throw std::logic_error("must call set_format() first");
}
send_message(CMD_START_RECORDING);
recording = true;
}
void JasonRecorder::stop_recording() {
send_message(CMD_STOP_RECORDING);
recording = false;
}
size_t JasonRecorder::receive_message(std::uint8_t *buffer, size_t length, 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, 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");
}
if (received && (buffer[0] != 0xfe)) {
throw std::runtime_error("received invalid message header from device");
}
return received;
}
void JasonRecorder::get_samples(std::vector<std::int16_t> &samples, bool planar) {
if (!num_channels || !sample_rate) {
throw std::logic_error("must call set_format() first");
}
std::array<std::uint8_t, 4 + MAX_PAYLOAD> buffer;
while (true) {
size_t received = receive_message(buffer.data(), buffer.size());
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[3] == MSG_SAMPLES) {
// the beginning of the payload should have 0x00, 0x01, timestamp (4 bytes)
if ((buffer[4] != 0x00) || (buffer[5] != 0x01)) {
throw std::runtime_error("received invalid sample data from device");
}
// find the beginning and length of the samples in the buffer
std::int16_t *received_samples = (std::int16_t*) &buffer[10];
size_t num_samples = (received - 10) / sizeof(std::int16_t);
num_samples = (num_samples / num_channels) * num_channels;
// copy data to provided vector
if (planar || (num_channels == 1)) {
// copy out directly
samples.resize(0);
samples.reserve(num_samples);
samples.insert(samples.end(), received_samples, received_samples + num_samples);
}
else {
// convert from blocked channels to interleaved channels
samples.resize(num_samples);
JasonRecorder::interleave_channels(received_samples,
samples.data(), num_samples / num_channels,
this->num_channels);
}
break;
}
}
}
}
void JasonRecorder::interleave_channels(std::int16_t *input, std::int16_t *output, size_t num_samples_per_channel, size_t num_channels) {
// 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, 5]");
}
// prepare one input pointer per channel
// hoping it will end up in registers (everything is known at compile time)
std::int16_t *inputs[MAX_CHANNELS];
for (size_t c = 0; c < MAX_CHANNELS; c++) {
inputs[c] = input + c * num_samples_per_channel;
}
// iterate over the samples, copying in interleaved fashion
while (num_samples_per_channel--) {
for (size_t c = 0; c < num_channels; c++) {
*(output++) = *(inputs[c]++);
}
}
}
/**
* SMIoT JASON Qualilife sound recording class.
*
* Author: Jan Schlüter <jan.schluter@lis-lab.fr>
*/
#ifndef RECORDER_H
#define RECORDER_H
#include <libusb.h>
#include <cstdint>
#include <vector>
/** 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;
private:
// libusb handles
struct libusb_context *ctx;
std::vector<struct libusb_device*> devices;
struct libusb_device_handle *handle = NULL;
// device control messages
static const std::uint8_t CMD_SET_CHANNELS = 0x19;
static const std::uint8_t CMD_SET_SAMPLE_RATE = 0x20;
static const std::uint8_t CMD_SET_RESOLUTION = 0x21;
static const std::uint8_t CMD_START_RECORDING = 0x22;
static const std::uint8_t CMD_STOP_RECORDING = 0x23;
static const std::uint8_t CMD_RESET_DEVICE = 0x99;
/** Sends a message to the device, without payload.
* \param[in] cmd The command identifier.
*/
void send_message(std::uint8_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::uint8_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::uint8_t cmd, std::uint8_t *payload, size_t length);
// device messages sent back
static const size_t MAX_PAYLOAD = 131082;
static const size_t MAX_CHANNELS = 5;
static const std::uint8_t MSG_SAMPLES = 0x17;
static const std::uint8_t MSG_PING = 0x32;
static const std::uint8_t MSG_PING_CHARGED = 0x33;
static const std::uint8_t MSG_DEBUG_INFO = 0x34;
/** 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(std::uint8_t *buffer, size_t length, size_t max_wait=0);
// device state, as far as known
size_t num_channels = 0;
size_t sample_rate = 0;
bool recording = false;
public:
JasonRecorder();
~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);
/** Sets the recording format. 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).
*/
void set_format(size_t num_channels, size_t sample_rate);
/** Starts recording from the device chosen with set_device(). */
void start_recording();
/** Stops recording from the device chosen with set_device(). */
void stop_recording();
/** Fetches a packet of samples from the device chosen with set_device().
* Requires the format to have been set before using set_format().
* \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.
*/
void get_samples(std::vector<std::int16_t> &samples, bool planar=false);
/** 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_samples_per_channel The number of samples (per channel)
* to convert.
* \param[in] num_channels The number of channels.
*/
static void interleave_channels(std::int16_t *input, std::int16_t *output, size_t num_samples_per_channel, size_t num_channels);
};
#endif // RECORDER_H
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment