EchoMap 2026-07-25 6d3977c
An experimental cross-platform digital signal processing application for sound-source localisation.
Loading...
Searching...
No Matches
echomap::SignalFactory Class Reference

Provides various convenience functions for constructing Signal objects in exotic ways. More...

#include <SignalFactory.hpp>

Public Member Functions

 SignalFactory ()
 Begin constructing a new Signal.
bool operator== (const SignalFactory &other) const
bool operator< (const SignalFactory &other) const
std::unique_ptr< Signaltake_signal () noexcept
const Signalobserve_signal () const noexcept
void emplace_sample (Signal::Sample::AmplitudeT amplitude) const
void emplace_sample (const Signal::Sample &sample) const
void emplace_sample (Signal::Sample::TimeT time, Signal::Sample::AmplitudeT amplitude) const
void emplace_sample_from_source (Signal::Sample::AmplitudeT amplitude) const
void emplace_sample_from_source (const Signal::Sample &sample) const
void emplace_sample_from_source (Signal::Sample::TimeT time, Signal::Sample::AmplitudeT amplitude) const
void set_signal_name (std::string_view name) const
void set_time_offset (Signal::Sample::TimeT time_offset) const noexcept
void set_sample_rate (std::size_t sample_rate) const noexcept
void set_source (const std::filesystem::path &path, std::size_t channel) const

Static Public Member Functions

static std::vector< std::unique_ptr< Signal > > load_wave_file (const char *file_path)
 Loads a WAV file from the file system.
static void load_wave_file (const char *file_path, const std::span< SignalFactory *const > channel_factories)
 Loads a WAV file from the file system.
static std::unique_ptr< Signaldownsample (const Signal &source, float downsample_factor, std::string_view name={})
 Downsamples an existing Signal instance across all channels to the given number of samples.

Static Private Member Functions

static void load_wave_file_into_channels (drwav &drwav_info, std::string_view file_path, std::span< Signal *const > signal_ptrs)
 Loads the time-series sampled data into the given Signal objects.
static std::unique_ptr< Signallttb_downsample (const Signal &source, size_t threshold, std::string_view name)
 Create a new Signal by downsampling the data points of an existing Signal to the given threshold.

Private Attributes

std::unique_ptr< Signaltarget
 The Signal being built by the factory.

Detailed Description

Provides various convenience functions for constructing Signal objects in exotic ways.

The SignalFactory is the unique source of a Signal: they may not be constructed in any other way.

Definition at line 29 of file SignalFactory.hpp.

Constructor & Destructor Documentation

◆ SignalFactory()

echomap::SignalFactory::SignalFactory ( )

Begin constructing a new Signal.

Definition at line 25 of file SignalFactory.cpp.

25 :
26 // Cannot use make_unique here since Signal c'tor is private, and SignalFactory is "just a friend".
27 target(std::unique_ptr<Signal>(new Signal()))
28{
29}
std::unique_ptr< Signal > target
The Signal being built by the factory.

Member Function Documentation

◆ downsample()

std::unique_ptr< Signal > echomap::SignalFactory::downsample ( const Signal & source,
float downsample_factor,
std::string_view name = {} )
staticnodiscard

Downsamples an existing Signal instance across all channels to the given number of samples.

Parameters
sourceThe existing Signal to downsample.
downsample_factorThe factor by which the number of samples should be reduced during downsampling.
nameOptional display name. If omitted, a sensible default based on the source signal and downsampling factor will be used.

Definition at line 140 of file SignalFactory.cpp.

145{
146 auto sample_count = static_cast<std::uint64_t>(static_cast<float>(source.get_sample_count()) / downsample_factor);
147 if (static_cast<float>(sample_count) < downsample_factor)
148 sample_count = static_cast<std::uint64_t>(downsample_factor);
149
150 auto downsampled = name.empty() ? lttb_downsample(
151 source,
152 sample_count,
153 std::format("{} ({}x downsampled)", source.get_name(), downsample_factor)
154 )
155 : lttb_downsample(source, sample_count, name);
156
158 "Created {} as {}x-LTTB variant of {} with {} samples.",
159 downsampled->get_name(),
160 downsample_factor,
161 source.get_name(),
162 downsampled->get_sample_count()
163 );
164
165 return downsampled;
166}
#define LOG_F_DEBUG(msg,...)
Conditionally logs a formatted debug-level message using echomap::Logger::log_f.
Definition Logger.hpp:94
static std::unique_ptr< Signal > lttb_downsample(const Signal &source, size_t threshold, std::string_view name)
Create a new Signal by downsampling the data points of an existing Signal to the given threshold.
T empty(T... args)
T format(T... args)

