move to new cspot

This commit is contained in:
philippe44
2023-03-25 16:48:41 -07:00
parent c712b78931
commit 008c36facf
2983 changed files with 465270 additions and 13569 deletions

View File

@@ -0,0 +1,89 @@
#pragma once
#include <vector>
#include <algorithm>
#include <cJSON.h>
#include "AudioTransform.h"
namespace bell
{
class AudioMixer : public bell::AudioTransform
{
public:
enum DownmixMode
{
DEFAULT
};
struct MixerConfig
{
std::vector<int> source;
int destination;
};
AudioMixer();
~AudioMixer(){};
// Amount of channels in the input
int from;
// Amount of channels in the output
int to;
// Configuration of each channels in the mixer
std::vector<MixerConfig> mixerConfig;
std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data) override;
void reconfigure() override
{
}
void fromJSON(cJSON *json)
{
cJSON *mappedChannels = cJSON_GetObjectItem(json, "mapped_channels");
if (mappedChannels == NULL || !cJSON_IsArray(mappedChannels))
{
throw std::invalid_argument("Mixer configuration invalid");
}
this->mixerConfig = std::vector<MixerConfig>();
cJSON *iterator = NULL;
cJSON_ArrayForEach(iterator, mappedChannels)
{
std::vector<int> sources(0);
cJSON *iteratorNested = NULL;
cJSON_ArrayForEach(iteratorNested, cJSON_GetObjectItem(iterator, "source"))
{
sources.push_back(iteratorNested->valueint);
}
int destination = cJSON_GetObjectItem(iterator, "destination")->valueint;
this->mixerConfig.push_back(MixerConfig{
.source = sources,
.destination = destination
});
}
std::vector<uint8_t> sources(0);
for (auto &config : mixerConfig)
{
for (auto &source : config.source)
{
if (std::find(sources.begin(), sources.end(), source) == sources.end())
{
sources.push_back(source);
}
}
}
this->from = sources.size();
this->to = mixerConfig.size();
}
};
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "AudioTransform.h"
#include "StreamInfo.h"
#include <memory>
#include "Gain.h"
#include <mutex>
namespace bell
{
class AudioPipeline
{
private:
std::shared_ptr<Gain> headroomGainTransform;
public:
AudioPipeline();
~AudioPipeline(){};
std::mutex accessMutex;
std::vector<std::shared_ptr<AudioTransform>> transforms;
void recalculateHeadroom();
void addTransform(std::shared_ptr<AudioTransform> transform);
void volumeUpdated(int volume);
std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data);
};
}; // namespace bell

View File

@@ -0,0 +1,29 @@
#pragma once
#include <memory>
#include <thread>
#include <mutex>
#include "StreamInfo.h"
#include "TransformConfig.h"
namespace bell
{
class AudioTransform
{
protected:
std::mutex accessMutex;
public:
virtual std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data) = 0;
virtual void sampleRateChanged(uint32_t sampleRate){};
virtual float calculateHeadroom() { return 0; };
virtual void reconfigure() {};
std::string filterType;
std::unique_ptr<TransformConfig> config;
AudioTransform() = default;
virtual ~AudioTransform() = default;
};
};

View File

@@ -0,0 +1,58 @@
#pragma once
#include <memory>
#include <mutex>
#include <vector>
#include "AudioPipeline.h"
#include "CentralAudioBuffer.h"
namespace bell {
#define MAX_INT16 32767
class BellDSP {
public:
BellDSP(std::shared_ptr<CentralAudioBuffer> centralAudioBuffer);
~BellDSP() {};
class AudioEffect {
public:
AudioEffect() = default;
~AudioEffect() = default;
size_t duration;
virtual void apply(float* sampleData, size_t samples, size_t relativePosition) = 0;
};
class FadeEffect: public AudioEffect {
private:
std::function<void()> onFinish;
bool isFadeIn;
public:
FadeEffect(size_t duration, bool isFadeIn, std::function<void()> onFinish = nullptr);
~FadeEffect() {};
void apply(float* sampleData, size_t samples, size_t relativePosition);
};
void applyPipeline(std::shared_ptr<AudioPipeline> pipeline);
void queryInstantEffect(std::unique_ptr<AudioEffect> instantEffect);
std::shared_ptr<AudioPipeline> getActivePipeline();
size_t process(uint8_t* data, size_t bytes, int channels,
uint32_t sampleRate, BitWidth bitWidth);
private:
std::shared_ptr<AudioPipeline> activePipeline;
std::shared_ptr<CentralAudioBuffer> buffer;
std::mutex accessMutex;
std::vector<float> dataLeft = std::vector<float>(1024);
std::vector<float> dataRight = std::vector<float>(1024);
std::unique_ptr<AudioEffect> underflowEffect = nullptr;
std::unique_ptr<AudioEffect> startEffect = nullptr;
std::unique_ptr<AudioEffect> instantEffect = nullptr;
size_t samplesSinceInstantQueued;
};
}; // namespace bell

