Compare commits

...

17 Commits

Author SHA1 Message Date
69e8f5e75b fix fft grad 2026-04-22 17:08:09 +03:00
45215c1529 add min max in the bin 2026-04-15 14:47:10 +03:00
ec1bd2f376 fix some code 2026-04-15 14:10:14 +03:00
39b62d6ccb fix fft size 2026-04-15 13:51:50 +03:00
3afccda759 fix adapter 2026-04-07 14:35:20 +03:00
83ae62ce19 remove init setup 2026-04-07 14:10:47 +03:00
8d860ce409 add init to hr::setup 2026-04-07 13:51:44 +03:00
a18cefb671 add size impl 2026-03-20 14:07:38 +03:00
b7f68f97c4 add min max to result 2026-03-20 13:39:50 +03:00
cab65a93ad remove size 2026-03-20 13:02:29 +03:00
f21a17b6c9 add fft plugin 2026-03-20 10:42:25 +03:00
f904e78792 add clear for fvec 2026-03-19 14:44:25 +03:00
901c71f4eb fix name??? 2026-03-18 16:26:34 +03:00
dce15d23bd fix magnitude 2026-03-18 15:59:49 +03:00
a49fc7d5e3 rebuid struct project 2026-03-18 15:46:48 +03:00
afe54548a0 fix size func 2026-03-16 16:32:29 +03:00
84701bcfde fix size 2026-03-16 14:48:10 +03:00
17 changed files with 292 additions and 79 deletions

24
bin/main.fft.cpp Normal file
View File

@@ -0,0 +1,24 @@
#include <hack/logger/logger.hpp>
#include "harmonica.hpp"
auto main() -> int
{
// setup создается для каждого файла свой
// т.к. при чтении данных из файла уже должен быть определен размер блока
// данных для чтения m_block_size; см. установки по умолчанию.
// Передается по ссылке и заполняется необходимыми данными
hr::setup setup;
setup.m_domain = hr::DOMAIN_PLUGIN::FREQUENSY;
setup.m_file = "/mnt/raid/projects/dsp/songs/base/MakSim: Знаешь ли ты?.mp3";
auto r = hr::run<hr::plugins::fft>(setup);
hack::log()("grad:", r.m_grad.size());
hack::log()("min:", r.m_min, "max:", r.m_max);
hack::log()("size:", r.m_size);
if (!r.empty())
{
for (auto& p : r.m_data)
hack::log()(p[10].m_values.size(), p[10].m_min, p[10].m_max);
}
}

View File

@@ -8,16 +8,17 @@ auto main() -> int
// данных для чтения m_block_size; см. установки по умолчанию.
// Передается по ссылке и заполняется необходимыми данными
hr::setup setup;
// setup.m_domain = hr::DOMAIN_PLUGIN::FREQUENSY;
setup.m_domain = hr::DOMAIN_PLUGIN::TIME;
setup.m_file = "./sin.wav";
auto r = hr::run<hr::plugins::raw_data>(setup);
hack::log()("size:", r.m_data.size());
hack::log()("grad:", r.m_grad);
hack::log()("min:", r.m_min, "max:", r.m_max);
hack::log()("size", r.m_size);
// if (!r.empty())
// {
// std::vector<float> s;
// for (auto p : r.m_data) s.push_back(p.m_value[0]);
// hack::log()(s);
// }
if (!r.empty())
{
for (auto& p : r.m_data)
hack::log()(p[0].m_value);
}
}

View File