◆ emplace_sample() [1/3]

void echomap::SignalFactory::emplace_sample ( const Signal::Sample & sample) const

Definition at line 187 of file SignalFactory.cpp.

190{
191 target->emplace_sample(sample);
192}

◆ emplace_sample() [2/3]

void echomap::SignalFactory::emplace_sample ( Signal::Sample::AmplitudeT amplitude) const

Definition at line 180 of file SignalFactory.cpp.

183{
184 target->emplace_sample(amplitude);
185}

◆ emplace_sample() [3/3]

void echomap::SignalFactory::emplace_sample ( Signal::Sample::TimeT time,
Signal::Sample::AmplitudeT amplitude ) const

Definition at line 194 of file SignalFactory.cpp.

198{
199 target->emplace_sample(time, amplitude);
200}

◆ emplace_sample_from_source() [1/3]

void echomap::SignalFactory::emplace_sample_from_source ( const Signal::Sample & sample) const

Definition at line 209 of file SignalFactory.cpp.

212{
213 target->emplace_sample_from_source(sample);
214}

◆ emplace_sample_from_source() [2/3]

void echomap::SignalFactory::emplace_sample_from_source ( Signal::Sample::AmplitudeT amplitude) const

Definition at line 202 of file SignalFactory.cpp.

205{
206 target->emplace_sample_from_source(amplitude);
207}

◆ emplace_sample_from_source() [3/3]

void echomap::SignalFactory::emplace_sample_from_source ( Signal::Sample::TimeT time,
Signal::Sample::AmplitudeT amplitude ) const

Definition at line 216 of file SignalFactory.cpp.

220{
221 target->emplace_sample_from_source(time, amplitude);
222}

◆ load_wave_file() [1/2]

std::vector< std::unique_ptr< Signal > > echomap::SignalFactory::load_wave_file ( const char * file_path)
staticnodiscard

Loads a WAV file from the file system.

The Signal objects are constructed by this function, with default names, in the order of the channels in the wave file. That is, the first Signal will represent the first channel in the file, etc.

Parameters
file_pathThe location of the WAV on the local file system.
Exceptions
ConfigurationErrorThe WAV file could not be loaded.
Returns
Series of owned Signal objects, one for each channel.

Definition at line 63 of file SignalFactory.cpp.

66{
67 drwav drwav_info;
68 if (drwav_init_file(&drwav_info, file_path, nullptr) == 0u)
69 throw ConfigurationError("Cannot open WAV file at " + std::string(file_path));
70
71 const auto typed_path = std::filesystem::path(file_path);
72 std::vector<std::unique_ptr<Signal>> signals;
73 signals.resize(drwav_info.channels);
74
75 std::size_t channel_num = 1;
76 for (auto& channel : signals) {
77 const auto formatted_name = std::format("{}#{}", typed_path.stem().c_str(), channel_num);
78 channel = std::unique_ptr<Signal>(new Signal(formatted_name, Signal::Source(typed_path, channel_num)));
79 ++channel_num;
80 }
81
82 try {
83 std::vector<Signal*> signal_ptrs;
84 signal_ptrs.reserve(signals.size());
86 signals,
87 std::back_inserter(signal_ptrs),
88 [](const std::unique_ptr<Signal>& signal) -> Signal* {
89 return signal.get();
90 }
91 );
92
93 load_wave_file_into_channels(drwav_info, file_path, signal_ptrs);
94 } catch (const std::runtime_error&) {
95 drwav_uninit(&drwav_info);
96 throw;
97 }
98
99 drwav_uninit(&drwav_info);
100 return signals;
101}
T back_inserter(T... args)
static void load_wave_file_into_channels(drwav &drwav_info, std::string_view file_path, std::span< Signal *const > signal_ptrs)
Loads the time-series sampled data into the given Signal objects.
T reserve(T... args)
T resize(T... args)
T signal(T... args)
T transform(T... args)

◆ load_wave_file() [2/2]

void echomap::SignalFactory::load_wave_file ( const char * file_path,
const std::span< SignalFactory *const > channel_factories )
static

Loads a WAV file from the file system.

The Signal objects are not directly constructed by this function. As many constructed SignalFactory objects as there are channels in the wave file must be provided through the SignalFactory spanning range.

Parameters
file_pathThe location of the WAV on the local file system.
channel_factoriesDestination factories of the Signal channels. In particular, a list of pointers to SignalFactory, each of which is constructing the Signal to receive the channel at the corresponding index. For each entry at index idx:
  • If channels[idx] == nullptr, the wave file channel idx is ignored; or
  • If channels[idx] != nullptr, the wave file channel idx is written to the Signal being constructed by the SignalFactory channels[idx].
