From 22b16884a2fb054ed219f07774cc16ec3af34c26 Mon Sep 17 00:00:00 2001 From: "J. Alexander Treuman" Date: Thu, 27 Jul 2006 00:50:59 +0000 Subject: [PATCH] Use AudioCompress for volume normalization git-svn-id: https://svn.musicpd.org/mpd/trunk@4474 09075e82-0dd4-0310-85a5-a0d7c8717e4f --- AUTHORS | 1 + src/Makefile.am | 2 + src/compress.c | 392 +++++++++++++++++++++++++++++++++++++++++++++ src/compress.h | 29 ++++ src/conf.c | 2 + src/main.c | 3 + src/normalize.c | 91 +++-------- src/normalize.h | 11 ++ src/outputBuffer.c | 21 +-- src/playlist.c | 11 +- 10 files changed, 463 insertions(+), 100 deletions(-) create mode 100644 src/compress.c create mode 100644 src/compress.h diff --git a/AUTHORS b/AUTHORS index 734779e43..6efbe8cef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,3 +7,4 @@ command.c and signal handling cleanup -> mackstann replayGain -> AliasMrJones mp4ff copyrighted by M. Bakker, Ahead Software AG, http://www.nero.com +compress.[ch] copyrighted by M. Hari Nezumi diff --git a/src/Makefile.am b/src/Makefile.am index 08f7ed012..e86c73eab 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -50,6 +50,7 @@ mpd_headers = \ mpd_types.h \ myfprintf.h \ normalize.h \ + compress.h \ outputBuffer.h \ path.h \ pcm_utils.h \ @@ -95,6 +96,7 @@ mpd_SOURCES = \ metadataChunk.c \ myfprintf.c \ normalize.c \ + compress.c \ outputBuffer.c \ path.c \ pcm_utils.c \ diff --git a/src/compress.c b/src/compress.c new file mode 100644 index 000000000..e6c6cd6fb --- /dev/null +++ b/src/compress.c @@ -0,0 +1,392 @@ +/* compress.c +** Compressor logic +*/ + +#include +#include +#include +#include + +#include "compress.h" + +#ifdef USE_X +#include +#include + +static Display *display; +static Window window; +static Visual *visual; +static int screen; +static GC blackGC, whiteGC, blueGC, yellowGC, dkyellowGC, redGC; +#endif + +static int *peaks = NULL; +static int gainCurrent, gainTarget; + +static struct { + int show_mon; + int anticlip; + int target; + int gainmax; + int gainsmooth; + int buckets; +} prefs; + +#ifdef USE_X +static int mon_init = 0; +#endif + +void CompressCfg(int show_mon, int anticlip, int target, int gainmax, + int gainsmooth, int buckets) +{ + static int lastsize = 0; + + prefs.show_mon = show_mon; + prefs.anticlip = anticlip; + prefs.target = target; + prefs.gainmax = gainmax; + prefs.gainsmooth = gainsmooth; + prefs.buckets = buckets; + + /* Allocate the peak structure */ + peaks = realloc(peaks, sizeof(int)*prefs.buckets); + + if (prefs.buckets > lastsize) + memset(peaks + lastsize, 0, sizeof(int)*(prefs.buckets + - lastsize)); + lastsize = prefs.buckets; + +#ifdef USE_X + /* Configure the monitor window if needed */ + if (show_mon && !mon_init) + { + display = XOpenDisplay(getenv("DISPLAY")); + + /* We really shouldn't try to init X if there's no X */ + if (!display) + { + fprintf(stderr, + "X not detected; disabling monitor window\n"); + show_mon = prefs.show_mon = 0; + } + } + + if (show_mon && !mon_init) + { + XGCValues gcv; + XColor col; + + gainCurrent = gainTarget = (1 << GAINSHIFT); + + + + screen = DefaultScreen(display); + visual = DefaultVisual(display, screen); + window = XCreateSimpleWindow(display, + RootWindow(display, screen), + 0, 0, prefs.buckets, 128 + 8, 0, + BlackPixel(display, screen), + WhitePixel(display, screen)); + XStoreName(display, window, "AudioCompress monitor"); + + gcv.foreground = BlackPixel(display, screen); + blackGC = XCreateGC(display, window, GCForeground, &gcv); + gcv.foreground = WhitePixel(display, screen); + whiteGC = XCreateGC(display, window, GCForeground, &gcv); + col.red = 0; + col.green = 0; + col.blue = 65535; + XAllocColor(display, DefaultColormap(display, screen), &col); + gcv.foreground = col.pixel; + blueGC = XCreateGC(display, window, GCForeground, &gcv); + col.red = 65535; + col.green = 65535; + col.blue = 0; + XAllocColor(display, DefaultColormap(display, screen), &col); + gcv.foreground = col.pixel; + yellowGC = XCreateGC(display, window, GCForeground, &gcv); + col.red = 32767; + col.green = 32767; + col.blue = 0; + XAllocColor(display, DefaultColormap(display, screen), &col); + gcv.foreground = col.pixel; + dkyellowGC = XCreateGC(display, window, GCForeground, &gcv); + col.red = 65535; + col.green = 0; + col.blue = 0; + XAllocColor(display, DefaultColormap(display, screen), &col); + gcv.foreground = col.pixel; + redGC = XCreateGC(display, window, GCForeground, &gcv); + mon_init = 1; + } + + if (mon_init) + { + if (show_mon) + XMapWindow(display, window); + else + XUnmapWindow(display, window); + XResizeWindow(display, window, prefs.buckets, 128 + 8); + XFlush(display); + } +#endif +} + +void CompressFree(void) +{ +#ifdef USE_X + if (mon_init) + { + XFreeGC(display, blackGC); + XFreeGC(display, whiteGC); + XFreeGC(display, blueGC); + XFreeGC(display, yellowGC); + XFreeGC(display, dkyellowGC); + XFreeGC(display, redGC); + XDestroyWindow(display, window); + XCloseDisplay(display); + } +#endif + + if (peaks) + free(peaks); +} + +void CompressDo(void *data, unsigned int length) +{ + int16_t *audio = (int16_t *)data, *ap; + int peak, pos; + int i; + int gr, gf, gn; + static int pn = -1; +#ifdef STATS + static int clip = 0; +#endif + static int clipped = 0; + + if (!peaks) + return; + + if (pn == -1) + { + for (i = 0; i < prefs.buckets; i++) + peaks[i] = 0; + } + pn = (pn + 1)%prefs.buckets; + +#ifdef DEBUG + fprintf(stderr, "modifyNative16(0x%08x, %d)\n",(unsigned int)data, + length); +#endif + + /* Determine peak's value and position */ + peak = 1; + pos = 0; + +#ifdef DEBUG + fprintf(stderr, "finding peak(b=%d)\n", pn); +#endif + + ap = audio; + for (i = 0; i < length/2; i++) + { + int val = *ap; + if (val > peak) + { + peak = val; + pos = i; + } else if (-val > peak) + { + peak = -val; + pos = i; + } + ap++; + } + peaks[pn] = peak; + + /* Only draw if needed, of course */ +#ifdef USE_X + if (prefs.show_mon) + { + /* current amplitude */ + XDrawLine(display, window, whiteGC, + pn, 0, + pn, + 127 - + (peaks[pn]*gainCurrent >> (GAINSHIFT + 8))); + + /* amplification */ + XDrawLine(display, window, yellowGC, + pn, + 127 - (peaks[pn]*gainCurrent + >> (GAINSHIFT + 8)), + pn, 127); + + /* peak */ + XDrawLine(display, window, blackGC, + pn, 127 - (peaks[pn] >> 8), pn, 127); + + /* clip indicator */ + if (clipped) + XDrawLine(display, window, redGC, + (pn + prefs.buckets - 1)%prefs.buckets, + 126 - clipped/(length*512), + (pn + prefs.buckets - 1)%prefs.buckets, + 127); + clipped = 0; + + // target line + //XDrawPoint(display, window, redGC, + // pn, 127 - TARGET/256); + // amplification edge + XDrawLine(display, window, dkyellowGC, + pn, + 127 - (peaks[pn]*gainCurrent + >> (GAINSHIFT + 8)), + pn - 1, + 127 - + (peaks[(pn + prefs.buckets + - 1)%prefs.buckets]*gainCurrent + >> (GAINSHIFT + 8))); + } +#endif + + for (i = 0; i < prefs.buckets; i++) + { + if (peaks[i] > peak) + { + peak = peaks[i]; + pos = 0; + } + } + + /* Determine target gain */ + gn = (1 << GAINSHIFT)*prefs.target/peak; + + if (gn <(1 << GAINSHIFT)) + gn = 1 << GAINSHIFT; + + gainTarget = (gainTarget *((1 << prefs.gainsmooth) - 1) + gn) + >> prefs.gainsmooth; + + /* Give it an extra insignifigant nudge to counteract possible + ** rounding error + */ + + if (gn < gainTarget) + gainTarget--; + else if (gn > gainTarget) + gainTarget++; + + if (gainTarget > prefs.gainmax << GAINSHIFT) + gainTarget = prefs.gainmax << GAINSHIFT; + + +#ifdef USE_X + if (prefs.show_mon) + { + int x; + + /* peak*gain */ + XDrawPoint(display, window, redGC, + pn, + 127 - (peak*gainCurrent + >> (GAINSHIFT + 8))); + + // gain indicator + XFillRectangle(display, window, whiteGC, 0, 128, + prefs.buckets, 8); + x = (gainTarget - (1 << GAINSHIFT))*prefs.buckets + / ((prefs.gainmax - 1) << GAINSHIFT); + XDrawLine(display, window, redGC, x, + 128, x, 128 + 8); + + x = (gn - (1 << GAINSHIFT))*prefs.buckets + / ((prefs.gainmax - 1) << GAINSHIFT); + + XDrawLine(display, window, blackGC, + x, 132 - 1, + x, 132 + 1); + + // blue peak line + XDrawLine(display, window, blueGC, + 0, 127 - (peak >> 8), prefs.buckets, + 127 - (peak >> 8)); + XFlush(display); + XDrawLine(display, window, whiteGC, + 0, 127 - (peak >> 8), prefs.buckets, + 127 - (peak >> 8)); + } +#endif + + /* See if a peak is going to clip */ + gn = (1 << GAINSHIFT)*32768/peak; + + if (gn < gainTarget) + { + gainTarget = gn; + + if (prefs.anticlip) + pos = 0; + + } else + { + /* We're ramping up, so draw it out over the whole frame */ + pos = length; + } + + /* Determine gain rate necessary to make target */ + if (!pos) + pos = 1; + + gr = ((gainTarget - gainCurrent) << 16)/pos; + + /* Do the shiznit */ + gf = gainCurrent << 16; + +#ifdef STATS + fprintf(stderr, "\rgain = %2.2f%+.2e ", + gainCurrent*1.0/(1 << GAINSHIFT), + (gainTarget - gainCurrent)*1.0/(1 << GAINSHIFT)); +#endif + + ap = audio; + for (i = 0; i < length/2; i++) + { + int sample; + + /* Interpolate the gain */ + gainCurrent = gf >> 16; + if (i < pos) + gf += gr; + else if (i == pos) + gf = gainTarget << 16; + + /* Amplify */ + sample = (*ap)*gainCurrent >> GAINSHIFT; + if (sample < -32768) + { +#ifdef STATS + clip++; +#endif + clipped += -32768 - sample; + sample = -32768; + } else if (sample > 32767) + { +#ifdef STATS + clip++; +#endif + clipped += sample - 32767; + sample = 32767; + } + *ap++ = sample; + } +#ifdef STATS + fprintf(stderr, "clip %d b%-3d ", clip, pn); +#endif + +#ifdef DEBUG + fprintf(stderr, "\ndone\n"); +#endif +} + diff --git a/src/compress.h b/src/compress.h new file mode 100644 index 000000000..1993b47ad --- /dev/null +++ b/src/compress.h @@ -0,0 +1,29 @@ +/* compress.h +** interface to audio compression +*/ + +#ifndef COMPRESS_H +#define COMPRESS_H + +/* These are copied from the AudioCompress config.h, mainly because CompressDo + * needs GAINSHIFT defined. The rest are here so they can be used as defaults + * to pass to CompressCfg. */ +#define ANTICLIP 0 /* Strict clipping protection */ +#define TARGET 25000 /* Target level */ +#define GAINMAX 32 /* The maximum amount to amplify by */ +#define GAINSHIFT 10 /* How fine-grained the gain is */ +#define GAINSMOOTH 8 /* How much inertia ramping has*/ +#define BUCKETS 400 /* How long of a history to store */ + +void CompressCfg(int monitor, + int anticlip, + int target, + int maxgain, + int smooth, + int buckets); + +void CompressDo(void *data, unsigned int numSamples); + +void CompressFree(void); + +#endif diff --git a/src/conf.c b/src/conf.c index 995cbf892..6fbc47340 100644 --- a/src/conf.c +++ b/src/conf.c @@ -373,6 +373,8 @@ int getBoolConfigParam(char *name) if (strcmp("yes", param->value) == 0) return 1; else if (strcmp("no", param->value) == 0) return 0; + ERROR("%s is not \"yes\" or \"no\" on line %i\n", name, param->line); + return -2; } diff --git a/src/main.c b/src/main.c index 7dcc5240a..40e0274ed 100644 --- a/src/main.c +++ b/src/main.c @@ -40,6 +40,7 @@ #include "dbUtils.h" #include "../config.h" #include "utils.h" +#include "normalize.h" #include #include @@ -552,6 +553,7 @@ int main(int argc, char *argv[]) initPaths(); initAudioConfig(); initAudioDriver(); + initNormalization(); initPlaylist(); openDB(&options, argv[0]); @@ -601,6 +603,7 @@ int main(int argc, char *argv[]) finishPlaylist(); freePlayerData(); + finishNormalization(); finishAudioDriver(); finishAudioConfig(); finishVolume(); diff --git a/src/normalize.c b/src/normalize.c index 8c48f13bd..31394cbfb 100644 --- a/src/normalize.c +++ b/src/normalize.c @@ -16,85 +16,32 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#include -#include - +#include "compress.h" #include "conf.h" #include "normalize.h" -#include "playlist.h" -/* silence level, apparently this is Wrong (tm) */ -#define SILENCE_LEVEL (SHRT_MAX * 0.01) -/* not sure what this is :) */ -#define MID (SHRT_MAX * 0.25) +#include -#define MUL_MIN 0.1 -#define MUL_MAX 5.0 -#define NSAMPLES 128 -#define MIN_SAMPLE_SIZE 32000 +int normalizationEnabled; -#define clamp(a,min,max) (((a)>(max))?(max):(((a)<(min))?(min):(a))) +void initNormalization() +{ + normalizationEnabled = getBoolConfigParam(CONF_VOLUME_NORMALIZATION); + if (normalizationEnabled == -1) normalizationEnabled = 0; + else if (normalizationEnabled < 0) exit(EXIT_FAILURE); + + if (normalizationEnabled) + CompressCfg(0, ANTICLIP, TARGET, GAINMAX, GAINSMOOTH, BUCKETS); +} + +void finishNormalization() +{ + if (normalizationEnabled) CompressFree(); +} void normalizeData(char *buffer, int bufferSize, AudioFormat *format) { - static float multiplier = 1.0; - static int current_id = 0; - float average = 0.0; - static int old_song = 0; - int new_song = 0; - int total_length = 0; - int temp = 0; - int i = 0; - float root_mean_square = 0.0; /* the rms of the data */ - mpd_sint16 *data = (mpd_sint16 *) buffer; /* the audio data */ - int length = bufferSize / 2; /* the number of samples */ - static struct { - float avg; /* average sample 'level' */ - int len; /* sample size (used to weigh sample) */ - } mem[NSAMPLES]; + if ((format->bits != 16) || (format->channels != 2)) return; - /* operate only on 16 bit, 2 channel audio */ - if (format->bits != 16 && format->channels != 2) return; - - /* calculate the root mean square of the data */ - for (i = 0; i < length; i++) - root_mean_square += (float)(data[i] * data[i]); - - root_mean_square = sqrt(root_mean_square / (float)length); - - /* reset the multiplier if the song has changed */ - if (old_song != (new_song = getPlaylistCurrentSong())) { - old_song = new_song; - /* re-zero 'mem' */ - for (i = 0; i < NSAMPLES; i++) { - mem[i].avg = 0.0; - mem[i].len = 0; - } - current_id = 0; - } - - /* and now do magic tricks */ - for (i = 0; i < NSAMPLES; i++) { - average += mem[i].avg * (float)mem[i].len; - total_length += mem[i].len; - } - - if (total_length > MIN_SAMPLE_SIZE) { - average /= (float) total_length; - if (average >= SILENCE_LEVEL) { - multiplier = MID / average; - /* clamp multiplier */ - multiplier = clamp(multiplier, MUL_MIN, MUL_MAX); - } - } - - /* scale and clamp the samples */ - for (i = 0; i < length; i++) { - temp = data[i] * multiplier; - data[i] = clamp(temp, SHRT_MIN, SHRT_MAX); - } - - mem[current_id].len = bufferSize / 2; - mem[current_id].avg = multiplier * root_mean_square; - current_id = (current_id + 1) % NSAMPLES; /* increment current_id */ + CompressDo(buffer, bufferSize); } diff --git a/src/normalize.h b/src/normalize.h index f131669a8..73e442882 100644 --- a/src/normalize.h +++ b/src/normalize.h @@ -16,6 +16,17 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#ifndef NORMALIZE_H +#define NORMALIZE_H + #include "audio.h" +extern int normalizationEnabled; + +void initNormalization(); + +void finishNormalization(); + void normalizeData(char *buffer, int bufferSize, AudioFormat *format); + +#endif /* !NORMALIZE_H */ diff --git a/src/outputBuffer.c b/src/outputBuffer.c index 90f2ef2db..fe868d54e 100644 --- a/src/outputBuffer.c +++ b/src/outputBuffer.c @@ -78,22 +78,6 @@ int sendDataToOutputBuffer(OutputBuffer * cb, InputStream * inStream, size_t datalen; static char *convBuffer = NULL; static long convBufferLen = 0; - static int normalEnable = -1; - ConfigParam *param; - - if (normalEnable == -1) { - normalEnable = getBoolConfigParam(CONF_VOLUME_NORMALIZATION); - if (normalEnable == -1) { - /* not set */ - normalEnable = 0; - } else if (normalEnable < 0) { - param = getConfigParam(CONF_VOLUME_NORMALIZATION); - WARNING("%s is not \"yes\" or \"no\" on line %i, " - "disabling\n", CONF_VOLUME_NORMALIZATION, - param->line); - normalEnable = 0; - } - } if (cmpAudioFormat(&(cb->audioFormat), &(dc->audioFormat)) == 0) { data = dataIn; @@ -115,11 +99,10 @@ int sendDataToOutputBuffer(OutputBuffer * cb, InputStream * inStream, &(cb->audioFormat), data); } - if (replayGainInfo && (replayGainState != REPLAYGAIN_OFF)) { + if (replayGainInfo && (replayGainState != REPLAYGAIN_OFF)) doReplayGain(replayGainInfo, data, datalen, &cb->audioFormat); - } else if (normalEnable) { + else if (normalizationEnabled) normalizeData(data, datalen, &cb->audioFormat); - } while (datalen) { if (currentChunk != cb->end) { diff --git a/src/playlist.c b/src/playlist.c index 37c2aeb5c..2f4feee7d 100644 --- a/src/playlist.c +++ b/src/playlist.c @@ -167,15 +167,8 @@ void initPlaylist(void) } playlist_saveAbsolutePaths = getBoolConfigParam(CONF_SAVE_ABSOLUTE_PATHS); - if (playlist_saveAbsolutePaths == -1) { - /* not set */ - playlist_saveAbsolutePaths = 0; - } else if (playlist_saveAbsolutePaths < 0) { - param = getConfigParam(CONF_SAVE_ABSOLUTE_PATHS); - ERROR("%s is not \"yes\" or \"no\" on line %i\n", - CONF_SAVE_ABSOLUTE_PATHS, param->line); - exit(EXIT_FAILURE); - } + if (playlist_saveAbsolutePaths == -1) playlist_saveAbsolutePaths = 0; + else if (playlist_saveAbsolutePaths < 0) exit(EXIT_FAILURE); playlist.songs = malloc(sizeof(Song *) * playlist_max_length); playlist.songMod = malloc(sizeof(mpd_uint32) * playlist_max_length);