Repository: keur/prettyeq Branch: master Commit: c6d300b9070d Files: 52 Total size: 102.5 KB Directory structure: gitextract__5e1dd8e/ ├── .gitignore ├── LICENSE.txt ├── README.md ├── build_tests.sh ├── equalizer/ │ ├── arena.c │ ├── arena.h │ ├── arena_test.c │ ├── equalizer.pro │ ├── fft.c │ ├── fft.h │ ├── fft_test.c │ ├── macro.h │ ├── pretty.c │ └── pretty.h ├── gui/ │ ├── collisionmanager.cpp │ ├── collisionmanager.h │ ├── curvepoint.cpp │ ├── curvepoint.h │ ├── eqhoverer.cpp │ ├── eqhoverer.h │ ├── filtercurve.cpp │ ├── filtercurve.h │ ├── frequencytick.cpp │ ├── frequencytick.h │ ├── frequencytickbuilder.cpp │ ├── frequencytickbuilder.h │ ├── gui.cpp │ ├── gui.h │ ├── gui.pro │ ├── gui.ui │ ├── highshelfcurve.cpp │ ├── highshelfcurve.h │ ├── lowshelfcurve.cpp │ ├── lowshelfcurve.h │ ├── macro.h │ ├── main.cpp │ ├── peakingcurve.cpp │ ├── peakingcurve.h │ ├── prettygraphicsscene.cpp │ ├── prettygraphicsscene.h │ ├── prettyshim.h │ ├── resources.qrc │ ├── ringbuffer.h │ ├── runguard.cpp │ ├── runguard.h │ ├── shelfcurve.cpp │ ├── shelfcurve.h │ ├── spectrumanalyzer.cpp │ ├── spectrumanalyzer.h │ ├── unixsignalhandler.cpp │ └── unixsignalhandler.h └── prettyeq.pro ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # C++ objects and libs *.slo *.lo *.o *.a *.la *.lai *.so *.so.* *.dll *.dylib # Qt-es object_script.*.Release object_script.*.Debug *_plugin_import.cpp /.qmake.cache /.qmake.stash *.pro.user *.pro.user.* *.qbs.user *.qbs.user.* *.moc moc_*.cpp moc_*.h qrc_*.cpp ui_*.h *.qmlc *.jsc Makefile* *build-* *.qm *.prl # Qt unit tests target_wrapper.* # QtCreator *.autosave # QtCreator Qml *.qmlproject.user *.qmlproject.user.* # QtCreator CMake CMakeLists.txt.user* # QtCreator 4.8< compilation database compile_commands.json # QtCreator local machine specific files for imported projects *creator.user* *_qmlcache.qrc /tags /TAGS /prettyeq /build /.vscode ================================================ FILE: LICENSE.txt ================================================ Copyright 2020 Kevin Kuehler Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ ## PrettyEQ prettyeq is a system-wide paramateric equalizer for pulseaudio. This software is in alpha. Use at your own discretion. ![prettyeq demo](https://i.fluffy.cc/0GFcjGmbrtCgnbRSjd4xjDcf7h6qNk4Q.gif) ### Building ``` mkdir build cd build qmake CONFIG+=release .. make -j4 ``` ### Usage When the program is executed all pulseaudio streams will be routed through the equalizer. With no filters activated prettyeq acts as a passthrough sink. Right now prettyeq only supports two-channel sound. ##### Filter types prettyeq has three filter types: * one **low shelf** filter mounted at 20Hz * one **high shelf** filter mounted at 20kHz * five **peakingEQ** filters that can move freely ##### Controls Click and drag points to boost and cut frequency bands. The dB gain range is ±12dB. Filter bandwidth and slope can be changed with the mousewheel. ##### Quitting If your desktop has a system tray, the close button will hide the GUI but the equalizer will still be in effect. There are context menus in the application and tray that have a "exit" option to quit the application. ================================================ FILE: build_tests.sh ================================================ #!/bin/sh mkdir -p build gcc -lm -ffast-math -march=skylake -fopenmp -O2 \ -o build/fft_test \ equalizer/fft.c equalizer/fft_test.c gcc -g -o build/arena_test \ equalizer/arena.c equalizer/arena_test.c ================================================ FILE: equalizer/arena.c ================================================ #include #include #include #include #include #include "arena.h" #define CHUNK_OFFSET(memory) ((void*)(memory)-(sizeof(chunk_t))) #define MEMORY_OFFSET(chunk) ((void*)(chunk)+(sizeof(chunk_t))) #define NEXT_CHUNK(chunk, size) (((void*)MEMORY_OFFSET((chunk)))+(size)) arena_t* arena_new(size_t num_chunks, size_t chunk_size) { assert(num_chunks > 0); assert(chunk_size > 0); arena_t *arena = calloc(sizeof(arena_t), 1); if (!arena) return NULL; size_t map_size = num_chunks * (chunk_size + sizeof(chunk_t)); void *mem = mmap( NULL, map_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (mem == MAP_FAILED) return NULL; chunk_t *chunk = mem; for (unsigned int i = 0; i < num_chunks - 1; i++) { chunk->next = NEXT_CHUNK(chunk, chunk_size); chunk = chunk->next; } chunk->next = NULL; arena->chunk_size = chunk_size; arena->map_size = map_size; arena->mem = mem; arena->avail_chunk = mem; return arena; } void* arena_alloc(arena_t *arena) { assert(arena); assert(arena->avail_chunk); void *p = MEMORY_OFFSET(arena->avail_chunk); assert(p); arena->avail_chunk = arena->avail_chunk->next; return p; } void arena_dealloc(arena_t *arena, void *mem) { assert(arena); assert(mem); chunk_t *chunk = CHUNK_OFFSET(mem); chunk->next = arena->avail_chunk; arena->avail_chunk = chunk; } void arena_destroy(arena_t **arena) { assert(arena); assert((*arena)->mem); munmap((*arena)->mem, (*arena)->map_size); free(*arena); *arena = NULL; } ================================================ FILE: equalizer/arena.h ================================================ #pragma once #include typedef struct chunk_t chunk_t; struct chunk_t { chunk_t *next; }; typedef struct arena_t { void *mem; size_t chunk_size; size_t map_size; chunk_t *avail_chunk; } arena_t; arena_t* arena_new(size_t num_chunks, size_t chunk_size); void* arena_alloc(arena_t *arena); void arena_dealloc(arena_t *arena, void *mem); void arena_destroy(arena_t **arena); ================================================ FILE: equalizer/arena_test.c ================================================ #include #include #include #include "arena.h" int main(int argc, char **argv) { arena_t *arena = arena_new(4, 5); assert(arena); assert(arena->avail_chunk != NULL); char *a = arena_alloc(arena); memset(a, 'a', 4); a[4] = '\0'; char *b = arena_alloc(arena); memset(b, 'b', 4); b[4] = '\0'; char *c = arena_alloc(arena); memset(c, 'c', 4); c[4] = '\0'; char *d = arena_alloc(arena); memset(d, 'd', 4); d[4] = '\0'; assert(arena->avail_chunk == NULL); assert(memcmp(a, "aaaa", 5) == 0); assert(memcmp(b, "bbbb", 5) == 0); assert(memcmp(c, "cccc", 5) == 0); assert(memcmp(d, "dddd", 5) == 0); arena_dealloc(arena, c); arena_dealloc(arena, a); char *e = arena_alloc(arena); memset(e, 'e', 4); e[4] = '\0'; char *f = arena_alloc(arena); memset(f, 'f', 4); f[4] = '\0'; assert(memcmp(e, "eeee", 5) == 0); assert(memcmp(a, "eeee", 5) == 0); assert(memcmp(f, "ffff", 5) == 0); assert(memcmp(c, "ffff", 5) == 0); arena_dealloc(arena, b); arena_dealloc(arena, e); arena_dealloc(arena, f); arena_dealloc(arena, d); assert((void *) arena->avail_chunk == (void *) d - sizeof(chunk_t)); return 0; } ================================================ FILE: equalizer/equalizer.pro ================================================ TEMPLATE = lib QMAKE_CFLAGS += -ffast-math -fopenmp QMAKE_CFLAGS_WARN_ON += -Wno-unused-parameter CONFIG += staticlib HEADERS = \ arena.h \ fft.h \ macro.h \ pretty.h pretty.h SOURCES = \ arena.c \ fft.c \ pretty.c ================================================ FILE: equalizer/fft.c ================================================ #include #include #include #include #include #include #include #include #include #include #include "macro.h" #include "fft.h" static bool initialized = false; alignas(32) static complex float omega_vec_log2[MAX_SAMPLES][K]; static inline unsigned int reverse_bits(unsigned int n, unsigned int num_bits) { int i, j; register unsigned int res = 0; i = 0; j = num_bits; while (i <= j) { unsigned int lower_mask = 1 << i; unsigned int upper_mask = (1 << num_bits) >> i; unsigned int shift = j - i; res |= ((n >> shift) & lower_mask) | ((n << shift) & upper_mask); i++; j--; } return res; } static inline unsigned int get_msb(unsigned int v) { return 31 - __builtin_clz(v); } void fft_init() { for (unsigned int nl = 0; nl < MAX_SAMPLES_LOG_2; nl++) { unsigned int n = (1u << nl); const double mul_div_n = (-2.0 * M_PI) / n; for (unsigned int k = 0; k < K; k++) { complex float *res = &omega_vec_log2[nl][k]; complex double theta = mul_div_n * k; _real_(*res) = cosf(theta); _imag_(*res) = sinf(theta); } } initialized = true; } void fft_run( const float *input_data, complex float *output_data, unsigned int N, unsigned int channels) { assert(initialized); { unsigned int msb; for (unsigned int i = 0, j = 0; i < N; j++, i+=channels) /* Taking just the left channel for now... */ output_data[j] = input_data[i]; N = N / channels; assert(N <= MAX_SAMPLES); msb = get_msb(N); if (_unlikely_((N & (N-1)))) { /* Pad out so FFT is a power of 2. */ msb = msb + 1; unsigned int new_N = 1 << msb; for (unsigned int i = N; i < new_N; i++) output_data[i] = 0.0f; N = new_N; } /* Reverse the input array. */ unsigned int hi_bit = msb - 1; for (unsigned int i = 0; i < N; i++) { unsigned int r = reverse_bits(i, hi_bit); if (i < r) SWAP(output_data[i], output_data[r]); } } { /* Simple radix-2 DIT FFT */ unsigned int wingspan = 1; unsigned int nl = 1; while (wingspan < N) { for (unsigned int j = 0; j < N; j+=wingspan*2) { for (unsigned int k = 0; k < wingspan; k++) { complex float omega = omega_vec_log2[nl][k]; complex float a0 = output_data[k+j]; complex float a1 = output_data[k+j+wingspan]; output_data[k+j] = a0 + omega*a1; output_data[k+j+wingspan] = a0 - omega*a1; } } nl++; wingspan *= 2; } } } ================================================ FILE: equalizer/fft.h ================================================ #pragma once #include #include #define MAX_SAMPLES_LOG_2 12 #define MAX_SAMPLES (1 << MAX_SAMPLES_LOG_2) #define K (MAX_SAMPLES / 2) #define FFT_BUCKET_WIDTH(NUM_SAMPLES) (44100/(NUM_SAMPLES)) #define FFT_SAMPLE_TO_FREQ(NUM_SAMPLES, SAMPLE_INDEX) (44100*(SAMPLE_INDEX)/(NUM_SAMPLES)) #define FFT_FREQ_TO_SAMPLE(NUM_SAMPLES, FREQ) ((int)roundf((FREQ)*(NUM_SAMPLES)/44100)) #define FFT_PSD(SAMPLE) ((float)crealf(cabsf((SAMPLE)))) void fft_init(); void fft_run( const float *input_data, complex float *output_data, unsigned int N, unsigned int channels); ================================================ FILE: equalizer/fft_test.c ================================================ #include "fft.h" #include #include #include #include static const float single_channel_5000HZ_data[] = { -0.120544, 0.222534, 0.457336, 0.469727, 0.253632, -0.085846, -0.383575, -0.494568, -0.365295, -0.058197, 0.277222, 0.477783, 0.445923, 0.197205, -0.147461, -0.420441, -0.488922, -0.319580, 0.005157, 0.327362, 0.490356, 0.414825, 0.137512, -0.206696, -0.450378, -0.475037, -0.268646, 0.068390, 0.372162, 0.494537, 0.376892, 0.075592, -0.262482, -0.472900, -0.453339, -0.213257, 0.130524, 0.410797, 0.491302, 0.332794, 0.012451, -0.313995, -0.487701, -0.424225, -0.154419, 0.190491, 0.442719, 0.479614, 0.283234, -0.050934, -0.360352, -0.494476, -0.388123, -0.093018, 0.247345, 0.467377, 0.460083, 0.229004, -0.113464, -0.400787, -0.493164, -0.345673, -0.030090, 0.300110, 0.484344, 0.432983, 0.171021, -0.174133, -0.434601, -0.483704, -0.297546, 0.033325, 0.347992, 0.493378, 0.398773, 0.110199, -0.231964, -0.461334, -0.466339, -0.244507, 0.096191, 0.390137, 0.494293, 0.358032, 0.047607, -0.285980, -0.480469, -0.441284, -0.187500, 0.157501, 0.425873, 0.487091, 0.311401, -0.015778, -0.335266, -0.491730, -0.409027, -0.127380, 0.216217, 0.454590, 0.471893, 0.259644, -0.078888, -0.379059, -0.494568, -0.370026, -0.065186, 0.271332, 0.475891, 0.448944, 0.203644, -0.140717, -0.416687, -0.489960, -0.324951, -0.001892, 0.322052, 0.489349, 0.418640, 0.144287, -0.200256, -0.447388, -0.476959, -0.274536, 0.061401, 0.367462, 0.494537, 0.381439, 0.082550, -0.256500, -0.470795, -0.456116, -0.219604, 0.123688, 0.406830, 0.492065, 0.337982, 0.019470, -0.308502, -0.486450, -0.427826, -0.161102, 0.183960, 0.439545, 0.481293, 0.288971, -0.043915, -0.355469, -0.494141, -0.392487, -0.099945, 0.241211, 0.465027, 0.462616, 0.235229, -0.106598, -0.396576, -0.493683, -0.350677, -0.037109, 0.294495, 0.482849, 0.436340, 0.177612, -0.167542, -0.431213, -0.485168, -0.303131, 0.026276, 0.342926, 0.492767, 0.402924, 0.117065, -0.225708, -0.458740, -0.468628, -0.250610, 0.089264, 0.385742, 0.494537, 0.362854, 0.054626, -0.280182, -0.478729, -0.444458, -0.194000, 0.150787, 0.422241, 0.488281, 0.316864, -0.008728, -0.330048, -0.490845, -0.412933, -0.134186, 0.209839, 0.451782, 0.473969, 0.265625, -0.071930, -0.374512, -0.494568, -0.374664, -0.072174, 0.265411, 0.473877, 0.451874, 0.210022, -0.133942, -0.412811, -0.490906, -0.330231, -0.008972, 0.316650, 0.488251, 0.422363, 0.151031, -0.193787, -0.444336, -0.478790, -0.280365, 0.054382, 0.362671, 0.494537, 0.385895, 0.089508, -0.250427, -0.468567, -0.458801, -0.225922, 0.116852, 0.402802, 0.492798, 0.343109, 0.026520, -0.302948, -0.485107, -0.431305, -0.167755, 0.177399, 0.436249, 0.482910, 0.294678, -0.036896, -0.350525, -0.493683, -0.396729, -0.106842, 0.235016, 0.462555, 0.465088, 0.241425, -0.099701, -0.392303, -0.494141, -0.355621, -0.044159, 0.288788, 0.481262, 0.439636, 0.184174, -0.160858, -0.427704, -0.486511, -0.308685, 0.019257, 0.337830, 0.492065, 0.406982, 0.123932, -0.219421, -0.456024, -0.470856, -0.256683, 0.082306, 0.381287, 0.494537, 0.367615, 0.061646, -0.274323, -0.476868, -0.447479, -0.200470, 0.144043, 0.418518, 0.489380, 0.322235, -0.001678, -0.324768, -0.489899, -0.416779, -0.140961, 0.203400, 0.448822, 0.475952, 0.271545, -0.064941, -0.369873, -0.494568, -0.379242, -0.079132, 0.259430, 0.471832, 0.454712, 0.216400, -0.127167, -0.408875, -0.491760, -0.335449, -0.016022, 0.311218, 0.487061, 0.425995, 0.157715, -0.187286, -0.441193, -0.480530, -0.286163, 0.047363, 0.357849, 0.494293, 0.390289, 0.096436, -0.244324, -0.466248, -0.461395, -0.232178, 0.109985, 0.398651, 0.493378, 0.348145, 0.033569, -0.297333, -0.483673, -0.434723, -0.174347, 0.170807, 0.432861, 0.484406, 0.300323, -0.029846, -0.345520, -0.493134, -0.400909, -0.113708, 0.228790, 0.459991, 0.467438, 0.247528, -0.092773, -0.388000, -0.494507, -0.360504, -0.051178, 0.283020, 0.479584, 0.442841, 0.190704, -0.154175, -0.424103, -0.487762, -0.314178, 0.012207, 0.332611, 0.491241, 0.410950, 0.130737, -0.213074, -0.453247, -0.472992, -0.262695, 0.075348, 0.376740, 0.494537, 0.372314, 0.068634, -0.268433, -0.474945, -0.450470, -0.206909, 0.137299, 0.414703, 0.490387, 0.327545, 0.005402, -0.319397, -0.488861, -0.420563, -0.147705, 0.196991, 0.445862, 0.477844, 0.277435, -0.057953, -0.365143, -0.494568, -0.383728, -0.086090, 0.253418, 0.469635, 0.457428, 0.222717, -0.120331, -0.404877, -0.492493, -0.340607, -0.023071, 0.305695, 0.485748, 0.429535, 0.164398, -0.180725, -0.437958, -0.482178, -0.291870, 0.040344, 0.352966, 0.493866, 0.394562, 0.103333, -0.238159, -0.463837, -0.463928, -0.238373, 0.103088, 0.394440, 0.493896, 0.353119, 0.040588, -0.291687, -0.482117, -0.438049, -0.180939, 0.164154, 0.429413, 0.485809, 0.305878, -0.022797, -0.340424, -0.492462, -0.404999, -0.120544, 0.222504, 0.457336, 0.469727, 0.253632, -0.085846, -0.383575, -0.494568, -0.365295, -0.058197, 0.277222, 0.477783, 0.445923, 0.197205, -0.147461, -0.420410, -0.488922, -0.319580, 0.005127, 0.327362, 0.490356, 0.414825, 0.137512, -0.206696, -0.450348, -0.475037, -0.268646, 0.068390, 0.372131, 0.494537, 0.376892, 0.075592, -0.262482, -0.472900, -0.453339, -0.213287, 0.130524, 0.410797, 0.491302, 0.332794, 0.012451, -0.313995, -0.487701, -0.424225, -0.154419, 0.190491, 0.442719, 0.479645, 0.283234, -0.050934, -0.360352, -0.494476, -0.388153, -0.093018, 0.247345, 0.467377, 0.460083, 0.229004, -0.113464, -0.400757, -0.493164, -0.345673, -0.030090, 0.300110, 0.484344, 0.432983, 0.171021, -0.174133, -0.434601, -0.483704, -0.297546, }; static const float dual_channel_micro[] = { -0.120544, -0.120544, 0.222534, 0.222534, }; typedef struct TestCases { int frequency; float expected_psd; } TestCases; TestCases cases[] = { { .frequency = 100, .expected_psd = 0.09595596265773097, }, { .frequency = 200, .expected_psd = 0.09558897341820413, }, { .frequency = 1000, .expected_psd = 0.10730647827342488, }, { .frequency = 2000, .expected_psd = 0.14449811458513645, }, { .frequency = 5000, .expected_psd = 126.14145371375102, }, { .frequency = 10000, .expected_psd = 0.14292403328440603, }, }; static inline bool float_approx_equal(float f1, float f2) { return fabsf(f1 - f2) > 0.00001 ? false : true; } void test_init() { clock_t start, end; double cpu_time; start = clock(); fft_init(); end = clock(); fprintf(stdout, "[FFT Init %d samples] time: %lf\n", MAX_SAMPLES, (double) (end - start) / CLOCKS_PER_SEC); } void test_single_channel() { clock_t start, end; double cpu_time; complex float output[MAX_SAMPLES]; unsigned int N = sizeof(single_channel_5000HZ_data) / sizeof(single_channel_5000HZ_data[0]); start = clock(); fft_run(single_channel_5000HZ_data, output, N, 1); end = clock(); for (int i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { int sample_index = FFT_FREQ_TO_SAMPLE(N, cases[i].frequency); float psd = FFT_PSD(output[sample_index]); assert(float_approx_equal(cases[i].expected_psd, FFT_PSD(output[sample_index]))); } fprintf(stdout, "[FFT Run %d samples] time: %lf\n", N, (double) (end - start) / CLOCKS_PER_SEC); } void test_dual_channel_micro() { clock_t start, end; double cpu_time; complex float output[MAX_SAMPLES]; unsigned int N = sizeof(dual_channel_micro) / sizeof(dual_channel_micro[0]); start = clock(); fft_run(dual_channel_micro, output, N, 2); end = clock(); fprintf(stdout, "[FFT Run %d samples] time: %lf\n", N, (double) (end - start) / CLOCKS_PER_SEC); } int main(int argc, char **argv) { test_init(); test_single_channel(); test_dual_channel_micro(); return 0; } ================================================ FILE: equalizer/macro.h ================================================ #pragma once #define PRETTY_EXPORT __attribute__ ((visibility ("default"))) #define MAY_ALIAS __attribute__((__may_alias__)) #define SWAP(a, b) \ ({ \ typeof(a) _tmp_ = b; \ b = a; \ a = _tmp_; \ }) #define BOX_USERDATA(var) ((void *) ((uint64_t) (var))) #define UNBOX_USERDATA(type, var) ((type) ((uint64_t) (var))) #define _likely_(x) __builtin_expect(!!(x), 1) #define _unlikely_(x) __builtin_expect(!!(x), 0) #define _real_ __real__ #define _imag_ __imag__ ================================================ FILE: equalizer/pretty.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include "arena.h" #include "fft.h" #include "pretty.h" #define NUM_FILTERS 7 #define LATENCY_MAX_MS 30 //============================================================================= static pa_mainloop_api *mainloop_api = NULL; static pa_context *context = NULL; static uint32_t prettyeq_module_index = PA_INVALID_INDEX; static pa_stream *read_stream = NULL, *write_stream = NULL; static pa_threaded_mainloop *m = NULL; static const pa_sample_spec sample_spec = { // https://freedesktop.org/software/pulseaudio/doxygen/sample_8h.html .format = PA_SAMPLE_FLOAT32LE, .rate = 44100, .channels = 2, }; //============================================================================= static _Atomic bool bypass; static arena_t *arena = NULL; struct _AudioFFT { pthread_spinlock_t lock; complex float data[MAX_SAMPLES]; unsigned int N; }; static AudioFFT audio_fft = {0}; typedef struct FilterParams { float a[3]; float b[3]; } FilterParams; struct _PrettyFilter { /* Filter parameters used in the audio loop. */ float xwin[3]; float ywin[2]; /* Temporary parameters for compare-exchange. */ _Atomic (FilterParams*) params; FilterParams *storage; }; static PrettyFilter filters[NUM_FILTERS]; static int user_enabled_filters = 0; //============================================================================= static void quit(int ret) { fprintf(stderr, "debug: quit(%d) called!", ret); if (prettyeq_module_index != PA_INVALID_INDEX) pa_operation_unref(pa_context_unload_module(context, prettyeq_module_index, NULL, NULL)); mainloop_api->quit(mainloop_api, ret); } static void cleanup() { if (read_stream) pa_stream_unref(read_stream); if (write_stream) pa_stream_unref(write_stream); if (context) pa_context_unref(context); if (m) pa_threaded_mainloop_free(m); if (arena) arena_destroy(&arena); if (audio_fft.lock) { pthread_spin_destroy(&audio_fft.lock); } munlockall(); } //============================================================================= static void success_callback(pa_context *c, int success, void *userdata) { assert(c); if ( !success) { fprintf(stderr, "Failure: %s", pa_strerror(pa_context_errno(c))); quit(1); } } static void sink_input_callback(pa_context *c, const pa_sink_input_info *i, int is_last, void *userdata) { assert(c); if (is_last < 0) { fprintf(stderr, "Failed to get sink information: %s", pa_strerror(pa_context_errno(c))); return; } if (is_last) return; assert(i); if (strcmp(SINK_NAME, i->name) != 0) { const char *app_name; app_name = pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_NAME); fprintf(stderr, "new sink-input: name=%s, index=%d, sink=%d\n", app_name, i->index, i->sink); pa_operation_unref(pa_context_move_sink_input_by_name(c, i->index, SINK_NAME, NULL, NULL)); } else { /* Pulseaudio keeps sink state, so if the user changed the prettyeq * playback stream volume, we reset back to 100% here. This is so the * equalizer "at rest" doesn't modify the input stream. */ pa_cvolume volume; volume = i->volume; pa_cvolume_reset(&volume, i->sample_spec.channels); pa_operation_unref(pa_context_set_sink_input_volume(c, i->index, &volume, success_callback, NULL)); } } static void subscribe_callback( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) { assert(c); assert((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT); if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) pa_operation_unref(pa_context_get_sink_input_info(c, idx, sink_input_callback, NULL)); } static void null_source_output_callback(pa_context *c, const pa_source_output_info *i, int is_last, void *userdata) { uint32_t prettyeq_source = UNBOX_USERDATA(uint32_t, userdata); assert(c); if (is_last < 0) { fprintf(stderr, "Failed to get null source output information: %s\n", pa_strerror(pa_context_errno(c))); quit(1); return; } if (is_last) return; assert(i); if (prettyeq_source == i->source) { /* The recording stream sometimes starts at 79% instead of 100%, so * let's fix that here or else we start at a lower volume. */ pa_cvolume volume; volume = i->volume; pa_cvolume_reset(&volume, i->sample_spec.channels); pa_operation_unref(pa_context_set_source_output_volume(c, i->index, &volume, success_callback, NULL)); } } static void null_sink_info_callback(pa_context *c, const pa_sink_info *i, int is_last, void *userdata) { pa_buffer_attr buffer_attr; assert(c); if (is_last < 0) { fprintf(stderr, "Failed to get null sink information: %s\n", pa_strerror(pa_context_errno(c))); quit(1); return; } if (is_last) return; assert(i); /* Hook up the read stream to the null sink monitor output. */ memset(&buffer_attr, 0, sizeof(buffer_attr)); buffer_attr.maxlength = (uint32_t) -1; buffer_attr.prebuf = (uint32_t) -1; buffer_attr.fragsize = pa_usec_to_bytes(LATENCY_MAX_MS * PA_USEC_PER_MSEC, &sample_spec); buffer_attr.tlength = pa_usec_to_bytes(LATENCY_MAX_MS * PA_USEC_PER_MSEC, &sample_spec); if (pa_stream_connect_record(read_stream, i->monitor_source_name, &buffer_attr, PA_STREAM_ADJUST_LATENCY) < 0) { fprintf(stderr, "pa_stream_connect_record() failed: %s\n", pa_strerror(pa_context_errno(c))); quit(1); } fprintf(stderr, "monitor source name=%s\n", i->monitor_source_name); pa_operation_unref(pa_context_get_source_output_info_list(c, null_source_output_callback, BOX_USERDATA(i->monitor_source))); } static void unload_module_callback(pa_context *c, int success, void *userdata) { pa_threaded_mainloop *m; m = userdata; assert(m); if (! success) fprintf(stderr, "pa_context_unload_module() failed: %s", pa_strerror(pa_context_errno(c))); pa_threaded_mainloop_signal(m, 0); } static void read_stream_callback(pa_stream *s, size_t length, void *userdata) { assert(s); assert(length > 0); while (pa_stream_readable_size(s) > 0) { const void *input_data = NULL; float *fp = NULL; size_t num_samples; if (pa_stream_peek(s, &input_data, &length) < 0) { fprintf(stderr, "pa_stream_peek() failed: %s\n", pa_strerror(pa_context_errno(context))); quit(1); return; } fp = (float *) input_data; num_samples = length / sizeof(float); if (bypass) goto play_frame; #ifndef EQ_DISABLE for (int k = 0; k < NUM_FILTERS; k++) { /* Swap in NULL to block a filter param update mid-iteration. */ FilterParams *params = atomic_exchange(&filters[k].params, NULL); float *xwin = filters[k].xwin; float *ywin = filters[k].ywin; float *a = params->a; float *b = params->b; for (unsigned long i = 0; i < length / sizeof(float); i++) { float f; /* Slide x-window */ xwin[2] = xwin[1]; xwin[1] = xwin[0]; xwin[0] = fp[i]; f = fp[i]; f = (b[0] / a[0] * xwin[0]) + (b[1] / a[0] * xwin[1]) + (b[2] / a[0] * xwin[2]) - (a[1] / a[0] * ywin[0]) - (a[2] / a[0] * ywin[1]); if (PRETTY_IS_DENORMAL(f) || PRETTY_IS_NAN(f)) f = 0.0f; fp[i] = f; /* Slide y-window */ ywin[1] = ywin[0]; ywin[0] = f; } /* Unblock any pending filter param updates. */ atomic_store(&filters[k].params, params); } #endif // EQ_DISABLE play_frame: for (;;) { size_t data_length = length; uint8_t *output_data = NULL; if (pa_stream_begin_write(write_stream, (void **)&output_data, &data_length) < 0) { fprintf(stderr, "pa_stream_begin_write() failed: %s\n", pa_strerror(pa_context_errno(context))); quit(1); return; } memcpy(output_data, input_data, data_length); if (pa_stream_write(write_stream, output_data, data_length, NULL, 0, PA_SEEK_RELATIVE) < 0) { fprintf(stderr, "pa_stream_write() failed: %s\n", pa_strerror(pa_context_errno(context))); quit(1); return; } if (data_length >= length) break; length -= data_length; input_data += data_length; } if (pthread_spin_trylock(&audio_fft.lock) >= 0) { /* Sigh... In no world should pulse really be handing us anything more than * uint16_t in a rapid callback. Fuck it, we downcast. */ assert(num_samples <= UINT_MAX); fft_run(fp, audio_fft.data, (unsigned int) num_samples, sample_spec.channels); audio_fft.N = (unsigned int) num_samples / sample_spec.channels; pthread_spin_unlock(&audio_fft.lock); } if (pa_stream_drop(s) < 0) { fprintf(stderr, "pa_stream_drop() failed: %s\n", pa_strerror(pa_context_errno(context))); quit(1); return; } } } static void load_module_callback(pa_context *c, uint32_t idx, void *userdata) { assert(c); if (idx == PA_INVALID_INDEX) { fprintf(stderr, "Bad index\n"); quit(1); return; } prettyeq_module_index = idx; pa_operation_unref(pa_context_get_sink_input_info_list(c, sink_input_callback, NULL)); pa_operation_unref(pa_context_get_sink_info_by_name(c, SINK_NAME, null_sink_info_callback, NULL)); /* Subscribe to new sink-inputs so we can send them through the equalizer. */ pa_context_set_subscribe_callback(c, subscribe_callback, NULL); pa_operation_unref(pa_context_subscribe(c, PA_SUBSCRIPTION_MASK_SINK_INPUT, NULL, NULL)); } static void module_list_callback(pa_context *c, const pa_module_info *i, int is_last, void *userdata) { pa_threaded_mainloop *m = userdata; static bool signal_on_last = true; assert(m); assert(c); if (is_last) { if (signal_on_last) pa_threaded_mainloop_signal(m, 0); return; } assert(i); if (strcmp(i->name, "module-null-sink") == 0 && i->argument && strcmp(i->argument, "sink_name=" SINK_NAME) == 0) { pa_operation_unref(pa_context_unload_module(c, i->index, unload_module_callback, m)); signal_on_last = false; } } static void drain_signal_callback(pa_context *c, void *userdata) { pa_threaded_mainloop *m = userdata; assert(c); assert(m); pa_threaded_mainloop_signal(m, 0); } static void context_state_callback(pa_context *c, void *userdata) { switch (pa_context_get_state(c)) { case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: break; case PA_CONTEXT_READY: { if ( !(read_stream = pa_stream_new(c, "prettyeq", &sample_spec, NULL))) { fprintf(stderr, "pa_stream_new() failed.\n"); quit(1); return; } if ( !(write_stream = pa_stream_new(c, "prettyeq", &sample_spec, NULL))) { fprintf(stderr, "pa_stream_new() failed.\n"); quit(1); return; } /* Setup the playback stream. We create the recording stream in the null sink callback. */ if (pa_stream_connect_playback(write_stream, NULL, NULL, 0, NULL, NULL) < 0) { fprintf(stderr, "pa_stream_connect_playback() failed: %s\n", pa_strerror(pa_context_errno(c))); quit(1); return; } /* Audio event callback functions. */ pa_stream_set_read_callback(read_stream, read_stream_callback, NULL); pa_threaded_mainloop_signal(m, 0); fprintf(stderr, "context is ready!\n"); break; } case PA_CONTEXT_FAILED: fprintf(stderr, "Unclean termination\n"); quit(1); break; case PA_CONTEXT_TERMINATED: fprintf(stderr, "Clean termination\n"); quit(0); break; } } //============================================================================= int pretty_init() { int r; /* Initialize the fft and user exposed data structures. */ fft_init(); audio_fft.N = 0; r = pthread_spin_init(&audio_fft.lock, PTHREAD_PROCESS_PRIVATE); if (r < 0) { fprintf(stderr, "Could not pthread_spin_init()"); goto err; } /* Initialize the memory arena used to create new filters. */ arena = arena_new(NUM_FILTERS * 2, sizeof(FilterParams)); if (!arena) { fprintf(stderr, "Could not arena_new()"); r = -ENOMEM; goto err; } /* Initialize filters. */ for (int i = 0; i < NUM_FILTERS; i++) { FilterParams *start_params; PrettyFilter *filter = &filters[i]; filter->xwin[0] = 0.0f; filter->xwin[1] = 0.0f; filter->xwin[2] = 0.0f; filter->ywin[0] = 0.0f; filter->ywin[1] = 0.0f; start_params = arena_alloc(arena); atomic_store(&filter->params, start_params); filter->storage = start_params; /* We don't want to page fault reading filters in the audio loop. */ r = mlock(&filter, sizeof(PrettyFilter)); if (r < 0) { fprintf(stderr, "mlock() failed: %s.", strerror(errno)); goto err; } } /* Start with bypass mode disabled. */ atomic_store(&bypass, false); /* Initialize pulseaudio mainloop. */ if ( !(m = pa_threaded_mainloop_new())) { fprintf(stderr, "pa_mainloop_new() failed."); r = -1; goto err; } mainloop_api = pa_threaded_mainloop_get_api(m); if ( !(context = pa_context_new_with_proplist(mainloop_api, NULL, NULL))) { fprintf(stderr, "pa_context_new() failed."); r = -1; goto err; } pa_context_set_state_callback(context, context_state_callback, NULL); if (pa_context_connect(context, NULL, 0, NULL) < 0) { fprintf(stderr, "pa_context_connect() failed."); r = -1; goto err; } r = pa_threaded_mainloop_start(m); if (r < 0) { fprintf(stderr, "pa_threaded_mainloop_start() failed."); goto err; } return 0; err: cleanup(); return r; } void pretty_setup_sink_io() { pa_operation *o; pa_threaded_mainloop_lock(m); /* Wait until context is ready. */ while(pa_context_get_state(context) != PA_CONTEXT_READY) pa_threaded_mainloop_wait(m); /* Handle an unclean termination and cleanup an existing prettyeq module. */ o = pa_context_get_module_info_list(context, module_list_callback, m); while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(m); pa_operation_cancel(o); pa_operation_unref(o); /* Drain the context. TODO(keur): Not sure if we actually need this. Poorly documented function */ if ((o = pa_context_drain(context, drain_signal_callback, m))) { while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(m); pa_operation_unref(o); } pa_threaded_mainloop_unlock(m); /* Load in the null sink to act as audio IO. */ pa_operation_unref(pa_context_load_module(context, "module-null-sink", "sink_name=" SINK_NAME, load_module_callback, NULL)); } int pretty_exit() { if (m) { pa_operation *o; if (prettyeq_module_index != PA_INVALID_INDEX) { pa_threaded_mainloop_lock(m); o = pa_context_unload_module(context, prettyeq_module_index, unload_module_callback, m); while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) pa_threaded_mainloop_wait(m); pa_operation_unref(o); pa_threaded_mainloop_unlock(m); } pa_threaded_mainloop_stop(m); } cleanup(); return 0; } int pretty_new_filter(PrettyFilter **filter) { assert(user_enabled_filters < NUM_FILTERS); *filter = &filters[user_enabled_filters++]; return 0; } static inline void safe_update_audio_loop(PrettyFilter *filter, FilterParams *new_params) { FilterParams *expected; assert(filter); assert(new_params); do { expected = filter->storage; atomic_compare_exchange_strong(&filter->params, &expected, new_params); } while (!expected); arena_dealloc(arena, filter->storage); filter->storage = new_params; } void pretty_set_peaking_eq(PrettyFilter *filter, float f0, float bandwidth, float db_gain) { FilterParams *new_params; float A, alpha, w0, sinw0, cosw0; new_params = arena_alloc(arena); A = powf(10, db_gain/40); w0 = 2*M_PI*f0/sample_spec.rate; sinw0 = sinf(w0); cosw0 = cosf(w0); alpha = sinw0*sinhf(logf(2)/2 * bandwidth * w0/sinw0); new_params->b[0] = 1 + alpha*A; new_params->b[1] = -2 * cosw0; new_params->b[2] = 1 - alpha*A; new_params->a[0] = 1 + alpha/A; new_params->a[1] = -2 * cosw0; new_params->a[2] = 1 - alpha/A; safe_update_audio_loop(filter, new_params); } void pretty_set_low_shelf(PrettyFilter *filter, float f0, float S, float db_gain) { FilterParams *new_params; float A, alpha, w0, sinw0, cosw0, sqrtA; new_params = arena_alloc(arena); A = powf(10, db_gain / 40); w0 = 2*M_PI*f0/sample_spec.rate; sinw0 = sinf(w0); cosw0 = cosf(w0); sqrtA = sqrtf(A); alpha = sinw0/2 * sqrtf((A + 1/A) * (1/S - 1) + 2); new_params->b[0] = A*((A + 1) - (A - 1)*cosw0 + 2*sqrtA*alpha); new_params->b[1] = 2*A*((A - 1) - (A + 1)*cosw0); new_params->b[2] = A*((A + 1) - (A - 1)*cosw0 - 2*sqrtA*alpha); new_params->a[0] = (A + 1) + (A - 1)*cosw0 + 2*sqrtA*alpha; new_params->a[1] = -2*((A - 1) + (A + 1)*cosw0); new_params->a[2] = (A + 1) + (A - 1)*cosw0 - 2*sqrtA*alpha; safe_update_audio_loop(filter, new_params); } void pretty_set_high_shelf(PrettyFilter *filter, float f0, float S, float db_gain) { FilterParams *new_params; float A, alpha, w0, sinw0, cosw0, sqrtA; new_params = arena_alloc(arena); A = powf(10, db_gain / 40); w0 = 2*M_PI*f0/sample_spec.rate; sinw0 = sinf(w0); cosw0 = cosf(w0); sqrtA = sqrtf(A); alpha = sinw0/2 * sqrtf((A + 1/A) * (1/S - 1) + 2); new_params->b[0] = A*((A + 1) + (A - 1)*cosw0 + 2*sqrtA*alpha); new_params->b[1] = -2*A*((A - 1) + (A + 1)*cosw0); new_params->b[2] = A*((A + 1) + (A - 1)*cosw0 - 2*sqrtA*alpha); new_params->a[0] = (A + 1) - (A - 1)*cosw0 + 2*sqrtA*alpha; new_params->a[1] = 2*((A - 1) - (A + 1)*cosw0); new_params->a[2] = (A + 1) - (A - 1)*cosw0 - 2*sqrtA*alpha; safe_update_audio_loop(filter, new_params); } void pretty_enable_bypass(bool should_bypass) { atomic_store(&bypass, should_bypass); } void pretty_acquire_audio_data(complex float **data, unsigned int *N) { int r = pthread_spin_lock(&audio_fft.lock); assert(r == 0); /* No deadlock. */ *data = audio_fft.data; *N = audio_fft.N; } void pretty_release_audio_data() { int r = pthread_spin_unlock(&audio_fft.lock); assert(r == 0); /* No deadlock. */ } ================================================ FILE: equalizer/pretty.h ================================================ #pragma once #include "macro.h" #include #include #include #define SINK_NAME "prettyeq" #define DRAIN_NO_CB(c) (pa_operation_unref(pa_context_drain(c, NULL, NULL))) /* with -ffast-math the standard macros have undefined behavior. */ #define PRETTY_IS_DENORMAL(f) \ ({ \ typeof(f) *_f_ = &(f); \ ((*(unsigned int *)_f_) & 0x7f800000u) == 0 || ((*(unsigned int *)_f_) & 0xff800000u) == 0; \ }) #define PRETTY_IS_NAN(f) \ ({ \ typeof(f) *_f_ = &(f); \ ((*(unsigned int *)_f_) << 1) > 0xff000000u; \ }) /* accomodating the stupid C++ template redefinition... */ #ifdef __cplusplus typedef std::complex* FFTComplexCompat; #else typedef complex float* FFTComplexCompat; #endif #ifdef __cplusplus extern "C" { #endif typedef struct _PrettyFilter PrettyFilter; typedef struct _AudioFFT AudioFFT; PRETTY_EXPORT int pretty_init(); PRETTY_EXPORT int pretty_exit(); PRETTY_EXPORT void pretty_setup_sink_io(); PRETTY_EXPORT int pretty_new_filter(PrettyFilter **filter); PRETTY_EXPORT void pretty_set_peaking_eq(PrettyFilter *filter, float f0, float bandwidth, float db_gain); PRETTY_EXPORT void pretty_set_low_shelf(PrettyFilter *filter, float f0, float S, float db_gain); PRETTY_EXPORT void pretty_set_high_shelf(PrettyFilter *filter, float f0, float S, float db_gain); PRETTY_EXPORT void pretty_enable_bypass(bool should_bypass); PRETTY_EXPORT void pretty_acquire_audio_data(FFTComplexCompat *data, unsigned int *N); PRETTY_EXPORT void pretty_release_audio_data(); #ifdef __cplusplus } #endif ================================================ FILE: gui/collisionmanager.cpp ================================================ #include "collisionmanager.h" #include "eqhoverer.h" #include "filtercurve.h" #include CollisionManager::CollisionManager() : numItems(0) { } void CollisionManager::addEqHoverer(EqHoverer *hover) { Q_ASSERT(hover); hoverItems[numItems++] = hover; } void CollisionManager::notifyFriends() { Q_ASSERT(numItems == NUM_FILTERS); for (int i = 0; i < numItems; i++) hoverItems[i]->collisionStateChanged(); } ================================================ FILE: gui/collisionmanager.h ================================================ #ifndef COLLISIONMANAGER_H #define COLLISIONMANAGER_H #define NUM_FILTERS 7 class EqHoverer; class FilterCurve; class CollisionManager { public: explicit CollisionManager(); void addEqHoverer(EqHoverer *hover); void notifyFriends(); private: int numItems; EqHoverer *hoverItems[NUM_FILTERS]; }; #endif // COLLISIONMANAGER_H ================================================ FILE: gui/curvepoint.cpp ================================================ #include "curvepoint.h" #include #include #include #include #include #define RADIUS 40 #define ESCALE 0.60 CurvePoint::CurvePoint(QBrush normalBrush, QBrush lightBrush, QObject *parent) : QObject(parent), normalBrush(normalBrush), lightBrush(lightBrush) { setZValue(100001); hide(); setFlags(GraphicsItemFlag::ItemIsSelectable | GraphicsItemFlag::ItemIsMovable); } QRectF CurvePoint::boundingRect() const { return QRectF(-RADIUS/2, -RADIUS/2, RADIUS, RADIUS); } void CurvePoint::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { painter->setRenderHint(QPainter::Antialiasing, true); painter->setPen(Qt::NoPen); /* outer circle */ painter->setBrush(lightBrush); painter->drawEllipse(-RADIUS/2, -RADIUS/2, RADIUS, RADIUS); /* inner circle */ painter->scale(ESCALE, ESCALE); painter->setBrush(normalBrush); painter->drawEllipse(-RADIUS/2, -RADIUS/2, RADIUS, RADIUS); } int CurvePoint::type() const { /* Make this type work with qgraphicsitem_cast */ return Type; } void CurvePoint::setResetPos(QPointF resetPoint) { sceneResetPoint = resetPoint; setPos(sceneResetPoint); } void CurvePoint::reset() { setPos(sceneResetPoint); emit pointPositionChanged(this); } void CurvePoint::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { QGraphicsItem::mouseMoveEvent(event); if (scene() && event->type() == QGraphicsSceneMouseEvent::GraphicsSceneMouseMove) { /* x bounds detection */ if (scene()->sceneRect().x() >= mapToScene(boundingRect().center()).x()) setPos(scene()->sceneRect().x(), pos().y()); else if (scene()->sceneRect().x() + scene()->width() <= mapToScene(boundingRect().center()).x()) setPos(scene()->sceneRect().x() + scene()->width(), pos().y()); /* y bounds detection */ if (scene()->sceneRect().y() >= mapToScene(boundingRect().center()).y()) setPos(pos().x(), scene()->sceneRect().y()); else if (scene()->sceneRect().y() + scene()->height() <= mapToScene(boundingRect().center()).y()) setPos(pos().x(), scene()->sceneRect().y() + scene()->sceneRect().height()); emit pointPositionChanged(this); } } void CurvePoint::wheelEvent(QGraphicsSceneWheelEvent *event) { QGraphicsItem::wheelEvent(event); wheelDeltaSum += event->delta(); if (wheelDeltaSum >= 120) { wheelDeltaSum = 0; emit pointSlopeChanged(-1); } else if (wheelDeltaSum <= -120) { wheelDeltaSum = 0; emit pointSlopeChanged(1); } } ================================================ FILE: gui/curvepoint.h ================================================ #ifndef CURVEPOINT_H #define CURVEPOINT_H #include "filtercurve.h" #include #include #include class CurvePoint : public QObject, public QGraphicsItem { Q_OBJECT Q_INTERFACES(QGraphicsItem) public: explicit CurvePoint(QBrush normalBrush, QBrush lightBrush, QObject *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; enum { Type = UserType + 2 }; int type() const override; void setResetPos(QPointF resetPoint); void reset(); protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void wheelEvent(QGraphicsSceneWheelEvent *event) override; signals: void pointPositionChanged(CurvePoint *point); void pointSlopeChanged(int delta); private: QBrush normalBrush, lightBrush; QPointF sceneResetPoint = QPointF(0, 0); int wheelDeltaSum = 0; }; #endif // CURVEPOINT_H ================================================ FILE: gui/eqhoverer.cpp ================================================ #include "collisionmanager.h" #include "eqhoverer.h" #include #include #include #include static const unsigned int Default = 1; static const unsigned int Collision = 1 << 1; static const unsigned int Hover = 1 << 2; static const unsigned int ContextMenu = 1 << 3; EqHoverer::EqHoverer(CollisionManager *mgr, FilterCurve *curve, CurvePoint *point, QObject *parent) : QObject(parent), curve(curve), point(point), pointState(0), mgr(mgr) { Q_ASSERT(curve); Q_ASSERT(point); setAcceptHoverEvents(true); setFlag(QGraphicsItem::ItemHasNoContents, true); reset(); } QRectF EqHoverer::boundingRect() const { QRectF r = curve->boundingRect(); if (scene()) { r.setY(scene()->sceneRect().y()); r.setHeight(scene()->height()); return r; } else return QRectF(QPointF(0, 0), QPointF(0, 0)); } void EqHoverer::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { #if 0 // debugging boundingRect() QPen wpen(Qt::white); wpen.setWidth(3); painter->save(); painter->setPen(wpen); painter->drawRect(boundingRect()); painter->restore(); #endif } int EqHoverer::type() const { /* Make this type work with qgraphicsitem_cast */ return Type; } void EqHoverer::collisionStateChanged() { pointState &= ~Collision; for(QGraphicsItem *i : collidingItems()) { EqHoverer *cc = qgraphicsitem_cast(i); if (cc && cc != this) { if (isUnderMouse() || cc->isUnderMouse()) { pointState |= Collision; break; } } } maybeShowPoint(); } void EqHoverer::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { #if 0 qDebug() << "hoverEnterEvent();"; #endif pointState |= Hover; mgr->notifyFriends(); } void EqHoverer::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { #if 0 qDebug() << "hoverLeaveEvent();"; #endif pointState &= ~Hover; mgr->notifyFriends(); } void EqHoverer::contextMenuToggle(bool on) { int contextBit = (ContextMenu & static_cast(on)); pointState |= contextBit; pointState &= (~ContextMenu | contextBit); maybeShowPoint(); mgr->notifyFriends(); } void EqHoverer::reset() { /* Order of these calls *does* matter here because * resetting the point signals resync. */ point->reset(); curve->reset(); pointState |= Default; maybeShowPoint(); } void EqHoverer::maybeShowPoint() { if (pointState == 0) { point->hide(); curve->setColorState(DefaultState); } else { point->show(); curve->setColorState(PrettyState); } } void EqHoverer::resync(FilterCurve *curve) { pointState &= ~Default; prepareGeometryChange(); mgr->notifyFriends(); } ================================================ FILE: gui/eqhoverer.h ================================================ #ifndef EQHOVERER_H #define EQHOVERER_H #include "curvepoint.h" #include "filtercurve.h" #include #include class CollisionManager; class EqHoverer : public QObject, public QGraphicsItem { Q_OBJECT Q_INTERFACES(QGraphicsItem) public: explicit EqHoverer(CollisionManager *mgr, FilterCurve *curve, CurvePoint *point, QObject *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; enum { Type = UserType + 1 }; int type() const override; void collisionStateChanged(); void contextMenuToggle(bool on = false); void reset(); protected: void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; private: void maybeShowPoint(); public slots: void resync(FilterCurve *curve); signals: public: FilterCurve *curve; CurvePoint *point; private: unsigned int pointState; CollisionManager *mgr; }; #endif // EQHOVERER_H ================================================ FILE: gui/filtercurve.cpp ================================================ #include "filtercurve.h" #include FilterCurve::FilterCurve(QPen togglePen, QBrush toggleBrush, bool guiOnly) : filter(nullptr) { colorState = 0; pens[0] = defaultPen; pens[1] = togglePen; brushes[0] = defaultBrush; brushes[1] = toggleBrush; if (!guiOnly) { /* initialize the filter */ PrettyShim::getInstance().new_filter(&filter); PrettyShim::getInstance().set_peaking_eq(filter, 100, 100, 0); } } FilterCurve::~FilterCurve() { } void FilterCurve::setColorState(ColorState state) { colorState = state; this->update(); } const QPen& FilterCurve::getActivePen() const { return pens[colorState]; } const QBrush& FilterCurve::getActiveBrush() const { return brushes[colorState]; } ================================================ FILE: gui/filtercurve.h ================================================ #ifndef FILTERCURVE_H #define FILTERCURVE_H #include "prettyshim.h" #include #include #include #include static QPen defaultPen = Qt::NoPen; static QBrush defaultBrush = QBrush(QColor(73, 137, 196, 255)); typedef enum ColorState { DefaultState = 0, PrettyState, } ColorState; class FilterCurve : public QGraphicsItem { public: explicit FilterCurve(QPen togglePen, QBrush toggleBrush, bool guiOnly = false); virtual ~FilterCurve(); virtual QPointF controlPoint() const = 0; virtual void reset() = 0; void setColorState(ColorState state); signals: protected: const QPen& getActivePen() const; const QBrush& getActiveBrush() const; protected: ShimFilterPtr filter; private: QPen pens[2]; QBrush brushes[2]; int colorState; }; #endif // FILTERCURVE_H ================================================ FILE: gui/frequencytick.cpp ================================================ #include "frequencytick.h" #include "macro.h" #include #include #include #include FrequencyTick::FrequencyTick(QGraphicsScene *scene, int x, int y0, int y1, int frequency) : scene(scene), x(x), y0(y0), y1(y1), frequency(frequency) { QFont ff = QFontDatabase::systemFont(QFontDatabase::FixedFont); ff.setPixelSize(20); /* Setup line */ line = new QGraphicsLineItem(x, y0, x, y1); line->setPen(QPen(Qt::white)); line->setOpacity(0.3); /* Setup text */ text = new QGraphicsTextItem(toQString()); text->setDefaultTextColor(Qt::white); text->setFont(ff); auto fm = QFontMetrics(text->font()); if (frequency == FMAX) { int offset = fm.horizontalAdvance(QLatin1Char('K')) * 4; text->setPos(x - offset, 30); } else if (frequency == FMIN) { text->setPos(x, 30); } else { int offset = fm.horizontalAdvance(QLatin1Char('K')) * toQString().length() + 7; text->setPos(x - offset / 2, 30); } text->setZValue(1000); scene->addItem(text); scene->addItem(line); } FrequencyTick::~FrequencyTick() { delete line; delete text; } QString FrequencyTick::toQString() { if (frequency >= 1000) return QString::number(frequency / 1000) + QString("K"); else return QString::number(frequency); } qreal FrequencyTick::getFrequency() const { return static_cast(frequency); } qreal FrequencyTick::getX() const { return x; } ================================================ FILE: gui/frequencytick.h ================================================ #ifndef FREQUENCYTICK_H #define FREQUENCYTICK_H #include #include class FrequencyTick { public: FrequencyTick(QGraphicsScene *scene, int x, int y0, int y1, int frequency); ~FrequencyTick(); QString toQString(); qreal getFrequency() const; qreal getX() const; private: QGraphicsScene *scene; int x, y0, y1; int frequency; QGraphicsLineItem *line; QGraphicsTextItem *text; }; #endif // FREQUENCYTICK_H ================================================ FILE: gui/frequencytickbuilder.cpp ================================================ #include "frequencytick.h" #include "frequencytickbuilder.h" #include "macro.h" #include FrequencyTickBuilder::FrequencyTickBuilder(QGraphicsScene *scene, int width, int xmin, int xmax, int ymin, int ymax) { /* y-axis frequency markers */ int tickWidth = width / (NUM_TICKS - 1); tick[0] = new FrequencyTick(scene, xmin, ymin, ymax, F1); tick[1] = new FrequencyTick(scene, xmin + tickWidth * 1, ymin, ymax, F2); tick[2] = new FrequencyTick(scene, xmin + tickWidth * 2, ymin, ymax, F3); tick[3] = new FrequencyTick(scene, xmin + tickWidth * 3, ymin, ymax, F4); tick[4] = new FrequencyTick(scene, xmin + tickWidth * 4, ymin, ymax, F5); tick[5] = new FrequencyTick(scene, xmin + tickWidth * 5, ymin, ymax, F6); tick[6] = new FrequencyTick(scene, xmin + tickWidth * 6, ymin, ymax, F7); tick[7] = new FrequencyTick(scene, xmin + tickWidth * 7, ymin, ymax, F8); tick[8] = new FrequencyTick(scene, xmin + tickWidth * 8, ymin, ymax, F9); tick[9] = new FrequencyTick(scene, xmax, ymin, ymax, F10); } qreal FrequencyTickBuilder::lerpTick(qreal x) { FrequencyTick *tp, *tq; for (int i = 1; i < NUM_TICKS; i++) { tp = tick[i-1]; tq = tick[i]; if (x >= tp->getX() && x <= tq->getX()) break; } return LINEAR_REMAP(x, tp->getX(), tq->getX(), tp->getFrequency(), tq->getFrequency()); } qreal FrequencyTickBuilder::unlerpTick(qreal f) { FrequencyTick *tp, *tq; for (int i = 1; i < NUM_TICKS; i++) { tp = tick[i-1]; tq = tick[i]; if (f >= tp->getFrequency() && f <= tq->getFrequency()) break; } return LINEAR_REMAP(f, tp->getFrequency(), tq->getFrequency(), tp->getX(), tq->getX()); } FrequencyTickBuilder::~FrequencyTickBuilder() { for (int i = 0; i < NUM_TICKS; i++) delete tick[i]; } ================================================ FILE: gui/frequencytickbuilder.h ================================================ #ifndef FREQUENCYTICKBUILDER_H #define FREQUENCYTICKBUILDER_H #include #include #define NUM_TICKS 10 class FrequencyTick; class FrequencyTickBuilder { public: FrequencyTickBuilder(QGraphicsScene *scene, int width, int xmin, int xmax, int ymin, int ymax); ~FrequencyTickBuilder(); qreal lerpTick(qreal x); qreal unlerpTick(qreal f); private: FrequencyTick *tick[NUM_TICKS]; }; #endif // FREQUENCYTICKBUILDER_H ================================================ FILE: gui/gui.cpp ================================================ #include "collisionmanager.h" #include "curvepoint.h" #include "eqhoverer.h" #include "frequencytick.h" #include "frequencytickbuilder.h" #include "gui.h" #include "highshelfcurve.h" #include "lowshelfcurve.h" #include "macro.h" #include "peakingcurve.h" #include "prettygraphicsscene.h" #include "prettyshim.h" #include "spectrumanalyzer.h" #include "ui_gui.h" #include #include #include #include #include #include #include #include #include #include #include #include #define WIDTH 2000 #define HEIGHT 1000 #define XMIN -1000 #define YMIN -500 #define XMAX (XMIN + WIDTH) #define YMAX (YMIN + HEIGHT) static QPen GreenFilterPen = QPen(QColor(138, 237, 152), 3); static QBrush GreenFilterBrush = QBrush(QColor(31, 204, 57, 127)); static QBrush GreenInnerRadiusBrush = QBrush(QColor(31, 204, 57)); static QBrush GreenOuterRadiusBrush = QBrush(QColor(31, 204, 57, 50)); static QPen RedFilterPen = QPen(QColor(231, 123, 131), 3); static QBrush RedFilterBrush = QBrush(QColor(236, 85, 95, 127)); static QBrush RedInnerRadiusBrush = QBrush(QColor(223, 59, 70)); static QBrush RedOuterRadiusBRush = QBrush(QColor(233, 59, 70, 50)); static QPen YellowFilterPen = QPen(QColor(231, 229, 123), 3); static QBrush YellowFilterBrush = QBrush(QColor(236, 225, 85, 127)); static QBrush YellowInnerRadiusBrush = QBrush(QColor(236, 225, 85)); static QBrush YellowOuterRadiusBrush = QBrush(QColor(236, 225, 85, 50)); static QPen PinkFilterPen = QPen(QColor(231, 123, 217), 3); static QBrush PinkFilterBrush = QBrush(QColor(236, 85, 216, 127)); static QBrush PinkInnerRadiusBrush = QBrush(QColor(236, 85, 216)); static QBrush PinkOuterRadiusBrush = QBrush(QColor(236, 85, 216, 50)); static QPen BlueFilterPen = QPen(QColor(123, 231, 191), 3); static QBrush BlueFilterBrush = QBrush(QColor(85, 236, 193, 127)); static QBrush BlueInnerRadiusBrush = QBrush(QColor(85, 236, 193)); static QBrush BlueOuterRadiusBrush = QBrush(QColor(85, 236, 193, 50)); static QPen OrangeFilterPen = QPen(QColor(231, 175, 123), 3); static QBrush OrangeFilterBrush = QBrush(QColor(236, 151, 85, 127)); static QBrush OrangeInnerRadiusBrush = QBrush(QColor(236, 151, 85)); static QBrush OrangeOuterRadiusBrush = QBrush(QColor(236, 151, 85, 50)); static QPen PurpleFilterPen = QPen(QColor(177, 123, 231), 3); static QBrush PurpleFilterBrush = QBrush(QColor(159, 85, 236, 127)); static QBrush PurpleInnerRadiusBrush = QBrush(QColor(159, 85, 236)); static QBrush PurpleOuterRadiusBrush = QBrush(QColor(159, 85, 236, 50)); Gui::Gui(QWidget *parent) : QDialog(parent) , ui(new Ui::Gui) { PrettyShim::getInstance().init(); PrettyShim::getInstance().setup_sink_io(); ui->setupUi(this); QRadialGradient backgroundGradient(QPoint(0, 0), WIDTH); backgroundGradient.setSpread(QGradient::ReflectSpread); backgroundGradient.setColorAt(0.15, QColor(4, 38, 69)); backgroundGradient.setColorAt(0.85, QColor(6, 17, 43)); scene = new PrettyGraphicsScene(ui->graphicsView); scene->setSceneRect(XMIN, YMIN, WIDTH, HEIGHT); scene->setBackgroundBrush(QBrush(backgroundGradient)); ui->graphicsView->setRenderHint(QPainter::Antialiasing); ui->graphicsView->setRenderHint(QPainter::TextAntialiasing); ui->graphicsView->setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ui->graphicsView->setResizeAnchor(QGraphicsView::AnchorViewCenter); ui->graphicsView->setScene(scene); /* x-axis (boost/cut) */ auto xaxis = new QGraphicsLineItem(XMIN, scene->sceneRect().center().y(), XMAX, scene->sceneRect().center().y()); xaxis->setPen(QPen(Qt::white)); xaxis->setOpacity(0.3); scene->addItem(xaxis); /* x-axis frequency markers */ xTickBuilder = new FrequencyTickBuilder(scene, WIDTH, XMIN, XMAX, YMIN, YMAX); addSpectrumAnalyzer(); collisionMgr = new CollisionManager(); addLowShelf (GreenFilterPen, GreenFilterBrush, GreenInnerRadiusBrush, GreenOuterRadiusBrush); addHighShelf(OrangeFilterPen, OrangeFilterBrush, OrangeInnerRadiusBrush, OrangeOuterRadiusBrush); addPeakingEq(150, RedFilterPen, RedFilterBrush, RedInnerRadiusBrush, RedOuterRadiusBRush); addPeakingEq(350, YellowFilterPen, YellowFilterBrush, YellowInnerRadiusBrush, YellowOuterRadiusBrush); addPeakingEq(750, PinkFilterPen, PinkFilterBrush, PinkInnerRadiusBrush, PinkOuterRadiusBrush); addPeakingEq(1500, BlueFilterPen, BlueFilterBrush, BlueInnerRadiusBrush, BlueOuterRadiusBrush); addPeakingEq(3500, PurpleFilterPen, PurpleFilterBrush, PurpleInnerRadiusBrush, PurpleOuterRadiusBrush); connectBypassButton(); maybeShowInSystemTray(); } //============================================================================= void Gui::addFilterItem(QGraphicsItemGroup *group, FilterCurve *curve, CurvePoint *point, EqHoverer *hover) { Q_ASSERT(itemCount < NUM_FILTERS); items[itemCount].group = group; items[itemCount].curve = curve; items[itemCount].point = point; items[itemCount].hover = hover; itemCount++; } void Gui::addPeakingEq(int frequency, QPen curvePen, QBrush filterBrush, QBrush innerRadiusBrush, QBrush outerRadiusBrush) { Q_ASSERT(collisionMgr); PeakingCurve *curve = new PeakingCurve(curvePen, filterBrush); CurvePoint *point = new CurvePoint(innerRadiusBrush, outerRadiusBrush); EqHoverer *hover = new EqHoverer(collisionMgr, curve, point); collisionMgr->addEqHoverer(hover); /* point signals */ QObject::connect(point, SIGNAL(pointPositionChanged(CurvePoint*)), curve, SLOT(pointPositionChanged(CurvePoint*))); QObject::connect(point, SIGNAL(pointSlopeChanged(int)), curve, SLOT(pointSlopeChanged(int))); /* curve signals */ QObject::connect(curve, SIGNAL(resync(FilterCurve*)), hover, SLOT(resync(FilterCurve*))); QObject::connect(curve, SIGNAL(filterParamsChanged(ShimFilterPtr, PeakingCurve*)), this, SLOT(peakingFilterParamsChanged(ShimFilterPtr, PeakingCurve*))); QGraphicsItemGroup *group = new QGraphicsItemGroup(); group->addToGroup(curve); group->addToGroup(hover); group->setHandlesChildEvents(false); group->setPos(xTickBuilder->unlerpTick(frequency) - group->boundingRect().width() / 2, 0); point->setResetPos(curve->controlPoint()); scene->addItem(group); scene->addItem(point); addFilterItem(group, curve, point, hover); } void Gui::addLowShelf(QPen curvePen, QBrush filterBrush, QBrush innerRadiusBrush, QBrush outerRadiusBrush) { Q_ASSERT(collisionMgr); ShelfCurve *curve = new LowShelfCurve(curvePen, filterBrush); CurvePoint *point = new CurvePoint(innerRadiusBrush, outerRadiusBrush); EqHoverer *hover = new EqHoverer(collisionMgr, curve, point); collisionMgr->addEqHoverer(hover); /* point signals */ QObject::connect(point, SIGNAL(pointPositionChanged(CurvePoint*)), curve, SLOT(pointPositionChanged(CurvePoint*))); QObject::connect(point, SIGNAL(pointSlopeChanged(int)), curve, SLOT(pointSlopeChanged(int))); /* curve signals */ QObject::connect(curve, SIGNAL(resync(FilterCurve*)), hover, SLOT(resync(FilterCurve*))); QObject::connect(curve, SIGNAL(filterParamsChanged(ShimFilterPtr, ShelfCurve*)), this, SLOT(lowshelfFilterParamsChanged(ShimFilterPtr, ShelfCurve*))); QGraphicsItemGroup *group = new QGraphicsItemGroup(); group->setHandlesChildEvents(false); group->addToGroup(curve); group->addToGroup(hover); group->setPos(XMIN, 0); point->setResetPos(curve->controlPoint()); scene->addItem(group); scene->addItem(point); addFilterItem(group, curve, point, hover); } void Gui::addHighShelf(QPen curvePen, QBrush filterBrush, QBrush innerRadiusBrush, QBrush outerRadiusBrush) { Q_ASSERT(collisionMgr); ShelfCurve *curve = new HighShelfCurve(curvePen, filterBrush); CurvePoint *point = new CurvePoint(innerRadiusBrush, outerRadiusBrush); EqHoverer *hover = new EqHoverer(collisionMgr, curve, point); collisionMgr->addEqHoverer(hover); /* point signals */ QObject::connect(point, SIGNAL(pointPositionChanged(CurvePoint*)), curve, SLOT(pointPositionChanged(CurvePoint*))); QObject::connect(point, SIGNAL(pointSlopeChanged(int)), curve, SLOT(pointSlopeChanged(int))); /* curve signals */ QObject::connect(curve, SIGNAL(resync(FilterCurve*)), hover, SLOT(resync(FilterCurve*))); QObject::connect(curve, SIGNAL(filterParamsChanged(ShimFilterPtr, ShelfCurve*)), this, SLOT(highshelfFilterParamsChanged(ShimFilterPtr, ShelfCurve*))); QGraphicsItemGroup *group = new QGraphicsItemGroup(); group->setHandlesChildEvents(false); group->addToGroup(curve); group->addToGroup(hover); group->setPos(XMAX, 0); point->setResetPos(curve->controlPoint()); scene->addItem(group); scene->addItem(point); addFilterItem(group, curve, point, hover); } void Gui::peakingFilterParamsChanged(ShimFilterPtr filter, PeakingCurve *curve) { QPointF c = curve->controlPoint(); QRectF r = curve->sceneBoundingRect(); qreal f0 = xTickBuilder->lerpTick(c.x()); qreal bw = xTickBuilder->lerpTick(r.bottomRight().x()) / xTickBuilder->lerpTick(r.bottomLeft().x()) / 2; qreal db_gain = LINEAR_REMAP( c.y(), scene->sceneRect().y(), scene->sceneRect().y() + scene->sceneRect().height(), DB_GAIN_MAX, -DB_GAIN_MAX); PrettyShim::getInstance().set_peaking_eq(filter, f0, bw, db_gain); } void Gui::lowshelfFilterParamsChanged(ShimFilterPtr filter, ShelfCurve *curve) { QPointF c = curve->controlPoint(); qreal f0 = xTickBuilder->lerpTick(c.x()); qreal S = curve->slope(); qreal db_gain = LINEAR_REMAP( c.y(), scene->sceneRect().y(), scene->sceneRect().y() + scene->sceneRect().height(), DB_GAIN_MAX, -DB_GAIN_MAX); PrettyShim::getInstance().set_low_shelf(filter, f0, S, db_gain); } void Gui::highshelfFilterParamsChanged(ShimFilterPtr filter, ShelfCurve *curve) { QPointF c = curve->controlPoint(); qreal f0 = xTickBuilder->lerpTick(c.x()); qreal S = curve->slope(); qreal db_gain = LINEAR_REMAP( c.y(), scene->sceneRect().y(), scene->sceneRect().y() + scene->sceneRect().height(), DB_GAIN_MAX, -DB_GAIN_MAX); PrettyShim::getInstance().set_high_shelf(filter, f0, S, db_gain); } void Gui::connectBypassButton() { QObject::connect(ui->pushButton, &QAbstractButton::clicked, this, [&](bool checked){ PrettyShim::getInstance().enable_bypass(checked); }); } void Gui::addSpectrumAnalyzer() { Q_ASSERT(xTickBuilder); spectrumAnalyzer = new SpectrumAnalyzer(xTickBuilder); spectrumAnalyzer->setPos(-scene->sceneRect().width() / 2, -scene->sceneRect().height() / 8); scene->addItem(spectrumAnalyzer); spectrumUpdateTimer = new QTimer(this); spectrumUpdateTimer->setInterval(1000 / 60); QObject::connect(spectrumUpdateTimer, &QTimer::timeout, [&]() { spectrumAnalyzer->updateFrameDelta(); spectrumAnalyzer->update(); }); spectrumUpdateTimer->start(); } //============================================================================= void Gui::maybeShowInSystemTray() { if (!QSystemTrayIcon::isSystemTrayAvailable()) return; trayMenu = new QMenu(); quitAct = new QAction("Quit"); trayMenu->addAction(quitAct); QObject::connect(quitAct, &QAction::triggered, qApp, QCoreApplication::quit); trayIcon = new QSystemTrayIcon(this); trayIcon->setContextMenu(trayMenu); trayIcon->setIcon(QIcon(":/images/images/prettyeq.png")); trayIcon->setContextMenu(trayMenu); QObject::connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(trayActivated(QSystemTrayIcon::ActivationReason))); trayIcon->show(); } //============================================================================= void Gui::resizeEvent(QResizeEvent *event) { QDialog::resizeEvent(event); qDebug() << this->size(); if (isVisible()) ui->graphicsView->fitInView(scene->sceneRect(), Qt::KeepAspectRatio); //ui->graphicsView->centerOn(0, 0); } void Gui::showEvent(QShowEvent *event) { ui->graphicsView->fitInView(scene->sceneRect(), Qt::KeepAspectRatio); } void Gui::on_actionQuit_triggered() { QCoreApplication::exit(); } void Gui::trayActivated(QSystemTrayIcon::ActivationReason reason) { this->show(); this->raise(); this->activateWindow(); } //============================================================================= Gui::~Gui() { delete collisionMgr; for (int i = 0; i < NUM_FILTERS; i++) { delete items[i].curve; delete items[i].hover; delete items[i].point; delete items[i].group; } if (QSystemTrayIcon::isSystemTrayAvailable()) { delete trayIcon; delete trayMenu; delete quitAct; } spectrumUpdateTimer->stop(); delete spectrumUpdateTimer; delete spectrumAnalyzer; delete ui; } void Gui::cleanup() { PrettyShim::getInstance().exit(); } ================================================ FILE: gui/gui.h ================================================ #ifndef GUI_H #define GUI_H #include "prettyshim.h" #include #include #include #include #include #include #define DB_GAIN_MAX 12 #define NUM_FILTERS 7 QT_BEGIN_NAMESPACE namespace Ui { class Gui; } QT_END_NAMESPACE class CollisionManager; class CurvePoint; class EqHoverer; class FilterCurve; class FrequencyTick; class FrequencyTickBuilder; class PeakingCurve; class ShelfCurve; class SpectrumAnalyzer; typedef struct FilterItem { QGraphicsItemGroup *group; FilterCurve *curve; CurvePoint *point; EqHoverer *hover; } FilterItem; class Gui : public QDialog { Q_OBJECT public: Gui(QWidget *parent = nullptr); ~Gui(); /* Equalizer slots */ public slots: void cleanup(); void peakingFilterParamsChanged(ShimFilterPtr filter, PeakingCurve *curve); void lowshelfFilterParamsChanged(ShimFilterPtr filter, ShelfCurve *curve); void highshelfFilterParamsChanged(ShimFilterPtr filter, ShelfCurve *curve); /* Menu Bar slots */ private slots: void on_actionQuit_triggered(); /* System Tray slots */ private slots: void trayActivated(QSystemTrayIcon::ActivationReason reason); protected: void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; private: void addFilterItem(QGraphicsItemGroup *group, FilterCurve *curve, CurvePoint *point, EqHoverer *hover); qreal lerpTick(qreal x); qreal unlerpTick(qreal f); void addLowShelf(QPen curvePen, QBrush filterBrush, QBrush innerRadiusBrush, QBrush outerRadiusBrush); void addHighShelf(QPen curvePen, QBrush filterBrush, QBrush innerRadiusBrush, QBrush outerRadiusBrush); void addPeakingEq(int frequency, QPen curvePen, QBrush filterBrush, QBrush innerRadiusBrush, QBrush outerRadiusBrush); void connectBypassButton(); void addSpectrumAnalyzer(); void maybeShowInSystemTray(); private: Ui::Gui *ui; QGraphicsScene *scene; CollisionManager *collisionMgr = nullptr;; FilterItem items[NUM_FILTERS]; SpectrumAnalyzer *spectrumAnalyzer = nullptr; FrequencyTickBuilder *xTickBuilder = nullptr; QTimer *spectrumUpdateTimer = nullptr; int itemCount = 0; /* System tray stuff. */ QMenu *trayMenu; QAction *quitAct; QSystemTrayIcon *trayIcon; }; #endif // GUI_H ================================================ FILE: gui/gui.pro ================================================ QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 # The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS # You can also make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ collisionmanager.cpp \ curvepoint.cpp \ eqhoverer.cpp \ filtercurve.cpp \ frequencytick.cpp \ frequencytickbuilder.cpp \ gui.cpp \ highshelfcurve.cpp \ lowshelfcurve.cpp \ main.cpp \ peakingcurve.cpp \ prettygraphicsscene.cpp \ runguard.cpp \ shelfcurve.cpp \ spectrumanalyzer.cpp \ unixsignalhandler.cpp HEADERS += \ collisionmanager.h \ curvepoint.h \ eqhoverer.h \ filtercurve.h \ frequencytick.h \ frequencytickbuilder.h \ gui.h \ highshelfcurve.h \ lowshelfcurve.h \ macro.h \ peakingcurve.h \ prettygraphicsscene.h \ prettyshim.h \ ringbuffer.h \ runguard.h \ shelfcurve.h \ spectrumanalyzer.h \ unixsignalhandler.h FORMS += \ gui.ui QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter INCLUDEPATH += '../equalizer' unix:LIBS += -L ../equalizer -lequalizer -lm -lpulse -lpthread -ffast-math -fopenmp TARGET = ../prettyeq # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target RESOURCES += \ resources.qrc ================================================ FILE: gui/gui.ui ================================================ Gui 0 0 1152 648 1152 648 1152 648 Gui false QWidget { background: #25282b; } QPushButton { color: white } true Options 115 0 100 16777215 Source Code Pro Semibold 75 true Qt::LeftToRight false BYPASS true false true false Quit ================================================ FILE: gui/highshelfcurve.cpp ================================================ #include "highshelfcurve.h" HighShelfCurve::HighShelfCurve(QPen pen, QBrush brush, bool guiOnly, QObject *parent) : ShelfCurve(pen, brush, guiOnly, parent) { reset(); updateCurveGeometry(); } HighShelfCurve::~HighShelfCurve() { } QPointF HighShelfCurve::clampP2() const { return (p2.x() > 0) ? QPointF(0, p2.y()) : p2; } void HighShelfCurve::pointSlopeChanged(int delta) { bool reduce = delta > 0; int offset = -delta * SLOPE_DELTA; if ((qAbs(p3.x() - p2.x()) - offset < SLOPE_MAX || !reduce) && (p2.x() - offset > p3.x() || reduce)) { p2.setX(p2.x() - offset); updateCurveGeometry(); this->update(); emit resync(this); emit filterParamsChanged(filter, this); } } void HighShelfCurve::reset() { p0 = QPointF(0, 0); p3 = QPointF(-330, 0); p1 = QPointF((p3.x() - p0.x()) * 0.3, p0.y()); p2 = QPointF((p1.x() + p3.x())/ 2, p3.y()); updateCurveGeometry(); this->update(); } ================================================ FILE: gui/highshelfcurve.h ================================================ #ifndef HIGHSHELFCURVE_H #define HIGHSHELFCURVE_H #include "shelfcurve.h" class HighShelfCurve : public ShelfCurve { public: explicit HighShelfCurve(QPen pen, QBrush brush, bool guiOnly = false, QObject *parent = nullptr); ~HighShelfCurve(); // ShelfCurve interface protected: QPointF clampP2() const override; // ShelfCurve interface public slots: void pointSlopeChanged(int delta) override; // FilterCurve interface public: void reset() override; }; #endif // HIGHSHELFCURVE_H ================================================ FILE: gui/lowshelfcurve.cpp ================================================ #include "lowshelfcurve.h" LowShelfCurve::LowShelfCurve(QPen pen, QBrush brush, bool guiOnly, QObject *parent) : ShelfCurve(pen, brush, guiOnly, parent) { reset(); updateCurveGeometry(); } LowShelfCurve::~LowShelfCurve() { } QPointF LowShelfCurve::clampP2() const { return p2.x() < 0 ? QPointF(0, p2.y()) : p2; } void LowShelfCurve::pointSlopeChanged(int delta) { bool reduce = delta > 0; int offset = delta * SLOPE_DELTA; if ((p3.x() - p2.x() - offset < SLOPE_MAX || !reduce) && (p2.x() - offset <= p3.x() || reduce)) { p2.setX(p2.x() - offset); updateCurveGeometry(); this->update(); emit resync(this); emit filterParamsChanged(filter, this); } } void LowShelfCurve::reset() { p0 = QPointF(0, 0); p3 = QPointF(330, 0); p1 = QPointF((p3.x() - p0.x()) * 0.3, p0.y()); p2 = QPointF((p1.x() + p3.x())/ 2, p3.y()); updateCurveGeometry(); this->update(); } ================================================ FILE: gui/lowshelfcurve.h ================================================ #ifndef LOWSHELFCURVE_H #define LOWSHELFCURVE_H #include "shelfcurve.h" #include #include class LowShelfCurve : public ShelfCurve { public: explicit LowShelfCurve(QPen pen, QBrush brush, bool guiOnly = false, QObject *parent = nullptr); ~LowShelfCurve(); protected: QPointF clampP2() const override; public slots: void pointSlopeChanged(int delta) override; // FilterCurve interface public: void reset() override; }; #endif // LOWSHELFCURVE_H ================================================ FILE: gui/macro.h ================================================ #ifndef MACRO_H #define MACRO_H #include #define F1 20 #define F2 50 #define F3 100 #define F4 200 #define F5 500 #define F6 1000 #define F7 2000 #define F8 5000 #define F9 10000 #define F10 20000 #define FMIN F1 #define FMAX F10 #define UNLERP( v, min, max ) ( ( (v) - (min) ) / ( (max) - (min) ) ) #define LERP( n, min, max ) ( (min) + (n) * ( (max) - (min) ) ) #define LINEAR_REMAP( i, imin, imax, omin, omax ) ( LERP( UNLERP( i, imin, imax ), omin, omax ) ) static inline QPointF cubic_bezier(qreal t, QPointF p0, QPointF p1, QPointF p2, QPointF p3) { return (1-t)*(1-t)*(1-t)*p0 + 3*(1-t)*(1-t)*t*p1 + 3*(1-t)*t*t*p2 + t*t*t*p3; } #define CUBIC_BEZIER cubic_bezier #endif // MACRO_H ================================================ FILE: gui/main.cpp ================================================ #include "gui.h" #include "runguard.h" #include "unixsignalhandler.h" #include #include #include #include #include static void setupUnixSignalHandlers(QList exitSignals) { struct sigaction sa; sigset_t blocking_mask; int r; sigemptyset(&blocking_mask); for (int sig : exitSignals) { r = sigaddset(&blocking_mask, sig); if (r < 0) qFatal("could not sigaddset(): %s", strerror(r)); } sa.sa_handler = UnixSignalHandler::getInstance().exitHandler; sa.sa_mask = blocking_mask; sa.sa_flags = 0; sa.sa_flags |= SA_RESTART; for (int sig : exitSignals) { r = sigaction(sig, &sa, nullptr); if (r < 0) qFatal("could not sigaction(): %s", strerror(r)); } } int main(int argc, char *argv[]) { RunGuard guard; if (guard.isRunning()) { qWarning() << "prettyeq is already running. Quitting!"; return 0; } QApplication a(argc, argv); setupUnixSignalHandlers(QList{SIGINT, SIGTERM, SIGQUIT, SIGABRT, SIGHUP}); Gui w; QObject::connect(&a, SIGNAL(aboutToQuit()), &w, SLOT(cleanup())); w.show(); return a.exec(); } ================================================ FILE: gui/peakingcurve.cpp ================================================ #include "peakingcurve.h" #include #include #include #include #include #define MIN_WIDTH 40 #define MAX_WIDTH 500 #define SLOPE 10 #define SPLINE_CAPACITY 16 PeakingCurve::PeakingCurve(QPen pen, QBrush brush, bool guiOnly, QObject *parent) : QObject(parent), FilterCurve(pen, brush, guiOnly) { setZValue(100000); lineSpline = std::unique_ptr(new QPainterPath()); fillSpline = std::unique_ptr(new QPainterPath()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) lineSpline->reserve(SPLINE_CAPACITY); fillSpline->reserve(SPLINE_CAPACITY); #endif reset(); updateSplineGeometry(); } QRectF PeakingCurve::boundingRect() const { return fillSpline->boundingRect(); } void PeakingCurve::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { painter->setRenderHint(QPainter::Antialiasing, true); painter->setPen(getActivePen()); painter->drawPath(*lineSpline); painter->fillPath(*fillSpline, getActiveBrush()); #if 0 // debugging - draw control points QPen rpen(Qt::red); rpen.setWidth(10); painter->save(); painter->setPen(rpen); painter->drawPoint(c1); painter->drawPoint(c2); painter->restore(); #endif #if 0 // debugging - draw boundingRect() QPen rpen(Qt::red); rpen.setWidth(2); painter->save(); painter->setPen(rpen); painter->drawRect(boundingRect()); painter->restore(); #endif } QPointF PeakingCurve::controlPoint() const { return mapToScene(ip); } void PeakingCurve::reset() { p0 = QPointF(0, 0); ip = QPointF(100, 0); p1 = QPointF(200, 0); c1 = QPointF((p0.x() + ip.x()) / 2, ip.y()); c2 = QPointF((ip.x() + p1.x()) / 2, ip.y()); updateSplineGeometry(); prepareGeometryChange(); this->update(); } void PeakingCurve::updateSplineGeometry() { #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) lineSpline->clear(); #else lineSpline.reset(new QPainterPath()); #endif lineSpline->moveTo(p0); lineSpline->quadTo(c1, ip); lineSpline->moveTo(ip); lineSpline->quadTo(c2, p1); Q_ASSERT(lineSpline->elementCount() == 8); #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) Q_ASSERT(lineSpline->capacity() == SPLINE_CAPACITY); #endif #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) fillSpline->clear(); #else fillSpline.reset(new QPainterPath()); #endif fillSpline->moveTo(p0); fillSpline->quadTo(c1, ip); fillSpline->moveTo(ip); fillSpline->quadTo(c2, p1); fillSpline->lineTo(p0); Q_ASSERT(fillSpline->elementCount() == 9); #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) Q_ASSERT(fillSpline->capacity() == SPLINE_CAPACITY); #endif } void PeakingCurve::pointPositionChanged(CurvePoint *point) { QPointF curvePoint = mapFromScene(point->pos()); QPointF delta = curvePoint - ip; ip = curvePoint; p0.setX(p0.x() + delta.x()); p1.setX(p1.x() + delta.x()); c1 = QPointF((p0.x() + ip.x()) / 2, ip.y()); c2 = QPointF((ip.x() + p1.x()) / 2, ip.y()); updateSplineGeometry(); prepareGeometryChange(); this->update(); emit resync(this); emit filterParamsChanged(filter, this); } void PeakingCurve::pointSlopeChanged(int delta) { qreal p0x = p0.x() + SLOPE*delta; qreal p1x = p1.x() - SLOPE*delta; if (p1x - p0x >= MIN_WIDTH && p1x - p0x <= MAX_WIDTH) { p0.setX(p0x); p1.setX(p1x); c1 = QPointF((p0.x() + ip.x()) / 2, ip.y()); c2 = QPointF((ip.x() + p1.x()) / 2, ip.y()); updateSplineGeometry(); prepareGeometryChange(); this->update(); emit resync(this); emit filterParamsChanged(filter, this); } } ================================================ FILE: gui/peakingcurve.h ================================================ #ifndef PEAKINGCURVE_H #define PEAKINGCURVE_H #include "curvepoint.h" #include "filtercurve.h" #include #include #include #include #include typedef enum SplinePart { SplineLeft, SplineRight, } SplinePart; class PeakingCurve : public QObject, public FilterCurve { Q_OBJECT Q_INTERFACES(QGraphicsItem) public: explicit PeakingCurve(QPen pen, QBrush brush, bool guiOnly = false, QObject *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; QPointF controlPoint() const override; // FilterCurve interface public: void reset() override; private: void updateSplineGeometry(); public slots: void pointPositionChanged(CurvePoint *point); void pointSlopeChanged(int delta); signals: void resync(FilterCurve *curve); void filterParamsChanged(ShimFilterPtr filter, PeakingCurve *curve); private: QPointF p0, p1, c1, c2, ip; std::unique_ptr lineSpline, fillSpline; }; #endif // PEAKINGCURVE_H ================================================ FILE: gui/prettygraphicsscene.cpp ================================================ #include "curvepoint.h" #include "eqhoverer.h" #include "prettygraphicsscene.h" #include #include #include #include PrettyGraphicsScene::PrettyGraphicsScene(QObject *parent) : QGraphicsScene(parent) {} void PrettyGraphicsScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { CurvePoint *point = nullptr; for (auto item : items()) { point = qgraphicsitem_cast(item); if ( !point) continue; if (point->sceneBoundingRect().contains(event->scenePos())) break; } if (point) { EqHoverer *hover = nullptr; for (auto item : items()) { hover = qgraphicsitem_cast(item); if (! hover) continue; if (point == hover->point) { hover->contextMenuToggle(true); break; } } QMenu menu(event->widget()); menu.addAction("Reset"); if (menu.exec(event->screenPos())) { Q_ASSERT(hover); hover->reset(); } for (auto item : items()) { EqHoverer *hover = qgraphicsitem_cast(item); if (! hover) continue; if (point == hover->point) { hover->contextMenuToggle(false); break; } } } } ================================================ FILE: gui/prettygraphicsscene.h ================================================ #ifndef PRETTYGRAPHICSSCENE_H #define PRETTYGRAPHICSSCENE_H #include class PrettyGraphicsScene : public QGraphicsScene { public: PrettyGraphicsScene(QObject *parent = nullptr); protected: void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; }; #endif // PRETTYGRAPHICSSCENE_H ================================================ FILE: gui/prettyshim.h ================================================ #ifndef PRETTYSHIM_H #define PRETTYSHIM_H #include #include #include #include #include "pretty.h" typedef PrettyFilter* ShimFilterPtr; /* The shim is for making C callbacks idiomatic with Qt signals/slots */ class PrettyShim : public QObject { Q_OBJECT public: static PrettyShim& getInstance() { static PrettyShim instance; return instance; } private: explicit PrettyShim(QObject *parent = nullptr) {} static PrettyShim *instance; public: PrettyShim(PrettyShim const&) = delete; void operator=(PrettyShim const&) = delete; void init() { qRegisterMetaType("uint32_t"); int r = pretty_init(); Q_ASSERT(r == 0); } void setup_sink_io() { pretty_setup_sink_io(); } void exit() { pretty_exit(); } void new_filter(ShimFilterPtr *filter) { int r = pretty_new_filter(filter); Q_ASSERT(*filter && r >= 0); } void set_peaking_eq(ShimFilterPtr filter, float f0, float bandwidth, float db_gain) { pretty_set_peaking_eq(filter, f0, bandwidth, db_gain); } void set_low_shelf(PrettyFilter *filter, float f0, float S, float db_gain) { pretty_set_low_shelf(filter, f0, S, db_gain); } void set_high_shelf(PrettyFilter *filter, float f0, float S, float db_gain) { pretty_set_high_shelf(filter, f0, S, db_gain); } void enable_bypass(bool should_bypass) { pretty_enable_bypass(should_bypass); } std::complex* get_audio_data(unsigned int *N) { std::complex *data; pretty_acquire_audio_data(&data, N); return std::move(data); } void release_audio_data() { pretty_release_audio_data(); } private: AudioFFT *audio_fft; }; #endif // PRETTYSHIM_H ================================================ FILE: gui/resources.qrc ================================================ images/prettyeq.png ================================================ FILE: gui/ringbuffer.h ================================================ #ifndef RINGBUFFER_H #define RINGBUFFER_H #include #include template class RingBuffer { public: RingBuffer() : pos(0) {}; const T& at(int i) { Q_ASSERT(i < size); return ring_buffer.at(i); } const std::array& buffer() { return ring_buffer; } void append(const T& value) { ring_buffer[pos] = value; pos = (pos + 1) % size; } private: int pos; std::array ring_buffer; }; #endif // RINGBUFFER_H ================================================ FILE: gui/runguard.cpp ================================================ #include "runguard.h" #include #include #include #include #include #include RunGuard::RunGuard() { ::memset(&addr, 0, sizeof(struct sockaddr_un)); addr.sun_family = AF_UNIX; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-truncation" ::strncpy(&addr.sun_path[1], PRETTY_ABSTRACT_SOCK, sizeof(PRETTY_ABSTRACT_SOCK) - 2); #pragma GCC diagnostic pop sockfd = ::socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd < 0) qFatal("socket() failed: %s", strerror(sockfd)); } bool RunGuard::isRunning() { int r = ::bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)); if (r < 0) { if (errno == EADDRINUSE) return true; else qFatal("bind() failed: %s", strerror(errno)); } return false; } ================================================ FILE: gui/runguard.h ================================================ #ifndef RUNGUARD_H #define RUNGUARD_H #include #define PRETTY_ABSTRACT_SOCK "prettyeq" /* Linux only implementation of a single application guard. * We bind to an abstract socket so cleanup is handled entirely * by the kernel. */ class RunGuard { public: RunGuard(); bool isRunning(); private: int sockfd; struct sockaddr_un addr; }; #endif // RUNGUARD_H ================================================ FILE: gui/shelfcurve.cpp ================================================ #include "macro.h" #include "shelfcurve.h" #include #include #include #include #include #define CURVE_CAPACITY 16 ShelfCurve::ShelfCurve(QPen pen, QBrush brush, bool guiOnly, QObject *parent) : QObject(parent), FilterCurve(pen, brush, guiOnly) { setZValue(100000); lineCurve = std::unique_ptr(new QPainterPath()); fillCurve = std::unique_ptr(new QPainterPath()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) lineCurve->reserve(CURVE_CAPACITY); fillCurve->reserve(CURVE_CAPACITY); #endif } ShelfCurve::~ShelfCurve() { } void ShelfCurve::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { painter->setRenderHint(QPainter::Antialiasing, true); painter->setPen(getActivePen()); painter->drawPath(*lineCurve); painter->fillPath(*fillCurve, getActiveBrush()); #if 0 // debugging - draw control points QPen rpen(Qt::red); QPen ypen(Qt::yellow); QPen gpen(Qt::green); QPen bpen(Qt::blue); rpen.setWidth(10); ypen.setWidth(10); bpen.setWidth(10); gpen.setWidth(10); painter->save(); painter->setPen(rpen); painter->drawPoint(p0); painter->setPen(ypen); painter->drawPoint(p1); painter->setPen(gpen); painter->drawPoint(p2); painter->setPen(bpen); painter->drawPoint(p3); painter->restore(); #endif #if 0 // debugging - draw boundingRect() QPen rpen(Qt::red); rpen.setWidth(10); QPen bpen(Qt::blue); bpen.setWidth(10); QPen gpen(Qt::green); gpen.setWidth(10); QPen ypen(Qt::yellow); ypen.setWidth(10); painter->save(); painter->setPen(rpen); painter->drawPoint(boundingRect().bottomLeft()); painter->setPen(bpen); painter->drawPoint(boundingRect().topLeft()); painter->setPen(gpen); painter->drawPoint(boundingRect().bottomRight()); painter->setPen(ypen); painter->drawPoint(boundingRect().topRight()); painter->restore(); #endif } QPointF ShelfCurve::controlPoint() const { return mapToScene(p1); } qreal ShelfCurve::slope() const { /* The i have no idea what the fuck I'm doing equation. */ return qAbs(bezierPainter().slopeAtPercent(0.7)) + 0.5; } void ShelfCurve::pointPositionChanged(CurvePoint *point) { QPointF curvePoint = mapFromScene(point->pos()); QPointF delta = curvePoint - p1; p0.setY(p0.y() + delta.y()); p1 = curvePoint; p2.setX(p2.x() + delta.x()); p3.setX(p3.x() + delta.x()); updateCurveGeometry(); prepareGeometryChange(); this->update(); emit resync(this); emit filterParamsChanged(filter, this); } QPainterPath ShelfCurve::bezierPainter() const { QPainterPath shelf; shelf.moveTo(p0); shelf.cubicTo(p1, clampP2(), p3); return shelf; } void ShelfCurve::updateCurveGeometry() { #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) lineCurve->clear(); #else lineCurve.reset(new QPainterPath()); #endif lineCurve->moveTo(p0); lineCurve->cubicTo(p1, clampP2(), p3); Q_ASSERT(lineCurve->elementCount() == 4); #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) Q_ASSERT(lineCurve->capacity() == CURVE_CAPACITY); #endif #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) fillCurve->clear(); #else fillCurve.reset(new QPainterPath()); #endif fillCurve->moveTo(p0); fillCurve->cubicTo(p1, clampP2(), p3); fillCurve->lineTo(0, 0); fillCurve->lineTo(p0); Q_ASSERT(fillCurve->elementCount() <= 6); #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) Q_ASSERT(fillCurve->capacity() == CURVE_CAPACITY); #endif } QRectF ShelfCurve::boundingRect() const { return bezierPainter().boundingRect(); } ================================================ FILE: gui/shelfcurve.h ================================================ #ifndef SHELFCURVE_H #define SHELFCURVE_H #include "curvepoint.h" #include "filtercurve.h" #include #include #include #include #include #define SLOPE_DELTA 20 #define SLOPE_MAX 330 class LowShelfCurve; class HighShelfCurve; class ShelfCurve : public QObject, public FilterCurve { Q_OBJECT Q_INTERFACES(QGraphicsItem) public: explicit ShelfCurve(QPen pen, QBrush brush, bool guiOnly = false, QObject *parent = nullptr); virtual ~ShelfCurve(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; QPointF controlPoint() const override; qreal slope() const; protected: virtual QPointF clampP2() const = 0; private: QPainterPath bezierPainter() const; void updateCurveGeometry(); public slots: void pointPositionChanged(CurvePoint *point); virtual void pointSlopeChanged(int delta) = 0; signals: void resync(FilterCurve *curve); void filterParamsChanged(ShimFilterPtr filter, ShelfCurve *curve); private: QPointF p0, p1, p2, p3; std::unique_ptr lineCurve, fillCurve; friend class LowShelfCurve; friend class HighShelfCurve; }; #endif // SHELFCURVE_H ================================================ FILE: gui/spectrumanalyzer.cpp ================================================ #include "frequencytickbuilder.h" #include "macro.h" #include "prettyshim.h" #include "spectrumanalyzer.h" #include #include #include #include #include #include #include #define FFT_SAMPLE_TO_FREQ(NUM_SAMPLES, SAMPLE_INDEX) (44100*(SAMPLE_INDEX)/(NUM_SAMPLES)) #define FFT_FREQ_TO_SAMPLE(NUM_SAMPLES, FREQ) ((int)roundf((FREQ)*(NUM_SAMPLES)/44100)) #define FFT_BUCKET_WIDTH(NUM_SAMPLES) (44100/(NUM_SAMPLES)) static inline qreal dampen(qreal start, qreal end, qreal smoothing_factor, qint64 dt) { return LERP(1 - qPow(smoothing_factor, dt), start, end); } SpectrumAnalyzer::SpectrumAnalyzer(FrequencyTickBuilder *xTickBuilder) : xTickBuilder(xTickBuilder), last_frame_time(0) { setZValue(-1); for (unsigned int i = 0; i < MAX_SAMPLES; i++) last_psds[i] = 0.0; } QRectF SpectrumAnalyzer::boundingRect() const { auto x = scene()->sceneRect().width(); auto y = scene()->sceneRect().height(); return QRectF(0, 0, x, y / 4); } inline QLineF SpectrumAnalyzer::pointForSample(qreal frequency, qreal max_psd, qreal max_psd_moving_avg) { qreal sceneX = xTickBuilder->unlerpTick(frequency); qreal startX = mapFromScene(sceneX, 0xbeefcafe).x(); qreal startY = qMax(boundingRect().top(), LINEAR_REMAP(max_psd, 0, max_psd_moving_avg, 0, boundingRect().height() / 2)); QLineF line(QPointF(startX, boundingRect().center().y()), QPointF(startX, boundingRect().center().y() - startY)); return line; } qint64 SpectrumAnalyzer::frame_dt() { return QDateTime::currentMSecsSinceEpoch() - last_frame_time; } void SpectrumAnalyzer::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { painter->setRenderHint(QPainter::Antialiasing, true); #if 0 QPen rpen(Qt::red); rpen.setWidth(5); QPen bpen(Qt::blue); bpen.setWidth(5); painter->setPen(rpen); painter->drawPoint(boundingRect().center()); painter->drawRect(boundingRect()); painter->drawPoint(boundingRect().topLeft()); auto line = QLineF(QPointF(100, 0), QPointF(100, boundingRect().height() / 2)); painter->drawLine(line); line.translate(0, boundingRect().height() / 2); painter->setPen(bpen); painter->drawLine(line); #else qreal max_psd_moving_avg = 0.0; static auto brush = QBrush(QColor(127, 153, 176, 128)); static auto pen = QPen(brush, 3); painter->setPen(pen); { auto &buf = max_psds.buffer(); for (auto it = buf.begin(); it != buf.end(); it++) max_psd_moving_avg += *it; max_psd_moving_avg /= MOVING_AVG_PERIOD; } { qint64 delta = frame_dt(); unsigned int N; auto data = PrettyShim::getInstance().get_audio_data(&N); Q_ASSERT(N < MAX_SAMPLES); qreal max_psd = 0.0; for (unsigned int i = FFT_FREQ_TO_SAMPLE(N, FMIN); i < N / 2; i++) { qreal raw_psd = (qreal) std::abs(data[i]); qreal min = qMin(raw_psd, last_psds[i]); qreal max = qMax(raw_psd, last_psds[i]); qreal dampening_factor = 1 - min/max; dampening_factor = qMax(0.5, dampening_factor); dampening_factor = qMin(0.95, dampening_factor); qreal smoothed_psd = dampen(raw_psd, last_psds[i], dampening_factor, delta); last_psds[i] = smoothed_psd; max_psd = qMax(max_psd, smoothed_psd); qreal frequency = FFT_SAMPLE_TO_FREQ(N, (qreal) i); qreal sceneX = xTickBuilder->unlerpTick(frequency); qreal startX = mapFromScene(sceneX, 0xbeefcafe).x(); qreal startY = LINEAR_REMAP(smoothed_psd, 0, max_psd_moving_avg, 0, boundingRect().height() / 2); startY = qMin(startY, boundingRect().height() / 2); QPointF p1 = QPointF(startX, boundingRect().center().y() - startY); QPointF p2 = QPointF(startX, boundingRect().center().y() + startY); lines[i].setP1(p1); lines[i].setP2(p2); } if (max_psd_moving_avg > 0.01) painter->drawLines(lines, N / 2); PrettyShim::getInstance().release_audio_data(); max_psds.append(max_psd); } #endif } void SpectrumAnalyzer::updateFrameDelta() { last_frame_time = QDateTime::currentMSecsSinceEpoch(); } ================================================ FILE: gui/spectrumanalyzer.h ================================================ #ifndef SPECTRUMANALYZER_H #define SPECTRUMANALYZER_H #include #include "ringbuffer.h" #define MOVING_AVG_PERIOD 128 #define MAX_SAMPLES 4096 class FrequencyTickBuilder; class SpectrumAnalyzer : public QGraphicsItem { public: SpectrumAnalyzer(FrequencyTickBuilder *xTickBuilder); public: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); void updateFrameDelta(); private: QLineF pointForSample(qreal frequency, qreal max_psd, qreal max_psd_moving_avg); inline qint64 frame_dt(); private: FrequencyTickBuilder *xTickBuilder; RingBuffer max_psds; QLineF lines[MAX_SAMPLES]; qreal last_psds[MAX_SAMPLES]; qint64 last_frame_time; }; #endif // SPECTRUMANALYZER_H ================================================ FILE: gui/unixsignalhandler.cpp ================================================ #include "unixsignalhandler.h" #include "prettyshim.h" #include #include #include #include int UnixSignalHandler::socketVector[2]; UnixSignalHandler &UnixSignalHandler::getInstance() { static UnixSignalHandler instance; return instance; } UnixSignalHandler::UnixSignalHandler(QObject *parent) : QObject(parent) { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, socketVector)) qFatal("Could not socketpair();"); sn = new QSocketNotifier(socketVector[1], QSocketNotifier::Read, this); QObject::connect(sn, SIGNAL(activated(QSocketDescriptor, QSocketNotifier::Type)), this, SLOT(safeHandleExit())); } void UnixSignalHandler::exitHandler(int sig) { /* Write junk to the socket to trigger Qt signal */ char junk = 1; ::write(socketVector[0], &junk, sizeof(char)); } void UnixSignalHandler::safeHandleExit() { sn->setEnabled(false); char junk; ::read(socketVector[1], &junk, sizeof(char)); QCoreApplication::quit(); sn->setEnabled(true); } UnixSignalHandler::~UnixSignalHandler() { delete sn; } ================================================ FILE: gui/unixsignalhandler.h ================================================ #ifndef UNIXSIGNALHANDLER_H #define UNIXSIGNALHANDLER_H #include #include class UnixSignalHandler : public QObject { Q_OBJECT public: static UnixSignalHandler& getInstance(); ~UnixSignalHandler(); UnixSignalHandler(UnixSignalHandler const&) = delete; void operator=(UnixSignalHandler const&) = delete; private: explicit UnixSignalHandler(QObject *parent = nullptr); public: /* SIGTERM, SIGINT, SIGQUIT, SIGABRT */ static void exitHandler(int sig); public slots: void safeHandleExit(); private: static UnixSignalHandler instance; /* All signals share the same socketpair and will mask each other. */ static int socketVector[2]; QSocketNotifier *sn; }; #endif // UNIXSIGNALHANDLER_H ================================================ FILE: prettyeq.pro ================================================ TEMPLATE = subdirs CONFIG += ordered SUBDIRS = \ equalizer \ gui gui.depends = equalizer