/* -*- linux-c -*- Copyright (C) 2004 Tom Szilagyi This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. $Id: volume.c 1009 2008-03-02 11:22:29Z tszilagyi $ */ #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif /* _WIN32 */ #include "common.h" #include "utils_gui.h" #include "core.h" #include "decoder/file_decoder.h" #include "options.h" #include "music_browser.h" #include "store_file.h" #include "playlist.h" #include "i18n.h" #include "volume.h" #define EPSILON 0.00000000001 extern options_t options; extern GtkTreeStore * music_store; extern GtkWidget * main_window; GtkWidget * vol_window; int vol_slot_count; void voladj2str(float voladj, char * str) { if (fabs(voladj) < 0.05f) { sprintf(str, " %.1f dB", 0.0f); } else { sprintf(str, "% .1f dB", voladj); } } volume_t * volume_new(GtkTreeStore * store, int type) { volume_t * vol = NULL; if ((vol = (volume_t *)calloc(1, sizeof(volume_t))) == NULL) { fprintf(stderr, "volume_new(): calloc error\n"); return NULL; } vol->store = store; vol->type = type; AQUALUNG_COND_INIT(vol->thread_wait); #ifdef _WIN32 vol->thread_mutex = g_mutex_new(); vol->wait_mutex = g_mutex_new(); vol->thread_wait = g_cond_new(); #endif return vol; } void volume_push(volume_t * vol, char * file, GtkTreeIter iter) { vol_item_t * item = NULL; if ((item = (vol_item_t *)calloc(1, sizeof(vol_item_t))) == NULL) { fprintf(stderr, "volume_push(): calloc error\n"); return; } item->file = strdup(file); item->iter = iter; vol->queue = g_list_append(vol->queue, item); } void vol_item_free(vol_item_t * item) { free(item->file); free(item); } void volume_free(volume_t * vol) { #ifdef _WIN32 g_mutex_free(vol->thread_mutex); g_mutex_free(vol->wait_mutex); g_cond_free(vol->thread_wait); #endif if (vol->volumes != NULL) { free(vol->volumes); } g_list_free(vol->queue); free(vol); } inline static float rms_env_process(rms_env_t * r, const float x) { r->sum -= r->buffer[r->pos]; r->sum += x; r->buffer[r->pos] = x; r->pos++; if (r->pos == RMSSIZE) { r->pos = 0; } return sqrt(r->sum / (float)RMSSIZE); } gboolean vol_window_event(GtkWidget * widget, GdkEvent * event, gpointer * data) { if (event->type == GDK_DELETE) { gtk_window_iconify(GTK_WINDOW(vol_window)); return TRUE; } return FALSE; } gboolean volume_finalize(gpointer data) { volume_t * vol = (volume_t *)data; gtk_window_resize(GTK_WINDOW(vol_window), vol_window->allocation.width, vol_window->allocation.height - vol->slot->allocation.height); gtk_widget_destroy(vol->slot); vol->slot = NULL; g_source_remove(vol->update_tag); volume_free(vol); --vol_slot_count; if (vol_slot_count == 0) { unregister_toplevel_window(vol_window); gtk_widget_destroy(vol_window); vol_window = NULL; } return FALSE; } void vol_pause_event(GtkButton * button, gpointer data) { volume_t * vol = (volume_t *)data; vol->paused = !vol->paused; if (vol->paused) { gtk_button_set_label(button, _("Resume")); } else { gtk_button_set_label(button, _("Pause")); } } void vol_cancel_event(GtkButton * button, gpointer data) { volume_t * vol = (volume_t *)data; vol->cancelled = 1; } gboolean vol_set_filename_text(gpointer data) { volume_t * vol = (volume_t *)data; AQUALUNG_MUTEX_LOCK(vol->wait_mutex); if (vol->slot) { char * utf8; AQUALUNG_MUTEX_LOCK(vol->thread_mutex); utf8 = g_filename_display_name(vol->item->file); AQUALUNG_MUTEX_UNLOCK(vol->thread_mutex); gtk_entry_set_text(GTK_ENTRY(vol->file_entry), utf8); gtk_editable_set_position(GTK_EDITABLE(vol->file_entry), -1); g_free(utf8); } AQUALUNG_COND_SIGNAL(vol->thread_wait); AQUALUNG_MUTEX_UNLOCK(vol->wait_mutex); return FALSE; } gboolean vol_update_progress(gpointer data) { volume_t * vol = (volume_t *)data; if (vol->slot) { float fraction = 0.0f; char str_progress[10]; AQUALUNG_MUTEX_LOCK(vol->thread_mutex); if (vol->n_chunks != 0) { fraction = (float)vol->chunks_read / vol->n_chunks; } AQUALUNG_MUTEX_UNLOCK(vol->thread_mutex); if (fraction < 0 || fraction > 1.0f) { fraction = 0.0f; } gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vol->progress), fraction); snprintf(str_progress, 10, "%.0f%%", fraction * 100.0f); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vol->progress), str_progress); } return TRUE; } void vol_store_voladj(GtkTreeStore * store, GtkTreeIter * iter, float voladj) { if (!gtk_tree_store_iter_is_valid(store, iter)) { return; } if (store == music_store) { /* music store */ track_data_t * data; gtk_tree_model_get(GTK_TREE_MODEL(music_store), iter, MS_COL_DATA, &data, -1); data->volume = voladj; music_store_mark_changed(iter); } else { /* playlist */ playlist_data_t * data; char str[32]; voladj2str(voladj, str); gtk_tree_model_get(GTK_TREE_MODEL(store), iter, PL_COL_DATA, &data, -1); data->voladj = voladj; gtk_tree_store_set(store, iter, PL_COL_VADJ, str, -1); } } gboolean vol_store_result_sep(gpointer data) { volume_t * vol = (volume_t *)data; AQUALUNG_MUTEX_LOCK(vol->wait_mutex); vol_store_voladj(vol->store, &vol->item->iter, (vol->store == music_store) ? vol->result : rva_from_volume(vol->result)); AQUALUNG_COND_SIGNAL(vol->thread_wait); AQUALUNG_MUTEX_UNLOCK(vol->wait_mutex); return FALSE; } gboolean vol_store_result_avg(gpointer data) { volume_t * vol = (volume_t *)data; GList * node = NULL; float voladj; AQUALUNG_MUTEX_LOCK(vol->wait_mutex); voladj = rva_from_multiple_volumes(vol->n_volumes, vol->volumes); for (node = vol->queue; node; node = node->next) { vol_item_t * item = (vol_item_t *)node->data; vol_store_voladj(vol->store, &item->iter, voladj); } AQUALUNG_COND_SIGNAL(vol->thread_wait); AQUALUNG_MUTEX_UNLOCK(vol->wait_mutex); return FALSE; } void create_volume_window() { GtkWidget * vbox; vol_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); register_toplevel_window(vol_window, TOP_WIN_SKIN | TOP_WIN_TRAY); gtk_window_set_transient_for(GTK_WINDOW(vol_window), GTK_WINDOW(main_window)); gtk_window_set_title(GTK_WINDOW(vol_window), _("Calculating volume level")); gtk_window_set_position(GTK_WINDOW(vol_window), GTK_WIN_POS_CENTER); gtk_window_resize(GTK_WINDOW(vol_window), 480, 110); g_signal_connect(G_OBJECT(vol_window), "event", G_CALLBACK(vol_window_event), NULL); gtk_container_set_border_width(GTK_CONTAINER(vol_window), 5); vbox = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(vol_window), vbox); gtk_widget_show_all(vol_window); } void vol_create_gui(volume_t * vol) { GtkWidget * label; GtkWidget * vbox; GtkWidget * hbox; ++vol_slot_count; if (vol_window == NULL) { create_volume_window(); } vbox = gtk_bin_get_child(GTK_BIN(vol_window)); vol->slot = gtk_table_new(4, 3, FALSE); gtk_box_pack_start(GTK_BOX(vbox), vol->slot, FALSE, FALSE, 0); gtk_table_attach(GTK_TABLE(vol->slot), gtk_hseparator_new(), 0, 3, 0, 1, GTK_FILL, GTK_FILL, 5, 5); hbox = gtk_hbox_new(FALSE, 0); label = gtk_label_new(_("File:")); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); gtk_table_attach(GTK_TABLE(vol->slot), hbox, 0, 1, 1, 2, GTK_FILL, GTK_FILL, 5, 5); vol->file_entry = gtk_entry_new(); gtk_editable_set_editable(GTK_EDITABLE(vol->file_entry), FALSE); gtk_table_attach(GTK_TABLE(vol->slot), vol->file_entry, 1, 2, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 5, 5); vol->pause_button = gtk_button_new_with_label(_("Pause")); g_signal_connect(vol->pause_button, "clicked", G_CALLBACK(vol_pause_event), vol); gtk_table_attach(GTK_TABLE(vol->slot), vol->pause_button, 2, 3, 1, 2, GTK_FILL, GTK_FILL, 5, 5); hbox = gtk_hbox_new(FALSE, 0); label = gtk_label_new(_("Progress:")); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); gtk_table_attach(GTK_TABLE(vol->slot), hbox, 0, 1, 2, 3, GTK_FILL, GTK_FILL, 5, 5); vol->progress = gtk_progress_bar_new(); gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vol->progress), 0.0f); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vol->progress), "0.0%"); gtk_table_attach(GTK_TABLE(vol->slot), vol->progress, 1, 2, 2, 3, GTK_EXPAND | GTK_FILL, GTK_FILL, 5, 5); vol->cancel_button = gui_stock_label_button (_("Abort"), GTK_STOCK_CANCEL); g_signal_connect(vol->cancel_button, "clicked", G_CALLBACK(vol_cancel_event), vol); gtk_table_attach(GTK_TABLE(vol->slot), vol->cancel_button, 2, 3, 2, 3, GTK_FILL, GTK_FILL, 5, 5); gtk_table_attach(GTK_TABLE(vol->slot), gtk_hseparator_new(), 0, 3, 3, 4, GTK_FILL, GTK_FILL, 5, 5); gtk_widget_show_all(vol->slot); } /* if returns 1, file will be skipped */ int process_volume_setup(volume_t * vol) { if ((vol->fdec = file_decoder_new()) == NULL) { fprintf(stderr, "calculate_volume: error: file_decoder_new() returned NULL\n"); return 1; } if (file_decoder_open(vol->fdec, vol->item->file)) { fprintf(stderr, "file_decoder_open() failed on %s\n", vol->item->file); return 1; } if ((vol->rms = (rms_env_t *)calloc(1, sizeof(rms_env_t))) == NULL) { fprintf(stderr, "calculate_volume(): calloc error\n"); return 1; } vol->chunks_read = 0; vol->chunk_size = vol->fdec->fileinfo.sample_rate / 100; vol->n_chunks = vol->fdec->fileinfo.total_samples / vol->chunk_size + 1; AQUALUNG_MUTEX_LOCK(vol->wait_mutex); aqualung_idle_add(vol_set_filename_text, vol); AQUALUNG_COND_WAIT(vol->thread_wait, vol->wait_mutex); AQUALUNG_MUTEX_UNLOCK(vol->wait_mutex); vol->result = 0.0f; return 0; } void * volume_thread(void * arg) { volume_t * vol = (volume_t *)arg; GList * node; float * samples = NULL; unsigned long numread; float chunk_power = 0.0f; float rms; unsigned long i; struct timespec req_time; struct timespec rem_time; req_time.tv_sec = 0; req_time.tv_nsec = 500000000; AQUALUNG_THREAD_DETACH(); for (node = vol->queue; node; node = node->next) { vol->item = (vol_item_t *)node->data; if (process_volume_setup(vol)) { continue; } if ((samples = (float *)malloc(vol->chunk_size * vol->fdec->fileinfo.channels * sizeof(float))) == NULL) { fprintf(stderr, "volume_thread(): malloc() error\n"); file_decoder_close(vol->fdec); file_decoder_delete(vol->fdec); free(vol->rms); break; } do { numread = file_decoder_read(vol->fdec, samples, vol->chunk_size); vol->chunks_read++; /* calculate signal power of chunk and feed it in the rms envelope */ if (numread > 0) { for (i = 0; i < numread * vol->fdec->fileinfo.channels; i++) { chunk_power += samples[i] * samples[i]; } chunk_power /= numread * vol->fdec->fileinfo.channels; rms = rms_env_process(vol->rms, chunk_power); if (rms > vol->result) { vol->result = rms; } } while (vol->paused && !vol->cancelled) { nanosleep(&req_time, &rem_time); } } while (numread == vol->chunk_size && !vol->cancelled); if (!vol->cancelled) { vol->result = 20.0f * log10f(vol->result); #ifdef HAVE_MPEG /* compensate for anti-clip vol.reduction in dec_mpeg.c/mpeg_output() */ if (vol->fdec->file_lib == MAD_LIB) { vol->result += 1.8f; } #endif /* HAVE_MPEG */ if (vol->type == VOLUME_SEPARATE) { AQUALUNG_MUTEX_LOCK(vol->wait_mutex); aqualung_idle_add(vol_store_result_sep, vol); AQUALUNG_COND_WAIT(vol->thread_wait, vol->wait_mutex); AQUALUNG_MUTEX_UNLOCK(vol->wait_mutex); } else if (vol->type == VOLUME_AVERAGE) { vol->n_volumes++; if ((vol->volumes = realloc(vol->volumes, vol->n_volumes * sizeof(float))) == NULL) { fprintf(stderr, "volume_thread(): realloc error\n"); return NULL; } vol->volumes[vol->n_volumes - 1] = vol->result; } } file_decoder_close(vol->fdec); file_decoder_delete(vol->fdec); free(vol->rms); free(samples); if (vol->cancelled) { break; } } if (!vol->cancelled && vol->type == VOLUME_AVERAGE) { AQUALUNG_MUTEX_LOCK(vol->wait_mutex); aqualung_idle_add(vol_store_result_avg, vol); AQUALUNG_COND_WAIT(vol->thread_wait, vol->wait_mutex); AQUALUNG_MUTEX_UNLOCK(vol->wait_mutex); } for (node = vol->queue; node; node = node->next) { vol_item_free((vol_item_t *)node->data); } aqualung_idle_add(volume_finalize, vol); return NULL; } void volume_start(volume_t * vol) { if (vol->queue == NULL) { return; } vol_create_gui(vol); AQUALUNG_THREAD_CREATE(vol->thread_id, NULL, volume_thread, vol); vol->update_tag = aqualung_timeout_add(250, vol_update_progress, vol); } float rva_from_volume(float volume) { return ((volume - options.rva_refvol) * (options.rva_steepness - 1.0f)); } /* Replaygain is almost always the "89 dB SPL" type. * This means a -14 dBFS pink noise reference signal is used. * A positive replaygain value means it's quiter than the reference signal. * The magic values are due to different ways of calculating the levels. * They're only approximations though. */ #define RG_REFERENCE_LEVEL -14.0 #define RG_MAGIC_VAL1 -2.1 /* emperical */ #define RG_MAGIC_VAL2 1.05 /* emperical */ #define RG_TO_VOLUME(replaygain) ((-replaygain + RG_REFERENCE_LEVEL + RG_MAGIC_VAL1) * RG_MAGIC_VAL2) float rva_from_replaygain(float rg) { return rva_from_volume(RG_TO_VOLUME(rg)); } float rva_from_multiple_volumes(int nlevels, float * volumes) { int i, files_to_avg; char * badlevels; double sum, level, mean_level, variance, std_dev; double level_difference, threshold; if ((badlevels = (char *)calloc(nlevels, sizeof(char))) == NULL) { fprintf(stderr, "rva_from_multiple_volumes() : calloc error\n"); return 0.0f; } sum = 0.0; for (i = 0; i < nlevels; i++) { sum += db2lin(volumes[i]); } mean_level = sum / nlevels; if (!options.rva_use_linear_thresh) { /* use stddev_thresh */ sum = 0; for (i = 0; i < nlevels; i++) { double tmp = 20.0 * log10(db2lin(volumes[i]) / mean_level); sum += tmp * tmp; } variance = sum / nlevels; /* get standard deviation */ if (variance < EPSILON) { std_dev = 0.0; } else { std_dev = sqrt(variance); } threshold = options.rva_avg_stddev_thresh * std_dev; } else { threshold = options.rva_avg_linear_thresh; } if (threshold > EPSILON && nlevels > 1) { for (i = 0; i < nlevels; i++) { level_difference = fabs(20.0 * log10(mean_level / db2lin(volumes[i]))); if (level_difference > threshold) { badlevels[i] = 1; } } } /* throw out the levels marked as bad */ files_to_avg = 0; sum = 0; for (i = 0; i < nlevels; i++) { if (!badlevels[i]) { sum += db2lin(volumes[i]); files_to_avg++; } } free(badlevels); if (files_to_avg == 0) { fprintf(stderr, "rva_from_multiple_volumes: all files ignored, using mean value.\n"); return rva_from_volume(20 * log10(mean_level)); } level = sum / files_to_avg; return rva_from_volume(20 * log10(level)); } // vim: shiftwidth=8:tabstop=8:softtabstop=8 :