Exceptions
ConfigurationErrorThe WAV file could not be loaded.
Precondition
There are sufficient SignalFactory objects in the destination range to store channels in the wave file.

Definition at line 103 of file SignalFactory.cpp.

107{
108 drwav drwav_info;
109 if (drwav_init_file(&drwav_info, file_path, nullptr) == 0u)
110 throw ConfigurationError("Cannot open WAV file at " + std::string(file_path));
111
112 assert(drwav_info.channels >= channel_factories.size());
113
114 try {
115 /*
116 * For convenience of callers, this function takes a span of factories responsible for constructing the signals
117 * for each of the channel slots, rather than the signals themselves. But for portability and simplicity, our
118 * internal functions need the signals directly. Hence, we cheaply construct a span-compliant collection of the
119 * mutating signal pointers, accessible to us as private member variables.
120 */
121 std::vector<Signal*> channels;
122 channels.resize(drwav_info.channels, nullptr);
123
124 std::size_t channel_idx = 0;
125 for (const auto* const factory : channel_factories) {
126 if (factory != nullptr && factory->target != nullptr)
127 channels[channel_idx] = factory->target.get();
128 ++channel_idx;
129 }
130
131 load_wave_file_into_channels(drwav_info, file_path, channels);
132 } catch (const std::runtime_error&) {
133 drwav_uninit(&drwav_info);
134 throw;
135 }
136
137 drwav_uninit(&drwav_info);
138}
T size(T... args)

◆ load_wave_file_into_channels()

void echomap::SignalFactory::load_wave_file_into_channels ( drwav & drwav_info,
std::string_view file_path,
std::span< Signal *const > signal_ptrs )
staticprivate

Loads the time-series sampled data into the given Signal objects.

Parameters
drwav_infoThe initialised and loaded dr_wav control structure.
file_pathThe location of the WAV on the local file system.
signal_ptrsDestination of the Signal channels.
Precondition
There are sufficient Signal objects in the destination range to store channels in the wave file.
See also
load_wave_file(const char*, mutable_signal_range auto&&) for semantics of the destination channels parameter.

Definition at line 253 of file SignalFactory.cpp.

258{
259 assert(drwav_info.channels <= std::ranges::size(signal_ptrs));
260
261 for (auto* const channel : signal_ptrs)
262 if (channel != nullptr) {
263 channel->reserve_samples(drwav_info.totalPCMFrameCount);
264 channel->set_sample_rate(drwav_info.sampleRate);
265 }
266
267 /*
268 * Dr_WAV provides audio data as amplitudes uniformly interleaved across the channels. That is, for a stereo signal,
269 * data is provided in the pattern L0, R0, L1, R1, ..., L(N-1), R(N-1). We receive the interleaved data in chunks of
270 * a fixed size and iteratively de-interleave it into our AudioPoint channels until all frames from the source file
271 * have been consumed.
272 */
273 constexpr drwav_uint64 chunk_frame_count = 8192;
274 std::vector<float> interleaved(chunk_frame_count * drwav_info.channels);
275 drwav_uint64 remaining_frames = drwav_info.totalPCMFrameCount;
276
277 while (remaining_frames > 0) {
278 const auto frame_count = std::min(remaining_frames, chunk_frame_count);
279 if (drwav_read_pcm_frames_f32(&drwav_info, frame_count, interleaved.data()) != frame_count)
280 // We couldn't read the expected number of frames. drwav_init_file must've provided the wrong count.
281 throw ConfigurationError("Cannot read WAV file at " + std::string(file_path) + ". Is it corrupted?");
282
283 for (drwav_uint64 frame_idx = 0; frame_idx < frame_count; ++frame_idx)
284 for (drwav_uint16 channel_idx = 0; channel_idx < drwav_info.channels; ++channel_idx) {
285 if (auto* const destination = std::ranges::begin(signal_ptrs)[channel_idx]; destination != nullptr)
286 /*
287 * The audio data is uniformly spaced, so we can infer the time values by taking the current frame
288 * offset for the chunk (total frames - remaining frames) and adding the current frame index.
289 */
290 destination->emplace_sample_from_source(interleaved[frame_idx * drwav_info.channels + channel_idx]);
291 }
292
293 remaining_frames -= frame_count;
294 }
295
296 if (remaining_frames != 0)
297 throw ConfigurationError("Cannot read entire WAV file at " + std::string(file_path) + ". Is it corrupted?");
298
299 for (auto* const channel : signal_ptrs | std::views::filter([](auto ptr) { return ptr; })) {
300 // Assert that any signal being constructed by these means should have an extant FS source.
301 assert(channel->fs_source.has_value());
302 channel->fs_source->is_loaded = true;
304 "Loaded signal \"{}\" with {} samples at {} Hz, starting at {} s.",
305 channel->get_name(),
306 channel->get_sample_count(),
307 channel->get_sample_rate(),
308 channel->get_time_offset()
309 );
310 }
311}
T begin(T... args)
T min(T... args)