@@ -1,6 +1,6 @@
executable(
meson.project_name(),
'main.cpp',
'main.fft.cpp',
dependencies : deps,
cpp_args: args,
include_directories : inc

View File

@@ -57,7 +57,7 @@ namespace hr
private:
void swap_buffer(fvec_t& in)
{
size_t i = 0;
std::size_t i = 0;
for (i = 0; i < m_end; ++i) m_data[i] = m_data_old[i];
for (i = 0; i < m_plugin.m_setup.m_step_size; ++i) m_data[m_end + i] = in[i];
for (i = 0; i < m_end; ++i) m_data_old[i] = m_data[i + m_plugin.m_setup.m_step_size];

View File

@@ -11,6 +11,7 @@
#include "plugins/raw_data/raw_data.hpp" // IWYU pragma: keep
#include "plugins/magnitude/magnitude.hpp" // IWYU pragma: keep
#include "plugins/fft/fft.hpp" // IWYU pragma: keep
namespace hr
{
@@ -23,36 +24,32 @@ namespace hr
template<typename Plugin>
inline result run(setup& setup)
{
// Инициализация структуры для libsndfile и открытие файла
SF_INFO sf_info;
SNDFILE* file = sf_open(setup.m_file.c_str(), SFM_READ, &sf_info);
if (!file)
auto deleter = [&](SNDFILE* f) { if (f) sf_close(f); };
std::unique_ptr<SNDFILE, decltype(deleter)> sf_file(sf_open(setup.m_file.c_str(), SFM_READ, &sf_info), deleter);
if (!sf_file)
{
// Обработка ошибки открытия файла
hack::exception ex;
hack::log().on_file();
hack::log().on_func();
hack::log().on_row();
ex.title("Error of open file");
ex.description(sf_strerror(file));
ex.description(sf_strerror(sf_file.get()));
ex.set("file", setup.m_file);
hack::error()(ex);
throw ex;
}
// Сохранение информации о файле в настройки
setup.m_sample_rate = sf_info.samplerate;
setup.m_frames = sf_info.frames;
setup.m_channels = sf_info.channels;
setup.check();
if (setup.m_channels == 0) throw std::runtime_error("Нет каналов в аудио файле");
// Инициализация переменных для чтения
std::size_t read = 0; // Количество обработанных кадров
fvec_t read_data(setup.m_channels * setup.m_step_size, .0); // Буфер для чтения (интерливированные данные)
fvec_t in(setup.m_step_size, .0); // Буфер для моно-данных
// Создание плагина и адаптера для обработки
Plugin pl { setup };
adapter ad { pl };
@@ -60,7 +57,7 @@ namespace hr
{
// Определение длины читаемого блока (защита от выхода за границы)
auto length = hack::math::min(setup.m_step_size, in.size());
auto read_samples = sf_read_float(file, read_data.data(), read_data.size());
auto read_samples = sf_read_float(sf_file.get(), read_data.data(), read_data.size());
uint_t read_length = read_samples / setup.m_channels; // Перевод в кадры
read_length = hack::math::min(length, read_length); // Ограничение длиной буфера
@@ -70,26 +67,23 @@ namespace hr
{
in[i] = 0.0;
// Суммирование всех каналов
for (int c = 0; c < setup.m_channels; ++c)
in[i] += read_data[setup.m_channels * i + c];
for (int c = 0; c < setup.m_channels; ++c) in[i] += read_data[setup.m_channels * i + c];
// Усреднение для получения моно-сигнала
in[i] /= static_cast<base_t>(setup.m_channels);
}
// Подготовка к следующей итерации
read = hack::math::min(length, static_cast<uint_t>(floorf(read_length + .5)));
read = hack::math::min(length, static_cast<uint_t>(std::floorf(read_length + .5)));
// Дополнение буфера нулями если считано неполный блок (конец файла)
if (in.size() > read) std::fill(in.begin() + read, in.end(), 0.0);
// Вычисление временной метки и обработка данных через адаптер
real_time timestamp = real_time::frame2rt(read, sf_info.samplerate);
real_time timestamp = real_time::frame2rt(read, setup.m_sample_rate);
ad.process(in, timestamp);
}
while (read == setup.m_step_size); // Продолжать пока читаются полные блоки
while (read == setup.m_step_size);
// Закрытие файла и возврат результата
sf_close(file);
return ad.get_result();
}
}

View File

@@ -16,6 +16,7 @@ headers = [
# plugins
'plugins/raw_data/raw_data.hpp',
'plugins/magnitude/magnitude.hpp',
'plugins/fft/fft.hpp',
'harmonica.hpp'
]
@@ -30,6 +31,7 @@ sources = [
# plugins
'plugins/raw_data/raw_data.cpp',
'plugins/magnitude/magnitude.cpp',
'plugins/fft/fft.cpp',
]
lib = library(

55
src/plugins/fft/fft.cpp Normal file
View File

@@ -0,0 +1,55 @@
#include "fft.hpp"
#include "utils/var.hpp"
#include "utils/math.hpp"
namespace hr::plugins
{
fft::fft(const setup& st) : plugin{ st }
{
m_setup.m_plugin_name = "FFT";
m_setup.m_plugin_description = "Вычисляет FFT и прокидывает данные дальше в вашу программу.";
GUARD_DOMAIN(FREQUENSY);
// Данные - амплитуда частот
m_result.init(1);
// заполняем градацию по частотам
// тут не нужно делить на 2. получаем = 513 исходя из базового m_setup
// т.к. реализация FFT (rdft) уже возвращает только уникальную часть спектра, а не полный симметричный массив из 1024 элементов.
auto step = m_setup.m_step_size + 1;
m_result.m_grad.reserve(step);
for (size_t i = 0; i < step; ++i)
m_result.m_grad.push_back(static_cast<float>(i) * m_setup.m_sample_rate / m_setup.m_block_size / 1'000);
}
void fft::process(fvec_t& base, real_time timestamp)
{
}
void fft::process(cvec_t& fft, fvec_t& base, real_time timestamp)
{
auto step = fft.size();
result::bit b;
b.m_name = "Amplitudes";
b.m_duration = timestamp;
b.m_values.reserve(step);
for (size_t i = 0; i < step; ++i)
{
// Конвертация в децибелы
auto v = fft.m_norm[i];
if (v > 0.000001f) v = 20.0f * log10(v);
else v = -120.0f; // Минимальное значение для логарифмической шкалы
b.m_values.push_back(v);
b.set_min_max(v);
m_result.set_min_max(v);
}
m_result.set_bit(0, b);
++m_result.m_size;
}
result fft::get_result()
{
return m_result;
}
}

21
src/plugins/fft/fft.hpp Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include "utils/workers/plugin.hpp"
namespace hr::plugins
{
class fft : public plugin
{
public:
fft(const setup& st);
virtual ~fft() = default;
private:
result m_result;
public:
void process(fvec_t& base, real_time timestamp) override;
void process(cvec_t& fft, fvec_t& base, real_time timestamp) override;
result get_result() override;
};
}

View File

@@ -1,9 +1,16 @@
#include "magnitude.hpp"
#include "utils/var.hpp"
namespace hr::plugins
{
magnitude::magnitude(const setup& st) : plugin{ st }
{
m_setup.m_plugin_name = "Magnitude";
m_setup.m_plugin_description = "Средняя амплитуда спектра (общая энергия сигнала).";
GUARD_DOMAIN(FREQUENSY);
m_result.init(1);
}
void magnitude::process(fvec_t& base, real_time timestamp)
@@ -13,15 +20,20 @@ namespace hr::plugins
void magnitude::process(cvec_t& fft, fvec_t& base, real_time timestamp)
{
std::size_t frames = fft.size();
base_t m = 0.f;
for (std::size_t i = 0; i < frames; ++i) m += fft.m_norm[i];
m /= frames;
base_t v = 0.f;
for (std::size_t i = 0; i < frames; ++i) v += fft.m_norm[i];
v /= frames;
m_result.m_max = hack::math::max(m_result.m_max, v);
m_result.m_min = hack::math::min(m_result.m_min, v);
result::bit b;
b.m_value.push_back(m);
b.m_value = v;
b.m_duration = timestamp;
m_result.set_bit(b);
++m_result.m_size;
m_result.set_bit(0, b);
m_result.set_min_max(v);
}
result magnitude::get_result()

View File

@@ -1,23 +1,33 @@
#include "raw_data.hpp"
#include "utils/var.hpp"
namespace hr::plugins
{
// Этот плагин ни чего не делает и предназначен при сохранении единственности интерфейса просто
// передавать сырые необработанные данные. Например дял отрисовки базового сигнала.
// передавать сырые необработанные данные. Например для отрисовки базового сигнала.
// Он не работает в частотной области
raw_data::raw_data(const setup& st) : plugin{ st }
{
if (st.m_domain != DOMAIN_PLUGIN::TIME)
hack::error()("Этот плагин работает только во временной области!");
m_setup.m_plugin_name = "Raw Data";
m_setup.m_plugin_description = "Ни чего не вычисляет, просто прокидывает сырые данные дальше в вашу программу, для сохранения единой концепции.";
GUARD_DOMAIN(TIME);
// Только 1 тип данных будет в этом плагине, а имеено
// сырые данные из файла
m_result.init(1);
}
void raw_data::process(fvec_t& base, real_time timestamp)
{
for (auto& v : base)
{
result::bit b;
b.m_value = base;
b.m_value = v;
b.m_duration = timestamp;
m_tmp.set_bit(b);
m_size += base.size();
m_result.set_bit(0, b);
m_result.set_min_max(v);
}
m_result.m_size += base.size();
}
void raw_data::process(cvec_t& fft, fvec_t& base, real_time timestamp)
@@ -26,23 +36,6 @@ namespace hr::plugins
result raw_data::get_result()
{
if (m_tmp.m_data.empty())
return m_result;
m_result.m_data.reserve(m_size);
std::size_t index = 0;
for (auto& t : m_tmp.m_data)
{
for (auto s : t.m_value)
{
result::bit b;
b.m_value.push_back(s);
b.m_duration = t.m_duration;
m_result.set_bit(b);
}
}
return m_result;
}
}

View File

@@ -12,8 +12,6 @@ namespace hr::plugins
private:
result m_result;
result m_tmp;
std::size_t m_size = 0;
public:
void process(fvec_t& base, real_time timestamp) override;

View File

@@ -41,6 +41,12 @@ namespace hr
m_data.reserve(size);
}
// очищает данные с сохранением выделенной под них памяти
void fvec_t::clear()
{
m_data.clear();
}
// Циклический сдвиг вектора: первая половина меняется местами со второй
// Пример: [1,2,3,4,5,6] -> [4,5,6,1,2,3]
// Для нечетных размеров: [1,2,3,4,5,6,7] -> [5,6,7,1,2,3,4]

View File

@@ -37,6 +37,7 @@ namespace hr
void resize(std::size_t new_size, const base_t el);
void reserve(std::size_t size);
void shift();
void clear();
private:
std::vector<base_t> m_data;

23
src/utils/math.hpp Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <type_traits>
#include <cmath>
namespace hr
{
template<typename T>
T log10(T value)
{
if constexpr (std::is_same_v<T, float>)
return std::log10f(value);
else if constexpr (std::is_same_v<T, double>)
return std::log10(value);
else if constexpr (std::is_same_v<T, long double>)
return std::log10l(value);
else
{
static_assert(std::is_same_v<T, float> || std::is_same_v<T, double> || std::is_same_v<T, long double>, "Unsupported type for log10");
return std::log10(value); // fallback
}
}
}

View File

@@ -1,3 +1,11 @@
#pragma once
namespace hr { }
#define GUARD_DOMAIN(x) if (m_setup.m_domain != DOMAIN_PLUGIN::x) \
{\
hack::error()("plugin name:", m_setup.m_plugin_name);\
hack::warn()("Этот плагин работает только в " #x " области!"); \
std::terminate();\
}
namespace hr {
}

View File

@@ -1,9 +1,10 @@
#pragma once
#include <vector>
#include "utils/real_time/real_time.hpp"
#include "utils/fvec/fvec.hpp"
#include <hack/math/math.hpp>
#include <hack/logger/logger.hpp>
#include "utils/real_time/real_time.hpp"
#include "utils/using.hpp"
namespace hr
{
@@ -11,29 +12,80 @@ namespace hr
{
struct bit
{
std::string m_name;
real_time m_duration;
fvec_t m_value;
// когда в расчете только одно значение. да может быть и нужно это использовать как
// массив с индексом 0 типа m_values[0], но как-то вот так.
// Потому как отрисовка графиков тот еще праздник...
// конечно в боевой задаче это можно и нужно оптимизировать, но в данном случае для вывода на экран и понимания процесса
// это можно опустить и использовать так как есть...
base_t m_value;
// когда у тебя получается на один бин большой массив данных, типа расчет fft (см. комент выше)
std::vector<base_t> m_values;
// максимальные и минимальные элементы в конкретном бине
// в основном нужны для графической реализации
// соответственно метод ниже в попощь
base_t m_max = std::numeric_limits<base_t>::min();
base_t m_min = std::numeric_limits<base_t>::max();
void set_min_max(base_t v)
{
m_max = hack::math::max(m_max, v);
m_min = hack::math::min(m_min, v);
}
};
void set_bit(bit& b)
void set_bit(std::size_t index, bit& b)
{
m_data.push_back(b);
try
{
m_data.at(index).push_back(b);
}
catch(std::exception& e)
{
hack::error()("Хрень какая-то с массивом данных");
hack::warn()(e.what());
}
}
bool empty() const
{
bool res = true;
try
{
if (!m_data.empty()) res = m_data.at(0).m_value.empty();
}
catch(std::exception& e)
{
hack::error()(e.what());
}
return res;
return m_data.empty();
}
std::vector<bit> m_data;
// инициализирует кол-во данных для расчета в плагине
// см. ниже
void init(std::size_t count_data)
{
for (std::size_t i = 0; i < count_data; ++i)
m_data.push_back(std::vector<bit>{});
}
// схема такая:
// Первый вектор - кол-во типов замеров, например 7 т.е можно выстроить 7 линий на графике
// если захотелось увидеть их
// Второй вектор - данные для каждой линии, т.е. именно сими биты
std::vector<std::vector<bit>> m_data;
// максимальные и минимальные элементы в принципе в данном расчете
// в основном нужны для графической реализации
// соответственно метод ниже в попощь
base_t m_max = std::numeric_limits<base_t>::min();
base_t m_min = std::numeric_limits<base_t>::max();
void set_min_max(base_t v)
{
m_max = hack::math::max(m_max, v);
m_min = hack::math::min(m_min, v);
}
// в данном случае вы сами решаете, что для вас значит размер
// это может быть размер всего массива m_data или размер массива данных хранящихся в каждом бине
// или этот же массив, но помноженный на размер m_data
std::size_t m_size = 0;
// иногда нужна градуировка одна и тажа для всех бинов
std::vector<base_t> m_grad;
};
}

View File

@@ -1,6 +1,9 @@
#pragma once
#include <filesystem>
#include <sndfile.h>
#include <hack/exception/exception.hpp>
#include <hack/logger/logger.hpp>
namespace hr
{
@@ -18,9 +21,29 @@ namespace hr
int m_channels;
std::filesystem::path m_file;
// Количество семплов, которые обрабатываются за один раз
std::size_t m_block_size = 1'024;
// На сколько семплов сдвигается окно при следующей обработке
std::size_t m_step_size = 512;
std::string m_plugin_name;
std::string m_plugin_description;
DOMAIN_PLUGIN m_domain = DOMAIN_PLUGIN::TIME;
void check()
{
if (m_channels == 0)
{
hack::exception ex;
hack::log().on_file();
hack::log().on_func();
hack::log().on_row();
ex.title("Нет каналов в аудиофайле");
ex.set("file", m_file);
hack::error()(ex);
throw ex;
}
}
};
}