From 1f7decc1a0a7e0f2fb547ee740ee0d7b659c0406 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 12 Feb 2013 09:53:33 +0100 Subject: Rename af_volnorm to af_drc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous name of this filter was misleading, because it doesn’t actually normalize volume levels. What it does is closer to performing low-quality dynamic range compression, hence it is now called af_drc. --- DOCS/man/en/af.rst | 8 +- Makefile | 2 +- audio/filter/af.c | 4 +- audio/filter/af_drc.c | 353 ++++++++++++++++++++++++++++++++++++++++++++++ audio/filter/af_volnorm.c | 353 ---------------------------------------------- 5 files changed, 362 insertions(+), 358 deletions(-) create mode 100644 audio/filter/af_drc.c delete mode 100644 audio/filter/af_volnorm.c diff --git a/DOCS/man/en/af.rst b/DOCS/man/en/af.rst index aff84114d0..a47a63b8e2 100644 --- a/DOCS/man/en/af.rst +++ b/DOCS/man/en/af.rst @@ -428,8 +428,9 @@ extrastereo[=mul] (average of both channels), with 1.0 sound will be unchanged, with -1.0 left and right channels will be swapped. -volnorm[=method:target] - Maximizes the volume without distorting the sound. +drc[=method:target] + Applies dynamic range compression. This maximizes the volume by compressing + the audio signal's dynamic range. Sets the used method. @@ -445,6 +446,9 @@ volnorm[=method:target] Sets the target amplitude as a fraction of the maximum for the sample type (default: 0.25). + *NOTE*: This filter can cause distortion with audio signals that have a + very large dynamic range. + ladspa=file:label[:controls...] Load a LADSPA (Linux Audio Developer's Simple Plugin API) plugin. This filter is reentrant, so multiple LADSPA plugins can be used at once. diff --git a/Makefile b/Makefile index 53f97951a1..91eda102b6 100644 --- a/Makefile +++ b/Makefile @@ -143,7 +143,7 @@ SOURCES = talloc.c \ audio/filter/af_surround.c \ audio/filter/af_sweep.c \ audio/filter/af_tools.c \ - audio/filter/af_volnorm.c \ + audio/filter/af_drc.c \ audio/filter/af_volume.c \ audio/filter/filter.c \ audio/filter/window.c \ diff --git a/audio/filter/af.c b/audio/filter/af.c index 8afedbcfe5..71f4c67b51 100644 --- a/audio/filter/af.c +++ b/audio/filter/af.c @@ -35,7 +35,7 @@ extern struct af_info af_info_pan; extern struct af_info af_info_surround; extern struct af_info af_info_sub; extern struct af_info af_info_export; -extern struct af_info af_info_volnorm; +extern struct af_info af_info_drc; extern struct af_info af_info_extrastereo; extern struct af_info af_info_lavcac3enc; extern struct af_info af_info_lavcresample; @@ -62,7 +62,7 @@ static struct af_info* filter_list[]={ #ifdef HAVE_SYS_MMAN_H &af_info_export, #endif - &af_info_volnorm, + &af_info_drc, &af_info_extrastereo, &af_info_lavcac3enc, &af_info_lavcresample, diff --git a/audio/filter/af_drc.c b/audio/filter/af_drc.c new file mode 100644 index 0000000000..ba1e10c0a7 --- /dev/null +++ b/audio/filter/af_drc.c @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2004 Alex Beregszaszi & Pierre Lombard + * + * This file is part of MPlayer. + * + * MPlayer 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. + * + * MPlayer 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 MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include +#include +#include + +#include "af.h" + +// Methods: +// 1: uses a 1 value memory and coefficients new=a*old+b*cur (with a+b=1) +// 2: uses several samples to smooth the variations (standard weighted mean +// on past samples) + +// Size of the memory array +// FIXME: should depend on the frequency of the data (should be a few seconds) +#define NSAMPLES 128 + +// If summing all the mem[].len is lower than MIN_SAMPLE_SIZE bytes, then we +// choose to ignore the computed value as it's not significant enough +// FIXME: should depend on the frequency of the data (0.5s maybe) +#define MIN_SAMPLE_SIZE 32000 + +// mul is the value by which the samples are scaled +// and has to be in [MUL_MIN, MUL_MAX] +#define MUL_INIT 1.0 +#define MUL_MIN 0.1 +#define MUL_MAX 5.0 + +// Silence level +// FIXME: should be relative to the level of the samples +#define SIL_S16 (SHRT_MAX * 0.01) +#define SIL_FLOAT 0.01 + +// smooth must be in ]0.0, 1.0[ +#define SMOOTH_MUL 0.06 +#define SMOOTH_LASTAVG 0.06 + +#define DEFAULT_TARGET 0.25 + +// Data for specific instances of this filter +typedef struct af_volume_s +{ + int method; // method used + float mul; + // method 1 + float lastavg; // history value of the filter + // method 2 + int idx; + struct { + float avg; // average level of the sample + int len; // sample size (weight) + } mem[NSAMPLES]; + // "Ideal" level + float mid_s16; + float mid_float; +}af_drc_t; + +// Initialization and runtime control +static int control(struct af_instance* af, int cmd, void* arg) +{ + af_drc_t* s = (af_drc_t*)af->setup; + + switch(cmd){ + case AF_CONTROL_REINIT: + // Sanity check + if(!arg) return AF_ERROR; + + af->data->rate = ((struct mp_audio*)arg)->rate; + af->data->nch = ((struct mp_audio*)arg)->nch; + + if(((struct mp_audio*)arg)->format == (AF_FORMAT_S16_NE)){ + af->data->format = AF_FORMAT_S16_NE; + af->data->bps = 2; + }else{ + af->data->format = AF_FORMAT_FLOAT_NE; + af->data->bps = 4; + } + return af_test_output(af,(struct mp_audio*)arg); + case AF_CONTROL_COMMAND_LINE:{ + int i = 0; + float target = DEFAULT_TARGET; + sscanf((char*)arg,"%d:%f", &i, &target); + if (i != 1 && i != 2) + return AF_ERROR; + s->method = i-1; + s->mid_s16 = ((float)SHRT_MAX) * target; + s->mid_float = target; + return AF_OK; + } + } + return AF_UNKNOWN; +} + +// Deallocate memory +static void uninit(struct af_instance* af) +{ + free(af->data); + free(af->setup); +} + +static void method1_int16(af_drc_t *s, struct mp_audio *c) +{ + register int i = 0; + int16_t *data = (int16_t*)c->audio; // Audio data + int len = c->len/2; // Number of samples + float curavg = 0.0, newavg, neededmul; + int tmp; + + for (i = 0; i < len; i++) + { + tmp = data[i]; + curavg += tmp * tmp; + } + curavg = sqrt(curavg / (float) len); + + // Evaluate an adequate 'mul' coefficient based on previous state, current + // samples level, etc + + if (curavg > SIL_S16) + { + neededmul = s->mid_s16 / (curavg * s->mul); + s->mul = (1.0 - SMOOTH_MUL) * s->mul + SMOOTH_MUL * neededmul; + + // clamp the mul coefficient + s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); + } + + // Scale & clamp the samples + for (i = 0; i < len; i++) + { + tmp = s->mul * data[i]; + tmp = clamp(tmp, SHRT_MIN, SHRT_MAX); + data[i] = tmp; + } + + // Evaulation of newavg (not 100% accurate because of values clamping) + newavg = s->mul * curavg; + + // Stores computed values for future smoothing + s->lastavg = (1.0 - SMOOTH_LASTAVG) * s->lastavg + SMOOTH_LASTAVG * newavg; +} + +static void method1_float(af_drc_t *s, struct mp_audio *c) +{ + register int i = 0; + float *data = (float*)c->audio; // Audio data + int len = c->len/4; // Number of samples + float curavg = 0.0, newavg, neededmul, tmp; + + for (i = 0; i < len; i++) + { + tmp = data[i]; + curavg += tmp * tmp; + } + curavg = sqrt(curavg / (float) len); + + // Evaluate an adequate 'mul' coefficient based on previous state, current + // samples level, etc + + if (curavg > SIL_FLOAT) // FIXME + { + neededmul = s->mid_float / (curavg * s->mul); + s->mul = (1.0 - SMOOTH_MUL) * s->mul + SMOOTH_MUL * neededmul; + + // clamp the mul coefficient + s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); + } + + // Scale & clamp the samples + for (i = 0; i < len; i++) + data[i] *= s->mul; + + // Evaulation of newavg (not 100% accurate because of values clamping) + newavg = s->mul * curavg; + + // Stores computed values for future smoothing + s->lastavg = (1.0 - SMOOTH_LASTAVG) * s->lastavg + SMOOTH_LASTAVG * newavg; +} + +static void method2_int16(af_drc_t *s, struct mp_audio *c) +{ + register int i = 0; + int16_t *data = (int16_t*)c->audio; // Audio data + int len = c->len/2; // Number of samples + float curavg = 0.0, newavg, avg = 0.0; + int tmp, totallen = 0; + + for (i = 0; i < len; i++) + { + tmp = data[i]; + curavg += tmp * tmp; + } + curavg = sqrt(curavg / (float) len); + + // Evaluate an adequate 'mul' coefficient based on previous state, current + // samples level, etc + for (i = 0; i < NSAMPLES; i++) + { + avg += s->mem[i].avg * (float)s->mem[i].len; + totallen += s->mem[i].len; + } + + if (totallen > MIN_SAMPLE_SIZE) + { + avg /= (float)totallen; + if (avg >= SIL_S16) + { + s->mul = s->mid_s16 / avg; + s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); + } + } + + // Scale & clamp the samples + for (i = 0; i < len; i++) + { + tmp = s->mul * data[i]; + tmp = clamp(tmp, SHRT_MIN, SHRT_MAX); + data[i] = tmp; + } + + // Evaulation of newavg (not 100% accurate because of values clamping) + newavg = s->mul * curavg; + + // Stores computed values for future smoothing + s->mem[s->idx].len = len; + s->mem[s->idx].avg = newavg; + s->idx = (s->idx + 1) % NSAMPLES; +} + +static void method2_float(af_drc_t *s, struct mp_audio *c) +{ + register int i = 0; + float *data = (float*)c->audio; // Audio data + int len = c->len/4; // Number of samples + float curavg = 0.0, newavg, avg = 0.0, tmp; + int totallen = 0; + + for (i = 0; i < len; i++) + { + tmp = data[i]; + curavg += tmp * tmp; + } + curavg = sqrt(curavg / (float) len); + + // Evaluate an adequate 'mul' coefficient based on previous state, current + // samples level, etc + for (i = 0; i < NSAMPLES; i++) + { + avg += s->mem[i].avg * (float)s->mem[i].len; + totallen += s->mem[i].len; + } + + if (totallen > MIN_SAMPLE_SIZE) + { + avg /= (float)totallen; + if (avg >= SIL_FLOAT) + { + s->mul = s->mid_float / avg; + s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); + } + } + + // Scale & clamp the samples + for (i = 0; i < len; i++) + data[i] *= s->mul; + + // Evaulation of newavg (not 100% accurate because of values clamping) + newavg = s->mul * curavg; + + // Stores computed values for future smoothing + s->mem[s->idx].len = len; + s->mem[s->idx].avg = newavg; + s->idx = (s->idx + 1) % NSAMPLES; +} + +// Filter data through filter +static struct mp_audio* play(struct af_instance* af, struct mp_audio* data) +{ + af_drc_t *s = af->setup; + + if(af->data->format == (AF_FORMAT_S16_NE)) + { + if (s->method) + method2_int16(s, data); + else + method1_int16(s, data); + } + else if(af->data->format == (AF_FORMAT_FLOAT_NE)) + { + if (s->method) + method2_float(s, data); + else + method1_float(s, data); + } + return data; +} + +// Allocate memory and set function pointers +static int af_open(struct af_instance* af){ + int i = 0; + af->control=control; + af->uninit=uninit; + af->play=play; + af->mul=1; + af->data=calloc(1,sizeof(struct mp_audio)); + af->setup=calloc(1,sizeof(af_drc_t)); + if(af->data == NULL || af->setup == NULL) + return AF_ERROR; + + ((af_drc_t*)af->setup)->mul = MUL_INIT; + ((af_drc_t*)af->setup)->lastavg = ((float)SHRT_MAX) * DEFAULT_TARGET; + ((af_drc_t*)af->setup)->idx = 0; + ((af_drc_t*)af->setup)->mid_s16 = ((float)SHRT_MAX) * DEFAULT_TARGET; + ((af_drc_t*)af->setup)->mid_float = DEFAULT_TARGET; + for (i = 0; i < NSAMPLES; i++) + { + ((af_drc_t*)af->setup)->mem[i].len = 0; + ((af_drc_t*)af->setup)->mem[i].avg = 0; + } + return AF_OK; +} + +// Description of this filter +struct af_info af_info_drc = { + "Dynamic range compression filter", + "drc", + "Alex Beregszaszi & Pierre Lombard", + "", + AF_FLAGS_NOT_REENTRANT, + af_open +}; diff --git a/audio/filter/af_volnorm.c b/audio/filter/af_volnorm.c deleted file mode 100644 index f49bbc185a..0000000000 --- a/audio/filter/af_volnorm.c +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright (C) 2004 Alex Beregszaszi & Pierre Lombard - * - * This file is part of MPlayer. - * - * MPlayer 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. - * - * MPlayer 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 MPlayer; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include -#include -#include - -#include -#include -#include - -#include "af.h" - -// Methods: -// 1: uses a 1 value memory and coefficients new=a*old+b*cur (with a+b=1) -// 2: uses several samples to smooth the variations (standard weighted mean -// on past samples) - -// Size of the memory array -// FIXME: should depend on the frequency of the data (should be a few seconds) -#define NSAMPLES 128 - -// If summing all the mem[].len is lower than MIN_SAMPLE_SIZE bytes, then we -// choose to ignore the computed value as it's not significant enough -// FIXME: should depend on the frequency of the data (0.5s maybe) -#define MIN_SAMPLE_SIZE 32000 - -// mul is the value by which the samples are scaled -// and has to be in [MUL_MIN, MUL_MAX] -#define MUL_INIT 1.0 -#define MUL_MIN 0.1 -#define MUL_MAX 5.0 - -// Silence level -// FIXME: should be relative to the level of the samples -#define SIL_S16 (SHRT_MAX * 0.01) -#define SIL_FLOAT 0.01 - -// smooth must be in ]0.0, 1.0[ -#define SMOOTH_MUL 0.06 -#define SMOOTH_LASTAVG 0.06 - -#define DEFAULT_TARGET 0.25 - -// Data for specific instances of this filter -typedef struct af_volume_s -{ - int method; // method used - float mul; - // method 1 - float lastavg; // history value of the filter - // method 2 - int idx; - struct { - float avg; // average level of the sample - int len; // sample size (weight) - } mem[NSAMPLES]; - // "Ideal" level - float mid_s16; - float mid_float; -}af_volnorm_t; - -// Initialization and runtime control -static int control(struct af_instance* af, int cmd, void* arg) -{ - af_volnorm_t* s = (af_volnorm_t*)af->setup; - - switch(cmd){ - case AF_CONTROL_REINIT: - // Sanity check - if(!arg) return AF_ERROR; - - af->data->rate = ((struct mp_audio*)arg)->rate; - af->data->nch = ((struct mp_audio*)arg)->nch; - - if(((struct mp_audio*)arg)->format == (AF_FORMAT_S16_NE)){ - af->data->format = AF_FORMAT_S16_NE; - af->data->bps = 2; - }else{ - af->data->format = AF_FORMAT_FLOAT_NE; - af->data->bps = 4; - } - return af_test_output(af,(struct mp_audio*)arg); - case AF_CONTROL_COMMAND_LINE:{ - int i = 0; - float target = DEFAULT_TARGET; - sscanf((char*)arg,"%d:%f", &i, &target); - if (i != 1 && i != 2) - return AF_ERROR; - s->method = i-1; - s->mid_s16 = ((float)SHRT_MAX) * target; - s->mid_float = target; - return AF_OK; - } - } - return AF_UNKNOWN; -} - -// Deallocate memory -static void uninit(struct af_instance* af) -{ - free(af->data); - free(af->setup); -} - -static void method1_int16(af_volnorm_t *s, struct mp_audio *c) -{ - register int i = 0; - int16_t *data = (int16_t*)c->audio; // Audio data - int len = c->len/2; // Number of samples - float curavg = 0.0, newavg, neededmul; - int tmp; - - for (i = 0; i < len; i++) - { - tmp = data[i]; - curavg += tmp * tmp; - } - curavg = sqrt(curavg / (float) len); - - // Evaluate an adequate 'mul' coefficient based on previous state, current - // samples level, etc - - if (curavg > SIL_S16) - { - neededmul = s->mid_s16 / (curavg * s->mul); - s->mul = (1.0 - SMOOTH_MUL) * s->mul + SMOOTH_MUL * neededmul; - - // clamp the mul coefficient - s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); - } - - // Scale & clamp the samples - for (i = 0; i < len; i++) - { - tmp = s->mul * data[i]; - tmp = clamp(tmp, SHRT_MIN, SHRT_MAX); - data[i] = tmp; - } - - // Evaulation of newavg (not 100% accurate because of values clamping) - newavg = s->mul * curavg; - - // Stores computed values for future smoothing - s->lastavg = (1.0 - SMOOTH_LASTAVG) * s->lastavg + SMOOTH_LASTAVG * newavg; -} - -static void method1_float(af_volnorm_t *s, struct mp_audio *c) -{ - register int i = 0; - float *data = (float*)c->audio; // Audio data - int len = c->len/4; // Number of samples - float curavg = 0.0, newavg, neededmul, tmp; - - for (i = 0; i < len; i++) - { - tmp = data[i]; - curavg += tmp * tmp; - } - curavg = sqrt(curavg / (float) len); - - // Evaluate an adequate 'mul' coefficient based on previous state, current - // samples level, etc - - if (curavg > SIL_FLOAT) // FIXME - { - neededmul = s->mid_float / (curavg * s->mul); - s->mul = (1.0 - SMOOTH_MUL) * s->mul + SMOOTH_MUL * neededmul; - - // clamp the mul coefficient - s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); - } - - // Scale & clamp the samples - for (i = 0; i < len; i++) - data[i] *= s->mul; - - // Evaulation of newavg (not 100% accurate because of values clamping) - newavg = s->mul * curavg; - - // Stores computed values for future smoothing - s->lastavg = (1.0 - SMOOTH_LASTAVG) * s->lastavg + SMOOTH_LASTAVG * newavg; -} - -static void method2_int16(af_volnorm_t *s, struct mp_audio *c) -{ - register int i = 0; - int16_t *data = (int16_t*)c->audio; // Audio data - int len = c->len/2; // Number of samples - float curavg = 0.0, newavg, avg = 0.0; - int tmp, totallen = 0; - - for (i = 0; i < len; i++) - { - tmp = data[i]; - curavg += tmp * tmp; - } - curavg = sqrt(curavg / (float) len); - - // Evaluate an adequate 'mul' coefficient based on previous state, current - // samples level, etc - for (i = 0; i < NSAMPLES; i++) - { - avg += s->mem[i].avg * (float)s->mem[i].len; - totallen += s->mem[i].len; - } - - if (totallen > MIN_SAMPLE_SIZE) - { - avg /= (float)totallen; - if (avg >= SIL_S16) - { - s->mul = s->mid_s16 / avg; - s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); - } - } - - // Scale & clamp the samples - for (i = 0; i < len; i++) - { - tmp = s->mul * data[i]; - tmp = clamp(tmp, SHRT_MIN, SHRT_MAX); - data[i] = tmp; - } - - // Evaulation of newavg (not 100% accurate because of values clamping) - newavg = s->mul * curavg; - - // Stores computed values for future smoothing - s->mem[s->idx].len = len; - s->mem[s->idx].avg = newavg; - s->idx = (s->idx + 1) % NSAMPLES; -} - -static void method2_float(af_volnorm_t *s, struct mp_audio *c) -{ - register int i = 0; - float *data = (float*)c->audio; // Audio data - int len = c->len/4; // Number of samples - float curavg = 0.0, newavg, avg = 0.0, tmp; - int totallen = 0; - - for (i = 0; i < len; i++) - { - tmp = data[i]; - curavg += tmp * tmp; - } - curavg = sqrt(curavg / (float) len); - - // Evaluate an adequate 'mul' coefficient based on previous state, current - // samples level, etc - for (i = 0; i < NSAMPLES; i++) - { - avg += s->mem[i].avg * (float)s->mem[i].len; - totallen += s->mem[i].len; - } - - if (totallen > MIN_SAMPLE_SIZE) - { - avg /= (float)totallen; - if (avg >= SIL_FLOAT) - { - s->mul = s->mid_float / avg; - s->mul = clamp(s->mul, MUL_MIN, MUL_MAX); - } - } - - // Scale & clamp the samples - for (i = 0; i < len; i++) - data[i] *= s->mul; - - // Evaulation of newavg (not 100% accurate because of values clamping) - newavg = s->mul * curavg; - - // Stores computed values for future smoothing - s->mem[s->idx].len = len; - s->mem[s->idx].avg = newavg; - s->idx = (s->idx + 1) % NSAMPLES; -} - -// Filter data through filter -static struct mp_audio* play(struct af_instance* af, struct mp_audio* data) -{ - af_volnorm_t *s = af->setup; - - if(af->data->format == (AF_FORMAT_S16_NE)) - { - if (s->method) - method2_int16(s, data); - else - method1_int16(s, data); - } - else if(af->data->format == (AF_FORMAT_FLOAT_NE)) - { - if (s->method) - method2_float(s, data); - else - method1_float(s, data); - } - return data; -} - -// Allocate memory and set function pointers -static int af_open(struct af_instance* af){ - int i = 0; - af->control=control; - af->uninit=uninit; - af->play=play; - af->mul=1; - af->data=calloc(1,sizeof(struct mp_audio)); - af->setup=calloc(1,sizeof(af_volnorm_t)); - if(af->data == NULL || af->setup == NULL) - return AF_ERROR; - - ((af_volnorm_t*)af->setup)->mul = MUL_INIT; - ((af_volnorm_t*)af->setup)->lastavg = ((float)SHRT_MAX) * DEFAULT_TARGET; - ((af_volnorm_t*)af->setup)->idx = 0; - ((af_volnorm_t*)af->setup)->mid_s16 = ((float)SHRT_MAX) * DEFAULT_TARGET; - ((af_volnorm_t*)af->setup)->mid_float = DEFAULT_TARGET; - for (i = 0; i < NSAMPLES; i++) - { - ((af_volnorm_t*)af->setup)->mem[i].len = 0; - ((af_volnorm_t*)af->setup)->mem[i].avg = 0; - } - return AF_OK; -} - -// Description of this filter -struct af_info af_info_volnorm = { - "Volume normalizer filter", - "volnorm", - "Alex Beregszaszi & Pierre Lombard", - "", - AF_FLAGS_NOT_REENTRANT, - af_open -}; -- cgit v1.2.3