◆ lttb_downsample()

std::unique_ptr< Signal > echomap::SignalFactory::lttb_downsample ( const Signal & source,
size_t threshold,
std::string_view name )
staticnodiscardprivate

Create a new Signal by downsampling the data points of an existing Signal to the given threshold.

This helper uses the well-known Largest-Triangle Three-Buckets (LTTB) downsampling algorithm, described in detail by the Master's Thesis Downsampling Time Series for Visual Representation, Sveinn Steinarsson (2013).

In addition to the thesis, reference implementations in all major languages are available online. This function uses a specialised implementation for the AudioPoint structure: https://github.com/sveinn-steinarsson/flot-downsample.

Parameters
sourceThe original Signal to be downsampled.
thresholdThe number of samples in the downsampled data.
nameName of the downsampled Signal.
Returns
The downsampled Signal, detained in an owning container.
Postcondition
The number of samples in the returned signal matches the threshold parameter.

Definition at line 313 of file SignalFactory.cpp.

318{
319 const auto source_size = source.get_sample_count();
320
321 // We don't need to assert for the post-condition on these trivial cases.
322
323 if (threshold == 0 || source_size == 0)
324 // Base case: the user requested zero samples (empty signal), or there were no samples available in the source.
325 return std::unique_ptr<Signal>(new Signal(name));
326
327 if (threshold >= source_size)
328 // Base case: the destination wants more samples than are available. Just copy the source signal.
329 return std::make_unique<Signal>(source, name);
330
331 auto downsampled = std::unique_ptr<Signal>(new Signal(name));
332 downsampled->reserve_samples(threshold);
333
334 if (threshold == 1) {
335 // Base case: the user only wants one sample. We choose the first by convention.
336 downsampled->emplace_sample(source.get_time_at_index(0), source[0]);
337 return downsampled;
338 }
339
340 if (threshold == 2) {
341 // Base case: the user only wants two samples. We choose the first and last by necessity.
342 downsampled->emplace_sample(source.get_time_at_index(0), source[0]);
343 downsampled->emplace_sample(source.get_time_at_index(source_size - 1), source[source_size - 1]);
344 return downsampled;
345 }
346
347 const auto bucket_size =
348 static_cast<unsigned int>(static_cast<double>(source_size - 2) / static_cast<double>(threshold - 2));
349 std::size_t fixed_point_idx = 0;
350
351 // Always add the first point.
352 downsampled->emplace_sample(source.get_time_at_index(0), source[0]);
353
354 for (std::size_t dst_point_idx = 0; dst_point_idx < threshold - 2; ++dst_point_idx) {
355 Signal::Sample::TimeT average_time = 0.0f;
356 Signal::Sample::AmplitudeT average_amplitude = 0.0f;
357
358 // Calculate the point-average for the next bucket, containing our fixed point.
359
360 const auto average_range_start = static_cast<std::size_t>(std::floor((dst_point_idx + 1) * bucket_size)) + 1;
361 const auto average_range_end =
362 std::min(static_cast<std::uint64_t>(std::floor((dst_point_idx + 2) * bucket_size)) + 1, source_size);
363 const auto average_range_length = average_range_end - average_range_start;
364
365 for (auto range_idx = average_range_start; range_idx < average_range_end; ++range_idx) {
366 average_time += source.get_time_at_index(range_idx);
367 average_amplitude += source[range_idx];
368 }
369
370 average_time /= static_cast<Signal::Sample::TimeT>(average_range_length);
371 average_amplitude /= static_cast<Signal::Sample::AmplitudeT>(average_range_length);
372
373 // Store the sample data at the fixed point.
374 const auto fp_time = source.get_time_at_index(fixed_point_idx);
375 const auto fp_amplitude = source[fixed_point_idx];
376
377 // Get the range for the current bucket and compute triangle areas over the three buckets.
378 const auto range_lower = static_cast<std::size_t>(std::floor(dst_point_idx * bucket_size)) + 1;
379 const auto range_upper = std::min(
380 static_cast<std::uint64_t>(std::floor((dst_point_idx + 1) * bucket_size)) + 1,
381 source_size - 1
382 );
383
384 // (C++ note: we need to combine Sample::TimeT and Sample::AmplitudeT here, so float seems like a safe choice.)
385 auto max_area = std::numeric_limits<float>::lowest();
386 auto next_fixed_point_idx = range_lower;
387
388 // Calculate triangle area formed by the vertices in the adjacent buckets, tracking the maximum.
389 for (auto range_idx = range_lower; range_idx < range_upper; ++range_idx) {
390 const float area = std::abs(
391 (fp_time - average_time) * (source[range_idx] - fp_amplitude) -
392 (fp_time - source.get_time_at_index(range_idx)) * (average_amplitude - fp_amplitude)
393 );
394
395 if (area > max_area) {
396 max_area = area;
397 next_fixed_point_idx = range_idx;
398 }
399 }
400
401 /*
402 * Pick the point from the bucket to include in the downsampled data, and set the index as our next starting
403 * point.
404 */
405 downsampled->emplace_sample(source.get_time_at_index(next_fixed_point_idx), source[next_fixed_point_idx]);
406
407 fixed_point_idx = next_fixed_point_idx;
408 }
409
410 // Always add the last point.
411 downsampled->emplace_sample(source.get_time_at_index(source_size - 1), source[source_size - 1]);
412
413 assert(downsampled->get_sample_count() == threshold);
414 return downsampled;
415}
T floor(T... args)
T lowest(T... args)
T make_unique(T... args)
float AmplitudeT
Type for sample amplitudes.
Definition Signal.hpp:48
float TimeT
Type for sample times.
Definition Signal.hpp:47

