diff --git a/Amuencha/SpiralDisplay.cpp b/Amuencha/SpiralDisplay.cpp new file mode 100644 index 0000000..a216183 --- /dev/null +++ b/Amuencha/SpiralDisplay.cpp @@ -0,0 +1,116 @@ +#include "SpiralDisplay.hpp" + +Amuencha::SpiralDisplay::SpiralDisplay() + : min_midi_note{24} + , max_midi_note{72} +{ + for (int i{0}; i < 12; i++) note_positions[i] = polar(.80f, half_pi - i * two_pi / 12); +} + +void Amuencha::SpiralDisplay::compute_frequencies() +{ + // Now the spiral + // Start with A440, but this could be parametrizable as well + const float fref = 440; + const float log2_fref = log2(fref); + const int aref = 69; // use the midi numbering scheme, because why not + float log2_fmin = (min_midi_note - aref) / 12. + log2_fref; + float log2_fmax = (max_midi_note - aref) / 12. + log2_fref; + int approx_pix_bin_width = 3; + // number of frequency bins is the number of pixels + // along the spiral path / approx_pix_bin_width + // According to mathworld, the correct formula for the path length + // from the origin involves sqrt and log computations. + // Here, we just want some approximate pixel count + // => use all circles for the approx + int num_octaves = (max_midi_note - min_midi_note + 11) / 12; + float approx_num_pix = 0.5 * half * pi * num_octaves; + int num_bins = (int)(approx_num_pix / approx_pix_bin_width); + // one more bound than number of bins + display_bins.resize(num_bins + 1); + bin_sizes.resize(num_bins); + spiral_positions.resize(num_bins + 1); + spiral_r_a.resize(num_bins + 1); + const float rmin = 0.1; + const float rmax = 0.9; + // The spiral and bounds are the same independently of how + // the log space is divided into notes (e.g. 12ET) + // Make it so c is on the y axis. Turn clockwise because people are + // used to it (e.g. wikipedia note circle) + const float theta_min = half_pi - two_pi * (min_midi_note % 12) / 12; + // wrap in anti-trigonometric direction + const float theta_max = theta_min - two_pi * (max_midi_note - min_midi_note) / 12; + + frequencies.resize(num_bins); + for (int b{0}; b < num_bins; ++b) + { + float bratio = (float)b / (num_bins - 1.); + frequencies[b] = exp2(log2_fmin + (log2_fmax - log2_fmin) * bratio); + bratio = (float)(b - 0.5) / (float)(num_bins - 1.); + display_bins[b] = exp2(log2_fmin + (log2_fmax - log2_fmin) * bratio); + spiral_r_a[b].r = rmin + (rmax - rmin) * bratio; + spiral_r_a[b].a = theta_min + (theta_max - theta_min) * bratio; + spiral_positions[b] = polar(spiral_r_a[b].r, spiral_r_a[b].a); + } + + // repeat one more time to avoid a second for loops + float bratio = (float)(num_bins - 0.5) / (float)(num_bins - 1.); + display_bins[num_bins] = exp2(log2_fmin + (log2_fmax - log2_fmin) * bratio); + spiral_r_a[num_bins].r = rmin + (rmax - rmin) * bratio; + spiral_r_a[num_bins].a = theta_min + (theta_max - theta_min) * bratio; + spiral_positions[num_bins] = polar(spiral_r_a[num_bins].r, spiral_r_a[num_bins].a); + + for (int b{0}; b < num_bins; ++b) + bin_sizes[b] = display_bins[b + 1] - display_bins[b]; + + for (int id{0}; id < num_ID; ++id) + { + display_spectrum[id].resize(num_bins); + fill(display_spectrum[id].begin(), display_spectrum[id].end(), 0.); + } +} + +void Amuencha::SpiralDisplay::power_handler(int ID, const std::vector &reassigned_frequencies, const std::vector &power_spectrum) +{ + fill(display_spectrum[ID].begin(), display_spectrum[ID].end(), 0.); + + // simple histogram-like sum, assuming power entries are normalized + int nidx = reassigned_frequencies.size(); + for (int idx{0}; idx < nidx; ++idx) + { + float rf = reassigned_frequencies[idx]; + int ri = idx; + // reassigned frequencies are never too far off the original + //if (rf>display_bins[idx] && rfdisplay_bins[ri+1]) + { + ++ri; + if (ri==nidx) break; + } + if (ri==nidx) continue; // ignore this frequency, it is above display max + + // Normalization: + // - for a given frequency, the sine/window size dependency was already + // handled in the frequency analyzer + // - but the result should not depend on how many frequencies are provided: + // increasing the resolution should not increase the power + // => we need a kind of density, not just the histogram-like sum of powers + // falling into each bin + // - consider the energy is coming from all the original bin size & sum + // - This way, using finer bins do not increase the total sum + display_spectrum[ID][ri] += power_spectrum[idx] * bin_sizes[idx]; + } + + // - Then, spread on the destination bin for getting uniform density + // measure independently of the target bin size + for (int idx{0}; idx < nidx; ++idx) display_spectrum[ID][idx] /= bin_sizes[idx]; +} + + diff --git a/Amuencha/SpiralDisplay.hpp b/Amuencha/SpiralDisplay.hpp index 3b6ca7c..985cf4a 100644 --- a/Amuencha/SpiralDisplay.hpp +++ b/Amuencha/SpiralDisplay.hpp @@ -14,42 +14,11 @@ using namespace std; struct SpiralDisplay { + SpiralDisplay(); + static consteval int width() { return 400; } static consteval int height() { return 400; } - // Cannot intialize note_positions array with consteval, as polar is not consterpx - // static consteval const array, 12> init_pos() - // { - // return [] (index_sequence) - // -> array, 12> - // { return { polar(0.9f, half_pi - I * two_pi/12) ... }; } - // (make_index_sequence<12>{}); - // } - - // static const constexpr array, 12> note_positions{inti_pos()}; - - array, 12> note_positions{}; - - SpiralDisplay() - { - for (int i{0}; i < 12; i++) note_positions[i] = polar(.80f, half_pi - i * two_pi / 12); - } - - static const constexpr string_view note_names[12] - {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; - - float half; - - float x(float x) const - { - return half + x * half; - } - - float y(float y) const - { - return half - y * half; - } - void paint(avnd::painter auto ctx) { half = height() * .5f; @@ -65,7 +34,90 @@ struct SpiralDisplay } ctx.stroke(); + + int num_octaves = (max_midi_note - min_midi_note + 11) / 12; + + for (int id{0}; id < num_ID; ++id) + { + ctx.move_to(x(spiral_positions[0].real()), y(spiral_positions[0].imag())); + + for (int b{0}; b < display_spectrum[id].size(); ++b) + { + float amplitude = 0.8 / num_octaves * min(1.f, display_spectrum[id][b] * gain); + //if (display_spectrum[id][b]>0) cout << display_spectrum[id][b] << endl; + // power normalised between 0 and 1 => 0.1 = spiral branch + float r = spiral_r_a[b].r + amplitude; + auto p = polar(r, spiral_r_a[b].a); + ctx.line_to(x(p.real()), y(p.imag())); + r = spiral_r_a[b + 1].r + amplitude; + p = polar(r, spiral_r_a[b + 1].a); + ctx.line_to(x(p.real()), y(p.imag())); + } + + for (int b{static_cast(spiral_positions.size()) - 1}; b >= 0; --b) + ctx.line_to(x(spiral_positions[b].real()), y(spiral_positions[b].imag())); + } } + +private: + static const constexpr string_view note_names[12] + {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; + + static const int num_ID = 2; + + // Cannot intialize note_positions array with consteval, as polar is not consterpx + // static consteval const array, 12> init_pos() + // { + // return [] (index_sequence) + // -> array, 12> + // { return { polar(0.9f, half_pi - I * two_pi/12) ... }; } + // (make_index_sequence<12>{}); + // } + + // static const constexpr array, 12> note_positions{inti_pos()}; + + array, 12> note_positions{}; + + // central frequencies (log space) + std::vector frequencies; + + // local copy for maintaining the display, adapted to the drawing bins + std::vector> display_spectrum; + + // bin low bounds, each bin consists of [f_b, f_b+1) + std::vector display_bins; + // duplicate info for faster processing = delta_f in each bin + std::vector bin_sizes; + + // xy position of that frequency bin bound on the spiral + std::vector> spiral_positions; + // same info, but r.exp(angle) + // avoid all the sqrt, cos and sin at each redraw + struct Radius_Angle {float r, a;}; + std::vector spiral_r_a; + + int min_midi_note, max_midi_note, visual_fading; + + float gain{1.f}, half; + + float x(float x) const + { + return half + x * half; + } + + float y(float y) const + { + return half - y * half; + } + + void compute_frequencies(); + + // // Callback when the power spectrum is available at the prescribed frequencies + // // The ID is that of the caller, setting the color of the display + void power_handler(int ID, + const std::vector& reassigned_frequencies, + const std::vector& power_spectrum); + }; } diff --git a/CMakeLists.txt b/CMakeLists.txt index 407f9b6..2331b05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ avnd_score_plugin_add( Amuencha/AmuenchaModel.cpp Amuencha/AmuenchaUi.hpp Amuencha/SpiralDisplay.hpp + Amuencha/SpiralDisplay.cpp TARGET amuencha MAIN_CLASS Analyser NAMESPACE Amuencha