View File

@@ -0,0 +1,158 @@
#pragma once
#include <cmath>
#include <mutex>
#include <map>
#include <unordered_map>
#include "AudioTransform.h"
extern "C" int dsps_biquad_f32_ae32(const float *input, float *output, int len, float *coef, float *w);
namespace bell
{
class Biquad : public bell::AudioTransform
{
public:
Biquad();
~Biquad(){};
enum class Type
{
Free,
Highpass,
Lowpass,
HighpassFO,
LowpassFO,
Peaking,
Highshelf,
HighshelfFO,
Lowshelf,
LowshelfFO,
Notch,
Bandpass,
Allpass,
AllpassFO
};
std::map<std::string, float> currentConfig;
std::unordered_map<std::string, Type> const strMapType = {
{"free", Type::Free},
{"highpass", Type::Highpass},
{"lowpass", Type::Lowpass},
{"highpass_fo", Type::HighpassFO},
{"lowpass_fo", Type::LowpassFO},
{"peaking", Type::Peaking},
{"highshelf", Type::Highshelf},
{"highshelf_fo", Type::HighpassFO},
{"lowshelf", Type::Lowshelf},
{"lowshelf_fo", Type::LowpassFO},
{"notch", Type::Notch},
{"bandpass", Type::Bandpass},
{"allpass", Type::Allpass},
{"allpass_fo", Type::AllpassFO},
};
float freq, q, gain;
int channel;
Biquad::Type type;
std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data) override;
void configure(Type type, std::map<std::string, float> &config);
void sampleRateChanged(uint32_t sampleRate) override;
void reconfigure() override
{
std::scoped_lock lock(this->accessMutex);
std::map<std::string, float> biquadConfig;
this->channel = config->getChannels()[0];
float invalid = -0x7C;
auto type = config->getString("biquad_type");
float bandwidth = config->getFloat("bandwidth", false, invalid);
float slope = config->getFloat("slope", false, invalid);
float gain = config->getFloat("gain", false, invalid);
float frequency = config->getFloat("frequency", false, invalid);
float q = config->getFloat("q", false, invalid);
if (currentConfig["bandwidth"] == bandwidth &&
currentConfig["slope"] == slope &&
currentConfig["gain"] == gain &&
currentConfig["frequency"] == frequency &&
currentConfig["q"] == q)
{
return;
}
if (bandwidth != invalid)
biquadConfig["bandwidth"] = bandwidth;
if (slope != invalid)
biquadConfig["slope"] = slope;
if (gain != invalid)
biquadConfig["gain"] = gain;
if (frequency != invalid)
biquadConfig["freq"] = frequency;
if (q != invalid)
biquadConfig["q"] = q;
if (type == "free")
{
biquadConfig["a1"] = config->getFloat("a1");
biquadConfig["a2"] = config->getFloat("a2");
biquadConfig["b0"] = config->getFloat("b0");
biquadConfig["b1"] = config->getFloat("b1");
biquadConfig["b2"] = config->getFloat("b2");
}
auto typeElement = strMapType.find(type);
if (typeElement != strMapType.end())
{
this->configure(typeElement->second, biquadConfig);
}
else
{
throw std::invalid_argument("No biquad of type " + type);
}
}
private:
float coeffs[5];
float w[2] = {1.0, 1.0};
float sampleRate = 44100;
// Generator methods for different filter types
void highPassCoEffs(float f, float q);
void highPassFOCoEffs(float f);
void lowPassCoEffs(float f, float q);
void lowPassFOCoEffs(float f);
void peakCoEffs(float f, float gain, float q);
void peakCoEffsBandwidth(float f, float gain, float bandwidth);
void highShelfCoEffs(float f, float gain, float q);
void highShelfCoEffsSlope(float f, float gain, float slope);
void highShelfFOCoEffs(float f, float gain);
void lowShelfCoEffs(float f, float gain, float q);
void lowShelfCoEffsSlope(float f, float gain, float slope);
void lowShelfFOCoEffs(float f, float gain);
void notchCoEffs(float f, float gain, float q);
void notchCoEffsBandwidth(float f, float gain, float bandwidth);
void bandPassCoEffs(float f, float q);
void bandPassCoEffsBandwidth(float f, float bandwidth);
void allPassCoEffs(float f, float q);
void allPassCoEffsBandwidth(float f, float bandwidth);
void allPassFOCoEffs(float f);
void normalizeCoEffs(float a0, float a1, float a2, float b0, float b1, float b2);
};
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include <vector>
#include <memory>
#include <cmath>
#include <mutex>
#include <map>
#include "Biquad.h"
#include "AudioTransform.h"
namespace bell
{
class BiquadCombo : public bell::AudioTransform
{
private:
std::vector<std::unique_ptr<bell::Biquad>> biquads;
// Calculates Q values for Nth order Butterworth / Linkwitz-Riley filters
std::vector<float> calculateBWQ(int order);
std::vector<float> calculateLRQ(int order);
public:
BiquadCombo();
~BiquadCombo(){};
int channel;
std::map<std::string, float> paramCache = {
{"order", 0.0f},
{"frequency", 0.0f}
};
enum class FilterType
{
Highpass,
Lowpass
};
void linkwitzRiley(float freq, int order, FilterType type);
void butterworth(float freq, int order, FilterType type);
std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data) override;
void sampleRateChanged(uint32_t sampleRate) override;
void reconfigure() override
{
std::scoped_lock lock(this->accessMutex);
float freq = config->getFloat("frequency");
int order = config->getInt("order");
if (paramCache["frequency"] == freq && paramCache["order"] == order)
{
return;
} else {
paramCache["frequency"] = freq;
paramCache["order"] = order;
}
this->channel = config->getChannels()[0];
this->biquads = std::vector<std::unique_ptr<bell::Biquad>>();
auto type = config->getString("combo_type");
if (type == "lr_lowpass")
{
this->linkwitzRiley(freq, order, FilterType::Lowpass);
}
else if (type == "lr_highpass")
{
this->linkwitzRiley(freq, order, FilterType::Highpass);
}
else if (type == "bw_highpass")
{
this->butterworth(freq, order, FilterType::Highpass);
}
else if (type == "bw_lowpass")
{
this->butterworth(freq, order, FilterType::Highpass);
}
else
{
throw std::invalid_argument("Invalid combo filter type");
}
}
};
};

View File

@@ -0,0 +1,172 @@
#pragma once
#include <atomic>
#include <cmath>
#include <iostream>
#include <memory>
#include <mutex>
#include <functional>
#include "BellUtils.h"
#include "CircularBuffer.h"
#include "StreamInfo.h"
#include "WrappedSemaphore.h"
#ifdef _WIN32
#define __attribute__(X)
#endif
typedef std::function<void(std::string)> shutdownEventHandler;
namespace bell {
class CentralAudioBuffer {
private:
std::mutex accessMutex;
std::atomic<bool> isLocked = false;
std::mutex dataAccessMutex;
public:
static const size_t PCM_CHUNK_SIZE = 4096;
std::unique_ptr<bell::WrappedSemaphore> chunkReady;
// Audio marker for track change detection, and DSP autoconfig
struct AudioChunk {
// Timeval
int32_t sec;
int32_t usec;
// Unique track hash, used for track change detection
size_t trackHash;
// Audio format
uint32_t sampleRate;
uint8_t channels;
uint8_t bitWidth;
// PCM data size
size_t pcmSize;
// PCM data
uint8_t pcmData[PCM_CHUNK_SIZE];
} __attribute__((packed));
CentralAudioBuffer(size_t chunks) {
audioBuffer = std::make_shared<CircularBuffer>(chunks * sizeof(AudioChunk));
chunkReady = std::make_unique<bell::WrappedSemaphore>(50);
}
std::shared_ptr<bell::CircularBuffer> audioBuffer;
uint32_t currentSampleRate = 44100;
/**
* Returns current sample rate
* @return sample rate
*/
uint32_t getSampleRate() { return currentSampleRate; }
/**
* Clears input buffer, to be called for track change and such
*/
void clearBuffer() {
std::scoped_lock lock(this->dataAccessMutex);
//size_t exceptSize = currentSampleRate + (sizeof(AudioChunk) - (currentSampleRate % sizeof(AudioChunk)));
audioBuffer->emptyBuffer();
}
void emptyCompletely() {
std::scoped_lock lock(this->dataAccessMutex);
audioBuffer->emptyBuffer();
}
bool hasAtLeast(size_t chunks) {
return this->audioBuffer->size() >= chunks * sizeof(AudioChunk);
}
/**
* Locks access to audio buffer. Call after starting playback
*/
void lockAccess() {
if (!isLocked) {
clearBuffer();
this->accessMutex.lock();
isLocked = true;
}
}
/**
* Frees access to the audio buffer. Call during shutdown
*/
void unlockAccess() {
if (isLocked) {
clearBuffer();
this->accessMutex.unlock();
isLocked = false;
}
}
AudioChunk currentChunk = { };
bool hasChunk = false;
AudioChunk lastReadChunk = { };
AudioChunk* readChunk() {
std::scoped_lock lock(this->dataAccessMutex);
if (audioBuffer->size() < sizeof(AudioChunk)) {
lastReadChunk.pcmSize = 0;
return nullptr;
}
auto readBytes =
audioBuffer->read((uint8_t*)&lastReadChunk, sizeof(AudioChunk));
currentSampleRate = static_cast<uint32_t>(lastReadChunk.sampleRate);
return &lastReadChunk;
}
size_t writePCM(const uint8_t* data, size_t dataSize, size_t hash,
uint32_t sampleRate = 44100, uint8_t channels = 2,
BitWidth bitWidth = BitWidth::BW_16, int32_t sec = 0,
int32_t usec = 0) {
std::scoped_lock lock(this->dataAccessMutex);
if (hasChunk && (currentChunk.trackHash != hash ||
currentChunk.pcmSize >= PCM_CHUNK_SIZE)) {
if ((audioBuffer->size() - audioBuffer->capacity()) <
sizeof(AudioChunk)) {
return 0;
}
// Track changed or buf full, return current chunk
hasChunk = false;
this->audioBuffer->write((uint8_t*)&currentChunk, sizeof(AudioChunk));
// this->chunkReady->give();
}
// New chunk requested, initialize
if (!hasChunk) {
currentChunk.trackHash = hash;
currentChunk.sampleRate = sampleRate;
currentChunk.channels = channels;
currentChunk.bitWidth = 16;
currentChunk.sec = sec;
currentChunk.usec = usec;
currentChunk.pcmSize = 0;
hasChunk = true;
}
// Calculate how much data we can write
size_t toWriteSize = dataSize;
if (currentChunk.pcmSize + toWriteSize > PCM_CHUNK_SIZE) {
toWriteSize = PCM_CHUNK_SIZE - currentChunk.pcmSize;
}
// Copy it over :)
memcpy(currentChunk.pcmData + currentChunk.pcmSize, data, toWriteSize);
currentChunk.pcmSize += toWriteSize;
return toWriteSize;
}
};
} // namespace bell

View File

@@ -0,0 +1,103 @@
#pragma once
#include <vector>
#include <memory>
#include <algorithm>
#include <cmath>
#include <math.h>
#include <iostream>
#include <mutex>
#include <map>
#include "Biquad.h"
#include "AudioTransform.h"
#define pow10f(x) expf(2.302585092994046f*x)
// This is a fast approximation to log2()
// Y = C[0]*F*F*F + C[1]*F*F + C[2]*F + C[3] + E;
float log2f_approx(float X);
#define log10f_fast(x) (log2f_approx(x)*0.3010299956639812f)
namespace bell
{
class Compressor : public bell::AudioTransform
{
private:
std::vector<int> channels;
std::vector<float> tmp;
std::map<std::string, float> paramCache;
float attack;
float release;
float threshold;
float factor;
float clipLimit;
float makeupGain;
float lastLoudness = -100.0f;
float sampleRate = 44100;
public:
Compressor();
~Compressor(){};
void configure(std::vector<int> channels, float attack, float release, float threshold, float factor, float makeupGain);
void sumChannels(std::unique_ptr<StreamInfo> &data);
void calLoudness();
void calGain();
void applyGain(std::unique_ptr<StreamInfo> &data);
void reconfigure() override
{
std::scoped_lock lock(this->accessMutex);
auto newChannels = config->getChannels();
float newAttack = config->getFloat("attack");
float newRelease = config->getFloat("release");
float newThreshold = config->getFloat("threshold");
float newFactor = config->getFloat("factor");
float newMakeupGain = config->getFloat("makeup_gain");
if (paramCache["attack"] == newAttack &&
paramCache["release"] == newRelease &&
paramCache["threshold"] == newThreshold &&
paramCache["factor"] == newFactor &&
paramCache["makeup_gain"] == newMakeupGain)
{
return;
}
else
{
paramCache["attack"] = newAttack;
paramCache["release"] = newRelease;
paramCache["threshold"] = newThreshold;
paramCache["factor"] = newFactor;
paramCache["makeup_gain"] = newMakeupGain;
}
this->configure(newChannels, newAttack, newRelease, newThreshold, newFactor, newMakeupGain);
}
// void fromJSON(cJSON* json) override {
// // get field channels
// channels = jsonGetChannels(json);
// float attack = jsonGetNumber<float>(json, "attack", false, 0);
// float release = jsonGetNumber<float>(json, "release", false, 0);
// float factor = jsonGetNumber<float>(json, "factor", false, 4);
// float makeupGain = jsonGetNumber<float>(json, "makeup_gain", false, 0);
// float threshold = jsonGetNumber<float>(json, "threshold", false, 0);
// this->configure(attack, release, clipLimit, threshold, factor, makeupGain);
// }
std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data) override;
void sampleRateChanged(uint32_t sampleRate) override { this->sampleRate = sampleRate; };
};
};

View File

@@ -0,0 +1,40 @@
#pragma once
#include <cmath>
#include <mutex>
#include <iostream>
#include "AudioTransform.h"
namespace bell
{
class Gain : public bell::AudioTransform
{
private:
float gainFactor = 1.0f;
std::vector<int> channels;
public:
Gain();
~Gain() {};
float gainDb = 0.0;
void configure(std::vector<int> channels, float gainDB);
std::unique_ptr<StreamInfo> process(std::unique_ptr<StreamInfo> data) override;
void reconfigure() override {
std::scoped_lock lock(this->accessMutex);
float gain = config->getFloat("gain");
this->channels = config->getChannels();
if (gainDb == gain) {
return;
}
this->configure(channels, gain);
}
};
}

View File

@@ -0,0 +1,110 @@
#pragma once
#include "TransformConfig.h"
#include "cJSON.h"
namespace bell
{
class JSONTransformConfig : public bell::TransformConfig
{
private:
cJSON *json;
public:
JSONTransformConfig(cJSON *body)
{
this->json = body;
};
~JSONTransformConfig(){};
std::string rawGetString(const std::string &field) override
{
cJSON *value = cJSON_GetObjectItem(json, field.c_str());
if (value != NULL && cJSON_IsString(value))
{
return std::string(value->valuestring);
}
return "invalid";
}
std::vector<int> rawGetIntArray(const std::string &field) override
{
std::vector<int> result;
cJSON *value = cJSON_GetObjectItem(json, field.c_str());
if (value != NULL && cJSON_IsArray(value))
{
for (int i = 0; i < cJSON_GetArraySize(value); i++)
{
cJSON *item = cJSON_GetArrayItem(value, i);
if (item != NULL && cJSON_IsNumber(item))
{
result.push_back(item->valueint);
}
}
}
return result;
}
std::vector<float> rawGetFloatArray(const std::string &field) override
{
std::vector<float> result;
cJSON *value = cJSON_GetObjectItem(json, field.c_str());
if (value != NULL && cJSON_IsArray(value))
{
for (int i = 0; i < cJSON_GetArraySize(value); i++)
{
cJSON *item = cJSON_GetArrayItem(value, i);
if (item != NULL && cJSON_IsNumber(item))
{
result.push_back(item->valuedouble);
}
}
}
return result;
}
int rawGetInt(const std::string &field) override
{
cJSON *value = cJSON_GetObjectItem(json, field.c_str());
if (value != NULL && cJSON_IsNumber(value))
{
return (int)value->valueint;
}
return invalidInt;
}
bool isArray(const std::string &field) override
{
cJSON *value = cJSON_GetObjectItem(json, field.c_str());
if (value != NULL && cJSON_IsArray(value))
{
return true;
}
return false;
}
float rawGetFloat(const std::string &field) override
{
cJSON *value = cJSON_GetObjectItem(json, field.c_str());
if (value != NULL && cJSON_IsNumber(value))
{
return (float)value->valuedouble;
}
return invalidInt;
}
};
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <memory>
#include <vector>
#include <string>
namespace bell
{
enum class Channels {
LEFT,
RIGHT,
LEFT_RIGHT
};
enum class SampleRate : uint32_t
{
SR_44100 = 44100,
SR_48000 = 48000,
};
enum class BitWidth : uint32_t
{
BW_16 = 16,
BW_24 = 24,
BW_32 = 32,
};
typedef struct
{
float** data;
BitWidth bitwidth;
int numChannels;
SampleRate sampleRate;
size_t numSamples;
} StreamInfo;
};

View File

@@ -0,0 +1,134 @@
#pragma once
#include <memory>
#include <vector>
#include <string>
#include <variant>
#include <iostream>
#include <map>
namespace bell
{
class TransformConfig
{
protected:
int invalidInt = -0x7C;
std::string invalidString = "_invalid";
public:
TransformConfig() = default;
virtual ~TransformConfig() = default;
int currentVolume = 60;
virtual std::string rawGetString(const std::string &field) = 0;
virtual int rawGetInt(const std::string &field) = 0;
virtual bool isArray(const std::string &field) = 0;
virtual float rawGetFloat(const std::string &field) = 0;
virtual std::vector<float> rawGetFloatArray(const std::string &field) = 0;
virtual std::vector<int> rawGetIntArray(const std::string &field) = 0;
typedef std::variant<int, float, std::string> Value;
std::map<std::string, std::vector<Value>> rawValues;
Value getRawValue(const std::string &field)
{
int index = this->currentVolume * (rawValues[field].size()) / 100;
if (index >= rawValues[field].size())
index = rawValues[field].size() - 1;
return rawValues[field][index];
}
std::string getString(const std::string &field, bool isRequired = false, std::string defaultValue = "")
{
if (rawValues.count(field) == 0)
{
rawValues[field] = std::vector<Value>({Value(rawGetString(field))});
}
auto val = std::get<std::string>(getRawValue(field));
if (val == invalidString)
{
if (isRequired)
throw std::invalid_argument("Field " + field + " is required");
else
return defaultValue;
}
else
return val;
}
int getInt(const std::string &field, bool isRequired = false, int defaultValue = 0)
{
if (rawValues.count(field) == 0)
{
if (isArray(field))
{
rawValues[field] = std::vector<Value>();
for (auto f : rawGetIntArray(field))
{
rawValues[field].push_back(f);
}
}
else
{
rawValues[field] = std::vector<Value>({Value(rawGetInt(field))});
}
}
auto val = std::get<int>(getRawValue(field));
if (val == invalidInt)
{
if (isRequired)
throw std::invalid_argument("Field " + field + " is required");
else
return defaultValue;
}
else
return val;
}
float getFloat(const std::string &field, bool isRequired = false, float defaultValue = 0)
{
if (rawValues.count(field) == 0)
{
if (isArray(field))
{
rawValues[field] = std::vector<Value>();
for (auto f : rawGetFloatArray(field))
{
rawValues[field].push_back(f);
}
}
else
{
rawValues[field] = std::vector<Value>({ Value(rawGetFloat(field)) });
}
}
auto val = std::get<float>(getRawValue(field));
if (val == invalidInt)
{
if (isRequired)
throw std::invalid_argument("Field " + field + " is required");
else
return defaultValue;
}
else
return val;
}
std::vector<int> getChannels()
{
auto channel = getInt("channel", false, invalidInt);
if (channel != invalidInt)
{
return std::vector<int>({channel});
}
return rawGetIntArray("channels");
}
};
}