◆ observe_signal()

const Signal & echomap::SignalFactory::observe_signal ( ) const
nodiscardnoexcept

Definition at line 175 of file SignalFactory.cpp.

176{
177 return *target;
178}

◆ operator<()

bool echomap::SignalFactory::operator< ( const SignalFactory & other) const
nodiscard

Definition at line 48 of file SignalFactory.cpp.

51{
52 if (target == nullptr || other.target == nullptr || !target->observe_source().has_value() ||
53 !other.target->observe_source().has_value())
54 return false;
55
56 // Safe to deference; attempt to compare by channel and then path.
57 const auto& us = *target->observe_source(); // NOLINT(*-identifier-length)
58 const auto& them = *target->observe_source();
59
60 return us < them;
61}

◆ operator==()

bool echomap::SignalFactory::operator== ( const SignalFactory & other) const
nodiscard

Definition at line 31 of file SignalFactory.cpp.

34{
35 if (target.get() == other.target.get())
36 return true;
37
38 if (target == nullptr)
39 return false; // We're null; the other isn't.
40
41 if (other.target == nullptr)
42 return false; // The other is null; we're not.
43
44 // Safe to dereference; compare by IDs.
45 return *target == *other.target;
46}

◆ set_sample_rate()

void echomap::SignalFactory::set_sample_rate ( std::size_t sample_rate) const
noexcept

Definition at line 238 of file SignalFactory.cpp.

241{
242 target->set_sample_rate(sample_rate);
243}

◆ set_signal_name()

void echomap::SignalFactory::set_signal_name ( std::string_view name) const

Definition at line 224 of file SignalFactory.cpp.

227{
228 target->set_name(name);
229}

◆ set_source()

void echomap::SignalFactory::set_source ( const std::filesystem::path & path,
std::size_t channel ) const

Definition at line 245 of file SignalFactory.cpp.

249{
250 target->set_source(path, channel);
251}

◆ set_time_offset()

void echomap::SignalFactory::set_time_offset ( Signal::Sample::TimeT time_offset) const
noexcept

Definition at line 231 of file SignalFactory.cpp.

234{
235 target->set_time_offset(time_offset);
236}

◆ take_signal()

std::unique_ptr< Signal > echomap::SignalFactory::take_signal ( )
nodiscardnoexcept

Definition at line 168 of file SignalFactory.cpp.

169{
170 auto signal = std::move(target);
171 target = std::unique_ptr<Signal>(new Signal()); // NOLINT(*-unhandled-exception-at-new)
172 return signal;
173}

Member Data Documentation

◆ target

std::unique_ptr<Signal> echomap::SignalFactory::target
private

The Signal being built by the factory.

Invariant
The target container always contains a valid Signal object, such that dereferencing is safe.

Definition at line 165 of file SignalFactory.hpp.


The documentation for this class was generated from the following files: