diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 718a544..3308c8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - os: ubuntu-24.04-arm arch: arm64 plat: linux - - os: macos-latest + - os: macos-15-intel arch: x64 plat: darwin - os: macos-14 @@ -47,6 +47,15 @@ jobs: - name: Install dependencies run: bun install + - name: Install fftw (cavacore build dependency) + run: | + if uname -s | grep -qi darwin; then + brew install fftw + else + sudo apt-get update + sudo apt-get install -y libfftw3-dev + fi + - name: Build native cavacore library run: scripts/build-cavacore.sh @@ -57,8 +66,14 @@ jobs: env: DIST_TAR: podtui-${{ matrix.plat }}-${{ matrix.arch }}.tar.gz run: | - tar -xzf dist/$DIST_TAR -C dist - ./dist/podtui --version + # The embedded runtime reads the launching process's CWD bunfig.toml. + # This repo's bunfig lists a preload the standalone can't resolve + # ("preload not found"), so kicking the binary from the workspace root + # would falsely fail every build. cd into a clean dir first. + SMOKE_DIR=$(mktemp -d) + tar -xzf "dist/$DIST_TAR" -C "$SMOKE_DIR" + cd "$SMOKE_DIR" + ./podtui-*/podtui --version - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/Makefile b/Makefile index d1f0efa..0de4a07 100644 --- a/Makefile +++ b/Makefile @@ -47,18 +47,19 @@ native: scripts/build-cavacore.sh ## Standalone binary + native-libs tarball for the current platform. -## Compiles against an empty bunfig so the binary does not bake the -## @opentui/solid/preload entry (which would break the compiled executable). +## Unaffected by bunfig.toml at build time. Note: the compiled runtime reads +## the launching process's CWD bunfig.toml, so smoke tests must run the binary +## from a bunfig-free dir (see release.yml). dist: - BUN_CONFIG=bunfig.standalone.toml bun run build.ts --compile + bun run build.ts --compile ## macOS build (run on a macOS runner / host). dist-mac: - BUN_CONFIG=bunfig.standalone.toml bun run build.ts --compile + bun run build.ts --compile ## Linux build (run on a Linux runner / host). dist-linux: - BUN_CONFIG=bunfig.standalone.toml bun run build.ts --compile + bun run build.ts --compile ## Remove build artifacts. clean: diff --git a/build.ts b/build.ts index c3f6797..e195c07 100644 --- a/build.ts +++ b/build.ts @@ -5,9 +5,8 @@ import { plugin } from "bun"; // Register the solid transform globally (dedup'd by name). This is what makes // `--compile` work: compile-mode builds only apply `onLoad` transform plugins -// that are registered via `plugin()`, not the `plugins:` array. The compiled -// binary is then built against an empty bunfig (PODTUI_COMPILE config) so the -// runtime bakes NO preload — the solid transform is already in the binary. +// that are registered via `plugin()`, not the `plugins:` array. The transform +// is fully embedded in the compiled binary. plugin(solidPlugin); const COMPILE = diff --git a/cava/LICENSE-cava.txt b/cava/LICENSE-cava.txt new file mode 100644 index 0000000..d3f3d12 --- /dev/null +++ b/cava/LICENSE-cava.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 Karl Stavestrand + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cava/cavacore.c b/cava/cavacore.c new file mode 100644 index 0000000..e85e341 --- /dev/null +++ b/cava/cavacore.c @@ -0,0 +1,588 @@ +#include "cavacore.h" +#ifndef M_PI +#define M_PI 3.1415926535897932385 +#endif +#include +#include +#include +#include +#ifdef __ANDROID__ +#include +struct cava_plan *plan; +double *cava_in; +double *cava_out; +#endif + +static double amplitude_to_decibels(double value) { + // Magic number 20 comes from converting amplitude ratios to decibels. + return 20 * log10(value); +} + +struct cava_plan *cava_init(int number_of_bars, unsigned int rate, int channels, int autosens, + double noise_reduction, int low_cut_off, int high_cut_off, + int scaling_mode) { + struct cava_plan *p = malloc(sizeof(struct cava_plan)); + p->status = 0; + + // sanity checks: + if (channels < 1 || channels > 2) { + snprintf(p->error_message, 1024, + "cava_init called with illegal number of channels: %d, number of channels " + "supported are " + "1 and 2", + channels); + p->status = -1; + return p; + } + if (rate < 1 || rate > 384000) { + snprintf(p->error_message, 1024, "cava_init called with illegal sample rate: %d\n", rate); + p->status = -1; + return p; + } + + int fft_buffer_size = 512; + + if (rate > 8125 && rate <= 16250) + fft_buffer_size *= 2; + else if (rate > 16250 && rate <= 32500) + fft_buffer_size *= 4; + else if (rate > 32500 && rate <= 75000) + fft_buffer_size *= 8; + else if (rate > 75000 && rate <= 150000) + fft_buffer_size *= 16; + else if (rate > 150000 && rate <= 300000) + fft_buffer_size *= 32; + else if (rate > 300000) + fft_buffer_size *= 64; + + if (number_of_bars < 1) { + snprintf(p->error_message, 1024, + "cava_init called with illegal number of bars: %d, number of channels must be " + "positive integer\n", + number_of_bars); + p->status = -1; + return p; + } + + if (number_of_bars > fft_buffer_size / 2 + 1) { + snprintf(p->error_message, 1024, + "cava_init called with illegal number of bars: %d, for %d sample rate number of " + "bars can't be more than %d\n", + number_of_bars, rate, fft_buffer_size / 2 + 1); + p->status = -1; + return p; + } + if (low_cut_off < 1 || high_cut_off < 1) { + snprintf(p->error_message, 1024, "low_cut_off must be a positive value\n"); + p->status = -1; + return p; + } + if (low_cut_off >= high_cut_off) { + snprintf(p->error_message, 1024, "high_cut_off must be a higher than low_cut_off\n"); + p->status = -1; + return p; + } + if ((unsigned int)high_cut_off > rate / 2) { + snprintf(p->error_message, 1024, + "high_cut_off can't be higher than sample rate / 2. (Nyquist Sampling Theorem)\n"); + p->status = -1; + return p; + } + if (scaling_mode != CAVA_SCALING_LINEAR && scaling_mode != CAVA_SCALING_DECIBEL) { + snprintf(p->error_message, 1024, "unknown scaling mode: %d\n", scaling_mode); + p->status = -1; + return p; + } + + p->number_of_bars = number_of_bars; + p->audio_channels = channels; + p->rate = rate; + p->autosens = 1; + p->sens_init = 1; + p->sens = 1.0; + p->autosens = autosens; + p->framerate = 75; + p->frame_skip = 1; + p->noise_reduction = noise_reduction; + p->scaling_mode = scaling_mode; + + int fftw_flag = FFTW_MEASURE; +#ifdef __ANDROID__ + fftw_flag = FFTW_ESTIMATE; +#endif + + p->FFTbassbufferSize = fft_buffer_size * 2; + p->FFTbufferSize = fft_buffer_size; + + p->input_buffer_size = p->FFTbassbufferSize * channels; + + p->input_buffer = (double *)malloc(p->input_buffer_size * sizeof(double)); + + p->FFTbuffer_lower_cut_off = (int *)malloc((number_of_bars + 1) * sizeof(int)); + p->FFTbuffer_upper_cut_off = (int *)malloc((number_of_bars + 1) * sizeof(int)); + p->eq = (double *)malloc((number_of_bars + 1) * sizeof(double)); + p->cut_off_frequency = (float *)malloc((number_of_bars + 1) * sizeof(float)); + + p->cava_fall = (double *)malloc(number_of_bars * channels * sizeof(double)); + p->cava_mem = (double *)malloc(number_of_bars * channels * sizeof(double)); + p->cava_peak = (double *)malloc(number_of_bars * channels * sizeof(double)); + p->prev_cava_out = (double *)malloc(number_of_bars * channels * sizeof(double)); + + // Hann Window calculate multipliers + p->bass_multiplier = (double *)malloc(p->FFTbassbufferSize * sizeof(double)); + p->multiplier = (double *)malloc(p->FFTbufferSize * sizeof(double)); + for (int i = 0; i < p->FFTbassbufferSize; i++) { + p->bass_multiplier[i] = 0.5 * (1 - cos(2 * M_PI * i / (p->FFTbassbufferSize - 1))); + } + for (int i = 0; i < p->FFTbufferSize; i++) { + p->multiplier[i] = 0.5 * (1 - cos(2 * M_PI * i / (p->FFTbufferSize - 1))); + } + + // BASS + p->in_bass_l = fftw_alloc_real(p->FFTbassbufferSize); + p->in_bass_l_raw = fftw_alloc_real(p->FFTbassbufferSize); + p->out_bass_l = fftw_alloc_complex(p->FFTbassbufferSize / 2 + 1); + p->p_bass_l = + fftw_plan_dft_r2c_1d(p->FFTbassbufferSize, p->in_bass_l, p->out_bass_l, fftw_flag); + + // MID + TREBLE + p->in_l = fftw_alloc_real(p->FFTbufferSize); + p->in_l_raw = fftw_alloc_real(p->FFTbufferSize); + p->out_l = fftw_alloc_complex(p->FFTbufferSize / 2 + 1); + p->p_l = fftw_plan_dft_r2c_1d(p->FFTbufferSize, p->in_l, p->out_l, fftw_flag); + + memset(p->in_bass_l, 0, sizeof(double) * p->FFTbassbufferSize); + memset(p->in_l, 0, sizeof(double) * p->FFTbufferSize); + memset(p->in_bass_l_raw, 0, sizeof(double) * p->FFTbassbufferSize); + memset(p->in_l_raw, 0, sizeof(double) * p->FFTbufferSize); + memset(p->out_bass_l, 0, (p->FFTbassbufferSize / 2 + 1) * sizeof(fftw_complex)); + memset(p->out_l, 0, (p->FFTbufferSize / 2 + 1) * sizeof(fftw_complex)); + if (p->audio_channels == 2) { + // BASS + p->in_bass_r = fftw_alloc_real(p->FFTbassbufferSize); + p->in_bass_r_raw = fftw_alloc_real(p->FFTbassbufferSize); + p->out_bass_r = fftw_alloc_complex(p->FFTbassbufferSize / 2 + 1); + p->p_bass_r = + fftw_plan_dft_r2c_1d(p->FFTbassbufferSize, p->in_bass_r, p->out_bass_r, fftw_flag); + + // MID + TREBLE + p->in_r = fftw_alloc_real(p->FFTbufferSize); + p->in_r_raw = fftw_alloc_real(p->FFTbufferSize); + p->out_r = fftw_alloc_complex(p->FFTbufferSize / 2 + 1); + + p->p_r = fftw_plan_dft_r2c_1d(p->FFTbufferSize, p->in_r, p->out_r, fftw_flag); + + memset(p->in_bass_r, 0, sizeof(double) * p->FFTbassbufferSize); + memset(p->in_r, 0, sizeof(double) * p->FFTbufferSize); + memset(p->in_bass_r_raw, 0, sizeof(double) * p->FFTbassbufferSize); + memset(p->in_r_raw, 0, sizeof(double) * p->FFTbufferSize); + memset(p->out_bass_r, 0, (p->FFTbassbufferSize / 2 + 1) * sizeof(fftw_complex)); + memset(p->out_r, 0, (p->FFTbufferSize / 2 + 1) * sizeof(fftw_complex)); + } + + memset(p->input_buffer, 0, sizeof(double) * p->input_buffer_size); + + memset(p->cava_fall, 0, sizeof(double) * number_of_bars * channels); + memset(p->cava_mem, 0, sizeof(double) * number_of_bars * channels); + memset(p->cava_peak, 0, sizeof(double) * number_of_bars * channels); + memset(p->prev_cava_out, 0, sizeof(double) * number_of_bars * channels); + + // process: calculate cutoff frequencies and eq + int lower_cut_off = low_cut_off; + int upper_cut_off = high_cut_off; + int bass_cut_off = 100; + + // calculate frequency constant (used to distribute bars across the frequency band) + double frequency_constant = log10((float)lower_cut_off / (float)upper_cut_off) / + (1 / ((float)p->number_of_bars + 1) - 1); + + float *relative_cut_off = (float *)malloc((p->number_of_bars + 1) * sizeof(float)); + + p->bass_cut_off_bar = 0; + int first_bar = 1; + + float min_bandwidth = p->rate / p->FFTbassbufferSize; + + for (int n = 0; n < p->number_of_bars + 1; n++) { + double bar_distribution_coefficient = frequency_constant * (-1); + bar_distribution_coefficient += + ((float)n + 1) / ((float)p->number_of_bars + 1) * frequency_constant; + p->cut_off_frequency[n] = upper_cut_off * pow(10, bar_distribution_coefficient); + + if (n > 0) { + if (p->cut_off_frequency[n - 1] >= p->cut_off_frequency[n]) + p->cut_off_frequency[n] = p->cut_off_frequency[n - 1] + min_bandwidth; + } + + // remember nyquist! + relative_cut_off[n] = p->cut_off_frequency[n] / (p->rate / 2); + + if (p->cut_off_frequency[n] < bass_cut_off) { + // BASS + p->FFTbuffer_lower_cut_off[n] = relative_cut_off[n] * (p->FFTbassbufferSize / 2); + p->bass_cut_off_bar++; + if (p->bass_cut_off_bar > 1) + first_bar = 0; + + if (p->FFTbuffer_lower_cut_off[n] > p->FFTbassbufferSize / 2) { + p->FFTbuffer_lower_cut_off[n] = p->FFTbassbufferSize / 2; + } + } else { + // MID + TREBLE + p->FFTbuffer_lower_cut_off[n] = + ceil(relative_cut_off[n] * (float)(p->FFTbufferSize / 2)); + if (n == p->bass_cut_off_bar) { + first_bar = 1; + if (n > 0) { + p->FFTbuffer_upper_cut_off[n - 1] = + relative_cut_off[n] * (p->FFTbassbufferSize / 2) - 1; + } + } else { + first_bar = 0; + } + + if (p->FFTbuffer_lower_cut_off[n] > p->FFTbufferSize / 2) { + p->FFTbuffer_lower_cut_off[n] = p->FFTbufferSize / 2; + } + } + + if (n > 0) { + if (!first_bar) { + p->FFTbuffer_upper_cut_off[n - 1] = p->FFTbuffer_lower_cut_off[n] - 1; + + // pushing the spectrum up if the exponential function gets "clumped" in the + // bass and calculating new cut off frequencies + if (p->FFTbuffer_lower_cut_off[n] <= p->FFTbuffer_lower_cut_off[n - 1]) { + + // check if there is room for more first + int room_for_more = 0; + + if (n < p->bass_cut_off_bar) { + if (p->FFTbuffer_lower_cut_off[n - 1] + 1 < p->FFTbassbufferSize / 2 + 1) + room_for_more = 1; + } else { + if (p->FFTbuffer_lower_cut_off[n - 1] + 1 < p->FFTbufferSize / 2 + 1) + room_for_more = 1; + } + + if (room_for_more) { + // push the spectrum up + p->FFTbuffer_lower_cut_off[n] = p->FFTbuffer_lower_cut_off[n - 1] + 1; + p->FFTbuffer_upper_cut_off[n - 1] = p->FFTbuffer_lower_cut_off[n] - 1; + } + } + } else { + if (p->FFTbuffer_upper_cut_off[n - 1] < p->FFTbuffer_lower_cut_off[n - 1]) + p->FFTbuffer_upper_cut_off[n - 1] = p->FFTbuffer_lower_cut_off[n - 1] + 1; + } + } + // calculate actual cut off frequency + if (n < p->bass_cut_off_bar) + relative_cut_off[n] = + (float)(p->FFTbuffer_lower_cut_off[n]) / ((float)p->FFTbassbufferSize / 2); + else + relative_cut_off[n] = + (float)(p->FFTbuffer_lower_cut_off[n]) / ((float)p->FFTbufferSize / 2); + + p->cut_off_frequency[n] = relative_cut_off[n] * ((float)p->rate / 2); + } + + // hard coded eq + for (int n = 0; n < p->number_of_bars; n++) { + + // the numbers that come out of the FFT are very high + // the EQ is used to "normalize" them by dividing with this very huge number + p->eq[n] = 1 / pow(2, 28); + + // need to boost the EQ for higher frequencies + p->eq[n] *= pow(p->cut_off_frequency[n + 1], 0.85); + + if (n < p->bass_cut_off_bar) { + p->eq[n] /= log2(p->FFTbassbufferSize); + } else { + p->eq[n] /= log2(p->FFTbufferSize); + } + + p->eq[n] /= p->FFTbuffer_upper_cut_off[n] - p->FFTbuffer_lower_cut_off[n] + 1; + } + free(relative_cut_off); + return p; +} + +void cava_execute(double *cava_in, int new_samples, double *cava_out, struct cava_plan *p) { + + // do not overflow + if (new_samples > p->input_buffer_size) { + new_samples = p->input_buffer_size; + } + + int silence = 1; + if (new_samples > 0) { + // process: approximate actual framerate. This will be off by +10% at 60 fps, but should be + // good enough for the autosens and smoothing algorithms to be adjusted accordingly if + // framerate is a lot more or less. + p->framerate -= p->framerate / 64.0; + p->framerate += + (double)(p->rate * p->frame_skip) / (new_samples / p->audio_channels) / 64.0; + p->frame_skip = 1; + + // shifting input buffer + for (int n = p->input_buffer_size - 1; n >= new_samples; n--) { + p->input_buffer[n] = p->input_buffer[n - new_samples]; + } + + // fill the input buffer + for (int n = 0; n < new_samples; n++) { + if (p->scaling_mode == CAVA_SCALING_DECIBEL) { + // Audio signals come in the range [-32768, 32768], normalize to [-1, 1]. + p->input_buffer[new_samples - n - 1] = cava_in[n] / 32768.0; + } else { + p->input_buffer[new_samples - n - 1] = cava_in[n]; + } + if (cava_in[n]) { + silence = 0; + } + } + } else { + p->frame_skip++; + } + + // fill the bass, mid and treble buffers + for (int n = 0; n < p->FFTbassbufferSize; n++) { + if (p->audio_channels == 2) { + p->in_bass_r_raw[n] = p->input_buffer[n * 2]; + p->in_bass_l_raw[n] = p->input_buffer[n * 2 + 1]; + } else { + p->in_bass_l_raw[n] = p->input_buffer[n]; + } + } + for (int n = 0; n < p->FFTbufferSize; n++) { + if (p->audio_channels == 2) { + p->in_r_raw[n] = p->input_buffer[n * 2]; + p->in_l_raw[n] = p->input_buffer[n * 2 + 1]; + } else { + p->in_l_raw[n] = p->input_buffer[n]; + } + } + + // Hann Window + for (int i = 0; i < p->FFTbassbufferSize; i++) { + p->in_bass_l[i] = p->bass_multiplier[i] * p->in_bass_l_raw[i]; + if (p->audio_channels == 2) + p->in_bass_r[i] = p->bass_multiplier[i] * p->in_bass_r_raw[i]; + } + for (int i = 0; i < p->FFTbufferSize; i++) { + p->in_l[i] = p->multiplier[i] * p->in_l_raw[i]; + if (p->audio_channels == 2) + p->in_r[i] = p->multiplier[i] * p->in_r_raw[i]; + } + + // process: execute FFT and sort frequency bands + + fftw_execute(p->p_bass_l); + fftw_execute(p->p_l); + if (p->audio_channels == 2) { + fftw_execute(p->p_bass_r); + fftw_execute(p->p_r); + } + + // process: separate frequency bands + for (int n = 0; n < p->number_of_bars; n++) { + + double temp_l = 0; + double temp_r = 0; + + // process: add upp FFT values within bands + for (int i = p->FFTbuffer_lower_cut_off[n]; i <= p->FFTbuffer_upper_cut_off[n]; i++) { + + if (n < p->bass_cut_off_bar) { + temp_l += hypot(p->out_bass_l[i][0], p->out_bass_l[i][1]); + if (p->audio_channels == 2) + temp_r += hypot(p->out_bass_r[i][0], p->out_bass_r[i][1]); + + } else { + temp_l += hypot(p->out_l[i][0], p->out_l[i][1]); + if (p->audio_channels == 2) + temp_r += hypot(p->out_r[i][0], p->out_r[i][1]); + } + } + + // getting average and applying configured scaling + if (p->scaling_mode == CAVA_SCALING_DECIBEL) { + const double max_db = 70; + temp_l = amplitude_to_decibels(temp_l) / max_db; + if (!isfinite(temp_l)) { + temp_l = 0; + } + } else { + temp_l *= p->eq[n]; + } + cava_out[n] = temp_l; + + if (p->audio_channels == 2) { + if (p->scaling_mode == CAVA_SCALING_DECIBEL) { + const double max_db = 70; + temp_r = amplitude_to_decibels(temp_r) / max_db; + if (!isfinite(temp_r)) { + temp_r = 0; + } + } else { + temp_r *= p->eq[n]; + } + cava_out[n + p->number_of_bars] = temp_r; + } + } + + // applying sens or getting max value + if (p->autosens) { + for (int n = 0; n < p->number_of_bars * p->audio_channels; n++) { + cava_out[n] *= p->sens; + } + } + // process [smoothing] + int overshoot = 0; + + double framerate_mod = 66 / p->framerate; + double gravity_mod = pow((framerate_mod), 2.5) * 2 / p->noise_reduction; + double integral_mod = pow((framerate_mod), 0.1); + + for (int n = 0; n < p->number_of_bars * p->audio_channels; n++) { + + // process [smoothing]: falloff + + if (cava_out[n] < p->prev_cava_out[n] && p->noise_reduction > 0.1) { + cava_out[n] = + p->cava_peak[n] * (1.0 - (p->cava_fall[n] * p->cava_fall[n] * gravity_mod)); + + if (cava_out[n] < 0.0) + cava_out[n] = 0.0; + p->cava_fall[n] += 0.028; + } else { + p->cava_peak[n] = cava_out[n]; + p->cava_fall[n] = 0.0; + } + p->prev_cava_out[n] = cava_out[n]; + + // process [smoothing]: integral + cava_out[n] = p->cava_mem[n] * p->noise_reduction / integral_mod + cava_out[n]; + + p->cava_mem[n] = cava_out[n]; + if (p->autosens) { + // check if we overshoot target height + if (cava_out[n] > 1.0) { + overshoot = 1; + cava_out[n] = 1.0; + } + } + } + + // calculating automatic sense adjustment + if (p->autosens) { + if (overshoot) { + p->sens = p->sens * (1 - (0.02 * framerate_mod)); + p->sens_init = 0; + } else { + if (!silence) { + p->sens = p->sens * (1 + (0.001 * framerate_mod * p->autosens)); + if (p->sens_init) + p->sens = p->sens * (1 + (0.1 * framerate_mod)); + } + } + } +} + +void cava_destroy(struct cava_plan *p) { + + free(p->input_buffer); + free(p->bass_multiplier); + free(p->multiplier); + free(p->eq); + free(p->cut_off_frequency); + free(p->FFTbuffer_lower_cut_off); + free(p->FFTbuffer_upper_cut_off); + free(p->cava_fall); + free(p->cava_mem); + free(p->cava_peak); + free(p->prev_cava_out); + + fftw_free(p->in_bass_l); + fftw_free(p->in_bass_l_raw); + fftw_free(p->out_bass_l); + fftw_destroy_plan(p->p_bass_l); + + fftw_free(p->in_l); + fftw_free(p->in_l_raw); + fftw_free(p->out_l); + fftw_destroy_plan(p->p_l); + + if (p->audio_channels == 2) { + fftw_free(p->in_bass_r); + fftw_free(p->in_bass_r_raw); + fftw_free(p->out_bass_r); + fftw_destroy_plan(p->p_bass_r); + + fftw_free(p->in_r); + fftw_free(p->out_r); + fftw_free(p->in_r_raw); + fftw_destroy_plan(p->p_r); + } +} + +#ifdef __ANDROID__ +JNIEXPORT jfloatArray JNICALL Java_com_karlstav_cava_MyGLRenderer_InitCava( + JNIEnv *env, jobject thiz, jint number_of_bars_set, jint refresh_rate, jint lower_cut_off, + jint higher_cut_off) { + jfloatArray cuttOffFreq = (*env)->NewFloatArray(env, number_of_bars_set + 1); + float noise_reduction = pow((float)refresh_rate / 130, 0.75); + + plan = cava_init(number_of_bars_set, 44100, 1, 1, noise_reduction, lower_cut_off, + higher_cut_off, CAVA_SCALING_LINEAR); + cava_in = (double *)malloc(plan->FFTbassbufferSize * sizeof(double)); + cava_out = (double *)malloc(plan->number_of_bars * sizeof(double)); + (*env)->SetFloatArrayRegion(env, cuttOffFreq, 0, plan->number_of_bars + 1, + plan->cut_off_frequency); + return cuttOffFreq; +} + +JNIEXPORT jdoubleArray JNICALL Java_com_karlstav_cava_MyGLRenderer_ExecCava(JNIEnv *env, + jobject thiz, + jdoubleArray cava_input, + jint new_samples) { + + jdoubleArray cavaReturn = (*env)->NewDoubleArray(env, plan->number_of_bars); + + cava_in = (*env)->GetDoubleArrayElements(env, cava_input, NULL); + + cava_execute(cava_in, new_samples, cava_out, plan); + (*env)->SetDoubleArrayRegion(env, cavaReturn, 0, plan->number_of_bars, cava_out); + (*env)->ReleaseDoubleArrayElements(env, cava_input, cava_in, JNI_ABORT); + + return cavaReturn; +} + +JNIEXPORT int JNICALL Java_com_karlstav_cava_CavaCoreTest_InitCava(JNIEnv *env, jobject thiz, + jint number_of_bars_set) { + + plan = cava_init(number_of_bars_set, 44100, 1, 1, 0.7, 50, 10000, CAVA_SCALING_LINEAR); + return 1; +} + +JNIEXPORT jdoubleArray JNICALL Java_com_karlstav_cava_CavaCoreTest_ExecCava(JNIEnv *env, + jobject thiz, + jdoubleArray cava_input, + jint new_samples) { + + jdoubleArray cavaReturn = (*env)->NewDoubleArray(env, plan->number_of_bars); + + cava_in = (*env)->GetDoubleArrayElements(env, cava_input, NULL); + + cava_execute(cava_in, new_samples, cava_out, plan); + (*env)->SetDoubleArrayRegion(env, cavaReturn, 0, plan->number_of_bars, cava_out); + (*env)->ReleaseDoubleArrayElements(env, cava_input, cava_in, JNI_ABORT); + + return cavaReturn; +} +JNIEXPORT void JNICALL Java_com_karlstav_cava_MyGLRenderer_DestroyCava(JNIEnv *env, jobject thiz) { + cava_destroy(plan); +} +#endif diff --git a/cava/cavacore.h b/cava/cavacore.h new file mode 100644 index 0000000..6e0f097 --- /dev/null +++ b/cava/cavacore.h @@ -0,0 +1,139 @@ +/* +Copyright (c) 2022 Karl Stavestrand + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#ifdef __cplusplus +extern "C" { +#endif +#pragma once +#include + +#include + +#define CAVA_SCALING_LINEAR 0 +#define CAVA_SCALING_DECIBEL 1 + +// cava_plan, parameters used internally by cavacore, do not modify these directly +// only the cut off frequencies is of any potential interest to read out, +// the rest should most likely be hidden somehow +struct cava_plan { + int FFTbassbufferSize; + int FFTbufferSize; + int number_of_bars; + int audio_channels; + int input_buffer_size; + int rate; + int bass_cut_off_bar; + int sens_init; + int autosens; + int frame_skip; + int status; + int scaling_mode; + char error_message[1024]; + + double sens; + double framerate; + double noise_reduction; + + fftw_plan p_bass_l, p_bass_r; + fftw_plan p_l, p_r; + + fftw_complex *out_bass_l, *out_bass_r; + fftw_complex *out_l, *out_r; + + double *bass_multiplier; + double *multiplier; + + double *in_bass_r_raw, *in_bass_l_raw; + double *in_r_raw, *in_l_raw; + double *in_bass_r, *in_bass_l; + double *in_r, *in_l; + double *prev_cava_out, *cava_mem; + double *input_buffer, *cava_peak; + + double *eq; + + float *cut_off_frequency; + int *FFTbuffer_lower_cut_off; + int *FFTbuffer_upper_cut_off; + double *cava_fall; +}; + +// cava_init, initialize visualization, takes the following parameters: + +// number_of_bars, number of wanted bars per channel + +// rate, sample rate of input signal + +// channels, number of interleaved channels in input + +// autosens, toggle automatic sensitivity adjustment 1 = on, 0 = off +// on, gives a dynamically adjusted output signal from 0 to 1 +// the output is continuously adjusted to use the entire range +// off, will pass the raw values from cava directly to the output +// the max values will then be dependent on the input + +// noise_reduction, adjust noise reduction filters. 0 - 1, recommended 0.77 +// the raw visualization is very noisy, this factor adjusts the integral +// and gravity filters inside cavacore to keep the signal smooth +// 1 will be very slow and smooth, 0 will be fast but noisy. + +// low_cut_off, high_cut_off cut off frequencies for visualization in Hz +// recommended: 50, 10000 + +// scaling_mode, output scaling mode: +// CAVA_SCALING_LINEAR = legacy linear scaling +// CAVA_SCALING_DECIBEL = dB-based logarithmic scaling + +// returns a cava_plan to be used by cava_execute. If cava_plan.status is 0 all is OK. +// If cava_plan.status is -1, cava_init was called with an illegal parameter, see error string in +// cava_plan.error_message +extern struct cava_plan *cava_init(int number_of_bars, unsigned int rate, int channels, + int autosens, double noise_reduction, int low_cut_off, + int high_cut_off, int scaling_mode); + +// cava_execute, executes visualization + +// cava_in, input buffer can be any size. internal buffers in cavacore is +// 4096 * number of channels at 44100 samples rate, if new_samples is greater +// then samples will be discarded. However it is recommended to use less +// new samples per execution as this determines your framerate. +// 512 samples at 44100 sample rate mono, gives about 86 frames per second. + +// new_samples, the number of samples in cava_in to be processed per execution +// in case of async reading of data this number is allowed to vary from execution to execution + +// cava_out, output buffer. Size must be number of bars * number of channels. Bars will +// be sorted from lowest to highest frequency. If stereo input channels are configured +// then all left channel bars will be first then the right. + +// plan, the cava_plan struct returned from cava_init + +// cava_execute assumes cava_in samples to be interleaved if more than one channel +// only up to two channels are supported. +extern void cava_execute(double *cava_in, int new_samples, double *cava_out, + struct cava_plan *plan); + +// cava_destroy, destroys the plan, frees up memory +extern void cava_destroy(struct cava_plan *plan); + +#ifdef __cplusplus +} +#endif diff --git a/scripts/build-cavacore.sh b/scripts/build-cavacore.sh index b149f22..9b92667 100755 --- a/scripts/build-cavacore.sh +++ b/scripts/build-cavacore.sh @@ -19,36 +19,58 @@ mkdir -p "$OUT_DIR" OS="$(uname -s)" ARCH="$(uname -m)" -# Resolve fftw3 paths +# Resolve fftw3 paths. The static archive lives in different places per +# platform: Homebrew (/opt/homebrew on arm64, /usr/local on Intel) and, on +# Debian/Ubuntu, the multiarch dir /usr/lib/ (e.g. +# x86_64-linux-gnu, aarch64-linux-gnu). if [ "$OS" = "Darwin" ]; then - if [ "$ARCH" = "arm64" ]; then - FFTW_PREFIX="${FFTW_PREFIX:-/opt/homebrew}" - else - FFTW_PREFIX="${FFTW_PREFIX:-/usr/local}" - fi - LIB_EXT="dylib" - SHARED_FLAG="-dynamiclib" - INSTALL_NAME="-install_name @rpath/libcavacore.dylib" + LIB_EXT="dylib" + SHARED_FLAG="-dynamiclib" + INSTALL_NAME="-install_name @rpath/libcavacore.dylib" + if [ "$ARCH" = "arm64" ]; then + FFTW_HINTS="/opt/homebrew /usr/local" + else + FFTW_HINTS="/usr/local /opt/homebrew" + fi else - FFTW_PREFIX="${FFTW_PREFIX:-/usr}" - LIB_EXT="so" - SHARED_FLAG="-shared" - INSTALL_NAME="" + LIB_EXT="so" + SHARED_FLAG="-shared" + INSTALL_NAME="" + FFTW_HINTS="/usr /usr/local" +fi + +FFTW_PREFIX="${FFTW_PREFIX:-}" +FFTW_STATIC="" +if [ -n "$FFTW_PREFIX" ]; then + FFTW_STATIC="$FFTW_PREFIX/lib/libfftw3.a" +else + for hint in $FFTW_HINTS; do + for cand in "$hint/lib/libfftw3.a" "$hint/lib/${ARCH}-linux-gnu/libfftw3.a"; do + if [ -f "$cand" ]; then + FFTW_STATIC="$cand" + FFTW_PREFIX="$hint" + break 2 + fi + done + done +fi + +if [ -z "$FFTW_STATIC" ] || [ ! -f "$FFTW_STATIC" ]; then + echo "Error: libfftw3.a not found (searched: ${FFTW_HINTS})" + echo "Install fftw3: brew install fftw (macOS) or apt install libfftw3-dev (Linux)" + echo "or point FFTW_PREFIX at a prefix containing lib/libfftw3.a." + exit 1 fi FFTW_INCLUDE="$FFTW_PREFIX/include" -FFTW_STATIC="$FFTW_PREFIX/lib/libfftw3.a" - -if [ ! -f "$FFTW_STATIC" ]; then - echo "Error: libfftw3.a not found at $FFTW_STATIC" - echo "Install fftw3: brew install fftw (macOS) or apt install libfftw3-dev (Linux)" - exit 1 +if [ ! -d "$FFTW_INCLUDE" ]; then + FFTW_INCLUDE="$FFTW_PREFIX/include/$(basename "$(dirname "$FFTW_STATIC")")" fi if [ ! -f "$SRC" ]; then - echo "Error: cavacore.c not found at $SRC" - echo "Ensure the cava submodule is initialized: git submodule update --init" - exit 1 + echo "Error: cavacore.c not found at $SRC" + echo "The cava source is vendored under cava/ (from github.com/karlstav/cava, MIT)." + exit 1 fi OUT="$OUT_DIR/libcavacore.$LIB_EXT" @@ -59,21 +81,21 @@ echo " FFTW3: $FFTW_STATIC" echo " Output: $OUT" cc -O2 \ - $SHARED_FLAG \ - $INSTALL_NAME \ - -fPIC \ - -I"$FFTW_INCLUDE" \ - -I"$ROOT/cava" \ - -o "$OUT" \ - "$SRC" \ - "$FFTW_STATIC" \ - -lm + $SHARED_FLAG \ + $INSTALL_NAME \ + -fPIC \ + -I"$FFTW_INCLUDE" \ + -I"$ROOT/cava" \ + -o "$OUT" \ + "$SRC" \ + "$FFTW_STATIC" \ + -lm echo "Built: $OUT" # Verify exported symbols if [ "$OS" = "Darwin" ]; then - echo "" - echo "Exported symbols:" - nm -gU "$OUT" | grep "cava_" + echo "" + echo "Exported symbols:" + nm -gU "$OUT" | grep "cava_" fi diff --git a/src/native/libcavacore.dylib b/src/native/libcavacore.dylib index f269f07..f38f6cd 100755 Binary files a/src/native/libcavacore.dylib and b/src/native/libcavacore.dylib differ