psd 等
This commit is contained in:
@@ -1,2 +1,57 @@
|
||||
#include "pch.h"
|
||||
#include "XYEegParser64.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint8_t kFrameHeader = 0xAA;
|
||||
constexpr std::uint8_t kFrameTail = 0x55;
|
||||
constexpr std::size_t kReservedByteCount = 6;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t> XYEegParser64::SerializeImpedanceCommand(ImpedanceSwitch impedance_switch)
|
||||
{
|
||||
std::vector<std::uint8_t> payload(kReservedByteCount, 0);
|
||||
payload.push_back(static_cast<std::uint8_t>(impedance_switch));
|
||||
return SerializeCommand(FunctionCode::kImpedance, payload);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> XYEegParser64::SerializeGainAndSampleRateCommand(GainSwitch gain,
|
||||
SampleRateSwitch sample_rate)
|
||||
{
|
||||
std::vector<std::uint8_t> payload(kReservedByteCount, 0);
|
||||
payload.push_back(static_cast<std::uint8_t>(gain));
|
||||
payload.push_back(static_cast<std::uint8_t>(sample_rate));
|
||||
return SerializeCommand(FunctionCode::kGainSampleRate, payload);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> XYEegParser64::SerializeCommand(FunctionCode function_code,
|
||||
const std::vector<std::uint8_t>& payload)
|
||||
{
|
||||
std::vector<std::uint8_t> command;
|
||||
command.reserve(1 + 1 + 2 + payload.size() + 1 + 2);
|
||||
|
||||
command.push_back(kFrameHeader);
|
||||
command.push_back(static_cast<std::uint8_t>(function_code));
|
||||
command.push_back(static_cast<std::uint8_t>((payload.size() >> 8) & 0xFF));
|
||||
command.push_back(static_cast<std::uint8_t>(payload.size() & 0xFF));
|
||||
command.insert(command.end(), payload.begin(), payload.end());
|
||||
command.push_back(Checksum(command, 1, command.size()));
|
||||
command.push_back(kFrameTail);
|
||||
command.push_back(kFrameTail);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
std::uint8_t XYEegParser64::Checksum(const std::vector<std::uint8_t>& bytes,
|
||||
std::size_t begin_offset,
|
||||
std::size_t end_offset_exclusive) noexcept
|
||||
{
|
||||
std::uint8_t checksum = 0;
|
||||
for (std::size_t i = begin_offset; i < end_offset_exclusive && i < bytes.size(); ++i) {
|
||||
checksum = static_cast<std::uint8_t>(checksum + bytes[i]);
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
@@ -7,4 +7,49 @@ using XYEegFrame64 = xyparser::XYEegFrame<64>;
|
||||
class XYEegParser64 final : public xyparser::XYEegTcpParserCommon<64> {
|
||||
public:
|
||||
using Frame = XYEegFrame64;
|
||||
|
||||
enum class FunctionCode : std::uint8_t {
|
||||
kImpedance = 0x01,
|
||||
kGainSampleRate = 0x02
|
||||
};
|
||||
|
||||
enum class ImpedanceSwitch : std::uint8_t {
|
||||
kClose = 0x00,
|
||||
kOpen = 0x01
|
||||
};
|
||||
|
||||
enum class GainSwitch : std::uint8_t {
|
||||
k1 = 1,
|
||||
k2 = 2,
|
||||
k3 = 3,
|
||||
k4 = 4,
|
||||
k6 = 6,
|
||||
k8 = 8,
|
||||
k12 = 12,
|
||||
k24 = 24
|
||||
};
|
||||
|
||||
enum class SampleRateSwitch : std::uint8_t {
|
||||
k250 = 0,
|
||||
k500 = 1,
|
||||
k1000 = 2,
|
||||
k2000 = 3
|
||||
};
|
||||
|
||||
static constexpr std::size_t kImpedancePayloadSize = 7;
|
||||
static constexpr std::size_t kGainSampleRatePayloadSize = 8;
|
||||
static constexpr std::size_t kCommandOverheadSize = 7;
|
||||
static constexpr std::size_t kImpedanceCommandSize = kImpedancePayloadSize + kCommandOverheadSize;
|
||||
static constexpr std::size_t kGainSampleRateCommandSize = kGainSampleRatePayloadSize + kCommandOverheadSize;
|
||||
|
||||
static std::vector<std::uint8_t> SerializeImpedanceCommand(ImpedanceSwitch impedance_switch);
|
||||
static std::vector<std::uint8_t> SerializeGainAndSampleRateCommand(GainSwitch gain,
|
||||
SampleRateSwitch sample_rate);
|
||||
|
||||
private:
|
||||
static std::vector<std::uint8_t> SerializeCommand(FunctionCode function_code,
|
||||
const std::vector<std::uint8_t>& payload);
|
||||
static std::uint8_t Checksum(const std::vector<std::uint8_t>& bytes,
|
||||
std::size_t begin_offset,
|
||||
std::size_t end_offset_exclusive) noexcept;
|
||||
};
|
||||
|
||||
@@ -115,3 +115,65 @@ void MinimalExampleFor64ChParser()
|
||||
|
||||
XYParser_DestroyParser(parser);
|
||||
}
|
||||
|
||||
void MinimalExampleFor64ChControlCommand()
|
||||
{
|
||||
std::array<std::uint8_t, 32> impedance_cmd{};
|
||||
std::array<std::uint8_t, 32> setting_cmd{};
|
||||
|
||||
const int impedance_size = XYParser_Serialize64ImpedanceCommand(
|
||||
1, impedance_cmd.data(), impedance_cmd.size());
|
||||
const int setting_size = XYParser_Serialize64GainSampleRateCommand(
|
||||
24, 1000, setting_cmd.data(), setting_cmd.size());
|
||||
|
||||
(void)impedance_size;
|
||||
(void)setting_size;
|
||||
}
|
||||
|
||||
void MinimalExampleFor8ChImpedanceCommand()
|
||||
{
|
||||
std::array<std::uint8_t, 7> command{};
|
||||
const int command_size = XYParser_Serialize8ChImpedanceCommand(
|
||||
1,
|
||||
command.data(),
|
||||
command.size());
|
||||
|
||||
if (command_size == static_cast<int>(command.size())) {
|
||||
const std::uint8_t first_byte = command.front();
|
||||
const std::uint8_t payload = command[3];
|
||||
(void)first_byte;
|
||||
(void)payload;
|
||||
}
|
||||
}
|
||||
|
||||
void MinimalExampleForImpedanceOutput()
|
||||
{
|
||||
XYParserHandle parser = XYParser_CreateParser(8);
|
||||
if (parser == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
XYParser_SetBypassChecksum(parser, 1);
|
||||
XYParser_SetSampleRate(parser, 250);
|
||||
XYParser_SetImpedanceDetection(parser, 1);
|
||||
|
||||
const std::vector<std::uint8_t> raw_bytes = BuildMinimalFrame(8);
|
||||
std::array<XYParserFrameSummary, 1> frame_summaries{};
|
||||
std::array<XYParserImpedanceSummary, 1> impedance_summaries{};
|
||||
|
||||
XYParser_Feed(parser,
|
||||
raw_bytes.data(),
|
||||
raw_bytes.size(),
|
||||
frame_summaries.data(),
|
||||
static_cast<int>(frame_summaries.size()));
|
||||
|
||||
const int impedance_count = XYParser_ReadImpedance(parser,
|
||||
impedance_summaries.data(),
|
||||
static_cast<int>(impedance_summaries.size()));
|
||||
if (impedance_count > 0) {
|
||||
const std::uint16_t po5_impedance = impedance_summaries.front().impedance_values[LeadChannel_PO5];
|
||||
(void)po5_impedance;
|
||||
}
|
||||
|
||||
XYParser_DestroyParser(parser);
|
||||
}
|
||||
|
||||
216
XYParser/XYImpedanceProcessor.cpp
Normal file
216
XYParser/XYImpedanceProcessor.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#include "pch.h"
|
||||
#include "XYImpedanceProcessor.h"
|
||||
|
||||
#include "third_party/kissfft/kiss_fftr.h"
|
||||
#include "third_party/kissfft/window_functions.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kDefaultSampleRate = 250;
|
||||
constexpr int k8ChLeadCount = 8;
|
||||
constexpr int k64ChLeadCount = 64;
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k8ChLeadCount> k8ChLeadMap = {
|
||||
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
||||
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k64ChLeadCount> k64ChLeadMap = {
|
||||
LeadChannel_FP1, LeadChannel_FP2, LeadChannel_PO6, LeadChannel_POZ,
|
||||
LeadChannel_F3, LeadChannel_F4, LeadChannel_FPZ, LeadChannel_AF4,
|
||||
LeadChannel_FC3, LeadChannel_PO8, LeadChannel_CP2, LeadChannel_CP1,
|
||||
LeadChannel_FCZ, LeadChannel_PO5, LeadChannel_FC2, LeadChannel_FC1,
|
||||
LeadChannel_C3, LeadChannel_C4, LeadChannel_FC4, LeadChannel_CP4,
|
||||
LeadChannel_P3, LeadChannel_P4, LeadChannel_F5, LeadChannel_C5,
|
||||
LeadChannel_F6, LeadChannel_PO4, LeadChannel_CP6, LeadChannel_CP5,
|
||||
LeadChannel_PO3, LeadChannel_CP3, LeadChannel_FC6, LeadChannel_FC5,
|
||||
LeadChannel_CB1, LeadChannel_CB2, LeadChannel_P5, LeadChannel_AF7,
|
||||
LeadChannel_A1, LeadChannel_T7, LeadChannel_FT7, LeadChannel_TP7,
|
||||
LeadChannel_FT8, LeadChannel_AF8, LeadChannel_F8, LeadChannel_F7,
|
||||
LeadChannel_P6, LeadChannel_C6, LeadChannel_O2, LeadChannel_O1,
|
||||
LeadChannel_T8, LeadChannel_P7, LeadChannel_CZ, LeadChannel_PZ,
|
||||
LeadChannel_P8, LeadChannel_FZ, LeadChannel_OZ, LeadChannel_PO7,
|
||||
LeadChannel_TP8, LeadChannel_AF3, LeadChannel_C2, LeadChannel_C1,
|
||||
LeadChannel_P2, LeadChannel_P1, LeadChannel_F2, LeadChannel_F1};
|
||||
|
||||
} // namespace
|
||||
|
||||
XYImpedanceProcessor::XYImpedanceProcessor(std::uint8_t channel_count)
|
||||
: channel_count_(channel_count)
|
||||
{
|
||||
}
|
||||
|
||||
void XYImpedanceProcessor::SetChannelCount(std::uint8_t channel_count)
|
||||
{
|
||||
channel_count_ = channel_count;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void XYImpedanceProcessor::SetSampleRate(int sample_rate)
|
||||
{
|
||||
if (sample_rate <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sample_rate_ != sample_rate) {
|
||||
sample_rate_ = sample_rate;
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void XYImpedanceProcessor::SetEnabled(bool enabled)
|
||||
{
|
||||
if (enabled_ == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabled_ = enabled;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void XYImpedanceProcessor::Reset()
|
||||
{
|
||||
cached_samples_.clear();
|
||||
pending_results_.clear();
|
||||
}
|
||||
|
||||
void XYImpedanceProcessor::PushFrame(const XYParserFrameSummary& frame)
|
||||
{
|
||||
if (!enabled_ || frame.channel_count != channel_count_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::size_t sample_index = 0; sample_index < frame.sample_count; ++sample_index) {
|
||||
Sample sample{};
|
||||
for (std::size_t channel_index = 0; channel_index < frame.channel_count; ++channel_index) {
|
||||
sample[channel_index] = frame.channel_values_uv[sample_index][channel_index];
|
||||
}
|
||||
cached_samples_.push_back(sample);
|
||||
}
|
||||
|
||||
ProcessPendingWindows();
|
||||
}
|
||||
|
||||
int XYImpedanceProcessor::Read(XYParserImpedanceSummary* out_summaries, int max_summaries)
|
||||
{
|
||||
if (out_summaries == nullptr || max_summaries <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int read_count = std::min<int>(static_cast<int>(pending_results_.size()), max_summaries);
|
||||
for (int i = 0; i < read_count; ++i) {
|
||||
out_summaries[i] = pending_results_.front();
|
||||
pending_results_.pop_front();
|
||||
}
|
||||
return read_count;
|
||||
}
|
||||
|
||||
void XYImpedanceProcessor::ProcessPendingWindows()
|
||||
{
|
||||
const int effective_sample_rate = sample_rate_ > 0 ? sample_rate_ : 1;
|
||||
const std::size_t window_sample_count = static_cast<std::size_t>(effective_sample_rate);
|
||||
while (cached_samples_.size() >= window_sample_count) {
|
||||
pending_results_.push_back(BuildSummaryFromWindow());
|
||||
cached_samples_.erase(cached_samples_.begin(), cached_samples_.begin() + static_cast<std::ptrdiff_t>(window_sample_count));
|
||||
}
|
||||
}
|
||||
|
||||
XYParserImpedanceSummary XYImpedanceProcessor::BuildSummaryFromWindow() const
|
||||
{
|
||||
XYParserImpedanceSummary summary{};
|
||||
summary.channel_count = channel_count_;
|
||||
summary.sample_rate = static_cast<std::uint32_t>(sample_rate_ > 0 ? sample_rate_ : kDefaultSampleRate);
|
||||
summary.window_sample_count = summary.sample_rate;
|
||||
|
||||
const auto fill_channel = [&](std::size_t channel_index, XYParserLeadChannelNumber lead) {
|
||||
summary.impedance_values[static_cast<std::size_t>(lead)] = EstimateImpedanceValue(channel_index);
|
||||
};
|
||||
|
||||
if (channel_count_ == 8) {
|
||||
for (std::size_t i = 0; i < k8ChLeadMap.size(); ++i) {
|
||||
fill_channel(i, k8ChLeadMap[i]);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < std::min<std::size_t>(channel_count_, k64ChLeadMap.size()); ++i) {
|
||||
fill_channel(i, k64ChLeadMap[i]);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
std::uint16_t XYImpedanceProcessor::EstimateImpedanceValue(std::size_t channel_index) const
|
||||
{
|
||||
const int effective_sample_rate = sample_rate_ > 0 ? sample_rate_ : 1;
|
||||
const int cached_sample_count = static_cast<int>(cached_samples_.size());
|
||||
const int raw_sample_count = cached_sample_count < effective_sample_rate ? cached_sample_count : effective_sample_rate;
|
||||
if (raw_sample_count < 2 || channel_index >= channel_count_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int fft_size = (raw_sample_count % 2 == 0) ? raw_sample_count : (raw_sample_count - 1);
|
||||
if (fft_size < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
kiss_fftr_cfg cfg = kiss_fftr_alloc(fft_size, 0, nullptr, nullptr);
|
||||
if (cfg == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<kiss_fft_scalar> time_data(static_cast<std::size_t>(fft_size), 0.0);
|
||||
std::vector<double> window(static_cast<std::size_t>(fft_size), 0.0);
|
||||
hanning_function(fft_size, window.data());
|
||||
|
||||
double window_sum = 0.0;
|
||||
for (int i = 0; i < fft_size; ++i) {
|
||||
time_data[static_cast<std::size_t>(i)] = cached_samples_[static_cast<std::size_t>(i)][channel_index] * window[static_cast<std::size_t>(i)];
|
||||
window_sum += window[static_cast<std::size_t>(i)];
|
||||
}
|
||||
if (window_sum <= 0.0) {
|
||||
kiss_fftr_free(cfg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<kiss_fft_cpx> freq_data(static_cast<std::size_t>(fft_size / 2 + 1));
|
||||
kiss_fftr(cfg, time_data.data(), freq_data.data());
|
||||
kiss_fftr_free(cfg);
|
||||
|
||||
const double sample_rate = sample_rate_ > 0 ? static_cast<double>(sample_rate_) : static_cast<double>(kDefaultSampleRate);
|
||||
const double freq_resolution = sample_rate / static_cast<double>(fft_size);
|
||||
constexpr double kLineNoiseFreq = 50.0;
|
||||
constexpr double kLineNoiseHarmonicFreq = 100.0;
|
||||
constexpr double kRejectHalfBand = 5.0;
|
||||
constexpr double kImpedanceScale = 0.0008;
|
||||
|
||||
double max_magnitude = 0.0;
|
||||
for (std::size_t k = 1; k < freq_data.size(); ++k) {
|
||||
const double frequency = static_cast<double>(k) * freq_resolution;
|
||||
if (std::abs(frequency - kLineNoiseFreq) <= kRejectHalfBand ||
|
||||
std::abs(frequency - kLineNoiseHarmonicFreq) <= kRejectHalfBand) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const double real = freq_data[k].r;
|
||||
const double imag = freq_data[k].i;
|
||||
double magnitude = std::sqrt(real * real + imag * imag);
|
||||
if (k == freq_data.size() - 1 && (fft_size % 2 == 0)) {
|
||||
magnitude /= window_sum;
|
||||
} else {
|
||||
magnitude *= 2.0 / window_sum;
|
||||
}
|
||||
|
||||
if (magnitude > max_magnitude) {
|
||||
max_magnitude = magnitude;
|
||||
}
|
||||
}
|
||||
|
||||
const double scaled_value = std::round(max_magnitude) * kImpedanceScale;
|
||||
const long rounded_value = std::lround(scaled_value);
|
||||
const long clamped_value = rounded_value < 0 ? 0 : (rounded_value > 65535 ? 65535 : rounded_value);
|
||||
return static_cast<std::uint16_t>(clamped_value);
|
||||
}
|
||||
34
XYParser/XYImpedanceProcessor.h
Normal file
34
XYParser/XYImpedanceProcessor.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "XYParserApi.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
|
||||
class XYImpedanceProcessor {
|
||||
public:
|
||||
explicit XYImpedanceProcessor(std::uint8_t channel_count = 0);
|
||||
|
||||
void SetChannelCount(std::uint8_t channel_count);
|
||||
void SetSampleRate(int sample_rate);
|
||||
void SetEnabled(bool enabled);
|
||||
void Reset();
|
||||
|
||||
void PushFrame(const XYParserFrameSummary& frame);
|
||||
int Read(XYParserImpedanceSummary* out_summaries, int max_summaries);
|
||||
|
||||
private:
|
||||
using Sample = std::array<double, XYPARSER_MAX_CHANNELS>;
|
||||
|
||||
void ProcessPendingWindows();
|
||||
XYParserImpedanceSummary BuildSummaryFromWindow() const;
|
||||
std::uint16_t EstimateImpedanceValue(std::size_t channel_index) const;
|
||||
|
||||
private:
|
||||
std::uint8_t channel_count_ = 0;
|
||||
int sample_rate_ = 250;
|
||||
bool enabled_ = false;
|
||||
std::deque<Sample> cached_samples_;
|
||||
std::deque<XYParserImpedanceSummary> pending_results_;
|
||||
};
|
||||
@@ -137,7 +137,15 @@
|
||||
<ItemGroup>
|
||||
<ClInclude Include="framework.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="third_party\kissfft\fir_filter.h" />
|
||||
<ClInclude Include="third_party\kissfft\_kiss_fft_guts.h" />
|
||||
<ClInclude Include="third_party\kissfft\kiss_fft.h" />
|
||||
<ClInclude Include="third_party\kissfft\kiss_fft_log.h" />
|
||||
<ClInclude Include="third_party\kissfft\kiss_fftr.h" />
|
||||
<ClInclude Include="third_party\kissfft\window_functions.h" />
|
||||
<ClInclude Include="XYImpedanceProcessor.h" />
|
||||
<ClInclude Include="XYParserApi.h" />
|
||||
<ClInclude Include="XYWelchProcessor.h" />
|
||||
<ClInclude Include="XYEegParser8.h" />
|
||||
<ClInclude Include="XYEegParser64.h" />
|
||||
<ClInclude Include="XYEegParserCommon.h" />
|
||||
@@ -150,7 +158,27 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="third_party\kissfft\fir_filter.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="third_party\kissfft\kiss_fft.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="third_party\kissfft\kiss_fftr.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XYImpedanceProcessor.cpp" />
|
||||
<ClCompile Include="XYParserApi.cpp" />
|
||||
<ClCompile Include="XYWelchProcessor.cpp" />
|
||||
<ClCompile Include="XYEegParser8.cpp" />
|
||||
<ClCompile Include="XYEegParser64.cpp" />
|
||||
<ClCompile Include="XYEegParserExample.cpp" />
|
||||
|
||||
@@ -21,9 +21,33 @@
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="third_party\kissfft\fir_filter.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="third_party\kissfft\_kiss_fft_guts.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="third_party\kissfft\kiss_fft.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="third_party\kissfft\kiss_fft_log.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="third_party\kissfft\kiss_fftr.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="third_party\kissfft\window_functions.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="XYImpedanceProcessor.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="XYParserApi.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="XYWelchProcessor.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="XYEegParser8.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
@@ -41,9 +65,24 @@
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="third_party\kissfft\fir_filter.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="third_party\kissfft\kiss_fft.c">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="third_party\kissfft\kiss_fftr.c">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XYImpedanceProcessor.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XYParserApi.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XYWelchProcessor.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="XYEegParser8.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -1,23 +1,106 @@
|
||||
#include "pch.h"
|
||||
#include "XYParserApi.h"
|
||||
#include "XYImpedanceProcessor.h"
|
||||
#include "XYWelchProcessor.h"
|
||||
|
||||
#include "XYEegParser8.h"
|
||||
#include "XYEegParser64.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint8_t kCommandFrameHeader = 0xAA;
|
||||
constexpr std::uint8_t kCommandFrameTail = 0x55;
|
||||
constexpr std::size_t k8ChImpedanceCommandSize = 7;
|
||||
constexpr int k8ChLeadCount = 8;
|
||||
constexpr int k64ChLeadCount = 64;
|
||||
constexpr std::uint8_t kAlgorithmChannelCount = 64;
|
||||
constexpr std::size_t kAlgorithmSampleByteCount =
|
||||
static_cast<std::size_t>(XYPARSER_FRAME_DATA_COLUMN_COUNT) * sizeof(double);
|
||||
|
||||
struct AlgorithmSample {
|
||||
std::array<double, XYPARSER_MAX_CHANNELS> channel_values_uv{};
|
||||
std::uint8_t trigger_type = 0;
|
||||
std::uint8_t trigger_index = 0;
|
||||
};
|
||||
|
||||
std::uint8_t CalculateChecksum(const std::uint8_t* data, std::size_t size)
|
||||
{
|
||||
std::uint8_t checksum = 0;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
checksum = static_cast<std::uint8_t>(checksum + data[i]);
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, k8ChImpedanceCommandSize> Build8ChImpedanceCommand(bool open)
|
||||
{
|
||||
const std::uint8_t payload = open ? XYPARSER_8CH_IMPEDANCE_OPEN : XYPARSER_8CH_IMPEDANCE_CLOSE;
|
||||
return {kCommandFrameHeader,
|
||||
0x00,
|
||||
0x01,
|
||||
payload,
|
||||
CalculateChecksum(&payload, 1),
|
||||
kCommandFrameTail,
|
||||
kCommandFrameTail};
|
||||
}
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k8ChLeadCount> k8ChLeadMap = {
|
||||
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
||||
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k64ChLeadCount> k64ChLeadMap = {
|
||||
LeadChannel_FP1, LeadChannel_FP2, LeadChannel_PO6, LeadChannel_POZ,
|
||||
LeadChannel_F3, LeadChannel_F4, LeadChannel_FPZ, LeadChannel_AF4,
|
||||
LeadChannel_FC3, LeadChannel_PO8, LeadChannel_CP2, LeadChannel_CP1,
|
||||
LeadChannel_FCZ, LeadChannel_PO5, LeadChannel_FC2, LeadChannel_FC1,
|
||||
LeadChannel_C3, LeadChannel_C4, LeadChannel_FC4, LeadChannel_CP4,
|
||||
LeadChannel_P3, LeadChannel_P4, LeadChannel_F5, LeadChannel_C5,
|
||||
LeadChannel_F6, LeadChannel_PO4, LeadChannel_CP6, LeadChannel_CP5,
|
||||
LeadChannel_PO3, LeadChannel_CP3, LeadChannel_FC6, LeadChannel_FC5,
|
||||
LeadChannel_CB1, LeadChannel_CB2, LeadChannel_P5, LeadChannel_AF7,
|
||||
LeadChannel_A1, LeadChannel_T7, LeadChannel_FT7, LeadChannel_TP7,
|
||||
LeadChannel_FT8, LeadChannel_AF8, LeadChannel_F8, LeadChannel_F7,
|
||||
LeadChannel_P6, LeadChannel_C6, LeadChannel_O2, LeadChannel_O1,
|
||||
LeadChannel_T8, LeadChannel_P7, LeadChannel_CZ, LeadChannel_PZ,
|
||||
LeadChannel_P8, LeadChannel_FZ, LeadChannel_OZ, LeadChannel_PO7,
|
||||
LeadChannel_TP8, LeadChannel_AF3, LeadChannel_C2, LeadChannel_C1,
|
||||
LeadChannel_P2, LeadChannel_P1, LeadChannel_F2, LeadChannel_F1};
|
||||
|
||||
struct ParserContext {
|
||||
std::uint8_t channel_count = 0;
|
||||
XYEegParser8 parser8;
|
||||
XYEegParser64 parser64;
|
||||
XYImpedanceProcessor impedance_processor;
|
||||
XYWelchProcessor welch_processor;
|
||||
double adc_vref = 4.5;
|
||||
double adc_gain = 6.0;
|
||||
std::vector<std::uint8_t> algorithm_byte_cache;
|
||||
std::vector<AlgorithmSample> algorithm_sample_cache;
|
||||
std::uint32_t next_algorithm_frame_index = 1;
|
||||
std::string last_error;
|
||||
};
|
||||
|
||||
struct WelchBandInfo {
|
||||
const char* name;
|
||||
double low_hz;
|
||||
double high_hz;
|
||||
};
|
||||
|
||||
constexpr std::array<WelchBandInfo, XYPARSER_WELCH_BAND_COUNT> kWelchBandInfos = {{
|
||||
{"delta", 0.5, 4.0},
|
||||
{"theta", 4.0, 8.0},
|
||||
{"alpha", 8.0, 13.0},
|
||||
{"beta", 13.0, 30.0},
|
||||
{"gamma", 30.0, 50.0},
|
||||
}};
|
||||
|
||||
void FillSummary(const XYEegFrame8& frame, XYParserFrameSummary& summary)
|
||||
{
|
||||
summary.frame_index = frame.index;
|
||||
@@ -25,8 +108,8 @@ void FillSummary(const XYEegFrame8& frame, XYParserFrameSummary& summary)
|
||||
summary.battery = frame.battery;
|
||||
summary.sample_count = static_cast<std::uint8_t>(frame.samples.size());
|
||||
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
|
||||
summary.trigger_types[sample_index] = frame.samples[sample_index].trigger_type;
|
||||
summary.trigger_indices[sample_index] = frame.samples[sample_index].trigger_index;
|
||||
summary.sample_trigger_types[sample_index] = frame.samples[sample_index].trigger_type;
|
||||
summary.sample_trigger_indices[sample_index] = frame.samples[sample_index].trigger_index;
|
||||
for (std::size_t channel_index = 0; channel_index < XYPARSER_MAX_CHANNELS; ++channel_index) {
|
||||
summary.channel_values_uv[sample_index][channel_index] =
|
||||
channel_index < frame.channel_count
|
||||
@@ -43,8 +126,8 @@ void FillSummary(const XYEegFrame64& frame, XYParserFrameSummary& summary)
|
||||
summary.battery = frame.battery;
|
||||
summary.sample_count = static_cast<std::uint8_t>(frame.samples.size());
|
||||
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
|
||||
summary.trigger_types[sample_index] = frame.samples[sample_index].trigger_type;
|
||||
summary.trigger_indices[sample_index] = frame.samples[sample_index].trigger_index;
|
||||
summary.sample_trigger_types[sample_index] = frame.samples[sample_index].trigger_type;
|
||||
summary.sample_trigger_indices[sample_index] = frame.samples[sample_index].trigger_index;
|
||||
for (std::size_t channel_index = 0; channel_index < XYPARSER_MAX_CHANNELS; ++channel_index) {
|
||||
summary.channel_values_uv[sample_index][channel_index] =
|
||||
channel_index < frame.channel_count
|
||||
@@ -54,6 +137,143 @@ void FillSummary(const XYEegFrame64& frame, XYParserFrameSummary& summary)
|
||||
}
|
||||
}
|
||||
|
||||
void Convert8ChSummaryTo64ChSummary(const XYParserFrameSummary& input_summary,
|
||||
XYParserFrameSummary& output_summary)
|
||||
{
|
||||
std::memset(&output_summary, 0, sizeof(output_summary));
|
||||
output_summary.frame_index = input_summary.frame_index;
|
||||
output_summary.channel_count = 64;
|
||||
output_summary.battery = input_summary.battery;
|
||||
output_summary.sample_count = input_summary.sample_count;
|
||||
|
||||
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
|
||||
output_summary.sample_trigger_types[sample_index] = input_summary.sample_trigger_types[sample_index];
|
||||
output_summary.sample_trigger_indices[sample_index] = input_summary.sample_trigger_indices[sample_index];
|
||||
|
||||
for (std::size_t eight_channel_index = 0; eight_channel_index < k8ChLeadMap.size(); ++eight_channel_index) {
|
||||
const XYParserLeadChannelNumber lead = k8ChLeadMap[eight_channel_index];
|
||||
output_summary.channel_values_uv[sample_index][static_cast<std::size_t>(lead)] =
|
||||
input_summary.channel_values_uv[sample_index][eight_channel_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Convert64ChSummaryToAlgorithmData(const XYParserFrameSummary& input_summary,
|
||||
double* output_data)
|
||||
{
|
||||
std::fill(output_data,
|
||||
output_data + XYPARSER_FRAME_ALGORITHM_VALUE_COUNT,
|
||||
0.0);
|
||||
for (std::size_t sample_index = 0; sample_index < XYPARSER_SAMPLES_PER_FRAME; ++sample_index) {
|
||||
const std::size_t sample_offset = sample_index * XYPARSER_FRAME_DATA_COLUMN_COUNT;
|
||||
for (std::size_t channel_index = 0; channel_index < XYPARSER_MAX_CHANNELS; ++channel_index) {
|
||||
output_data[sample_offset + channel_index] =
|
||||
input_summary.channel_values_uv[sample_index][channel_index];
|
||||
}
|
||||
output_data[sample_offset + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX] =
|
||||
static_cast<double>(input_summary.sample_trigger_types[sample_index]);
|
||||
output_data[sample_offset + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX] =
|
||||
static_cast<double>(input_summary.sample_trigger_indices[sample_index]);
|
||||
}
|
||||
}
|
||||
|
||||
void FillSummaryFromAlgorithmSamples(const std::vector<AlgorithmSample>& samples,
|
||||
std::uint32_t frame_index,
|
||||
XYParserFrameSummary& summary)
|
||||
{
|
||||
std::memset(&summary, 0, sizeof(summary));
|
||||
summary.frame_index = frame_index;
|
||||
summary.channel_count = kAlgorithmChannelCount;
|
||||
summary.sample_count = static_cast<std::uint8_t>(
|
||||
std::min<std::size_t>(samples.size(), static_cast<std::size_t>(XYPARSER_SAMPLES_PER_FRAME)));
|
||||
|
||||
for (std::size_t sample_index = 0; sample_index < samples.size() &&
|
||||
sample_index < static_cast<std::size_t>(XYPARSER_SAMPLES_PER_FRAME);
|
||||
++sample_index) {
|
||||
summary.sample_trigger_types[sample_index] = samples[sample_index].trigger_type;
|
||||
summary.sample_trigger_indices[sample_index] = samples[sample_index].trigger_index;
|
||||
for (std::size_t channel_index = 0; channel_index < XYPARSER_MAX_CHANNELS; ++channel_index) {
|
||||
summary.channel_values_uv[sample_index][channel_index] = samples[sample_index].channel_values_uv[channel_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CopyCommandBytes(const std::vector<std::uint8_t>& command,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size)
|
||||
{
|
||||
if (out_buffer == nullptr || buffer_size < command.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(out_buffer, command.data(), command.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMapSampleRate(std::uint16_t sample_rate, XYEegParser64::SampleRateSwitch& mapped_sample_rate)
|
||||
{
|
||||
switch (sample_rate) {
|
||||
case 250:
|
||||
mapped_sample_rate = XYEegParser64::SampleRateSwitch::k250;
|
||||
return true;
|
||||
case 500:
|
||||
mapped_sample_rate = XYEegParser64::SampleRateSwitch::k500;
|
||||
return true;
|
||||
case 1000:
|
||||
mapped_sample_rate = XYEegParser64::SampleRateSwitch::k1000;
|
||||
return true;
|
||||
case 2000:
|
||||
mapped_sample_rate = XYEegParser64::SampleRateSwitch::k2000;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryMapGain(std::uint8_t gain, XYEegParser64::GainSwitch& mapped_gain)
|
||||
{
|
||||
switch (gain) {
|
||||
case 1:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k1;
|
||||
return true;
|
||||
case 2:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k2;
|
||||
return true;
|
||||
case 3:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k3;
|
||||
return true;
|
||||
case 4:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k4;
|
||||
return true;
|
||||
case 6:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k6;
|
||||
return true;
|
||||
case 8:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k8;
|
||||
return true;
|
||||
case 12:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k12;
|
||||
return true;
|
||||
case 24:
|
||||
mapped_gain = XYEegParser64::GainSwitch::k24;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int GetLeadMapCount(std::uint8_t channel_count)
|
||||
{
|
||||
switch (channel_count) {
|
||||
case 8:
|
||||
return k8ChLeadCount;
|
||||
case 64:
|
||||
return k64ChLeadCount;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
@@ -70,6 +290,8 @@ XYParserHandle XYParser_CreateParser(std::uint8_t channel_count)
|
||||
}
|
||||
|
||||
context->channel_count = channel_count;
|
||||
context->impedance_processor.SetChannelCount(channel_count);
|
||||
context->welch_processor.SetChannelCount(channel_count);
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -86,6 +308,13 @@ void XYParser_SetAdcParams(XYParserHandle handle, double vref, double gain)
|
||||
return;
|
||||
}
|
||||
|
||||
if (vref > 0.0) {
|
||||
context->adc_vref = vref;
|
||||
}
|
||||
if (gain > 0.0) {
|
||||
context->adc_gain = gain;
|
||||
}
|
||||
|
||||
if (context->channel_count == 8) {
|
||||
context->parser8.SetAdcParams(vref, gain);
|
||||
} else {
|
||||
@@ -108,6 +337,87 @@ void XYParser_SetBypassChecksum(XYParserHandle handle, int bypass)
|
||||
}
|
||||
}
|
||||
|
||||
void XYParser_SetSampleRate(XYParserHandle handle, int sample_rate)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
context->impedance_processor.SetSampleRate(sample_rate);
|
||||
context->welch_processor.SetSampleRate(sample_rate);
|
||||
}
|
||||
|
||||
void XYParser_SetImpedanceDetection(XYParserHandle handle, int enabled)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool impedance_enabled = enabled != 0;
|
||||
const double impedance_gain = impedance_enabled ? 24.0 : 6.0;
|
||||
context->adc_gain = impedance_gain;
|
||||
|
||||
if (context->channel_count == 8) {
|
||||
context->parser8.SetAdcParams(context->adc_vref, impedance_gain);
|
||||
} else {
|
||||
context->parser64.SetAdcParams(context->adc_vref, impedance_gain);
|
||||
}
|
||||
|
||||
context->impedance_processor.SetEnabled(impedance_enabled);
|
||||
}
|
||||
|
||||
void XYParser_SetWelchDetection(XYParserHandle handle, int enabled)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
context->welch_processor.SetEnabled(enabled != 0);
|
||||
}
|
||||
|
||||
std::size_t XYParser_Get64ImpedanceCommandSize(void)
|
||||
{
|
||||
return XYEegParser64::kImpedanceCommandSize;
|
||||
}
|
||||
|
||||
std::size_t XYParser_Get64GainSampleRateCommandSize(void)
|
||||
{
|
||||
return XYEegParser64::kGainSampleRateCommandSize;
|
||||
}
|
||||
|
||||
int XYParser_Serialize64ImpedanceCommand(int open,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size)
|
||||
{
|
||||
const auto command = XYEegParser64::SerializeImpedanceCommand(
|
||||
open != 0 ? XYEegParser64::ImpedanceSwitch::kOpen : XYEegParser64::ImpedanceSwitch::kClose);
|
||||
if (!CopyCommandBytes(command, out_buffer, buffer_size)) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(command.size());
|
||||
}
|
||||
|
||||
int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
||||
std::uint16_t sample_rate,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size)
|
||||
{
|
||||
XYEegParser64::GainSwitch mapped_gain{};
|
||||
XYEegParser64::SampleRateSwitch mapped_sample_rate{};
|
||||
if (!TryMapGain(gain, mapped_gain) || !TryMapSampleRate(sample_rate, mapped_sample_rate)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto command = XYEegParser64::SerializeGainAndSampleRateCommand(mapped_gain, mapped_sample_rate);
|
||||
if (!CopyCommandBytes(command, out_buffer, buffer_size)) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(command.size());
|
||||
}
|
||||
|
||||
int XYParser_Feed(XYParserHandle handle,
|
||||
const std::uint8_t* data,
|
||||
std::size_t size,
|
||||
@@ -127,8 +437,13 @@ int XYParser_Feed(XYParserHandle handle,
|
||||
const std::vector<XYEegFrame8> frames = context->parser8.Feed(data, size);
|
||||
context->last_error = context->parser8.LastError();
|
||||
const int write_count = std::min<int>(static_cast<int>(frames.size()), max_summaries);
|
||||
for (int i = 0; i < write_count; ++i) {
|
||||
FillSummary(frames[static_cast<std::size_t>(i)], out_summaries[i]);
|
||||
for (std::size_t i = 0; i < frames.size(); ++i) {
|
||||
XYParserFrameSummary summary{};
|
||||
FillSummary(frames[i], summary);
|
||||
context->impedance_processor.PushFrame(summary);
|
||||
if (static_cast<int>(i) < write_count) {
|
||||
out_summaries[static_cast<int>(i)] = summary;
|
||||
}
|
||||
}
|
||||
return write_count;
|
||||
}
|
||||
@@ -136,12 +451,299 @@ int XYParser_Feed(XYParserHandle handle,
|
||||
const std::vector<XYEegFrame64> frames = context->parser64.Feed(data, size);
|
||||
context->last_error = context->parser64.LastError();
|
||||
const int write_count = std::min<int>(static_cast<int>(frames.size()), max_summaries);
|
||||
for (int i = 0; i < write_count; ++i) {
|
||||
FillSummary(frames[static_cast<std::size_t>(i)], out_summaries[i]);
|
||||
for (std::size_t i = 0; i < frames.size(); ++i) {
|
||||
XYParserFrameSummary summary{};
|
||||
FillSummary(frames[i], summary);
|
||||
context->impedance_processor.PushFrame(summary);
|
||||
if (static_cast<int>(i) < write_count) {
|
||||
out_summaries[static_cast<int>(i)] = summary;
|
||||
}
|
||||
}
|
||||
return write_count;
|
||||
}
|
||||
|
||||
int XYParser_Convert8ChFramesTo64Ch(const XYParserFrameSummary* input_summaries,
|
||||
int input_count,
|
||||
XYParserFrameSummary* output_summaries,
|
||||
int max_summaries)
|
||||
{
|
||||
if (input_summaries == nullptr || output_summaries == nullptr || input_count <= 0 || max_summaries <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int convert_count = input_count < max_summaries ? input_count : max_summaries;
|
||||
for (int i = 0; i < convert_count; ++i) {
|
||||
if (input_summaries[i].channel_count != 8) {
|
||||
return i;
|
||||
}
|
||||
Convert8ChSummaryTo64ChSummary(input_summaries[i], output_summaries[i]);
|
||||
}
|
||||
return convert_count;
|
||||
}
|
||||
|
||||
std::size_t XYParser_GetAlgorithmDataValueCount()
|
||||
{
|
||||
return XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
|
||||
}
|
||||
|
||||
int XYParser_ConvertSampleFramesToAlgorithmData(const XYParserFrameSummary* input_summary,
|
||||
double* output_data)
|
||||
{
|
||||
if (input_summary == nullptr || output_data == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
if (input_summary->channel_count != 64) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Convert64ChSummaryToAlgorithmData(*input_summary, output_data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int XYParser_FeedAlgorithmData(XYParserHandle handle,
|
||||
const std::uint8_t* data,
|
||||
std::size_t size,
|
||||
XYParserFrameSummary* out_summaries,
|
||||
int max_summaries)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount ||
|
||||
data == nullptr || size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (max_summaries < 0) {
|
||||
max_summaries = 0;
|
||||
}
|
||||
if (max_summaries > 0 && out_summaries == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
context->algorithm_byte_cache.insert(context->algorithm_byte_cache.end(), data, data + size);
|
||||
|
||||
while (context->algorithm_byte_cache.size() >= kAlgorithmSampleByteCount) {
|
||||
AlgorithmSample sample{};
|
||||
std::array<double, XYPARSER_FRAME_DATA_COLUMN_COUNT> row{};
|
||||
std::memcpy(row.data(), context->algorithm_byte_cache.data(), kAlgorithmSampleByteCount);
|
||||
for (std::size_t channel_index = 0; channel_index < XYPARSER_MAX_CHANNELS; ++channel_index) {
|
||||
sample.channel_values_uv[channel_index] = row[channel_index];
|
||||
}
|
||||
sample.trigger_type = static_cast<std::uint8_t>(row[XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX]);
|
||||
sample.trigger_index = static_cast<std::uint8_t>(row[XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX]);
|
||||
context->welch_processor.PushSample(sample.channel_values_uv);
|
||||
context->algorithm_sample_cache.push_back(sample);
|
||||
context->algorithm_byte_cache.erase(
|
||||
context->algorithm_byte_cache.begin(),
|
||||
context->algorithm_byte_cache.begin() + static_cast<std::ptrdiff_t>(kAlgorithmSampleByteCount));
|
||||
}
|
||||
|
||||
int output_count = 0;
|
||||
while (context->algorithm_sample_cache.size() >= static_cast<std::size_t>(XYPARSER_SAMPLES_PER_FRAME)) {
|
||||
const std::vector<AlgorithmSample> frame_samples(
|
||||
context->algorithm_sample_cache.begin(),
|
||||
context->algorithm_sample_cache.begin() + XYPARSER_SAMPLES_PER_FRAME);
|
||||
XYParserFrameSummary summary{};
|
||||
FillSummaryFromAlgorithmSamples(frame_samples, context->next_algorithm_frame_index++, summary);
|
||||
if (output_count < max_summaries) {
|
||||
out_summaries[output_count] = summary;
|
||||
++output_count;
|
||||
}
|
||||
context->algorithm_sample_cache.erase(
|
||||
context->algorithm_sample_cache.begin(),
|
||||
context->algorithm_sample_cache.begin() + XYPARSER_SAMPLES_PER_FRAME);
|
||||
}
|
||||
|
||||
return output_count;
|
||||
}
|
||||
|
||||
void XYParser_ResetAlgorithmDataCache(XYParserHandle handle)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
context->algorithm_byte_cache.clear();
|
||||
context->algorithm_sample_cache.clear();
|
||||
context->next_algorithm_frame_index = 1;
|
||||
}
|
||||
|
||||
int XYParser_FlushAlgorithmData(XYParserHandle handle,
|
||||
XYParserFrameSummary* out_summary)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr || context->channel_count != kAlgorithmChannelCount ||
|
||||
out_summary == nullptr || context->algorithm_sample_cache.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FillSummaryFromAlgorithmSamples(
|
||||
context->algorithm_sample_cache,
|
||||
context->next_algorithm_frame_index++,
|
||||
*out_summary);
|
||||
context->algorithm_sample_cache.clear();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int XYParser_GetLeadMapCount(std::uint8_t channel_count)
|
||||
{
|
||||
return GetLeadMapCount(channel_count);
|
||||
}
|
||||
|
||||
int XYParser_GetLeadMap(std::uint8_t channel_count,
|
||||
XYParserLeadChannelNumber* out_leads,
|
||||
int max_leads)
|
||||
{
|
||||
const int required_count = GetLeadMapCount(channel_count);
|
||||
if (required_count <= 0 || out_leads == nullptr || max_leads < required_count) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (channel_count == 8) {
|
||||
std::copy(k8ChLeadMap.begin(), k8ChLeadMap.end(), out_leads);
|
||||
return required_count;
|
||||
}
|
||||
|
||||
std::copy(k64ChLeadMap.begin(), k64ChLeadMap.end(), out_leads);
|
||||
return required_count;
|
||||
}
|
||||
|
||||
const char* XYParser_GetLeadName(XYParserLeadChannelNumber lead)
|
||||
{
|
||||
switch (lead) {
|
||||
case LeadChannel_FP1: return "FP1";
|
||||
case LeadChannel_FP2: return "FP2";
|
||||
case LeadChannel_PO6: return "PO6";
|
||||
case LeadChannel_POZ: return "POZ";
|
||||
case LeadChannel_F3: return "F3";
|
||||
case LeadChannel_F4: return "F4";
|
||||
case LeadChannel_FPZ: return "FPZ";
|
||||
case LeadChannel_AF4: return "AF4";
|
||||
case LeadChannel_FC3: return "FC3";
|
||||
case LeadChannel_PO8: return "PO8";
|
||||
case LeadChannel_CP2: return "CP2";
|
||||
case LeadChannel_CP1: return "CP1";
|
||||
case LeadChannel_FCZ: return "FCZ";
|
||||
case LeadChannel_PO5: return "PO5";
|
||||
case LeadChannel_FC2: return "FC2";
|
||||
case LeadChannel_FC1: return "FC1";
|
||||
case LeadChannel_C3: return "C3";
|
||||
case LeadChannel_C4: return "C4";
|
||||
case LeadChannel_FC4: return "FC4";
|
||||
case LeadChannel_CP4: return "CP4";
|
||||
case LeadChannel_P3: return "P3";
|
||||
case LeadChannel_P4: return "P4";
|
||||
case LeadChannel_F5: return "F5";
|
||||
case LeadChannel_C5: return "C5";
|
||||
case LeadChannel_F6: return "F6";
|
||||
case LeadChannel_PO4: return "PO4";
|
||||
case LeadChannel_CP6: return "CP6";
|
||||
case LeadChannel_CP5: return "CP5";
|
||||
case LeadChannel_PO3: return "PO3";
|
||||
case LeadChannel_CP3: return "CP3";
|
||||
case LeadChannel_FC6: return "FC6";
|
||||
case LeadChannel_FC5: return "FC5";
|
||||
case LeadChannel_CB1: return "CB1";
|
||||
case LeadChannel_CB2: return "CB2";
|
||||
case LeadChannel_P5: return "P5";
|
||||
case LeadChannel_AF7: return "AF7";
|
||||
case LeadChannel_A1: return "A1";
|
||||
case LeadChannel_T7: return "T7";
|
||||
case LeadChannel_FT7: return "FT7";
|
||||
case LeadChannel_TP7: return "TP7";
|
||||
case LeadChannel_FT8: return "FT8";
|
||||
case LeadChannel_AF8: return "AF8";
|
||||
case LeadChannel_F8: return "F8";
|
||||
case LeadChannel_F7: return "F7";
|
||||
case LeadChannel_P6: return "P6";
|
||||
case LeadChannel_C6: return "C6";
|
||||
case LeadChannel_O2: return "O2";
|
||||
case LeadChannel_O1: return "O1";
|
||||
case LeadChannel_T8: return "T8";
|
||||
case LeadChannel_P7: return "P7";
|
||||
case LeadChannel_CZ: return "CZ";
|
||||
case LeadChannel_PZ: return "PZ";
|
||||
case LeadChannel_P8: return "P8";
|
||||
case LeadChannel_FZ: return "FZ";
|
||||
case LeadChannel_OZ: return "OZ";
|
||||
case LeadChannel_PO7: return "PO7";
|
||||
case LeadChannel_TP8: return "TP8";
|
||||
case LeadChannel_AF3: return "AF3";
|
||||
case LeadChannel_C2: return "C2";
|
||||
case LeadChannel_C1: return "C1";
|
||||
case LeadChannel_P2: return "P2";
|
||||
case LeadChannel_P1: return "P1";
|
||||
case LeadChannel_F2: return "F2";
|
||||
case LeadChannel_F1: return "F1";
|
||||
case LeadChannel_GND: return "GND";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char* XYParser_GetWelchBandName(int band_index)
|
||||
{
|
||||
if (band_index < 0 || band_index >= static_cast<int>(kWelchBandInfos.size())) {
|
||||
return "";
|
||||
}
|
||||
return kWelchBandInfos[static_cast<std::size_t>(band_index)].name;
|
||||
}
|
||||
|
||||
int XYParser_GetWelchBandRange(int band_index, double* out_low_hz, double* out_high_hz)
|
||||
{
|
||||
if (band_index < 0 || band_index >= static_cast<int>(kWelchBandInfos.size()) ||
|
||||
out_low_hz == nullptr || out_high_hz == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const WelchBandInfo& band_info = kWelchBandInfos[static_cast<std::size_t>(band_index)];
|
||||
*out_low_hz = band_info.low_hz;
|
||||
*out_high_hz = band_info.high_hz;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int XYParser_ReadImpedance(XYParserHandle handle,
|
||||
XYParserImpedanceSummary* out_summaries,
|
||||
int max_summaries)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return context->impedance_processor.Read(out_summaries, max_summaries);
|
||||
}
|
||||
|
||||
int XYParser_ReadWelch(XYParserHandle handle,
|
||||
XYParserWelchSummary* out_summaries,
|
||||
int max_summaries)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
if (context == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return context->welch_processor.Read(out_summaries, max_summaries);
|
||||
}
|
||||
|
||||
std::size_t XYParser_Get8ChImpedanceCommandSize()
|
||||
{
|
||||
return k8ChImpedanceCommandSize;
|
||||
}
|
||||
|
||||
int XYParser_Serialize8ChImpedanceCommand(int open,
|
||||
std::uint8_t* out_command,
|
||||
std::size_t command_size)
|
||||
{
|
||||
if (out_command == nullptr || command_size < k8ChImpedanceCommandSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto command = Build8ChImpedanceCommand(open != 0);
|
||||
std::copy(command.begin(), command.end(), out_command);
|
||||
return static_cast<int>(command.size());
|
||||
}
|
||||
|
||||
const char* XYParser_GetLastError(XYParserHandle handle)
|
||||
{
|
||||
ParserContext* context = static_cast<ParserContext*>(handle);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -15,7 +15,87 @@ typedef void* XYParserHandle;
|
||||
|
||||
enum {
|
||||
XYPARSER_MAX_CHANNELS = 64,
|
||||
XYPARSER_SAMPLES_PER_FRAME = 5
|
||||
XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX = XYPARSER_MAX_CHANNELS,
|
||||
XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX = XYPARSER_MAX_CHANNELS + 1,
|
||||
XYPARSER_FRAME_DATA_COLUMN_COUNT = XYPARSER_MAX_CHANNELS + 2,
|
||||
XYPARSER_SAMPLES_PER_FRAME = 5,
|
||||
XYPARSER_FRAME_ALGORITHM_VALUE_COUNT =
|
||||
XYPARSER_SAMPLES_PER_FRAME * XYPARSER_FRAME_DATA_COLUMN_COUNT,
|
||||
XYPARSER_WELCH_MAX_FREQUENCY_COUNT = 50,
|
||||
XYPARSER_WELCH_BAND_COUNT = 5
|
||||
};
|
||||
|
||||
enum XYParser8ChImpedanceSwitch : std::uint8_t {
|
||||
XYPARSER_8CH_IMPEDANCE_CLOSE = 0xA0,
|
||||
XYPARSER_8CH_IMPEDANCE_OPEN = 0xA1
|
||||
};
|
||||
|
||||
enum XYParserLeadChannelNumber {
|
||||
LeadChannel_FP1 = 0,
|
||||
LeadChannel_FP2,
|
||||
LeadChannel_PO6,
|
||||
LeadChannel_POZ,
|
||||
LeadChannel_F3,
|
||||
LeadChannel_F4,
|
||||
LeadChannel_FPZ,
|
||||
LeadChannel_AF4,
|
||||
LeadChannel_FC3,
|
||||
LeadChannel_PO8,
|
||||
LeadChannel_CP2,
|
||||
LeadChannel_CP1,
|
||||
LeadChannel_FCZ,
|
||||
LeadChannel_PO5,
|
||||
LeadChannel_FC2,
|
||||
LeadChannel_FC1,
|
||||
LeadChannel_C3,
|
||||
LeadChannel_C4,
|
||||
LeadChannel_FC4,
|
||||
LeadChannel_CP4,
|
||||
LeadChannel_P3,
|
||||
LeadChannel_P4,
|
||||
LeadChannel_F5,
|
||||
LeadChannel_C5,
|
||||
LeadChannel_F6,
|
||||
LeadChannel_PO4,
|
||||
LeadChannel_CP6,
|
||||
LeadChannel_CP5,
|
||||
LeadChannel_PO3,
|
||||
LeadChannel_CP3,
|
||||
LeadChannel_FC6,
|
||||
LeadChannel_FC5,
|
||||
LeadChannel_CB1,
|
||||
LeadChannel_CB2,
|
||||
LeadChannel_P5,
|
||||
LeadChannel_AF7,
|
||||
LeadChannel_A1,
|
||||
LeadChannel_T7,
|
||||
LeadChannel_FT7,
|
||||
LeadChannel_TP7,
|
||||
LeadChannel_FT8,
|
||||
LeadChannel_AF8,
|
||||
LeadChannel_F8,
|
||||
LeadChannel_F7,
|
||||
LeadChannel_P6,
|
||||
LeadChannel_C6,
|
||||
LeadChannel_O2,
|
||||
LeadChannel_O1,
|
||||
LeadChannel_T8,
|
||||
LeadChannel_P7,
|
||||
LeadChannel_CZ,
|
||||
LeadChannel_PZ,
|
||||
LeadChannel_P8,
|
||||
LeadChannel_FZ,
|
||||
LeadChannel_OZ,
|
||||
LeadChannel_PO7,
|
||||
LeadChannel_TP8,
|
||||
LeadChannel_AF3,
|
||||
LeadChannel_C2,
|
||||
LeadChannel_C1,
|
||||
LeadChannel_P2,
|
||||
LeadChannel_P1,
|
||||
LeadChannel_F2,
|
||||
LeadChannel_F1,
|
||||
LeadChannel_GND
|
||||
};
|
||||
|
||||
struct XYParserFrameSummary {
|
||||
@@ -24,19 +104,228 @@ struct XYParserFrameSummary {
|
||||
std::uint8_t battery;
|
||||
std::uint8_t sample_count;
|
||||
double channel_values_uv[XYPARSER_SAMPLES_PER_FRAME][XYPARSER_MAX_CHANNELS];
|
||||
std::uint8_t trigger_types[XYPARSER_SAMPLES_PER_FRAME];
|
||||
std::uint8_t trigger_indices[XYPARSER_SAMPLES_PER_FRAME];
|
||||
std::uint8_t sample_trigger_types[XYPARSER_SAMPLES_PER_FRAME];
|
||||
std::uint8_t sample_trigger_indices[XYPARSER_SAMPLES_PER_FRAME];
|
||||
};
|
||||
|
||||
struct XYParserImpedanceSummary {
|
||||
std::uint8_t channel_count;
|
||||
std::uint32_t sample_rate;
|
||||
std::uint32_t window_sample_count;
|
||||
std::uint16_t impedance_values[XYPARSER_MAX_CHANNELS];
|
||||
};
|
||||
|
||||
struct XYParserWelchSummary {
|
||||
std::uint8_t ok;
|
||||
std::uint8_t channel_count;
|
||||
std::uint32_t sample_rate;
|
||||
std::uint32_t window_sample_count;
|
||||
std::uint32_t frequency_count;
|
||||
double frequencies[XYPARSER_WELCH_MAX_FREQUENCY_COUNT];
|
||||
double psd_values[XYPARSER_MAX_CHANNELS][XYPARSER_WELCH_MAX_FREQUENCY_COUNT];
|
||||
double band_values[XYPARSER_WELCH_BAND_COUNT][XYPARSER_MAX_CHANNELS];
|
||||
};
|
||||
|
||||
// 创建解析器实例。
|
||||
// @param channel_count 解析器支持的导联数。
|
||||
// @return 创建成功时返回解析器句柄,失败时返回 nullptr。
|
||||
XYPARSER_API XYParserHandle XYParser_CreateParser(std::uint8_t channel_count);
|
||||
|
||||
// 销毁解析器实例并释放相关资源。
|
||||
// @param handle 要销毁的解析器句柄。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_DestroyParser(XYParserHandle handle);
|
||||
|
||||
// 设置 ADC 参考电压和增益参数。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param vref ADC 的参考电压。
|
||||
// @param gain ADC 的增益值。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_SetAdcParams(XYParserHandle handle, double vref, double gain);
|
||||
|
||||
// 设置是否跳过校验和检查。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param bypass 非 0 表示跳过校验和检查,0 表示执行校验和检查。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_SetBypassChecksum(XYParserHandle handle, int bypass);
|
||||
|
||||
// 设置解析器使用的采样率。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param sample_rate 采样率,单位为 Hz。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_SetSampleRate(XYParserHandle handle, int sample_rate);
|
||||
|
||||
// Frame parsing and algorithm result reading.
|
||||
// 向解析器输入原始数据并读取解析得到的帧。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param data 输入的原始字节数据。
|
||||
// @param size 输入数据的字节数。
|
||||
// @param out_summaries 输出帧数组。
|
||||
// @param max_summaries 输出数组可写入的最大帧数,用于避免越界写入。
|
||||
// @return 本次实际输出的帧数;如果参数无效则返回 0。
|
||||
XYPARSER_API int XYParser_Feed(XYParserHandle handle,
|
||||
const std::uint8_t* data,
|
||||
std::size_t size,
|
||||
XYParserFrameSummary* out_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// 设置是否启用阻抗检测。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param enabled 非 0 表示启用阻抗检测,0 表示禁用阻抗检测。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_SetImpedanceDetection(XYParserHandle handle, int enabled);
|
||||
|
||||
// 读取当前可用的阻抗结果。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param out_summaries 输出的阻抗结果数组。
|
||||
// @param max_summaries 输出数组可写入的最大结果数,用于避免越界写入。
|
||||
// @return 本次实际读取的结果数;如果参数无效则返回 0。
|
||||
XYPARSER_API int XYParser_ReadImpedance(XYParserHandle handle,
|
||||
XYParserImpedanceSummary* out_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// 设置是否启用基于算法数据的 Welch 检测。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param enabled 非 0 表示启用 Welch 检测,0 表示禁用 Welch 检测。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_SetWelchDetection(XYParserHandle handle, int enabled);
|
||||
|
||||
// 读取当前基于算法数据计算得到的 Welch 结果。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param out_summaries 输出的 Welch 结果数组。
|
||||
// @param max_summaries 输出数组可写入的最大结果数,用于避免越界写入。
|
||||
// @return 本次实际读取的结果数;如果参数无效则返回 0。
|
||||
XYPARSER_API int XYParser_ReadWelch(XYParserHandle handle,
|
||||
XYParserWelchSummary* out_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// Command serialization.
|
||||
// 获取 8 导阻抗命令的字节长度。
|
||||
// @return 8 导阻抗命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get8ChImpedanceCommandSize();
|
||||
|
||||
// 生成 8 导阻抗开关命令。
|
||||
// @param open 非 0 表示打开阻抗检测,0 表示关闭阻抗检测。
|
||||
// @param out_command 输出的命令缓冲区。
|
||||
// @param command_size 输出缓冲区大小,单位为字节。
|
||||
// @return 生成成功时返回写入的字节数,失败时返回 0。
|
||||
XYPARSER_API int XYParser_Serialize8ChImpedanceCommand(int open,
|
||||
std::uint8_t* out_command,
|
||||
std::size_t command_size);
|
||||
|
||||
// 获取 64 导阻抗命令的字节长度。
|
||||
// @return 64 导阻抗命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get64ImpedanceCommandSize(void);
|
||||
|
||||
// 生成 64 导阻抗开关命令。
|
||||
// @param open 非 0 表示打开阻抗检测,0 表示关闭阻抗检测。
|
||||
// @param out_buffer 输出的命令缓冲区。
|
||||
// @param buffer_size 输出缓冲区大小,单位为字节。
|
||||
// @return 生成成功时返回写入的字节数,失败时返回 0。
|
||||
XYPARSER_API int XYParser_Serialize64ImpedanceCommand(int open,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size);
|
||||
|
||||
// 获取 64 导增益和采样率命令的字节长度。
|
||||
// @return 64 导增益和采样率命令的字节长度。
|
||||
XYPARSER_API std::size_t XYParser_Get64GainSampleRateCommandSize(void);
|
||||
|
||||
// 生成 64 导增益和采样率设置命令。
|
||||
// @param gain 要设置的增益值。
|
||||
// @param sample_rate 要设置的采样率。
|
||||
// @param out_buffer 输出的命令缓冲区。
|
||||
// @param buffer_size 输出缓冲区大小,单位为字节。
|
||||
// @return 生成成功时返回写入的字节数,失败时返回 0。
|
||||
XYPARSER_API int XYParser_Serialize64GainSampleRateCommand(std::uint8_t gain,
|
||||
std::uint16_t sample_rate,
|
||||
std::uint8_t* out_buffer,
|
||||
std::size_t buffer_size);
|
||||
|
||||
// Frame conversion and algorithm data output.
|
||||
// 将 8 导帧批量转换为 64 导帧,未映射的导联补 0。
|
||||
// @param input_summaries 输入的 8 导帧数组。
|
||||
// @param input_count 输入数组中的帧数。
|
||||
// @param output_summaries 输出的 64 导帧数组。
|
||||
// @param max_summaries 输出数组可写入的最大帧数,用于避免越界写入。
|
||||
// @return 实际成功转换的帧数;如果参数无效则返回 0,如果中途遇到非 8 导帧则返回已完成转换的数量。
|
||||
XYPARSER_API int XYParser_Convert8ChFramesTo64Ch(const XYParserFrameSummary* input_summaries,
|
||||
int input_count,
|
||||
XYParserFrameSummary* output_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// 获取算法数据数组所需的元素数量。
|
||||
// @return 算法数据数组所需的元素数量。
|
||||
XYPARSER_API std::size_t XYParser_GetAlgorithmDataValueCount();
|
||||
|
||||
// 将单帧数据转换为算法接口使用的连续数组。
|
||||
// @param input_summary 输入的单帧数据。
|
||||
// @param output_data 输出的算法数据数组。
|
||||
// @return 转换成功时返回非 0,失败时返回 0。
|
||||
XYPARSER_API int XYParser_ConvertSampleFramesToAlgorithmData(const XYParserFrameSummary* input_summary,
|
||||
double* output_data);
|
||||
|
||||
// 向算法数据缓存输入字节流,并按每 5 个采样组装为帧,同时驱动 Welch 计算。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param data 输入的算法数据字节流。
|
||||
// @param size 输入数据的字节数。
|
||||
// @param out_summaries 输出的帧数组。
|
||||
// @param max_summaries 输出数组可写入的最大帧数,用于避免越界写入。
|
||||
// @return 实际输出的帧数;如果参数无效则返回 0。
|
||||
XYPARSER_API int XYParser_FeedAlgorithmData(XYParserHandle handle,
|
||||
const std::uint8_t* data,
|
||||
std::size_t size,
|
||||
XYParserFrameSummary* out_summaries,
|
||||
int max_summaries);
|
||||
|
||||
// 清空算法数据缓存。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @return 无返回值。
|
||||
XYPARSER_API void XYParser_ResetAlgorithmDataCache(XYParserHandle handle);
|
||||
|
||||
// 将缓存中不足 5 个采样的尾数据补齐并输出为 1 帧。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @param out_summary 输出的帧。
|
||||
// @return 成功输出 1 帧时返回 1;没有可输出的数据或参数无效时返回 0。
|
||||
XYPARSER_API int XYParser_FlushAlgorithmData(XYParserHandle handle,
|
||||
XYParserFrameSummary* out_summary);
|
||||
|
||||
// Lead and Welch metadata.
|
||||
// 获取指定导联数对应的导联映射数量。
|
||||
// @param channel_count 导联数。
|
||||
// @return 导联映射数量;如果导联数不支持则返回 0。
|
||||
XYPARSER_API int XYParser_GetLeadMapCount(std::uint8_t channel_count);
|
||||
|
||||
// 获取指定导联数对应的导联映射表。
|
||||
// @param channel_count 导联数。
|
||||
// @param out_leads 输出的导联映射数组。
|
||||
// @param max_leads 输出数组可写入的最大导联数,用于避免越界写入。
|
||||
// @return 实际写入的导联数;如果参数无效则返回 0。
|
||||
XYPARSER_API int XYParser_GetLeadMap(std::uint8_t channel_count,
|
||||
XYParserLeadChannelNumber* out_leads,
|
||||
int max_leads);
|
||||
|
||||
// 获取导联编号对应的名称字符串。
|
||||
// @param lead 导联编号。
|
||||
// @return 导联名称字符串;如果导联编号无效则返回 nullptr。
|
||||
XYPARSER_API const char* XYParser_GetLeadName(XYParserLeadChannelNumber lead);
|
||||
|
||||
// 获取 Welch 频段索引对应的名称字符串。
|
||||
// @param band_index Welch 频段索引。
|
||||
// @return 频段名称字符串;如果索引无效则返回 nullptr。
|
||||
XYPARSER_API const char* XYParser_GetWelchBandName(int band_index);
|
||||
|
||||
// 获取 Welch 频段索引对应的频率范围。
|
||||
// @param band_index Welch 频段索引。
|
||||
// @param out_low_hz 输出的频段下限频率,单位为 Hz。
|
||||
// @param out_high_hz 输出的频段上限频率,单位为 Hz。
|
||||
// @return 获取成功时返回非 0,失败时返回 0。
|
||||
XYPARSER_API int XYParser_GetWelchBandRange(int band_index,
|
||||
double* out_low_hz,
|
||||
double* out_high_hz);
|
||||
|
||||
// 获取解析器最近一次错误的描述信息。
|
||||
// @param handle 目标解析器句柄。
|
||||
// @return 错误描述字符串;如果当前没有错误信息则返回 nullptr。
|
||||
XYPARSER_API const char* XYParser_GetLastError(XYParserHandle handle);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "../XYParserApi.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -106,6 +108,70 @@ std::vector<std::uint8_t> BuildMinimalFrame(std::uint8_t channel_count)
|
||||
return BuildMinimalFrame(channel_count, 1U);
|
||||
}
|
||||
|
||||
void WriteSigned24(std::vector<std::uint8_t>& frame, std::size_t& offset, int raw_value)
|
||||
{
|
||||
constexpr int kMinSigned24 = -8388608;
|
||||
constexpr int kMaxSigned24 = 8388607;
|
||||
raw_value = std::max(kMinSigned24, std::min(kMaxSigned24, raw_value));
|
||||
std::uint32_t encoded = 0;
|
||||
if (raw_value < 0) {
|
||||
encoded = static_cast<std::uint32_t>((1 << 24) + raw_value);
|
||||
} else {
|
||||
encoded = static_cast<std::uint32_t>(raw_value);
|
||||
}
|
||||
|
||||
frame[offset++] = static_cast<std::uint8_t>((encoded >> 16) & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>((encoded >> 8) & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>(encoded & 0xFF);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> BuildFrameWithRawSamples(
|
||||
std::uint8_t channel_count,
|
||||
std::uint32_t frame_index,
|
||||
const std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME>& raw_samples)
|
||||
{
|
||||
constexpr std::uint8_t kHeader = 0xAA;
|
||||
constexpr std::uint8_t kTail = 0x55;
|
||||
constexpr std::size_t kTagLen = 25;
|
||||
|
||||
const std::size_t sample_bytes = static_cast<std::size_t>(channel_count) * 3 + 2;
|
||||
const std::uint16_t payload_length = static_cast<std::uint16_t>(sample_bytes * XYPARSER_SAMPLES_PER_FRAME);
|
||||
const std::size_t frame_size = 1 + kTagLen + payload_length + 2;
|
||||
|
||||
std::vector<std::uint8_t> frame(frame_size, 0);
|
||||
std::size_t offset = 0;
|
||||
frame[offset++] = kHeader;
|
||||
frame[offset++] = static_cast<std::uint8_t>(frame_index & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 8) & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 16) & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>((frame_index >> 24) & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>((payload_length >> 8) & 0xFF);
|
||||
frame[offset++] = static_cast<std::uint8_t>(payload_length & 0xFF);
|
||||
frame[offset++] = 95;
|
||||
frame[offset++] = channel_count;
|
||||
offset += 2 + 2 + 2 + 2 + 2 + 6;
|
||||
|
||||
for (std::size_t sample = 0; sample < XYPARSER_SAMPLES_PER_FRAME; ++sample) {
|
||||
for (std::size_t channel = 0; channel < channel_count; ++channel) {
|
||||
WriteSigned24(frame, offset, raw_samples[sample][channel]);
|
||||
}
|
||||
frame[offset++] = 0x00;
|
||||
frame[offset++] = 0x00;
|
||||
}
|
||||
|
||||
frame[offset++] = 0x00;
|
||||
frame[offset++] = kTail;
|
||||
frame[offset++] = kTail;
|
||||
return frame;
|
||||
}
|
||||
|
||||
int BuildSineRawValue(int sample_index, int sample_rate, double frequency_hz, double amplitude)
|
||||
{
|
||||
const double angle = 2.0 * 3.14159265358979323846 * frequency_hz *
|
||||
static_cast<double>(sample_index) / static_cast<double>(sample_rate);
|
||||
return static_cast<int>(std::lround(std::sin(angle) * amplitude));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// 测试:创建解析器时拒绝不支持的通道数
|
||||
@@ -122,6 +188,507 @@ TEST(XYParserApiTests, GetLastErrorReturnsMessageForNullParser)
|
||||
EXPECT_EQ(std::string(XYParser_GetLastError(nullptr)), std::string("invalid parser handle"));
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Serialize8ChImpedanceOpenCommandMatchesWirelessEegPacket)
|
||||
{
|
||||
constexpr std::size_t kCommandSize = 7;
|
||||
std::array<std::uint8_t, kCommandSize> command{};
|
||||
const int command_size = XYParser_Serialize8ChImpedanceCommand(1, command.data(), command.size());
|
||||
|
||||
ASSERT_EQ(command_size, static_cast<int>(kCommandSize));
|
||||
const std::array<std::uint8_t, kCommandSize> expected = {
|
||||
0xAA, 0x00, 0x01, 0xA1, 0xA1, 0x55, 0x55};
|
||||
EXPECT_EQ(command, expected);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Serialize8ChImpedanceCloseCommandMatchesWirelessEegPacket)
|
||||
{
|
||||
constexpr std::size_t kCommandSize = 7;
|
||||
std::array<std::uint8_t, kCommandSize> command{};
|
||||
const int command_size = XYParser_Serialize8ChImpedanceCommand(0, command.data(), command.size());
|
||||
|
||||
ASSERT_EQ(command_size, static_cast<int>(kCommandSize));
|
||||
const std::array<std::uint8_t, kCommandSize> expected = {
|
||||
0xAA, 0x00, 0x01, 0xA0, 0xA0, 0x55, 0x55};
|
||||
EXPECT_EQ(command, expected);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Serialize8ChImpedanceCommandRejectsSmallBuffer)
|
||||
{
|
||||
std::array<std::uint8_t, 6> command{};
|
||||
EXPECT_EQ(XYParser_Serialize8ChImpedanceCommand(1, command.data(), command.size()), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Get8ChImpedanceCommandSizeMatchesSerializedLength)
|
||||
{
|
||||
EXPECT_EQ(XYParser_Get8ChImpedanceCommandSize(), static_cast<std::size_t>(7));
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetAlgorithmDataValueCountMatchesFrameLayout)
|
||||
{
|
||||
EXPECT_EQ(XYParser_GetAlgorithmDataValueCount(),
|
||||
static_cast<std::size_t>(XYPARSER_FRAME_ALGORITHM_VALUE_COUNT));
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapCountReturnsExpectedCounts)
|
||||
{
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(8), 8);
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(64), 64);
|
||||
EXPECT_EQ(XYParser_GetLeadMapCount(7), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapReturnsExpected8ChannelSubset)
|
||||
{
|
||||
std::array<XYParserLeadChannelNumber, 8> leads{};
|
||||
const int count = XYParser_GetLeadMap(8, leads.data(), static_cast<int>(leads.size()));
|
||||
|
||||
ASSERT_EQ(count, 8);
|
||||
const std::array<XYParserLeadChannelNumber, 8> expected = {
|
||||
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
||||
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
||||
EXPECT_EQ(leads, expected);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapReturnsExpected64ChannelOrder)
|
||||
{
|
||||
std::array<XYParserLeadChannelNumber, 64> leads{};
|
||||
const int count = XYParser_GetLeadMap(64, leads.data(), static_cast<int>(leads.size()));
|
||||
|
||||
ASSERT_EQ(count, 64);
|
||||
EXPECT_EQ(leads.front(), LeadChannel_FP1);
|
||||
EXPECT_EQ(leads[1], LeadChannel_FP2);
|
||||
EXPECT_EQ(leads[13], LeadChannel_PO5);
|
||||
EXPECT_EQ(leads[55], LeadChannel_PO7);
|
||||
EXPECT_EQ(leads.back(), LeadChannel_F1);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadMapRejectsUnsupportedChannelCountOrSmallBuffer)
|
||||
{
|
||||
std::array<XYParserLeadChannelNumber, 7> small_buffer{};
|
||||
EXPECT_EQ(XYParser_GetLeadMap(8, small_buffer.data(), static_cast<int>(small_buffer.size())), 0);
|
||||
EXPECT_EQ(XYParser_GetLeadMap(7, nullptr, 0), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Convert8ChFramesTo64ChMapsKnownLeadsAndPadsOthersWithZero)
|
||||
{
|
||||
std::array<XYParserFrameSummary, 2> input{};
|
||||
input[0].frame_index = 7U;
|
||||
input[0].channel_count = 8U;
|
||||
input[0].battery = 88U;
|
||||
input[0].sample_count = 5U;
|
||||
input[0].sample_trigger_types[0] = 0xB2;
|
||||
input[0].sample_trigger_indices[0] = 3U;
|
||||
input[0].channel_values_uv[0][0] = 11.0;
|
||||
input[0].channel_values_uv[0][1] = 12.0;
|
||||
input[0].channel_values_uv[0][2] = 13.0;
|
||||
input[0].channel_values_uv[0][3] = 14.0;
|
||||
input[0].channel_values_uv[0][4] = 15.0;
|
||||
input[0].channel_values_uv[0][5] = 16.0;
|
||||
input[0].channel_values_uv[0][6] = 17.0;
|
||||
input[0].channel_values_uv[0][7] = 18.0;
|
||||
|
||||
input[1].frame_index = 8U;
|
||||
input[1].channel_count = 8U;
|
||||
input[1].battery = 77U;
|
||||
input[1].sample_count = 5U;
|
||||
input[1].channel_values_uv[1][0] = 21.0;
|
||||
|
||||
std::array<XYParserFrameSummary, 2> output{};
|
||||
ASSERT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), static_cast<int>(input.size()), output.data(), static_cast<int>(output.size())), 2);
|
||||
EXPECT_EQ(output[0].frame_index, 7U);
|
||||
EXPECT_EQ(output[0].channel_count, 64U);
|
||||
EXPECT_EQ(output[0].battery, 88U);
|
||||
EXPECT_EQ(output[0].sample_count, 5U);
|
||||
EXPECT_EQ(output[0].sample_trigger_types[0], 0xB2);
|
||||
EXPECT_EQ(output[0].sample_trigger_indices[0], 3U);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO5], 11.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_POZ], 12.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO6], 13.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO7], 14.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_O1], 15.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_OZ], 16.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_O2], 17.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_PO8], 18.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_FP1], 0.0);
|
||||
EXPECT_DOUBLE_EQ(output[0].channel_values_uv[0][LeadChannel_CZ], 0.0);
|
||||
EXPECT_EQ(output[1].frame_index, 8U);
|
||||
EXPECT_EQ(output[1].channel_count, 64U);
|
||||
EXPECT_DOUBLE_EQ(output[1].channel_values_uv[1][LeadChannel_PO5], 21.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, Convert8ChFramesTo64ChRejectsInvalidArgumentsAndStopsAtNon8ChannelInput)
|
||||
{
|
||||
std::array<XYParserFrameSummary, 2> input{};
|
||||
input[0].channel_count = 8U;
|
||||
input[1].channel_count = 64U;
|
||||
std::array<XYParserFrameSummary, 2> output{};
|
||||
|
||||
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(nullptr, 1, output.data(), static_cast<int>(output.size())), 0);
|
||||
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), 1, nullptr, static_cast<int>(output.size())), 0);
|
||||
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), 0, output.data(), static_cast<int>(output.size())), 0);
|
||||
EXPECT_EQ(XYParser_Convert8ChFramesTo64Ch(input.data(), static_cast<int>(input.size()), output.data(), static_cast<int>(output.size())), 1);
|
||||
EXPECT_EQ(output[0].channel_count, 64U);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, ConvertSampleFramesToAlgorithmDataAppendsTriggerColumnsPerSample)
|
||||
{
|
||||
constexpr std::size_t kFrameValueCount = XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
|
||||
XYParserFrameSummary input{};
|
||||
input.channel_count = 64U;
|
||||
input.sample_count = 5U;
|
||||
input.channel_values_uv[0][0] = 101.0;
|
||||
input.channel_values_uv[0][63] = 164.0;
|
||||
input.sample_trigger_types[0] = 0xA5;
|
||||
input.sample_trigger_indices[0] = 9U;
|
||||
input.channel_values_uv[1][10] = 210.0;
|
||||
input.sample_trigger_types[1] = 0x7F;
|
||||
input.sample_trigger_indices[1] = 5U;
|
||||
input.channel_values_uv[4][3] = 403.0;
|
||||
input.sample_trigger_types[4] = 0xB2;
|
||||
input.sample_trigger_indices[4] = 11U;
|
||||
|
||||
std::array<double, kFrameValueCount> output{};
|
||||
ASSERT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&input, output.data()), 1);
|
||||
|
||||
const std::size_t sample0 = 0;
|
||||
const std::size_t sample1 = XYPARSER_FRAME_DATA_COLUMN_COUNT;
|
||||
const std::size_t sample4 = 4 * XYPARSER_FRAME_DATA_COLUMN_COUNT;
|
||||
|
||||
EXPECT_DOUBLE_EQ(output[sample0 + 0], 101.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample0 + 63], 164.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample0 + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 165.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample0 + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX], 9.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample4 + 3], 403.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample4 + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 178.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample4 + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX], 11.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample1 + 10], 210.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample1 + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 127.0);
|
||||
EXPECT_DOUBLE_EQ(output[sample1 + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX], 5.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, ConvertSampleFramesToAlgorithmDataRejectsInvalidArgumentsAndNon64ChannelInput)
|
||||
{
|
||||
constexpr std::size_t kFrameValueCount = XYPARSER_FRAME_ALGORITHM_VALUE_COUNT;
|
||||
XYParserFrameSummary input{};
|
||||
input.channel_count = 8U;
|
||||
std::array<double, kFrameValueCount> output{};
|
||||
|
||||
EXPECT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(nullptr, output.data()), 0);
|
||||
EXPECT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&input, nullptr), 0);
|
||||
EXPECT_EQ(XYParser_ConvertSampleFramesToAlgorithmData(&input, output.data()), 0);
|
||||
EXPECT_DOUBLE_EQ(output[XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX], 0.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, FeedAlgorithmDataCachesSamplesBuildsFramesAndFlushesTailSamples)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(64));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
constexpr int kTotalSamples = 7;
|
||||
constexpr int kColumnCount = XYPARSER_FRAME_DATA_COLUMN_COUNT;
|
||||
std::array<double, static_cast<std::size_t>(kTotalSamples) * kColumnCount> input_data{};
|
||||
for (int sample_index = 0; sample_index < kTotalSamples; ++sample_index) {
|
||||
const std::size_t row_offset = static_cast<std::size_t>(sample_index) * kColumnCount;
|
||||
input_data[row_offset + 0] = 1000.0 + sample_index;
|
||||
input_data[row_offset + 10] = 2000.0 + sample_index;
|
||||
input_data[row_offset + 63] = 3000.0 + sample_index;
|
||||
input_data[row_offset + XYPARSER_FRAME_DATA_TRIGGER_TYPE_INDEX] = 10.0 + sample_index;
|
||||
input_data[row_offset + XYPARSER_FRAME_DATA_TRIGGER_INDEX_INDEX] = 20.0 + sample_index;
|
||||
}
|
||||
const std::uint8_t* input_bytes = reinterpret_cast<const std::uint8_t*>(input_data.data());
|
||||
const std::size_t first_chunk_size = 3 * static_cast<std::size_t>(kColumnCount) * sizeof(double) + sizeof(double);
|
||||
const std::size_t second_chunk_size = sizeof(input_data) - first_chunk_size;
|
||||
|
||||
std::array<XYParserFrameSummary, 1> summaries{};
|
||||
EXPECT_EQ(XYParser_FeedAlgorithmData(
|
||||
parser.get(),
|
||||
input_bytes,
|
||||
first_chunk_size,
|
||||
summaries.data(),
|
||||
static_cast<int>(summaries.size())),
|
||||
0);
|
||||
|
||||
ASSERT_EQ(XYParser_FeedAlgorithmData(
|
||||
parser.get(),
|
||||
input_bytes + first_chunk_size,
|
||||
second_chunk_size,
|
||||
summaries.data(),
|
||||
static_cast<int>(summaries.size())),
|
||||
1);
|
||||
EXPECT_EQ(summaries[0].frame_index, 1U);
|
||||
EXPECT_EQ(summaries[0].channel_count, 64U);
|
||||
EXPECT_EQ(summaries[0].sample_count, 5U);
|
||||
EXPECT_DOUBLE_EQ(summaries[0].channel_values_uv[0][0], 1000.0);
|
||||
EXPECT_DOUBLE_EQ(summaries[0].channel_values_uv[2][10], 2002.0);
|
||||
EXPECT_DOUBLE_EQ(summaries[0].channel_values_uv[4][63], 3004.0);
|
||||
EXPECT_EQ(summaries[0].sample_trigger_types[0], 10U);
|
||||
EXPECT_EQ(summaries[0].sample_trigger_indices[4], 24U);
|
||||
|
||||
XYParserFrameSummary tail_summary{};
|
||||
ASSERT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 1);
|
||||
EXPECT_EQ(tail_summary.frame_index, 2U);
|
||||
EXPECT_EQ(tail_summary.channel_count, 64U);
|
||||
EXPECT_EQ(tail_summary.sample_count, 2U);
|
||||
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[0][0], 1005.0);
|
||||
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[1][63], 3006.0);
|
||||
EXPECT_EQ(tail_summary.sample_trigger_types[0], 15U);
|
||||
EXPECT_EQ(tail_summary.sample_trigger_indices[1], 26U);
|
||||
EXPECT_DOUBLE_EQ(tail_summary.channel_values_uv[2][0], 0.0);
|
||||
EXPECT_EQ(tail_summary.sample_trigger_types[2], 0U);
|
||||
|
||||
XYParser_ResetAlgorithmDataCache(parser.get());
|
||||
EXPECT_EQ(XYParser_FlushAlgorithmData(parser.get(), &tail_summary), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadNameReturnsExpectedNames)
|
||||
{
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_FP1)), "FP1");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO5)), "PO5");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_OZ)), "OZ");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_GND)), "GND");
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadNameCovers8ChannelSubsetNames)
|
||||
{
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_POZ)), "POZ");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO6)), "PO6");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO7)), "PO7");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_PO8)), "PO8");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_O1)), "O1");
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(LeadChannel_O2)), "O2");
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetLeadNameReturnsEmptyStringForInvalidLead)
|
||||
{
|
||||
EXPECT_EQ(std::string(XYParser_GetLeadName(static_cast<XYParserLeadChannelNumber>(255))), "");
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetWelchBandNameReturnsExpectedNames)
|
||||
{
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(0)), "delta");
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(1)), "theta");
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(2)), "alpha");
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(3)), "beta");
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(4)), "gamma");
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetWelchBandNameReturnsEmptyStringForInvalidIndex)
|
||||
{
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(-1)), "");
|
||||
EXPECT_EQ(std::string(XYParser_GetWelchBandName(XYPARSER_WELCH_BAND_COUNT)), "");
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetWelchBandRangeReturnsExpectedRanges)
|
||||
{
|
||||
double low_hz = 0.0;
|
||||
double high_hz = 0.0;
|
||||
|
||||
ASSERT_EQ(XYParser_GetWelchBandRange(0, &low_hz, &high_hz), 1);
|
||||
EXPECT_DOUBLE_EQ(low_hz, 0.5);
|
||||
EXPECT_DOUBLE_EQ(high_hz, 4.0);
|
||||
|
||||
ASSERT_EQ(XYParser_GetWelchBandRange(4, &low_hz, &high_hz), 1);
|
||||
EXPECT_DOUBLE_EQ(low_hz, 30.0);
|
||||
EXPECT_DOUBLE_EQ(high_hz, 50.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, GetWelchBandRangeRejectsInvalidArguments)
|
||||
{
|
||||
double low_hz = 0.0;
|
||||
double high_hz = 0.0;
|
||||
|
||||
EXPECT_EQ(XYParser_GetWelchBandRange(-1, &low_hz, &high_hz), 0);
|
||||
EXPECT_EQ(XYParser_GetWelchBandRange(XYPARSER_WELCH_BAND_COUNT, &low_hz, &high_hz), 0);
|
||||
EXPECT_EQ(XYParser_GetWelchBandRange(0, nullptr, &high_hz), 0);
|
||||
EXPECT_EQ(XYParser_GetWelchBandRange(0, &low_hz, nullptr), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, SetImpedanceDetectionSwitchesParserGainBetween24And6)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(8));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
XYParser_SetAdcParams(parser.get(), 4.5, 6.0);
|
||||
XYParser_SetBypassChecksum(parser.get(), 1);
|
||||
|
||||
const std::vector<std::uint8_t> frame1 = BuildMinimalFrame(8, 1U);
|
||||
const std::vector<std::uint8_t> frame2 = BuildMinimalFrame(8, 2U);
|
||||
const std::vector<std::uint8_t> frame3 = BuildMinimalFrame(8, 3U);
|
||||
std::array<XYParserFrameSummary, 1> summaries{};
|
||||
|
||||
ASSERT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
const double gain6_value_before = summaries[0].channel_values_uv[0][0];
|
||||
|
||||
XYParser_SetImpedanceDetection(parser.get(), 1);
|
||||
ASSERT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
const double gain24_value = summaries[0].channel_values_uv[0][0];
|
||||
|
||||
XYParser_SetImpedanceDetection(parser.get(), 0);
|
||||
ASSERT_EQ(XYParser_Feed(parser.get(), frame3.data(), frame3.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
const double gain6_value_after = summaries[0].channel_values_uv[0][0];
|
||||
|
||||
EXPECT_NEAR(gain24_value * 4.0, gain6_value_before, 1e-9);
|
||||
EXPECT_NEAR(gain6_value_after, gain6_value_before, 1e-9);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, ImpedanceReturnsOneResultAfterOneSecondFor8Channels)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(8));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
XYParser_SetBypassChecksum(parser.get(), 1);
|
||||
XYParser_SetSampleRate(parser.get(), 10);
|
||||
XYParser_SetImpedanceDetection(parser.get(), 1);
|
||||
|
||||
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
|
||||
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
|
||||
for (int sample = 0; sample < 10; ++sample) {
|
||||
const int raw_value = BuildSineRawValue(sample, 10, 2.0, 1000000.0);
|
||||
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
|
||||
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
|
||||
} else {
|
||||
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(8, 1U, raw_samples1);
|
||||
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(8, 2U, raw_samples2);
|
||||
std::array<XYParserFrameSummary, 2> summaries{};
|
||||
std::array<XYParserImpedanceSummary, 2> impedance{};
|
||||
|
||||
EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
EXPECT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 0);
|
||||
|
||||
EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
|
||||
EXPECT_EQ(impedance[0].channel_count, 8);
|
||||
EXPECT_EQ(impedance[0].sample_rate, 10U);
|
||||
EXPECT_EQ(impedance[0].window_sample_count, 10U);
|
||||
EXPECT_GT(impedance[0].impedance_values[LeadChannel_PO5], 0);
|
||||
EXPECT_EQ(impedance[0].impedance_values[LeadChannel_FP1], 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, ImpedanceUsesUnifiedLeadIndexesFor64Channels)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(64));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
XYParser_SetBypassChecksum(parser.get(), 1);
|
||||
XYParser_SetSampleRate(parser.get(), 10);
|
||||
XYParser_SetImpedanceDetection(parser.get(), 1);
|
||||
|
||||
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
|
||||
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
|
||||
for (int sample = 0; sample < 10; ++sample) {
|
||||
const int raw_value = BuildSineRawValue(sample, 10, 3.0, 1000000.0);
|
||||
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
|
||||
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
|
||||
} else {
|
||||
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(64, 1U, raw_samples1);
|
||||
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(64, 2U, raw_samples2);
|
||||
std::array<XYParserFrameSummary, 2> summaries{};
|
||||
std::array<XYParserImpedanceSummary, 2> impedance{};
|
||||
|
||||
EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
ASSERT_EQ(XYParser_ReadImpedance(parser.get(), impedance.data(), static_cast<int>(impedance.size())), 1);
|
||||
EXPECT_EQ(impedance[0].channel_count, 64);
|
||||
EXPECT_GT(impedance[0].impedance_values[LeadChannel_FP1], 0);
|
||||
EXPECT_EQ(impedance[0].impedance_values[LeadChannel_FP2], 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, WelchReturnsOneResultAfterOneSecondFromAlgorithmData)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(64));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
XYParser_SetSampleRate(parser.get(), 10);
|
||||
XYParser_SetWelchDetection(parser.get(), 1);
|
||||
|
||||
std::vector<double> algorithm_data(static_cast<std::size_t>(10 * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0);
|
||||
for (int sample = 0; sample < 10; ++sample) {
|
||||
const std::size_t sample_offset = static_cast<std::size_t>(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
|
||||
algorithm_data[sample_offset] = static_cast<double>(BuildSineRawValue(sample, 10, 2.0, 1000000.0));
|
||||
}
|
||||
|
||||
std::array<XYParserWelchSummary, 2> welch{};
|
||||
|
||||
EXPECT_EQ(XYParser_FeedAlgorithmData(
|
||||
parser.get(),
|
||||
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
|
||||
algorithm_data.size() * sizeof(double),
|
||||
nullptr,
|
||||
0),
|
||||
0);
|
||||
ASSERT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 1);
|
||||
EXPECT_EQ(welch[0].ok, 1);
|
||||
EXPECT_EQ(welch[0].channel_count, 64);
|
||||
EXPECT_EQ(welch[0].sample_rate, 10U);
|
||||
EXPECT_EQ(welch[0].window_sample_count, 10U);
|
||||
EXPECT_GT(welch[0].frequency_count, 0U);
|
||||
EXPECT_DOUBLE_EQ(welch[0].frequencies[0], 1.0);
|
||||
EXPECT_GT(welch[0].psd_values[LeadChannel_FP1][0], 0.0);
|
||||
EXPECT_EQ(welch[0].psd_values[LeadChannel_FP2][0], 0.0);
|
||||
EXPECT_GT(welch[0].band_values[0][LeadChannel_FP1], 0.0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, WelchFrameFeedDoesNotProduceResults)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(64));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
XYParser_SetBypassChecksum(parser.get(), 1);
|
||||
XYParser_SetSampleRate(parser.get(), 10);
|
||||
XYParser_SetWelchDetection(parser.get(), 1);
|
||||
|
||||
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples1{};
|
||||
std::array<std::array<int, XYPARSER_MAX_CHANNELS>, XYPARSER_SAMPLES_PER_FRAME> raw_samples2{};
|
||||
for (int sample = 0; sample < 10; ++sample) {
|
||||
const int raw_value = BuildSineRawValue(sample, 10, 2.0, 1000000.0);
|
||||
if (sample < static_cast<int>(XYPARSER_SAMPLES_PER_FRAME)) {
|
||||
raw_samples1[static_cast<std::size_t>(sample)][0] = raw_value;
|
||||
} else {
|
||||
raw_samples2[static_cast<std::size_t>(sample - XYPARSER_SAMPLES_PER_FRAME)][0] = raw_value;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> frame1 = BuildFrameWithRawSamples(64, 1U, raw_samples1);
|
||||
const std::vector<std::uint8_t> frame2 = BuildFrameWithRawSamples(64, 2U, raw_samples2);
|
||||
std::array<XYParserFrameSummary, 2> summaries{};
|
||||
std::array<XYParserWelchSummary, 1> welch{};
|
||||
|
||||
EXPECT_EQ(XYParser_Feed(parser.get(), frame1.data(), frame1.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
EXPECT_EQ(XYParser_Feed(parser.get(), frame2.data(), frame2.size(), summaries.data(), static_cast<int>(summaries.size())), 1);
|
||||
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
|
||||
}
|
||||
|
||||
TEST(XYParserApiTests, WelchDisabledDoesNotProduceResultsFromAlgorithmData)
|
||||
{
|
||||
ParserGuard parser(XYParser_CreateParser(64));
|
||||
ASSERT_NE(parser.get(), nullptr);
|
||||
|
||||
XYParser_SetSampleRate(parser.get(), 10);
|
||||
|
||||
std::vector<double> algorithm_data(static_cast<std::size_t>(10 * XYPARSER_FRAME_DATA_COLUMN_COUNT), 0.0);
|
||||
for (int sample = 0; sample < 10; ++sample) {
|
||||
const std::size_t sample_offset = static_cast<std::size_t>(sample) * XYPARSER_FRAME_DATA_COLUMN_COUNT;
|
||||
algorithm_data[sample_offset] = static_cast<double>(BuildSineRawValue(sample, 10, 2.0, 1000000.0));
|
||||
}
|
||||
|
||||
std::array<XYParserWelchSummary, 1> welch{};
|
||||
EXPECT_EQ(XYParser_FeedAlgorithmData(
|
||||
parser.get(),
|
||||
reinterpret_cast<const std::uint8_t*>(algorithm_data.data()),
|
||||
algorithm_data.size() * sizeof(double),
|
||||
nullptr,
|
||||
0),
|
||||
0);
|
||||
EXPECT_EQ(XYParser_ReadWelch(parser.get(), welch.data(), static_cast<int>(welch.size())), 0);
|
||||
}
|
||||
|
||||
/// 测试:Feed 函数能正确解析完整的 8 通道帧
|
||||
TEST(XYParserApiTests, FeedParsesAComplete8ChannelFrame)
|
||||
{
|
||||
@@ -151,8 +718,8 @@ TEST(XYParserApiTests, FeedParsesAComplete8ChannelFrame)
|
||||
EXPECT_EQ(summaries[0].battery, 95U); // 电池电量应为 95
|
||||
EXPECT_EQ(summaries[0].sample_count, 5U); // 采样数应为 5
|
||||
EXPECT_GT(summaries[0].channel_values_uv[0][0], 0.0); // 通道值应大于 0
|
||||
EXPECT_EQ(summaries[0].trigger_types[0], 0U); // 触发类型应为 0
|
||||
EXPECT_EQ(summaries[0].trigger_indices[0], 0U); // 触发索引应为 0
|
||||
EXPECT_EQ(summaries[0].sample_trigger_types[0], 0U); // 触发类型应为 0
|
||||
EXPECT_EQ(summaries[0].sample_trigger_indices[0], 0U); // 触发索引应为 0
|
||||
}
|
||||
|
||||
/// 测试:Feed 函数能缓冲部分数据直到完整帧可用
|
||||
@@ -268,6 +835,56 @@ TEST(XYParserApiTests, SetBypassChecksumOnNullHandle)
|
||||
EXPECT_NO_THROW(XYParser_SetBypassChecksum(nullptr, 1));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 64 导控制协议序列化测试
|
||||
// ============================================================================
|
||||
|
||||
/// 测试:64 导开启阻抗命令序列化与 WirelessEEG 协议保持一致
|
||||
TEST(XYParserApiTests, Serialize64ImpedanceOpenCommandMatchesWirelessEegProtocol)
|
||||
{
|
||||
std::array<std::uint8_t, 32> buffer{};
|
||||
const int size = XYParser_Serialize64ImpedanceCommand(1, buffer.data(), buffer.size());
|
||||
|
||||
ASSERT_EQ(size, static_cast<int>(XYParser_Get64ImpedanceCommandSize()));
|
||||
|
||||
const std::vector<std::uint8_t> expected = {
|
||||
0xAA, 0x01, 0x00, 0x07,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x09, 0x55, 0x55
|
||||
};
|
||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
|
||||
}
|
||||
|
||||
/// 测试:64 导增益和采样率命令序列化与 WirelessEEG 协议保持一致
|
||||
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandMatchesWirelessEegProtocol)
|
||||
{
|
||||
std::array<std::uint8_t, 32> buffer{};
|
||||
const int size = XYParser_Serialize64GainSampleRateCommand(24, 1000, buffer.data(), buffer.size());
|
||||
|
||||
ASSERT_EQ(size, static_cast<int>(XYParser_Get64GainSampleRateCommandSize()));
|
||||
|
||||
const std::vector<std::uint8_t> expected = {
|
||||
0xAA, 0x02, 0x00, 0x08,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x18, 0x02, 0x24, 0x55, 0x55
|
||||
};
|
||||
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), buffer.begin()));
|
||||
}
|
||||
|
||||
/// 测试:64 导增益和采样率命令拒绝非法采样率
|
||||
TEST(XYParserApiTests, Serialize64GainAndSampleRateCommandRejectsUnsupportedSampleRate)
|
||||
{
|
||||
std::array<std::uint8_t, 32> buffer{};
|
||||
EXPECT_EQ(XYParser_Serialize64GainSampleRateCommand(24, 256, buffer.data(), buffer.size()), 0);
|
||||
}
|
||||
|
||||
/// 测试:64 导阻抗命令在输出缓冲区不足时返回失败
|
||||
TEST(XYParserApiTests, Serialize64ImpedanceCommandRejectsSmallBuffer)
|
||||
{
|
||||
std::array<std::uint8_t, 4> buffer{};
|
||||
EXPECT_EQ(XYParser_Serialize64ImpedanceCommand(1, buffer.data(), buffer.size()), 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feed 函数帧解析测试
|
||||
// ============================================================================
|
||||
|
||||
285
XYParser/XYWelchProcessor.cpp
Normal file
285
XYParser/XYWelchProcessor.cpp
Normal file
@@ -0,0 +1,285 @@
|
||||
#include "pch.h"
|
||||
#include "XYWelchProcessor.h"
|
||||
|
||||
#include "third_party/kissfft/kiss_fftr.h"
|
||||
#include "third_party/kissfft/window_functions.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kDefaultSampleRate = 250;
|
||||
constexpr int k8ChLeadCount = 8;
|
||||
constexpr int k64ChLeadCount = 64;
|
||||
|
||||
struct PsdBandDefinition {
|
||||
double low_hz;
|
||||
double high_hz;
|
||||
};
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k8ChLeadCount> k8ChLeadMap = {
|
||||
LeadChannel_PO5, LeadChannel_POZ, LeadChannel_PO6, LeadChannel_PO7,
|
||||
LeadChannel_O1, LeadChannel_OZ, LeadChannel_O2, LeadChannel_PO8};
|
||||
|
||||
constexpr std::array<XYParserLeadChannelNumber, k64ChLeadCount> k64ChLeadMap = {
|
||||
LeadChannel_FP1, LeadChannel_FP2, LeadChannel_PO6, LeadChannel_POZ,
|
||||
LeadChannel_F3, LeadChannel_F4, LeadChannel_FPZ, LeadChannel_AF4,
|
||||
LeadChannel_FC3, LeadChannel_PO8, LeadChannel_CP2, LeadChannel_CP1,
|
||||
LeadChannel_FCZ, LeadChannel_PO5, LeadChannel_FC2, LeadChannel_FC1,
|
||||
LeadChannel_C3, LeadChannel_C4, LeadChannel_FC4, LeadChannel_CP4,
|
||||
LeadChannel_P3, LeadChannel_P4, LeadChannel_F5, LeadChannel_C5,
|
||||
LeadChannel_F6, LeadChannel_PO4, LeadChannel_CP6, LeadChannel_CP5,
|
||||
LeadChannel_PO3, LeadChannel_CP3, LeadChannel_FC6, LeadChannel_FC5,
|
||||
LeadChannel_CB1, LeadChannel_CB2, LeadChannel_P5, LeadChannel_AF7,
|
||||
LeadChannel_A1, LeadChannel_T7, LeadChannel_FT7, LeadChannel_TP7,
|
||||
LeadChannel_FT8, LeadChannel_AF8, LeadChannel_F8, LeadChannel_F7,
|
||||
LeadChannel_P6, LeadChannel_C6, LeadChannel_O2, LeadChannel_O1,
|
||||
LeadChannel_T8, LeadChannel_P7, LeadChannel_CZ, LeadChannel_PZ,
|
||||
LeadChannel_P8, LeadChannel_FZ, LeadChannel_OZ, LeadChannel_PO7,
|
||||
LeadChannel_TP8, LeadChannel_AF3, LeadChannel_C2, LeadChannel_C1,
|
||||
LeadChannel_P2, LeadChannel_P1, LeadChannel_F2, LeadChannel_F1};
|
||||
|
||||
constexpr std::array<PsdBandDefinition, XYPARSER_WELCH_BAND_COUNT> kWelchBands = {{
|
||||
{0.5, 4.0},
|
||||
{4.0, 8.0},
|
||||
{8.0, 13.0},
|
||||
{13.0, 30.0},
|
||||
{30.0, 50.0},
|
||||
}};
|
||||
|
||||
} // namespace
|
||||
|
||||
XYWelchProcessor::XYWelchProcessor(std::uint8_t channel_count)
|
||||
: channel_count_(channel_count)
|
||||
{
|
||||
}
|
||||
|
||||
void XYWelchProcessor::SetChannelCount(std::uint8_t channel_count)
|
||||
{
|
||||
channel_count_ = channel_count;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void XYWelchProcessor::SetSampleRate(int sample_rate)
|
||||
{
|
||||
if (sample_rate <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sample_rate_ != sample_rate) {
|
||||
sample_rate_ = sample_rate;
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void XYWelchProcessor::SetEnabled(bool enabled)
|
||||
{
|
||||
if (enabled_ == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabled_ = enabled;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void XYWelchProcessor::Reset()
|
||||
{
|
||||
cached_samples_.clear();
|
||||
pending_results_.clear();
|
||||
}
|
||||
|
||||
void XYWelchProcessor::PushFrame(const XYParserFrameSummary& frame)
|
||||
{
|
||||
if (!enabled_ || frame.channel_count != channel_count_) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::size_t sample_index = 0; sample_index < frame.sample_count; ++sample_index) {
|
||||
Sample sample{};
|
||||
for (std::size_t channel_index = 0; channel_index < frame.channel_count; ++channel_index) {
|
||||
sample[channel_index] = frame.channel_values_uv[sample_index][channel_index];
|
||||
}
|
||||
cached_samples_.push_back(sample);
|
||||
}
|
||||
|
||||
ProcessPendingWindows();
|
||||
}
|
||||
|
||||
void XYWelchProcessor::PushSample(const std::array<double, XYPARSER_MAX_CHANNELS>& sample)
|
||||
{
|
||||
if (!enabled_ || channel_count_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
cached_samples_.push_back(sample);
|
||||
ProcessPendingWindows();
|
||||
}
|
||||
|
||||
int XYWelchProcessor::Read(XYParserWelchSummary* out_summaries, int max_summaries)
|
||||
{
|
||||
if (out_summaries == nullptr || max_summaries <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int available_count = static_cast<int>(pending_results_.size());
|
||||
const int read_count = available_count < max_summaries ? available_count : max_summaries;
|
||||
for (int i = 0; i < read_count; ++i) {
|
||||
out_summaries[i] = pending_results_.front();
|
||||
pending_results_.pop_front();
|
||||
}
|
||||
return read_count;
|
||||
}
|
||||
|
||||
void XYWelchProcessor::ProcessPendingWindows()
|
||||
{
|
||||
const int effective_sample_rate = sample_rate_ > 0 ? sample_rate_ : 1;
|
||||
const std::size_t window_sample_count = static_cast<std::size_t>(effective_sample_rate);
|
||||
while (cached_samples_.size() >= window_sample_count) {
|
||||
pending_results_.push_back(BuildSummaryFromWindow());
|
||||
cached_samples_.erase(cached_samples_.begin(), cached_samples_.begin() + static_cast<std::ptrdiff_t>(window_sample_count));
|
||||
}
|
||||
}
|
||||
|
||||
XYParserWelchSummary XYWelchProcessor::BuildSummaryFromWindow() const
|
||||
{
|
||||
XYParserWelchSummary summary{};
|
||||
summary.channel_count = channel_count_;
|
||||
summary.sample_rate = static_cast<std::uint32_t>(sample_rate_ > 0 ? sample_rate_ : kDefaultSampleRate);
|
||||
summary.window_sample_count = summary.sample_rate;
|
||||
|
||||
const int sample_count = static_cast<int>(summary.window_sample_count);
|
||||
if (channel_count_ == 0 || sample_count < 2) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
int n_per_seg = sample_count;
|
||||
if (n_per_seg % 2 != 0) {
|
||||
--n_per_seg;
|
||||
}
|
||||
if (n_per_seg < 2) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
const int n_overlap = n_per_seg / 2;
|
||||
const int n_step = (n_per_seg - n_overlap) > 0 ? (n_per_seg - n_overlap) : 1;
|
||||
const int n_freq_bin_count = n_per_seg / 2 + 1;
|
||||
|
||||
std::vector<double> window(static_cast<std::size_t>(n_per_seg), 0.0);
|
||||
hanning_function(n_per_seg, window.data());
|
||||
|
||||
double window_power = 0.0;
|
||||
for (int i = 0; i < n_per_seg; ++i) {
|
||||
window_power += window[static_cast<std::size_t>(i)] * window[static_cast<std::size_t>(i)];
|
||||
}
|
||||
if (window_power <= std::numeric_limits<double>::epsilon()) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
std::vector<std::vector<double>> psd_accum(static_cast<std::size_t>(channel_count_),
|
||||
std::vector<double>(static_cast<std::size_t>(n_freq_bin_count), 0.0));
|
||||
std::vector<kiss_fft_scalar> segment(static_cast<std::size_t>(n_per_seg), 0.0);
|
||||
std::vector<kiss_fft_cpx> fft_output(static_cast<std::size_t>(n_freq_bin_count));
|
||||
|
||||
kiss_fftr_cfg cfg = kiss_fftr_alloc(n_per_seg, 0, nullptr, nullptr);
|
||||
if (cfg == nullptr) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
int segment_count = 0;
|
||||
for (int start = 0; start + n_per_seg <= sample_count; start += n_step) {
|
||||
++segment_count;
|
||||
for (std::size_t channel_index = 0; channel_index < channel_count_; ++channel_index) {
|
||||
double mean = 0.0;
|
||||
for (int i = 0; i < n_per_seg; ++i) {
|
||||
mean += cached_samples_[static_cast<std::size_t>(start + i)][channel_index];
|
||||
}
|
||||
mean /= static_cast<double>(n_per_seg);
|
||||
|
||||
for (int i = 0; i < n_per_seg; ++i) {
|
||||
segment[static_cast<std::size_t>(i)] =
|
||||
(cached_samples_[static_cast<std::size_t>(start + i)][channel_index] - mean) *
|
||||
window[static_cast<std::size_t>(i)];
|
||||
}
|
||||
|
||||
kiss_fftr(cfg, segment.data(), fft_output.data());
|
||||
for (int bin = 0; bin < n_freq_bin_count; ++bin) {
|
||||
const double real = fft_output[static_cast<std::size_t>(bin)].r;
|
||||
const double imag = fft_output[static_cast<std::size_t>(bin)].i;
|
||||
double psd = (real * real + imag * imag) /
|
||||
(static_cast<double>(summary.sample_rate) * window_power);
|
||||
if (bin != 0 && bin != (n_per_seg / 2)) {
|
||||
psd *= 2.0;
|
||||
}
|
||||
psd_accum[channel_index][static_cast<std::size_t>(bin)] += psd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kiss_fftr_free(cfg);
|
||||
if (segment_count <= 0) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
std::vector<double> all_frequencies(static_cast<std::size_t>(n_freq_bin_count), 0.0);
|
||||
const double freq_resolution = static_cast<double>(summary.sample_rate) / static_cast<double>(n_per_seg);
|
||||
for (int bin = 0; bin < n_freq_bin_count; ++bin) {
|
||||
all_frequencies[static_cast<std::size_t>(bin)] = freq_resolution * static_cast<double>(bin);
|
||||
}
|
||||
|
||||
for (int bin = 0; bin < n_freq_bin_count; ++bin) {
|
||||
const double frequency = all_frequencies[static_cast<std::size_t>(bin)];
|
||||
if (frequency >= 0.5 && frequency <= 50.0 &&
|
||||
summary.frequency_count < XYPARSER_WELCH_MAX_FREQUENCY_COUNT) {
|
||||
summary.frequencies[summary.frequency_count] = frequency;
|
||||
++summary.frequency_count;
|
||||
}
|
||||
}
|
||||
|
||||
const auto fill_channel_result = [&](std::size_t raw_channel_index, XYParserLeadChannelNumber lead) {
|
||||
const std::size_t lead_index = static_cast<std::size_t>(lead);
|
||||
|
||||
std::uint32_t selected_index = 0;
|
||||
for (int bin = 0; bin < n_freq_bin_count; ++bin) {
|
||||
const double frequency = all_frequencies[static_cast<std::size_t>(bin)];
|
||||
if (frequency >= 0.5 && frequency <= 50.0 &&
|
||||
selected_index < summary.frequency_count) {
|
||||
summary.psd_values[lead_index][selected_index] =
|
||||
psd_accum[raw_channel_index][static_cast<std::size_t>(bin)] / static_cast<double>(segment_count);
|
||||
++selected_index;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t band_index = 0; band_index < kWelchBands.size(); ++band_index) {
|
||||
int band_bin_count = 0;
|
||||
double band_sum = 0.0;
|
||||
for (int bin = 0; bin < n_freq_bin_count; ++bin) {
|
||||
const double frequency = all_frequencies[static_cast<std::size_t>(bin)];
|
||||
if (frequency < kWelchBands[band_index].low_hz || frequency > kWelchBands[band_index].high_hz) {
|
||||
continue;
|
||||
}
|
||||
++band_bin_count;
|
||||
band_sum += psd_accum[raw_channel_index][static_cast<std::size_t>(bin)] / static_cast<double>(segment_count);
|
||||
}
|
||||
if (band_bin_count > 0) {
|
||||
summary.band_values[band_index][lead_index] = band_sum / static_cast<double>(band_bin_count);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (channel_count_ == 8) {
|
||||
for (std::size_t i = 0; i < k8ChLeadMap.size(); ++i) {
|
||||
fill_channel_result(i, k8ChLeadMap[i]);
|
||||
}
|
||||
} else {
|
||||
const std::size_t fill_count = channel_count_ < k64ChLeadMap.size() ? channel_count_ : k64ChLeadMap.size();
|
||||
for (std::size_t i = 0; i < fill_count; ++i) {
|
||||
fill_channel_result(i, k64ChLeadMap[i]);
|
||||
}
|
||||
}
|
||||
|
||||
summary.ok = 1;
|
||||
return summary;
|
||||
}
|
||||
34
XYParser/XYWelchProcessor.h
Normal file
34
XYParser/XYWelchProcessor.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "XYParserApi.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
|
||||
class XYWelchProcessor {
|
||||
public:
|
||||
explicit XYWelchProcessor(std::uint8_t channel_count = 0);
|
||||
|
||||
void SetChannelCount(std::uint8_t channel_count);
|
||||
void SetSampleRate(int sample_rate);
|
||||
void SetEnabled(bool enabled);
|
||||
void Reset();
|
||||
|
||||
void PushFrame(const XYParserFrameSummary& frame);
|
||||
void PushSample(const std::array<double, XYPARSER_MAX_CHANNELS>& sample);
|
||||
int Read(XYParserWelchSummary* out_summaries, int max_summaries);
|
||||
|
||||
private:
|
||||
using Sample = std::array<double, XYPARSER_MAX_CHANNELS>;
|
||||
|
||||
void ProcessPendingWindows();
|
||||
XYParserWelchSummary BuildSummaryFromWindow() const;
|
||||
|
||||
private:
|
||||
std::uint8_t channel_count_ = 0;
|
||||
int sample_rate_ = 250;
|
||||
bool enabled_ = false;
|
||||
std::deque<Sample> cached_samples_;
|
||||
std::deque<XYParserWelchSummary> pending_results_;
|
||||
};
|
||||
156
XYParser/third_party/kissfft/_kiss_fft_guts.h
vendored
Normal file
156
XYParser/third_party/kissfft/_kiss_fft_guts.h
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
* See COPYING file for more information.
|
||||
*/
|
||||
|
||||
#ifndef _kiss_fft_guts_h
|
||||
#define _kiss_fft_guts_h
|
||||
|
||||
#include "kiss_fft.h"
|
||||
#include "kiss_fft_log.h"
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
#define MAXFACTORS 32
|
||||
|
||||
struct kiss_fft_state
|
||||
{
|
||||
int nfft;
|
||||
int inverse;
|
||||
int factors[2 * MAXFACTORS];
|
||||
kiss_fft_cpx twiddles[1];
|
||||
};
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
#include <stdint.h>
|
||||
#if (FIXED_POINT == 32)
|
||||
#define FRACBITS 31
|
||||
#define SAMPPROD int64_t
|
||||
#define SAMP_MAX INT32_MAX
|
||||
#define SAMP_MIN INT32_MIN
|
||||
#else
|
||||
#define FRACBITS 15
|
||||
#define SAMPPROD int32_t
|
||||
#define SAMP_MAX INT16_MAX
|
||||
#define SAMP_MIN INT16_MIN
|
||||
#endif
|
||||
|
||||
#if defined(CHECK_OVERFLOW)
|
||||
#define CHECK_OVERFLOW_OP(a, op, b) \
|
||||
if((SAMPPROD)(a) op(SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op(SAMPPROD)(b) < SAMP_MIN) { \
|
||||
KISS_FFT_WARNING("overflow (%d " #op " %d) = %ld", (a), (b), (SAMPPROD)(a) op(SAMPPROD)(b)); \
|
||||
}
|
||||
#endif
|
||||
|
||||
#define smul(a, b) ((SAMPPROD)(a) * (b))
|
||||
#define sround(x) (kiss_fft_scalar)(((x) + (1 << (FRACBITS - 1))) >> FRACBITS)
|
||||
#define S_MUL(a, b) sround(smul(a, b))
|
||||
|
||||
#define C_MUL(m, a, b) \
|
||||
do { \
|
||||
(m).r = sround(smul((a).r, (b).r) - smul((a).i, (b).i)); \
|
||||
(m).i = sround(smul((a).r, (b).i) + smul((a).i, (b).r)); \
|
||||
} while(0)
|
||||
|
||||
#define DIVSCALAR(x, k) ((x) = sround(smul(x, SAMP_MAX / (k))))
|
||||
|
||||
#define C_FIXDIV(c, div) \
|
||||
do { \
|
||||
DIVSCALAR((c).r, div); \
|
||||
DIVSCALAR((c).i, div); \
|
||||
} while(0)
|
||||
|
||||
#define C_MULBYSCALAR(c, s) \
|
||||
do { \
|
||||
(c).r = sround(smul((c).r, s)); \
|
||||
(c).i = sround(smul((c).i, s)); \
|
||||
} while(0)
|
||||
|
||||
#else
|
||||
|
||||
#define S_MUL(a, b) ((a) * (b))
|
||||
#define C_MUL(m, a, b) \
|
||||
do { \
|
||||
(m).r = (a).r * (b).r - (a).i * (b).i; \
|
||||
(m).i = (a).r * (b).i + (a).i * (b).r; \
|
||||
} while(0)
|
||||
#define C_FIXDIV(c, div)
|
||||
#define C_MULBYSCALAR(c, s) \
|
||||
do { \
|
||||
(c).r *= (s); \
|
||||
(c).i *= (s); \
|
||||
} while(0)
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef CHECK_OVERFLOW_OP
|
||||
#define CHECK_OVERFLOW_OP(a, op, b)
|
||||
#endif
|
||||
|
||||
#define C_ADD(res, a, b) \
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((a).r, +, (b).r) \
|
||||
CHECK_OVERFLOW_OP((a).i, +, (b).i) \
|
||||
(res).r = (a).r + (b).r; \
|
||||
(res).i = (a).i + (b).i; \
|
||||
} while(0)
|
||||
|
||||
#define C_SUB(res, a, b) \
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((a).r, -, (b).r) \
|
||||
CHECK_OVERFLOW_OP((a).i, -, (b).i) \
|
||||
(res).r = (a).r - (b).r; \
|
||||
(res).i = (a).i - (b).i; \
|
||||
} while(0)
|
||||
|
||||
#define C_ADDTO(res, a) \
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((res).r, +, (a).r) \
|
||||
CHECK_OVERFLOW_OP((res).i, +, (a).i) \
|
||||
(res).r += (a).r; \
|
||||
(res).i += (a).i; \
|
||||
} while(0)
|
||||
|
||||
#define C_SUBFROM(res, a) \
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((res).r, -, (a).r) \
|
||||
CHECK_OVERFLOW_OP((res).i, -, (a).i) \
|
||||
(res).r -= (a).r; \
|
||||
(res).i -= (a).i; \
|
||||
} while(0)
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
#define KISS_FFT_COS(phase) floor(.5 + SAMP_MAX * cos(phase))
|
||||
#define KISS_FFT_SIN(phase) floor(.5 + SAMP_MAX * sin(phase))
|
||||
#define HALF_OF(x) ((x) >> 1)
|
||||
#elif defined(USE_SIMD)
|
||||
#define KISS_FFT_COS(phase) _mm_set1_ps(cos(phase))
|
||||
#define KISS_FFT_SIN(phase) _mm_set1_ps(sin(phase))
|
||||
#define HALF_OF(x) ((x) * _mm_set1_ps(.5))
|
||||
#else
|
||||
#define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
|
||||
#define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
|
||||
#define HALF_OF(x) ((x) * ((kiss_fft_scalar).5))
|
||||
#endif
|
||||
|
||||
#define kf_cexp(x, phase) \
|
||||
do { \
|
||||
(x)->r = KISS_FFT_COS(phase); \
|
||||
(x)->i = KISS_FFT_SIN(phase); \
|
||||
} while(0)
|
||||
|
||||
#define pcpx(c) KISS_FFT_DEBUG("%g + %gi\n", (double)((c)->r), (double)((c)->i))
|
||||
|
||||
#ifdef KISS_FFT_USE_ALLOCA
|
||||
#include <alloca.h>
|
||||
#define KISS_FFT_TMP_ALLOC(nbytes) alloca(nbytes)
|
||||
#define KISS_FFT_TMP_FREE(ptr)
|
||||
#else
|
||||
#define KISS_FFT_TMP_ALLOC(nbytes) KISS_FFT_MALLOC(nbytes)
|
||||
#define KISS_FFT_TMP_FREE(ptr) KISS_FFT_FREE(ptr)
|
||||
#endif
|
||||
|
||||
#endif /* _kiss_fft_guts_h */
|
||||
186
XYParser/third_party/kissfft/fir_filter.cpp
vendored
Normal file
186
XYParser/third_party/kissfft/fir_filter.cpp
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "fir_filter.h"
|
||||
|
||||
namespace {
|
||||
|
||||
double sinc(const double x)
|
||||
{
|
||||
if(x == 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
return std::sin(M_PI * x) / (M_PI * x);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FIR_filter::FIR_filter(int taps, double f1, double f2, char *type, char *window)
|
||||
: h(taps, 0.0), samples(taps, 0.0)
|
||||
{
|
||||
this->idx = 0;
|
||||
this->taps = taps;
|
||||
|
||||
std::vector<double> coeffs;
|
||||
std::vector<double> weights;
|
||||
const char *safeType = type != nullptr ? type : const_cast<char *>("");
|
||||
const char *safeWindow = window != nullptr ? window : const_cast<char *>("");
|
||||
|
||||
if(std::strcmp(safeType, "lp") == 0) {
|
||||
coeffs = lowPass_coefficient(taps, f1);
|
||||
} else if(std::strcmp(safeType, "hp") == 0) {
|
||||
coeffs = highPass_coefficient(taps, f1);
|
||||
} else if(std::strcmp(safeType, "bp") == 0) {
|
||||
coeffs = bandPass_coefficient(taps, f1, f2);
|
||||
} else if(std::strcmp(safeType, "sb") == 0) {
|
||||
coeffs = bandStop_coefficient(taps, f1, f2);
|
||||
}
|
||||
|
||||
if(std::strcmp(safeWindow, "hamming") == 0) {
|
||||
weights = window_hammig(taps);
|
||||
} else if(std::strcmp(safeWindow, "triangle") == 0) {
|
||||
weights = window_triangle(taps);
|
||||
} else if(std::strcmp(safeWindow, "hanning") == 0) {
|
||||
weights = window_hanning(taps);
|
||||
} else if(std::strcmp(safeWindow, "blackman") == 0) {
|
||||
weights = window_blackman(taps);
|
||||
}
|
||||
|
||||
if(std::strcmp(safeWindow, "") == 0) {
|
||||
this->h = coeffs;
|
||||
} else {
|
||||
for(int n = 0; n < taps; n++) {
|
||||
this->h[n] = coeffs[n] * weights[n];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FIR_filter::~FIR_filter() = default;
|
||||
|
||||
std::vector<double> FIR_filter::getCoefficients()
|
||||
{
|
||||
return this->h;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::lowPass_coefficient(int taps, double f)
|
||||
{
|
||||
std::vector<int> n(taps, 0);
|
||||
std::vector<double> coeffs(taps, 0.0);
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
n[i] = i - taps / 2;
|
||||
}
|
||||
for(int i = 0; i < taps; i++) {
|
||||
coeffs[i] = 2.0 * f * sinc(2.0 * f * n[i]);
|
||||
}
|
||||
|
||||
return coeffs;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::highPass_coefficient(int taps, double f)
|
||||
{
|
||||
std::vector<int> n(taps, 0);
|
||||
std::vector<double> coeffs(taps, 0.0);
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
n[i] = i - taps / 2;
|
||||
}
|
||||
for(int i = 0; i < taps; i++) {
|
||||
coeffs[i] = sinc(n[i]) - 2.0 * f * sinc(2.0 * f * n[i]);
|
||||
}
|
||||
|
||||
return coeffs;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::bandPass_coefficient(int taps, double f1, double f2)
|
||||
{
|
||||
std::vector<int> n(taps, 0);
|
||||
std::vector<double> coeffs(taps, 0.0);
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
n[i] = i - taps / 2;
|
||||
}
|
||||
for(int i = 0; i < taps; i++) {
|
||||
coeffs[i] = 2.0 * f1 * sinc(2.0 * f1 * n[i]) - 2.0 * f2 * sinc(2.0 * f2 * n[i]);
|
||||
}
|
||||
|
||||
return coeffs;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::bandStop_coefficient(int taps, double f1, double f2)
|
||||
{
|
||||
std::vector<int> n(taps, 0);
|
||||
std::vector<double> coeffs(taps, 0.0);
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
n[i] = i - taps / 2;
|
||||
}
|
||||
for(int i = 0; i < taps; i++) {
|
||||
coeffs[i] = 2.0 * f1 * sinc(2.0 * f1 * n[i]) - 2.0 * f2 * sinc(2.0 * f2 * n[i])
|
||||
+ sinc(n[i]);
|
||||
}
|
||||
|
||||
return coeffs;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::window_hammig(int taps)
|
||||
{
|
||||
std::vector<double> weights(taps, 0.0);
|
||||
constexpr double alpha = 0.54;
|
||||
constexpr double beta = 0.46;
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
weights[i] = alpha - beta * std::cos(2.0 * M_PI * i / (taps - 1));
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::window_hanning(int taps)
|
||||
{
|
||||
std::vector<double> weights(taps, 0.0);
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
const double angle = static_cast<double>(M_PI) * i / (taps - 1);
|
||||
weights[i] = std::sin(angle) * std::sin(angle);
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::window_triangle(int taps)
|
||||
{
|
||||
std::vector<double> weights(taps, 0.0);
|
||||
const double length = static_cast<double>(taps);
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
weights[i] = 1.0 - std::abs((i - ((taps - 1) / 2.0)) / (length / 2.0));
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
std::vector<double> FIR_filter::window_blackman(int taps)
|
||||
{
|
||||
std::vector<double> weights(taps, 0.0);
|
||||
constexpr double alpha0 = 0.42;
|
||||
constexpr double alpha1 = 0.5;
|
||||
constexpr double alpha2 = 0.08;
|
||||
|
||||
for(int i = 0; i < taps; i++) {
|
||||
weights[i] = alpha0 - alpha1 * std::cos(2.0 * M_PI * i / (taps - 1))
|
||||
- alpha2 * std::cos(4.0 * M_PI * i / (taps - 1));
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
double FIR_filter::filter(double new_sample)
|
||||
{
|
||||
double result = 0.0;
|
||||
|
||||
this->samples[this->idx] = new_sample;
|
||||
for(int n = 0; n < this->taps; n++) {
|
||||
result += this->samples[(this->idx + n) % this->taps] * this->h[n];
|
||||
}
|
||||
this->idx = (this->idx + 1) % this->taps;
|
||||
|
||||
return result;
|
||||
}
|
||||
42
XYParser/third_party/kissfft/fir_filter.h
vendored
Normal file
42
XYParser/third_party/kissfft/fir_filter.h
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
#ifndef FIR_FILTER_H
|
||||
#define FIR_FILTER_H
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
class FIR_filter
|
||||
{
|
||||
public:
|
||||
FIR_filter(int taps = 0, double f1 = 0, double f2 = 0, char *type = nullptr,
|
||||
char *window = nullptr);
|
||||
~FIR_filter();
|
||||
|
||||
std::vector<double> getCoefficients();
|
||||
|
||||
double filter(double new_sample);
|
||||
|
||||
private:
|
||||
std::vector<double> lowPass_coefficient(int taps, double f);
|
||||
std::vector<double> highPass_coefficient(int taps, double f);
|
||||
std::vector<double> bandPass_coefficient(int taps, double f1, double f2);
|
||||
std::vector<double> bandStop_coefficient(int taps, double f1, double f2);
|
||||
|
||||
std::vector<double> window_hammig(int taps);
|
||||
std::vector<double> window_triangle(int taps);
|
||||
std::vector<double> window_hanning(int taps);
|
||||
std::vector<double> window_blackman(int taps);
|
||||
|
||||
std::vector<double> h;
|
||||
std::vector<double> samples;
|
||||
|
||||
int idx = 0;
|
||||
int taps = 0;
|
||||
};
|
||||
|
||||
#endif // FIR_FILTER_H
|
||||
349
XYParser/third_party/kissfft/kiss_fft.c
vendored
Normal file
349
XYParser/third_party/kissfft/kiss_fft.c
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
* See COPYING file for more information.
|
||||
*/
|
||||
|
||||
#include "_kiss_fft_guts.h"
|
||||
|
||||
static void kf_bfly2(kiss_fft_cpx *Fout, const size_t fstride, const kiss_fft_cfg st, int m)
|
||||
{
|
||||
kiss_fft_cpx *Fout2 = Fout + m;
|
||||
kiss_fft_cpx *tw1 = st->twiddles;
|
||||
kiss_fft_cpx t;
|
||||
|
||||
do {
|
||||
C_FIXDIV(*Fout, 2);
|
||||
C_FIXDIV(*Fout2, 2);
|
||||
C_MUL(t, *Fout2, *tw1);
|
||||
tw1 += fstride;
|
||||
C_SUB(*Fout2, *Fout, t);
|
||||
C_ADDTO(*Fout, t);
|
||||
++Fout2;
|
||||
++Fout;
|
||||
} while(--m);
|
||||
}
|
||||
|
||||
static void kf_bfly4(kiss_fft_cpx *Fout, const size_t fstride, const kiss_fft_cfg st,
|
||||
const size_t m)
|
||||
{
|
||||
kiss_fft_cpx *tw1;
|
||||
kiss_fft_cpx *tw2;
|
||||
kiss_fft_cpx *tw3;
|
||||
kiss_fft_cpx scratch[6];
|
||||
size_t k = m;
|
||||
const size_t m2 = 2 * m;
|
||||
const size_t m3 = 3 * m;
|
||||
|
||||
tw3 = tw2 = tw1 = st->twiddles;
|
||||
do {
|
||||
C_FIXDIV(*Fout, 4);
|
||||
C_FIXDIV(Fout[m], 4);
|
||||
C_FIXDIV(Fout[m2], 4);
|
||||
C_FIXDIV(Fout[m3], 4);
|
||||
|
||||
C_MUL(scratch[0], Fout[m], *tw1);
|
||||
C_MUL(scratch[1], Fout[m2], *tw2);
|
||||
C_MUL(scratch[2], Fout[m3], *tw3);
|
||||
|
||||
C_SUB(scratch[5], *Fout, scratch[1]);
|
||||
C_ADDTO(*Fout, scratch[1]);
|
||||
C_ADD(scratch[3], scratch[0], scratch[2]);
|
||||
C_SUB(scratch[4], scratch[0], scratch[2]);
|
||||
C_SUB(Fout[m2], *Fout, scratch[3]);
|
||||
tw1 += fstride;
|
||||
tw2 += fstride * 2;
|
||||
tw3 += fstride * 3;
|
||||
C_ADDTO(*Fout, scratch[3]);
|
||||
|
||||
if(st->inverse) {
|
||||
Fout[m].r = scratch[5].r - scratch[4].i;
|
||||
Fout[m].i = scratch[5].i + scratch[4].r;
|
||||
Fout[m3].r = scratch[5].r + scratch[4].i;
|
||||
Fout[m3].i = scratch[5].i - scratch[4].r;
|
||||
} else {
|
||||
Fout[m].r = scratch[5].r + scratch[4].i;
|
||||
Fout[m].i = scratch[5].i - scratch[4].r;
|
||||
Fout[m3].r = scratch[5].r - scratch[4].i;
|
||||
Fout[m3].i = scratch[5].i + scratch[4].r;
|
||||
}
|
||||
++Fout;
|
||||
} while(--k);
|
||||
}
|
||||
|
||||
static void kf_bfly3(kiss_fft_cpx *Fout, const size_t fstride, const kiss_fft_cfg st, size_t m)
|
||||
{
|
||||
size_t k = m;
|
||||
const size_t m2 = 2 * m;
|
||||
kiss_fft_cpx *tw1 = st->twiddles;
|
||||
kiss_fft_cpx *tw2 = st->twiddles;
|
||||
kiss_fft_cpx scratch[5];
|
||||
kiss_fft_cpx epi3 = st->twiddles[fstride * m];
|
||||
|
||||
do {
|
||||
C_FIXDIV(*Fout, 3);
|
||||
C_FIXDIV(Fout[m], 3);
|
||||
C_FIXDIV(Fout[m2], 3);
|
||||
|
||||
C_MUL(scratch[1], Fout[m], *tw1);
|
||||
C_MUL(scratch[2], Fout[m2], *tw2);
|
||||
|
||||
C_ADD(scratch[3], scratch[1], scratch[2]);
|
||||
C_SUB(scratch[0], scratch[1], scratch[2]);
|
||||
tw1 += fstride;
|
||||
tw2 += fstride * 2;
|
||||
|
||||
Fout[m].r = Fout->r - HALF_OF(scratch[3].r);
|
||||
Fout[m].i = Fout->i - HALF_OF(scratch[3].i);
|
||||
|
||||
C_MULBYSCALAR(scratch[0], epi3.i);
|
||||
C_ADDTO(*Fout, scratch[3]);
|
||||
|
||||
Fout[m2].r = Fout[m].r + scratch[0].i;
|
||||
Fout[m2].i = Fout[m].i - scratch[0].r;
|
||||
Fout[m].r -= scratch[0].i;
|
||||
Fout[m].i += scratch[0].r;
|
||||
++Fout;
|
||||
} while(--k);
|
||||
}
|
||||
|
||||
static void kf_bfly5(kiss_fft_cpx *Fout, const size_t fstride, const kiss_fft_cfg st, int m)
|
||||
{
|
||||
kiss_fft_cpx *Fout0 = Fout;
|
||||
kiss_fft_cpx *Fout1 = Fout0 + m;
|
||||
kiss_fft_cpx *Fout2 = Fout0 + 2 * m;
|
||||
kiss_fft_cpx *Fout3 = Fout0 + 3 * m;
|
||||
kiss_fft_cpx *Fout4 = Fout0 + 4 * m;
|
||||
kiss_fft_cpx scratch[13];
|
||||
kiss_fft_cpx *tw = st->twiddles;
|
||||
kiss_fft_cpx ya = st->twiddles[fstride * m];
|
||||
kiss_fft_cpx yb = st->twiddles[fstride * 2 * m];
|
||||
int u;
|
||||
|
||||
for(u = 0; u < m; ++u) {
|
||||
C_FIXDIV(*Fout0, 5);
|
||||
C_FIXDIV(*Fout1, 5);
|
||||
C_FIXDIV(*Fout2, 5);
|
||||
C_FIXDIV(*Fout3, 5);
|
||||
C_FIXDIV(*Fout4, 5);
|
||||
scratch[0] = *Fout0;
|
||||
|
||||
C_MUL(scratch[1], *Fout1, tw[u * fstride]);
|
||||
C_MUL(scratch[2], *Fout2, tw[2 * u * fstride]);
|
||||
C_MUL(scratch[3], *Fout3, tw[3 * u * fstride]);
|
||||
C_MUL(scratch[4], *Fout4, tw[4 * u * fstride]);
|
||||
|
||||
C_ADD(scratch[7], scratch[1], scratch[4]);
|
||||
C_SUB(scratch[10], scratch[1], scratch[4]);
|
||||
C_ADD(scratch[8], scratch[2], scratch[3]);
|
||||
C_SUB(scratch[9], scratch[2], scratch[3]);
|
||||
|
||||
Fout0->r += scratch[7].r + scratch[8].r;
|
||||
Fout0->i += scratch[7].i + scratch[8].i;
|
||||
|
||||
scratch[5].r = scratch[0].r + S_MUL(scratch[7].r, ya.r) + S_MUL(scratch[8].r, yb.r);
|
||||
scratch[5].i = scratch[0].i + S_MUL(scratch[7].i, ya.r) + S_MUL(scratch[8].i, yb.r);
|
||||
scratch[6].r = S_MUL(scratch[10].i, ya.i) + S_MUL(scratch[9].i, yb.i);
|
||||
scratch[6].i = -S_MUL(scratch[10].r, ya.i) - S_MUL(scratch[9].r, yb.i);
|
||||
|
||||
C_SUB(*Fout1, scratch[5], scratch[6]);
|
||||
C_ADD(*Fout4, scratch[5], scratch[6]);
|
||||
|
||||
scratch[11].r = scratch[0].r + S_MUL(scratch[7].r, yb.r) + S_MUL(scratch[8].r, ya.r);
|
||||
scratch[11].i = scratch[0].i + S_MUL(scratch[7].i, yb.r) + S_MUL(scratch[8].i, ya.r);
|
||||
scratch[12].r = -S_MUL(scratch[10].i, yb.i) + S_MUL(scratch[9].i, ya.i);
|
||||
scratch[12].i = S_MUL(scratch[10].r, yb.i) - S_MUL(scratch[9].r, ya.i);
|
||||
|
||||
C_ADD(*Fout2, scratch[11], scratch[12]);
|
||||
C_SUB(*Fout3, scratch[11], scratch[12]);
|
||||
|
||||
++Fout0;
|
||||
++Fout1;
|
||||
++Fout2;
|
||||
++Fout3;
|
||||
++Fout4;
|
||||
}
|
||||
}
|
||||
|
||||
static void kf_bfly_generic(kiss_fft_cpx *Fout, const size_t fstride, const kiss_fft_cfg st,
|
||||
int m, int p)
|
||||
{
|
||||
int u;
|
||||
kiss_fft_cpx *twiddles = st->twiddles;
|
||||
kiss_fft_cpx t;
|
||||
int Norig = st->nfft;
|
||||
kiss_fft_cpx *scratch = (kiss_fft_cpx *) KISS_FFT_TMP_ALLOC(sizeof(kiss_fft_cpx) * p);
|
||||
|
||||
if(scratch == NULL) {
|
||||
KISS_FFT_ERROR("Memory allocation failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
for(u = 0; u < m; ++u) {
|
||||
int k = u;
|
||||
int q1;
|
||||
for(q1 = 0; q1 < p; ++q1) {
|
||||
scratch[q1] = Fout[k];
|
||||
C_FIXDIV(scratch[q1], p);
|
||||
k += m;
|
||||
}
|
||||
|
||||
k = u;
|
||||
for(q1 = 0; q1 < p; ++q1) {
|
||||
int q;
|
||||
int twidx = 0;
|
||||
Fout[k] = scratch[0];
|
||||
for(q = 1; q < p; ++q) {
|
||||
twidx += (int) fstride * k;
|
||||
if(twidx >= Norig) {
|
||||
twidx -= Norig;
|
||||
}
|
||||
C_MUL(t, scratch[q], twiddles[twidx]);
|
||||
C_ADDTO(Fout[k], t);
|
||||
}
|
||||
k += m;
|
||||
}
|
||||
}
|
||||
KISS_FFT_TMP_FREE(scratch);
|
||||
}
|
||||
|
||||
static void kf_work(kiss_fft_cpx *Fout, const kiss_fft_cpx *f, const size_t fstride,
|
||||
int in_stride, int *factors, const kiss_fft_cfg st)
|
||||
{
|
||||
kiss_fft_cpx *Fout_beg = Fout;
|
||||
const int p = *factors++;
|
||||
const int m = *factors++;
|
||||
const kiss_fft_cpx *Fout_end = Fout + p * m;
|
||||
|
||||
if(m == 1) {
|
||||
do {
|
||||
*Fout = *f;
|
||||
f += fstride * in_stride;
|
||||
} while(++Fout != Fout_end);
|
||||
} else {
|
||||
do {
|
||||
kf_work(Fout, f, fstride * p, in_stride, factors, st);
|
||||
f += fstride * in_stride;
|
||||
} while((Fout += m) != Fout_end);
|
||||
}
|
||||
|
||||
Fout = Fout_beg;
|
||||
switch(p) {
|
||||
case 2: kf_bfly2(Fout, fstride, st, m); break;
|
||||
case 3: kf_bfly3(Fout, fstride, st, m); break;
|
||||
case 4: kf_bfly4(Fout, fstride, st, m); break;
|
||||
case 5: kf_bfly5(Fout, fstride, st, m); break;
|
||||
default: kf_bfly_generic(Fout, fstride, st, m, p); break;
|
||||
}
|
||||
}
|
||||
|
||||
static void kf_factor(int n, int *facbuf)
|
||||
{
|
||||
int p = 4;
|
||||
double floor_sqrt = floor(sqrt((double) n));
|
||||
|
||||
do {
|
||||
while(n % p) {
|
||||
switch(p) {
|
||||
case 4: p = 2; break;
|
||||
case 2: p = 3; break;
|
||||
default: p += 2; break;
|
||||
}
|
||||
if(p > floor_sqrt) {
|
||||
p = n;
|
||||
}
|
||||
}
|
||||
n /= p;
|
||||
*facbuf++ = p;
|
||||
*facbuf++ = n;
|
||||
} while(n > 1);
|
||||
}
|
||||
|
||||
kiss_fft_cfg kiss_fft_alloc(int nfft, int inverse_fft, void *mem, size_t *lenmem)
|
||||
{
|
||||
KISS_FFT_ALIGN_CHECK(mem)
|
||||
|
||||
kiss_fft_cfg st = NULL;
|
||||
size_t memneeded = KISS_FFT_ALIGN_SIZE_UP(sizeof(struct kiss_fft_state)
|
||||
+ sizeof(kiss_fft_cpx) * (nfft - 1));
|
||||
|
||||
if(lenmem == NULL) {
|
||||
st = (kiss_fft_cfg) KISS_FFT_MALLOC(memneeded);
|
||||
} else {
|
||||
if(mem != NULL && *lenmem >= memneeded) {
|
||||
st = (kiss_fft_cfg) mem;
|
||||
}
|
||||
*lenmem = memneeded;
|
||||
}
|
||||
|
||||
if(st) {
|
||||
int i;
|
||||
st->nfft = nfft;
|
||||
st->inverse = inverse_fft;
|
||||
for(i = 0; i < nfft; ++i) {
|
||||
const double pi = 3.141592653589793238462643383279502884197169399375105820974944;
|
||||
double phase = -2 * pi * i / nfft;
|
||||
if(st->inverse) {
|
||||
phase *= -1;
|
||||
}
|
||||
kf_cexp(st->twiddles + i, phase);
|
||||
}
|
||||
kf_factor(nfft, st->factors);
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
||||
void kiss_fft_stride(kiss_fft_cfg st, const kiss_fft_cpx *fin, kiss_fft_cpx *fout, int in_stride)
|
||||
{
|
||||
if(fin == fout) {
|
||||
kiss_fft_cpx *tmpbuf;
|
||||
if(fout == NULL) {
|
||||
KISS_FFT_ERROR("fout buffer NULL.");
|
||||
return;
|
||||
}
|
||||
|
||||
tmpbuf = (kiss_fft_cpx *) KISS_FFT_TMP_ALLOC(sizeof(kiss_fft_cpx) * st->nfft);
|
||||
if(tmpbuf == NULL) {
|
||||
KISS_FFT_ERROR("Memory allocation error.");
|
||||
return;
|
||||
}
|
||||
|
||||
kf_work(tmpbuf, fin, 1, in_stride, st->factors, st);
|
||||
memcpy(fout, tmpbuf, sizeof(kiss_fft_cpx) * st->nfft);
|
||||
KISS_FFT_TMP_FREE(tmpbuf);
|
||||
} else {
|
||||
kf_work(fout, fin, 1, in_stride, st->factors, st);
|
||||
}
|
||||
}
|
||||
|
||||
void kiss_fft(kiss_fft_cfg cfg, const kiss_fft_cpx *fin, kiss_fft_cpx *fout)
|
||||
{
|
||||
kiss_fft_stride(cfg, fin, fout, 1);
|
||||
}
|
||||
|
||||
void kiss_fft_cleanup(void)
|
||||
{
|
||||
}
|
||||
|
||||
int kiss_fft_next_fast_size(int n)
|
||||
{
|
||||
while(1) {
|
||||
int m = n;
|
||||
while((m % 2) == 0) {
|
||||
m /= 2;
|
||||
}
|
||||
while((m % 3) == 0) {
|
||||
m /= 3;
|
||||
}
|
||||
while((m % 5) == 0) {
|
||||
m /= 5;
|
||||
}
|
||||
if(m <= 1) {
|
||||
break;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
93
XYParser/third_party/kissfft/kiss_fft.h
vendored
Normal file
93
XYParser/third_party/kissfft/kiss_fft.h
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
* See COPYING file for more information.
|
||||
*/
|
||||
|
||||
#ifndef KISS_FFT_H
|
||||
#define KISS_FFT_H
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef KISS_FFT_SHARED
|
||||
#if defined(_WIN32)
|
||||
#if defined(KISS_FFT_BUILD)
|
||||
#define KISS_FFT_API __declspec(dllexport)
|
||||
#else
|
||||
#define KISS_FFT_API __declspec(dllimport)
|
||||
#endif
|
||||
#else
|
||||
#define KISS_FFT_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
#else
|
||||
#define KISS_FFT_API
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef USE_SIMD
|
||||
#include <xmmintrin.h>
|
||||
#define kiss_fft_scalar __m128
|
||||
#ifndef KISS_FFT_MALLOC
|
||||
#define KISS_FFT_MALLOC(nbytes) _mm_malloc(nbytes, 16)
|
||||
#define KISS_FFT_ALIGN_CHECK(ptr)
|
||||
#define KISS_FFT_ALIGN_SIZE_UP(size) ((size + 15UL) & ~0xFUL)
|
||||
#endif
|
||||
#ifndef KISS_FFT_FREE
|
||||
#define KISS_FFT_FREE _mm_free
|
||||
#endif
|
||||
#else
|
||||
#define KISS_FFT_ALIGN_CHECK(ptr)
|
||||
#define KISS_FFT_ALIGN_SIZE_UP(size) (size)
|
||||
#ifndef KISS_FFT_MALLOC
|
||||
#define KISS_FFT_MALLOC malloc
|
||||
#endif
|
||||
#ifndef KISS_FFT_FREE
|
||||
#define KISS_FFT_FREE free
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
#include <stdint.h>
|
||||
#if (FIXED_POINT == 32)
|
||||
#define kiss_fft_scalar int32_t
|
||||
#else
|
||||
#define kiss_fft_scalar int16_t
|
||||
#endif
|
||||
#else
|
||||
#ifndef kiss_fft_scalar
|
||||
#define kiss_fft_scalar double
|
||||
#endif
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
kiss_fft_scalar r;
|
||||
kiss_fft_scalar i;
|
||||
} kiss_fft_cpx;
|
||||
|
||||
typedef struct kiss_fft_state *kiss_fft_cfg;
|
||||
|
||||
kiss_fft_cfg KISS_FFT_API kiss_fft_alloc(int nfft, int inverse_fft, void *mem, size_t *lenmem);
|
||||
void KISS_FFT_API kiss_fft(kiss_fft_cfg cfg, const kiss_fft_cpx *fin, kiss_fft_cpx *fout);
|
||||
void KISS_FFT_API kiss_fft_stride(kiss_fft_cfg cfg, const kiss_fft_cpx *fin, kiss_fft_cpx *fout,
|
||||
int fin_stride);
|
||||
|
||||
#define kiss_fft_free KISS_FFT_FREE
|
||||
|
||||
void KISS_FFT_API kiss_fft_cleanup(void);
|
||||
int KISS_FFT_API kiss_fft_next_fast_size(int n);
|
||||
|
||||
#define kiss_fftr_next_fast_size_real(n) (kiss_fft_next_fast_size(((n) + 1) >> 1) << 1)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
34
XYParser/third_party/kissfft/kiss_fft_log.h
vendored
Normal file
34
XYParser/third_party/kissfft/kiss_fft_log.h
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
|
||||
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
* See COPYING file for more information.
|
||||
*/
|
||||
|
||||
#ifndef kiss_fft_log_h
|
||||
#define kiss_fft_log_h
|
||||
|
||||
#define ERROR 1
|
||||
#define WARNING 2
|
||||
#define INFO 3
|
||||
#define DEBUG 4
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
#define TOSTRING(x) STRINGIFY(x)
|
||||
|
||||
#if defined(NDEBUG)
|
||||
#define KISS_FFT_LOG_MSG(severity, ...) ((void)0)
|
||||
#else
|
||||
#define KISS_FFT_LOG_MSG(severity, ...) \
|
||||
fprintf(stderr, "[" #severity "] " __FILE__ ":" TOSTRING(__LINE__) " "); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fprintf(stderr, "\n")
|
||||
#endif
|
||||
|
||||
#define KISS_FFT_ERROR(...) KISS_FFT_LOG_MSG(ERROR, __VA_ARGS__)
|
||||
#define KISS_FFT_WARNING(...) KISS_FFT_LOG_MSG(WARNING, __VA_ARGS__)
|
||||
#define KISS_FFT_INFO(...) KISS_FFT_LOG_MSG(INFO, __VA_ARGS__)
|
||||
#define KISS_FFT_DEBUG(...) KISS_FFT_LOG_MSG(DEBUG, __VA_ARGS__)
|
||||
|
||||
#endif /* kiss_fft_log_h */
|
||||
159
XYParser/third_party/kissfft/kiss_fftr.c
vendored
Normal file
159
XYParser/third_party/kissfft/kiss_fftr.c
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
|
||||
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
* See COPYING file for more information.
|
||||
*/
|
||||
|
||||
#include "kiss_fftr.h"
|
||||
#include "_kiss_fft_guts.h"
|
||||
|
||||
struct kiss_fftr_state
|
||||
{
|
||||
kiss_fft_cfg substate;
|
||||
kiss_fft_cpx *tmpbuf;
|
||||
kiss_fft_cpx *super_twiddles;
|
||||
#ifdef USE_SIMD
|
||||
void *pad;
|
||||
#endif
|
||||
};
|
||||
|
||||
kiss_fftr_cfg kiss_fftr_alloc(int nfft, int inverse_fft, void *mem, size_t *lenmem)
|
||||
{
|
||||
KISS_FFT_ALIGN_CHECK(mem)
|
||||
|
||||
int i;
|
||||
kiss_fftr_cfg st = NULL;
|
||||
size_t subsize = 0;
|
||||
size_t memneeded;
|
||||
|
||||
if(nfft & 1) {
|
||||
KISS_FFT_ERROR("Real FFT optimization must be even.");
|
||||
return NULL;
|
||||
}
|
||||
nfft >>= 1;
|
||||
|
||||
kiss_fft_alloc(nfft, inverse_fft, NULL, &subsize);
|
||||
memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * (nfft * 3 / 2);
|
||||
|
||||
if(lenmem == NULL) {
|
||||
st = (kiss_fftr_cfg) KISS_FFT_MALLOC(memneeded);
|
||||
} else {
|
||||
if(*lenmem >= memneeded) {
|
||||
st = (kiss_fftr_cfg) mem;
|
||||
}
|
||||
*lenmem = memneeded;
|
||||
}
|
||||
if(!st) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
st->substate = (kiss_fft_cfg) (st + 1);
|
||||
st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize);
|
||||
st->super_twiddles = st->tmpbuf + nfft;
|
||||
kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize);
|
||||
|
||||
for(i = 0; i < nfft / 2; ++i) {
|
||||
double phase = -3.14159265358979323846264338327 * ((double) (i + 1) / nfft + .5);
|
||||
if(inverse_fft) {
|
||||
phase *= -1;
|
||||
}
|
||||
kf_cexp(st->super_twiddles + i, phase);
|
||||
}
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
void kiss_fftr(kiss_fftr_cfg st, const kiss_fft_scalar *timedata, kiss_fft_cpx *freqdata)
|
||||
{
|
||||
int k;
|
||||
int ncfft;
|
||||
kiss_fft_cpx fpnk;
|
||||
kiss_fft_cpx fpk;
|
||||
kiss_fft_cpx f1k;
|
||||
kiss_fft_cpx f2k;
|
||||
kiss_fft_cpx tw;
|
||||
kiss_fft_cpx tdc;
|
||||
|
||||
if(st->substate->inverse) {
|
||||
KISS_FFT_ERROR("kiss fft usage error: improper alloc");
|
||||
return;
|
||||
}
|
||||
|
||||
ncfft = st->substate->nfft;
|
||||
kiss_fft(st->substate, (const kiss_fft_cpx *) timedata, st->tmpbuf);
|
||||
|
||||
tdc.r = st->tmpbuf[0].r;
|
||||
tdc.i = st->tmpbuf[0].i;
|
||||
C_FIXDIV(tdc, 2);
|
||||
CHECK_OVERFLOW_OP(tdc.r, +, tdc.i);
|
||||
CHECK_OVERFLOW_OP(tdc.r, -, tdc.i);
|
||||
freqdata[0].r = tdc.r + tdc.i;
|
||||
freqdata[ncfft].r = tdc.r - tdc.i;
|
||||
#ifdef USE_SIMD
|
||||
freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0);
|
||||
#else
|
||||
freqdata[ncfft].i = freqdata[0].i = 0;
|
||||
#endif
|
||||
|
||||
for(k = 1; k <= ncfft / 2; ++k) {
|
||||
fpk = st->tmpbuf[k];
|
||||
fpnk.r = st->tmpbuf[ncfft - k].r;
|
||||
fpnk.i = -st->tmpbuf[ncfft - k].i;
|
||||
C_FIXDIV(fpk, 2);
|
||||
C_FIXDIV(fpnk, 2);
|
||||
|
||||
C_ADD(f1k, fpk, fpnk);
|
||||
C_SUB(f2k, fpk, fpnk);
|
||||
C_MUL(tw, f2k, st->super_twiddles[k - 1]);
|
||||
|
||||
freqdata[k].r = HALF_OF(f1k.r + tw.r);
|
||||
freqdata[k].i = HALF_OF(f1k.i + tw.i);
|
||||
freqdata[ncfft - k].r = HALF_OF(f1k.r - tw.r);
|
||||
freqdata[ncfft - k].i = HALF_OF(tw.i - f1k.i);
|
||||
}
|
||||
}
|
||||
|
||||
void kiss_fftri(kiss_fftr_cfg st, const kiss_fft_cpx *freqdata, kiss_fft_scalar *timedata)
|
||||
{
|
||||
int k;
|
||||
int ncfft;
|
||||
|
||||
if(st->substate->inverse == 0) {
|
||||
KISS_FFT_ERROR("kiss fft usage error: improper alloc");
|
||||
return;
|
||||
}
|
||||
|
||||
ncfft = st->substate->nfft;
|
||||
st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r;
|
||||
st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r;
|
||||
C_FIXDIV(st->tmpbuf[0], 2);
|
||||
|
||||
for(k = 1; k <= ncfft / 2; ++k) {
|
||||
kiss_fft_cpx fk;
|
||||
kiss_fft_cpx fnkc;
|
||||
kiss_fft_cpx fek;
|
||||
kiss_fft_cpx fok;
|
||||
kiss_fft_cpx tmp;
|
||||
|
||||
fk = freqdata[k];
|
||||
fnkc.r = freqdata[ncfft - k].r;
|
||||
fnkc.i = -freqdata[ncfft - k].i;
|
||||
C_FIXDIV(fk, 2);
|
||||
C_FIXDIV(fnkc, 2);
|
||||
|
||||
C_ADD(fek, fk, fnkc);
|
||||
C_SUB(tmp, fk, fnkc);
|
||||
C_MUL(fok, tmp, st->super_twiddles[k - 1]);
|
||||
C_ADD(st->tmpbuf[k], fek, fok);
|
||||
C_SUB(st->tmpbuf[ncfft - k], fek, fok);
|
||||
#ifdef USE_SIMD
|
||||
st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0);
|
||||
#else
|
||||
st->tmpbuf[ncfft - k].i *= -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
kiss_fft(st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata);
|
||||
}
|
||||
32
XYParser/third_party/kissfft/kiss_fftr.h
vendored
Normal file
32
XYParser/third_party/kissfft/kiss_fftr.h
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
|
||||
* This file is part of KISS FFT - https://github.com/mborgerding/kissfft
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
* See COPYING file for more information.
|
||||
*/
|
||||
|
||||
#ifndef KISS_FTR_H
|
||||
#define KISS_FTR_H
|
||||
|
||||
#include "kiss_fft.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct kiss_fftr_state *kiss_fftr_cfg;
|
||||
|
||||
kiss_fftr_cfg KISS_FFT_API kiss_fftr_alloc(int nfft, int inverse_fft, void *mem, size_t *lenmem);
|
||||
void KISS_FFT_API kiss_fftr(kiss_fftr_cfg cfg, const kiss_fft_scalar *timedata,
|
||||
kiss_fft_cpx *freqdata);
|
||||
void KISS_FFT_API kiss_fftri(kiss_fftr_cfg cfg, const kiss_fft_cpx *freqdata,
|
||||
kiss_fft_scalar *timedata);
|
||||
|
||||
#define kiss_fftr_free KISS_FFT_FREE
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
37
XYParser/third_party/kissfft/window_functions.h
vendored
Normal file
37
XYParser/third_party/kissfft/window_functions.h
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
inline void no_window_function(int window_len, double *wind)
|
||||
{
|
||||
for(int i = 0; i < window_len; i++) {
|
||||
wind[i] = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
inline void hanning_function(int window_len, double *wind)
|
||||
{
|
||||
for(int i = 0; i < window_len; i++) {
|
||||
wind[i] = 0.5 - 0.5 * std::cos(2.0 * M_PI * i / window_len);
|
||||
}
|
||||
}
|
||||
|
||||
inline void hamming_function(int window_len, double *wind)
|
||||
{
|
||||
for(int i = 0; i < window_len; i++) {
|
||||
wind[i] = 0.54 - 0.46 * std::cos(2.0 * M_PI * i / window_len);
|
||||
}
|
||||
}
|
||||
|
||||
inline void blackman_harris_function(int window_len, double *wind)
|
||||
{
|
||||
for(int i = 0; i < window_len; i++) {
|
||||
wind[i] = 0.355768 - 0.487396 * std::cos(2.0 * M_PI * i / window_len)
|
||||
+ 0.144232 * std::cos(4.0 * M_PI * i / window_len)
|
||||
- 0.012604 * std::cos(6.0 * M_PI * i / window_len);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user