#ifndef PW_AUDIO_H #define PW_AUDIO_H #include #include #include #include // Shared audio state typedef struct { float left; float right; float peak_left; float peak_right; int terminate; pthread_mutex_t lock; struct pw_main_loop *loop; struct pw_stream *stream; } pw_audio_t; static pw_audio_t g_audio; static float amplitude_to_db(float amplitude) { if (amplitude <= 0.00001f) return -60.0f; float db = 20.0f * log10f(amplitude); if (db < -60.0f) db = -60.0f; if (db > 0.0f) db = 0.0f; return db; } // Normalize dB (-60..0) to 0..100 static float db_to_level(float db) { if (db <= -60.0f) return 0.0f; return ((db + 60.0f) / 60.0f) * 100.0f; } static void on_process(void *userdata) { pw_audio_t *data = userdata; struct pw_buffer *b; struct spa_buffer *buf; float *samples; uint32_t n_channels, n_samples; if (data->terminate) { pw_main_loop_quit(data->loop); return; } if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) return; buf = b->buffer; if ((samples = buf->datas[0].data) == NULL) { pw_stream_queue_buffer(data->stream, b); return; } n_channels = 2; // stereo n_samples = buf->datas[0].chunk->size / sizeof(float); // Calculate RMS (Root Mean Square) instead of just peak values // This gives a more accurate analog needle movement and prevents immediate // pegging. float sum_l = 0.0f, sum_r = 0.0f; for (uint32_t n = 0; n + 1 < n_samples; n += n_channels) { float l = samples[n]; float r = samples[n + 1]; sum_l += l * l; sum_r += r * r; } float rms_l = sqrtf(sum_l / (n_samples / n_channels)); float rms_r = sqrtf(sum_r / (n_samples / n_channels)); float db_l = amplitude_to_db(rms_l); float db_r = amplitude_to_db(rms_r); float level_l = db_to_level(db_l); float level_r = db_to_level(db_r); pthread_mutex_lock(&data->lock); data->left = level_l; data->right = level_r; // Peak hold if (level_l > data->peak_left) data->peak_left = level_l; if (level_r > data->peak_right) data->peak_right = level_r; pthread_mutex_unlock(&data->lock); pw_stream_queue_buffer(data->stream, b); } static void on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) { pw_audio_t *data = _data; struct spa_audio_info format; if (param == NULL || id != SPA_PARAM_Format) return; if (spa_format_parse(param, &format.media_type, &format.media_subtype) < 0) return; if (format.media_type != SPA_MEDIA_TYPE_audio || format.media_subtype != SPA_MEDIA_SUBTYPE_raw) return; } static const struct pw_stream_events stream_events = { PW_VERSION_STREAM_EVENTS, .param_changed = on_stream_param_changed, .process = on_process, }; static void *pw_audio_thread(void *arg) { pw_audio_t *data = &g_audio; const struct spa_pod *params[1]; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); pw_init(0, NULL); data->loop = pw_main_loop_new(NULL); if (data->loop == NULL) { data->terminate = 1; return NULL; } struct pw_properties *props = pw_properties_new( PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_CONFIG_NAME, "client-rt.conf", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Music", NULL); pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); data->stream = pw_stream_new_simple(pw_main_loop_get_loop(data->loop), "vumeter-capture", props, &stream_events, data); params[0] = spa_format_audio_raw_build( &b, SPA_PARAM_EnumFormat, &SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_F32)); pw_stream_connect(data->stream, PW_DIRECTION_INPUT, PW_ID_ANY, PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, params, 1); pw_main_loop_run(data->loop); pw_stream_destroy(data->stream); pw_main_loop_destroy(data->loop); pw_deinit(); return NULL; } static int pw_audio_start() { g_audio.left = 0; g_audio.right = 0; g_audio.peak_left = 0; g_audio.peak_right = 0; g_audio.terminate = 0; pthread_mutex_init(&g_audio.lock, NULL); pthread_t thread; return pthread_create(&thread, NULL, pw_audio_thread, NULL); } static void pw_audio_get_levels(float *left, float *right) { pthread_mutex_lock(&g_audio.lock); *left = g_audio.left; *right = g_audio.right; pthread_mutex_unlock(&g_audio.lock); } static void pw_audio_stop() { g_audio.terminate = 1; if (g_audio.loop) { pw_main_loop_quit(g_audio.loop); } } #endif