avfilter/af_sofalizer: switch to libmysofa

Signed-off-by: Paul B Mahol <onemda@gmail.com>
pull/262/head
Paul B Mahol 8 years ago
parent a32a6b4201
commit 2336c76b22
  1. 1
      Changelog
  2. 8
      configure
  3. 2
      doc/filters.texi
  4. 700
      libavfilter/af_sofalizer.c

@ -17,6 +17,7 @@ version <next>:
- remove the libnut muxer/demuxer wrappers - remove the libnut muxer/demuxer wrappers
- remove the libschroedinger encoder/decoder wrappers - remove the libschroedinger encoder/decoder wrappers
- surround audio filter - surround audio filter
- sofalizer filter switched to libmysofa
version 3.3: version 3.3:
- CrystalHD decoder moved to new decode API - CrystalHD decoder moved to new decode API

8
configure vendored

@ -277,7 +277,7 @@ External library support:
--disable-lzma disable lzma [autodetect] --disable-lzma disable lzma [autodetect]
--enable-decklink enable Blackmagic DeckLink I/O support [no] --enable-decklink enable Blackmagic DeckLink I/O support [no]
--enable-mediacodec enable Android MediaCodec support [no] --enable-mediacodec enable Android MediaCodec support [no]
--enable-netcdf enable NetCDF, needed for sofalizer filter [no] --enable-libmysofa enable libmysofa, needed for sofalizer filter [no]
--enable-openal enable OpenAL 1.1 capture support [no] --enable-openal enable OpenAL 1.1 capture support [no]
--enable-opencl enable OpenCL code --enable-opencl enable OpenCL code
--enable-opengl enable OpenGL rendering [no] --enable-opengl enable OpenGL rendering [no]
@ -1550,6 +1550,7 @@ EXTERNAL_LIBRARY_LIST="
libkvazaar libkvazaar
libmodplug libmodplug
libmp3lame libmp3lame
libmysofa
libopencv libopencv
libopenh264 libopenh264
libopenjpeg libopenjpeg
@ -1576,7 +1577,6 @@ EXTERNAL_LIBRARY_LIST="
libzmq libzmq
libzvbi libzvbi
mediacodec mediacodec
netcdf
openal openal
opencl opencl
opengl opengl
@ -3156,7 +3156,7 @@ showspectrumpic_filter_deps="avcodec"
showspectrumpic_filter_select="fft" showspectrumpic_filter_select="fft"
signature_filter_deps="gpl avcodec avformat" signature_filter_deps="gpl avcodec avformat"
smartblur_filter_deps="gpl swscale" smartblur_filter_deps="gpl swscale"
sofalizer_filter_deps="netcdf avcodec" sofalizer_filter_deps="libmysofa avcodec"
sofalizer_filter_select="fft" sofalizer_filter_select="fft"
spectrumsynth_filter_deps="avcodec" spectrumsynth_filter_deps="avcodec"
spectrumsynth_filter_select="fft" spectrumsynth_filter_select="fft"
@ -5822,6 +5822,7 @@ enabled libmfx && { use_pkg_config libmfx "mfx/mfxvideo.h" MFXInit ||
{ require libmfx "mfx/mfxvideo.h" MFXInit -llibmfx && warn "using libmfx without pkg-config"; } } { require libmfx "mfx/mfxvideo.h" MFXInit -llibmfx && warn "using libmfx without pkg-config"; } }
enabled libmodplug && require_pkg_config libmodplug libmodplug/modplug.h ModPlug_Load enabled libmodplug && require_pkg_config libmodplug libmodplug/modplug.h ModPlug_Load
enabled libmp3lame && require "libmp3lame >= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame enabled libmp3lame && require "libmp3lame >= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame
enabled libmysofa && require libmysofa "mysofa.h" mysofa_load -lmysofa
enabled libnpp && require libnpp npp.h nppGetLibVersion -lnppi -lnppc enabled libnpp && require libnpp npp.h nppGetLibVersion -lnppi -lnppc
enabled libopencore_amrnb && require libopencore_amrnb opencore-amrnb/interf_dec.h Decoder_Interface_init -lopencore-amrnb enabled libopencore_amrnb && require libopencore_amrnb opencore-amrnb/interf_dec.h Decoder_Interface_init -lopencore-amrnb
enabled libopencore_amrwb && require libopencore_amrwb opencore-amrwb/dec_if.h D_IF_init -lopencore-amrwb enabled libopencore_amrwb && require libopencore_amrwb opencore-amrwb/dec_if.h D_IF_init -lopencore-amrwb
@ -5919,7 +5920,6 @@ enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_co
check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } || check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } ||
die "ERROR: mmal not found" && die "ERROR: mmal not found" &&
check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; } check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; }
enabled netcdf && require_pkg_config netcdf netcdf.h nc_inq_libvers
enabled openal && { { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do enabled openal && { { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do
check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } || check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } ||
die "ERROR: openal not found"; } && die "ERROR: openal not found"; } &&

@ -3571,7 +3571,7 @@ SOFAlizer is developed at the Acoustics Research Institute (ARI) of the
Austrian Academy of Sciences. Austrian Academy of Sciences.
To enable compilation of this filter you need to configure FFmpeg with To enable compilation of this filter you need to configure FFmpeg with
@code{--enable-netcdf}. @code{--enable-libmysofa}.
The filter accepts the following options: The filter accepts the following options:

@ -26,7 +26,7 @@
*****************************************************************************/ *****************************************************************************/
#include <math.h> #include <math.h>
#include <netcdf.h> #include <mysofa.h>
#include "libavcodec/avfft.h" #include "libavcodec/avfft.h"
#include "libavutil/avstring.h" #include "libavutil/avstring.h"
@ -41,18 +41,12 @@
#define TIME_DOMAIN 0 #define TIME_DOMAIN 0
#define FREQUENCY_DOMAIN 1 #define FREQUENCY_DOMAIN 1
typedef struct NCSofa { /* contains data of one SOFA file */ typedef struct MySofa { /* contains data of one SOFA file */
int ncid; /* netCDF ID of the opened SOFA file */ struct MYSOFA_EASY *easy;
int n_samples; /* length of one impulse response (IR) */ int n_samples; /* length of one impulse response (IR) */
int m_dim; /* number of measurement positions */ float *lir, *rir; /* IRs (time-domain) */
int *data_delay; /* broadband delay of each IR */ int max_delay;
/* all measurement positions for each receiver (i.e. ear): */ } MySofa;
float *sp_a; /* azimuth angles */
float *sp_e; /* elevation angles */
float *sp_r; /* radii */
/* data at each measurement position for each receiver: */
float *data_ir; /* IRs (time-domain) */
} NCSofa;
typedef struct VirtualSpeaker { typedef struct VirtualSpeaker {
uint8_t set; uint8_t set;
@ -64,7 +58,7 @@ typedef struct SOFAlizerContext {
const AVClass *class; const AVClass *class;
char *filename; /* name of SOFA file */ char *filename; /* name of SOFA file */
NCSofa sofa; /* contains data of the SOFA file */ MySofa sofa; /* contains data of the SOFA file */
int sample_rate; /* sample rate from SOFA file */ int sample_rate; /* sample rate from SOFA file */
float *speaker_azim; /* azimuth of the virtual loudspeakers */ float *speaker_azim; /* azimuth of the virtual loudspeakers */
@ -107,271 +101,33 @@ typedef struct SOFAlizerContext {
AVFloatDSPContext *fdsp; AVFloatDSPContext *fdsp;
} SOFAlizerContext; } SOFAlizerContext;
static int close_sofa(struct NCSofa *sofa) static int close_sofa(struct MySofa *sofa)
{ {
av_freep(&sofa->data_delay); mysofa_close(sofa->easy);
av_freep(&sofa->sp_a); sofa->easy = NULL;
av_freep(&sofa->sp_e);
av_freep(&sofa->sp_r);
av_freep(&sofa->data_ir);
nc_close(sofa->ncid);
sofa->ncid = 0;
return 0; return 0;
} }
static int load_sofa(AVFilterContext *ctx, char *filename, int *samplingrate) static int preload_sofa(AVFilterContext *ctx, char *filename, int *samplingrate)
{ {
struct SOFAlizerContext *s = ctx->priv; struct SOFAlizerContext *s = ctx->priv;
/* variables associated with content of SOFA file: */ struct MYSOFA_HRTF *mysofa;
int ncid, n_dims, n_vars, n_gatts, n_unlim_dim_id, status; int ret;
char data_delay_dim_name[NC_MAX_NAME];
float *sp_a, *sp_e, *sp_r, *data_ir;
char *sofa_conventions;
char dim_name[NC_MAX_NAME]; /* names of netCDF dimensions */
size_t *dim_length; /* lengths of netCDF dimensions */
char *text;
unsigned int sample_rate;
int data_delay_dim_id[2];
int samplingrate_id;
int data_delay_id;
int n_samples;
int m_dim_id = -1;
int n_dim_id = -1;
int data_ir_id;
size_t att_len;
int m_dim;
int *data_delay;
int sp_id;
int i, ret;
s->sofa.ncid = 0;
status = nc_open(filename, NC_NOWRITE, &ncid); /* open SOFA file read-only */
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Can't find SOFA-file '%s'\n", filename);
return AVERROR(EINVAL);
}
/* get number of dimensions, vars, global attributes and Id of unlimited dimensions: */
nc_inq(ncid, &n_dims, &n_vars, &n_gatts, &n_unlim_dim_id);
/* -- get number of measurements ("M") and length of one IR ("N") -- */
dim_length = av_malloc_array(n_dims, sizeof(*dim_length));
if (!dim_length) {
nc_close(ncid);
return AVERROR(ENOMEM);
}
for (i = 0; i < n_dims; i++) { /* go through all dimensions of file */
nc_inq_dim(ncid, i, (char *)&dim_name, &dim_length[i]); /* get dimensions */
if (!strncmp("M", (const char *)&dim_name, 1)) /* get ID of dimension "M" */
m_dim_id = i;
if (!strncmp("N", (const char *)&dim_name, 1)) /* get ID of dimension "N" */
n_dim_id = i;
}
if ((m_dim_id == -1) || (n_dim_id == -1)) { /* dimension "M" or "N" couldn't be found */
av_log(ctx, AV_LOG_ERROR, "Can't find required dimensions in SOFA file.\n");
av_freep(&dim_length);
nc_close(ncid);
return AVERROR(EINVAL);
}
n_samples = dim_length[n_dim_id]; /* get length of one IR */
m_dim = dim_length[m_dim_id]; /* get number of measurements */
av_freep(&dim_length);
/* -- check file type -- */
/* get length of attritube "Conventions" */
status = nc_inq_attlen(ncid, NC_GLOBAL, "Conventions", &att_len);
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Can't get length of attribute \"Conventions\".\n");
nc_close(ncid);
return AVERROR_INVALIDDATA;
}
/* check whether file is SOFA file */
text = av_malloc(att_len + 1);
if (!text) {
nc_close(ncid);
return AVERROR(ENOMEM);
}
nc_get_att_text(ncid, NC_GLOBAL, "Conventions", text); mysofa = mysofa_load(filename, &ret);
*(text + att_len) = 0; if (ret || !mysofa) {
if (strncmp("SOFA", text, 4)) { av_log(ctx, AV_LOG_ERROR, "Can't find SOFA-file '%s'\n", filename);
av_log(ctx, AV_LOG_ERROR, "Not a SOFA file!\n");
av_freep(&text);
nc_close(ncid);
return AVERROR(EINVAL); return AVERROR(EINVAL);
} }
av_freep(&text);
status = nc_inq_attlen(ncid, NC_GLOBAL, "License", &att_len);
if (status == NC_NOERR) {
text = av_malloc(att_len + 1);
if (text) {
nc_get_att_text(ncid, NC_GLOBAL, "License", text);
*(text + att_len) = 0;
av_log(ctx, AV_LOG_INFO, "SOFA file License: %s\n", text);
av_freep(&text);
}
}
status = nc_inq_attlen(ncid, NC_GLOBAL, "SourceDescription", &att_len);
if (status == NC_NOERR) {
text = av_malloc(att_len + 1);
if (text) {
nc_get_att_text(ncid, NC_GLOBAL, "SourceDescription", text);
*(text + att_len) = 0;
av_log(ctx, AV_LOG_INFO, "SOFA file SourceDescription: %s\n", text);
av_freep(&text);
}
}
status = nc_inq_attlen(ncid, NC_GLOBAL, "Comment", &att_len);
if (status == NC_NOERR) {
text = av_malloc(att_len + 1);
if (text) {
nc_get_att_text(ncid, NC_GLOBAL, "Comment", text);
*(text + att_len) = 0;
av_log(ctx, AV_LOG_INFO, "SOFA file Comment: %s\n", text);
av_freep(&text);
}
}
status = nc_inq_attlen(ncid, NC_GLOBAL, "SOFAConventions", &att_len);
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Can't get length of attribute \"SOFAConventions\".\n");
nc_close(ncid);
return AVERROR_INVALIDDATA;
}
sofa_conventions = av_malloc(att_len + 1); if (mysofa->DataSamplingRate.elements != 1)
if (!sofa_conventions) {
nc_close(ncid);
return AVERROR(ENOMEM);
}
nc_get_att_text(ncid, NC_GLOBAL, "SOFAConventions", sofa_conventions);
*(sofa_conventions + att_len) = 0;
if (strncmp("SimpleFreeFieldHRIR", sofa_conventions, att_len)) {
av_log(ctx, AV_LOG_ERROR, "Not a SimpleFreeFieldHRIR file!\n");
av_freep(&sofa_conventions);
nc_close(ncid);
return AVERROR(EINVAL); return AVERROR(EINVAL);
} *samplingrate = mysofa->DataSamplingRate.values[0];
av_freep(&sofa_conventions); s->sofa.n_samples = mysofa->N;
mysofa_free(mysofa);
/* -- get sampling rate of HRTFs -- */
/* read ID, then value */
status = nc_inq_varid(ncid, "Data.SamplingRate", &samplingrate_id);
status += nc_get_var_uint(ncid, samplingrate_id, &sample_rate);
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Couldn't read Data.SamplingRate.\n");
nc_close(ncid);
return AVERROR(EINVAL);
}
*samplingrate = sample_rate; /* remember sampling rate */
/* -- allocate memory for one value for each measurement position: -- */
sp_a = s->sofa.sp_a = av_malloc_array(m_dim, sizeof(float));
sp_e = s->sofa.sp_e = av_malloc_array(m_dim, sizeof(float));
sp_r = s->sofa.sp_r = av_malloc_array(m_dim, sizeof(float));
/* delay and IR values required for each ear and measurement position: */
data_delay = s->sofa.data_delay = av_calloc(m_dim, 2 * sizeof(int));
data_ir = s->sofa.data_ir = av_calloc(m_dim * FFALIGN(n_samples, 16), sizeof(float) * 2);
if (!data_delay || !sp_a || !sp_e || !sp_r || !data_ir) {
/* if memory could not be allocated */
close_sofa(&s->sofa);
return AVERROR(ENOMEM);
}
/* get impulse responses (HRTFs): */
/* get corresponding ID */
status = nc_inq_varid(ncid, "Data.IR", &data_ir_id);
status += nc_get_var_float(ncid, data_ir_id, data_ir); /* read and store IRs */
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Couldn't read Data.IR!\n");
ret = AVERROR(EINVAL);
goto error;
}
/* get source positions of the HRTFs in the SOFA file: */
status = nc_inq_varid(ncid, "SourcePosition", &sp_id); /* get corresponding ID */
status += nc_get_vara_float(ncid, sp_id, (size_t[2]){ 0, 0 } ,
(size_t[2]){ m_dim, 1}, sp_a); /* read & store azimuth angles */
status += nc_get_vara_float(ncid, sp_id, (size_t[2]){ 0, 1 } ,
(size_t[2]){ m_dim, 1}, sp_e); /* read & store elevation angles */
status += nc_get_vara_float(ncid, sp_id, (size_t[2]){ 0, 2 } ,
(size_t[2]){ m_dim, 1}, sp_r); /* read & store radii */
if (status != NC_NOERR) { /* if any source position variable coudn't be read */
av_log(ctx, AV_LOG_ERROR, "Couldn't read SourcePosition.\n");
ret = AVERROR(EINVAL);
goto error;
}
/* read Data.Delay, check for errors and fit it to data_delay */
status = nc_inq_varid(ncid, "Data.Delay", &data_delay_id);
status += nc_inq_vardimid(ncid, data_delay_id, &data_delay_dim_id[0]);
status += nc_inq_dimname(ncid, data_delay_dim_id[0], data_delay_dim_name);
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Couldn't read Data.Delay.\n");
ret = AVERROR(EINVAL);
goto error;
}
/* Data.Delay dimension check */
/* dimension of Data.Delay is [I R]: */
if (!strncmp(data_delay_dim_name, "I", 2)) {
/* check 2 characters to assure string is 0-terminated after "I" */
int delay[2]; /* delays get from SOFA file: */
int *data_delay_r;
av_log(ctx, AV_LOG_DEBUG, "Data.Delay has dimension [I R]\n");
status = nc_get_var_int(ncid, data_delay_id, &delay[0]);
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Couldn't read Data.Delay\n");
ret = AVERROR(EINVAL);
goto error;
}
data_delay_r = data_delay + m_dim;
for (i = 0; i < m_dim; i++) { /* extend given dimension [I R] to [M R] */
/* assign constant delay value for all measurements to data_delay fields */
data_delay[i] = delay[0];
data_delay_r[i] = delay[1];
}
/* dimension of Data.Delay is [M R] */
} else if (!strncmp(data_delay_dim_name, "M", 2)) {
av_log(ctx, AV_LOG_ERROR, "Data.Delay in dimension [M R]\n");
/* get delays from SOFA file: */
status = nc_get_var_int(ncid, data_delay_id, data_delay);
if (status != NC_NOERR) {
av_log(ctx, AV_LOG_ERROR, "Couldn't read Data.Delay\n");
ret = AVERROR(EINVAL);
goto error;
}
} else { /* dimension of Data.Delay is neither [I R] nor [M R] */
av_log(ctx, AV_LOG_ERROR, "Data.Delay does not have the required dimensions [I R] or [M R].\n");
ret = AVERROR(EINVAL);
goto error;
}
/* save information in SOFA struct: */
s->sofa.m_dim = m_dim; /* no. measurement positions */
s->sofa.n_samples = n_samples; /* length on one IR */
s->sofa.ncid = ncid; /* netCDF ID of SOFA file */
nc_close(ncid); /* close SOFA file */
av_log(ctx, AV_LOG_DEBUG, "m_dim: %d n_samples %d\n", m_dim, n_samples);
return 0; return 0;
error:
close_sofa(&s->sofa);
return ret;
} }
static int parse_channel_name(char **arg, int *rchannel, char *buf) static int parse_channel_name(char **arg, int *rchannel, char *buf)
@ -507,83 +263,6 @@ static int get_speaker_pos(AVFilterContext *ctx,
} }
static int max_delay(struct NCSofa *sofa)
{
int i, max = 0;
for (i = 0; i < sofa->m_dim * 2; i++) {
/* search maximum delay in given SOFA file */
max = FFMAX(max, sofa->data_delay[i]);
}
return max;
}
static int find_m(SOFAlizerContext *s, int azim, int elev, float radius)
{
/* get source positions and M of currently selected SOFA file */
float *sp_a = s->sofa.sp_a; /* azimuth angle */
float *sp_e = s->sofa.sp_e; /* elevation angle */
float *sp_r = s->sofa.sp_r; /* radius */
int m_dim = s->sofa.m_dim; /* no. measurements */
int best_id = 0; /* index m currently closest to desired source pos. */
float delta = 1000; /* offset between desired and currently best pos. */
float current;
int i;
for (i = 0; i < m_dim; i++) {
/* search through all measurements in currently selected SOFA file */
/* distance of current to desired source position: */
current = fabs(sp_a[i] - azim) +
fabs(sp_e[i] - elev) +
fabs(sp_r[i] - radius);
if (current <= delta) {
/* if current distance is smaller than smallest distance so far */
delta = current;
best_id = i; /* remember index */
}
}
return best_id;
}
static int compensate_volume(AVFilterContext *ctx)
{
struct SOFAlizerContext *s = ctx->priv;
float compensate;
float energy = 0;
float *ir;
int m;
if (s->sofa.ncid) {
/* find IR at front center position in the SOFA file (IR closest to 0°,0°,1m) */
struct NCSofa *sofa = &s->sofa;
m = find_m(s, 0, 0, 1);
/* get energy of that IR and compensate volume */
ir = sofa->data_ir + 2 * m * sofa->n_samples;
if (sofa->n_samples & 31) {
energy = avpriv_scalarproduct_float_c(ir, ir, sofa->n_samples);
} else {
energy = s->fdsp->scalarproduct_float(ir, ir, sofa->n_samples);
}
compensate = 256 / (sofa->n_samples * sqrt(energy));
av_log(ctx, AV_LOG_DEBUG, "Compensate-factor: %f\n", compensate);
ir = sofa->data_ir;
/* apply volume compensation to IRs */
if (sofa->n_samples & 31) {
int i;
for (i = 0; i < sofa->n_samples * sofa->m_dim * 2; i++) {
ir[i] = ir[i] * compensate;
}
} else {
s->fdsp->vector_fmul_scalar(ir, ir, compensate, sofa->n_samples * sofa->m_dim * 2);
emms_c();
}
}
return 0;
}
typedef struct ThreadData { typedef struct ThreadData {
AVFrame *in, *out; AVFrame *in, *out;
int *write; int *write;
@ -629,10 +308,10 @@ static int sofalizer_convolute(AVFilterContext *ctx, void *arg, int jobnr, int n
for (i = 0; i < in->nb_samples; i++) { for (i = 0; i < in->nb_samples; i++) {
const float *temp_ir = ir; /* using same set of IRs for each sample */ const float *temp_ir = ir; /* using same set of IRs for each sample */
*dst = 0; dst[0] = 0;
for (l = 0; l < in_channels; l++) { for (l = 0; l < in_channels; l++) {
/* write current input sample to ringbuffer (for each channel) */ /* write current input sample to ringbuffer (for each channel) */
*(buffer[l] + wr) = src[l]; buffer[l][wr] = src[l];
} }
/* loop goes through all channels to be convolved */ /* loop goes through all channels to be convolved */
@ -643,31 +322,31 @@ static int sofalizer_convolute(AVFilterContext *ctx, void *arg, int jobnr, int n
/* LFE is an input channel but requires no convolution */ /* LFE is an input channel but requires no convolution */
/* apply gain to LFE signal and add to output buffer */ /* apply gain to LFE signal and add to output buffer */
*dst += *(buffer[s->lfe_channel] + wr) * s->gain_lfe; *dst += *(buffer[s->lfe_channel] + wr) * s->gain_lfe;
temp_ir += FFALIGN(n_samples, 16); temp_ir += FFALIGN(n_samples, 32);
continue; continue;
} }
/* current read position in ringbuffer: input sample write position /* current read position in ringbuffer: input sample write position
* - delay for l-th ch. + diff. betw. IR length and buffer length * - delay for l-th ch. + diff. betw. IR length and buffer length
* (mod buffer length) */ * (mod buffer length) */
read = (wr - *(delay + l) - (n_samples - 1) + buffer_length) & modulo; read = (wr - delay[l] - (n_samples - 1) + buffer_length) & modulo;
if (read + n_samples < buffer_length) { if (read + n_samples < buffer_length) {
memcpy(temp_src, bptr + read, n_samples * sizeof(*temp_src)); memmove(temp_src, bptr + read, n_samples * sizeof(*temp_src));
} else { } else {
int len = FFMIN(n_samples - (read % n_samples), buffer_length - read); int len = FFMIN(n_samples - (read % n_samples), buffer_length - read);
memcpy(temp_src, bptr + read, len * sizeof(*temp_src)); memmove(temp_src, bptr + read, len * sizeof(*temp_src));
memcpy(temp_src + len, bptr, (n_samples - len) * sizeof(*temp_src)); memmove(temp_src + len, bptr, (n_samples - len) * sizeof(*temp_src));
} }
/* multiply signal and IR, and add up the results */ /* multiply signal and IR, and add up the results */
dst[0] += s->fdsp->scalarproduct_float(temp_ir, temp_src, n_samples); dst[0] += s->fdsp->scalarproduct_float(temp_ir, temp_src, n_samples);
temp_ir += FFALIGN(n_samples, 16); temp_ir += FFALIGN(n_samples, 32);
} }
/* clippings counter */ /* clippings counter */
if (fabs(*dst) > 1) if (fabs(dst[0]) > 1)
*n_clippings += 1; *n_clippings += 1;
/* move output buffer pointer by +2 to get to next sample of processed channel: */ /* move output buffer pointer by +2 to get to next sample of processed channel: */
@ -875,14 +554,14 @@ static int query_formats(AVFilterContext *ctx)
return ff_set_common_samplerates(ctx, formats); return ff_set_common_samplerates(ctx, formats);
} }
static int load_data(AVFilterContext *ctx, int azim, int elev, float radius) static int load_data(AVFilterContext *ctx, int azim, int elev, float radius, int sample_rate)
{ {
struct SOFAlizerContext *s = ctx->priv; struct SOFAlizerContext *s = ctx->priv;
const int n_samples = s->sofa.n_samples; int n_samples;
int n_conv = s->n_conv; /* no. channels to convolve */ int n_conv = s->n_conv; /* no. channels to convolve */
int n_fft = s->n_fft; int n_fft;
int delay_l[16]; /* broadband delay for each IR */ float delay_l; /* broadband delay for each IR */
int delay_r[16]; float delay_r;
int nb_input_channels = ctx->inputs[0]->channels; /* no. input channels */ int nb_input_channels = ctx->inputs[0]->channels; /* no. input channels */
float gain_lin = expf((s->gain - 3 * nb_input_channels) / 20 * M_LN10); /* gain - 3dB/channel */ float gain_lin = expf((s->gain - 3 * nb_input_channels) / 20 * M_LN10); /* gain - 3dB/channel */
FFTComplex *data_hrtf_l = NULL; FFTComplex *data_hrtf_l = NULL;
@ -892,68 +571,166 @@ static int load_data(AVFilterContext *ctx, int azim, int elev, float radius)
float *data_ir_l = NULL; float *data_ir_l = NULL;
float *data_ir_r = NULL; float *data_ir_r = NULL;
int offset = 0; /* used for faster pointer arithmetics in for-loop */ int offset = 0; /* used for faster pointer arithmetics in for-loop */
int m[16]; /* measurement index m of IR closest to required source positions */
int i, j, azim_orig = azim, elev_orig = elev; int i, j, azim_orig = azim, elev_orig = elev;
int filter_length, ret = 0;
int n_current;
int n_max = 0;
if (!s->sofa.ncid) { /* if an invalid SOFA file has been selected */ s->sofa.easy = mysofa_open(s->filename, sample_rate, &filter_length, &ret);
if (!s->sofa.easy || ret) { /* if an invalid SOFA file has been selected */
av_log(ctx, AV_LOG_ERROR, "Selected SOFA file is invalid. Please select valid SOFA file.\n"); av_log(ctx, AV_LOG_ERROR, "Selected SOFA file is invalid. Please select valid SOFA file.\n");
return AVERROR_INVALIDDATA; return AVERROR_INVALIDDATA;
} }
n_samples = s->sofa.n_samples;
s->data_ir[0] = av_calloc(FFALIGN(n_samples, 32), sizeof(float) * s->n_conv);
s->data_ir[1] = av_calloc(FFALIGN(n_samples, 32), sizeof(float) * s->n_conv);
s->delay[0] = av_calloc(s->n_conv, sizeof(int));
s->delay[1] = av_calloc(s->n_conv, sizeof(int));
if (!s->data_ir[0] || !s->data_ir[1] || !s->delay[0] || !s->delay[1]) {
ret = AVERROR(ENOMEM);
goto fail;
}
/* get temporary IR for L and R channel */
data_ir_l = av_calloc(n_conv * FFALIGN(n_samples, 32), sizeof(*data_ir_l));
data_ir_r = av_calloc(n_conv * FFALIGN(n_samples, 32), sizeof(*data_ir_r));
if (!data_ir_r || !data_ir_l) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (s->type == TIME_DOMAIN) { if (s->type == TIME_DOMAIN) {
s->temp_src[0] = av_calloc(FFALIGN(n_samples, 16), sizeof(float)); s->temp_src[0] = av_calloc(FFALIGN(n_samples, 32), sizeof(float));
s->temp_src[1] = av_calloc(FFALIGN(n_samples, 16), sizeof(float)); s->temp_src[1] = av_calloc(FFALIGN(n_samples, 32), sizeof(float));
if (!s->temp_src[0] || !s->temp_src[1]) {
/* get temporary IR for L and R channel */ ret = AVERROR(ENOMEM);
data_ir_l = av_calloc(n_conv * FFALIGN(n_samples, 16), sizeof(*data_ir_l)); goto fail;
data_ir_r = av_calloc(n_conv * FFALIGN(n_samples, 16), sizeof(*data_ir_r)); }
if (!data_ir_r || !data_ir_l || !s->temp_src[0] || !s->temp_src[1]) { }
av_free(data_ir_l);
av_free(data_ir_r); s->speaker_azim = av_calloc(s->n_conv, sizeof(*s->speaker_azim));
return AVERROR(ENOMEM); s->speaker_elev = av_calloc(s->n_conv, sizeof(*s->speaker_elev));
if (!s->speaker_azim || !s->speaker_elev) {
ret = AVERROR(ENOMEM);
goto fail;
}
/* get speaker positions */
if ((ret = get_speaker_pos(ctx, s->speaker_azim, s->speaker_elev)) < 0) {
av_log(ctx, AV_LOG_ERROR, "Couldn't get speaker positions. Input channel configuration not supported.\n");
goto fail;
}
for (i = 0; i < s->n_conv; i++) {
float coordinates[3];
/* load and store IRs and corresponding delays */
azim = (int)(s->speaker_azim[i] + azim_orig) % 360;
elev = (int)(s->speaker_elev[i] + elev_orig) % 90;
coordinates[0] = azim;
coordinates[1] = elev;
coordinates[2] = radius;
mysofa_s2c(coordinates);
/* get id of IR closest to desired position */
mysofa_getfilter_float(s->sofa.easy, coordinates[0], coordinates[1], coordinates[2],
data_ir_l + FFALIGN(n_samples, 32) * i,
data_ir_r + FFALIGN(n_samples, 32) * i,
&delay_l, &delay_r);
s->delay[0][i] = delay_l * sample_rate;
s->delay[1][i] = delay_r * sample_rate;
s->sofa.max_delay = FFMAX3(s->sofa.max_delay, s->delay[0][i], s->delay[1][i]);
}
/* get size of ringbuffer (longest IR plus max. delay) */
/* then choose next power of 2 for performance optimization */
n_current = s->sofa.n_samples + s->sofa.max_delay;
/* length of longest IR plus max. delay */
n_max = FFMAX(n_max, n_current);
/* buffer length is longest IR plus max. delay -> next power of 2
(32 - count leading zeros gives required exponent) */
s->buffer_length = 1 << (32 - ff_clz(n_max));
s->n_fft = n_fft = 1 << (32 - ff_clz(n_max + sample_rate));
if (s->type == FREQUENCY_DOMAIN) {
av_fft_end(s->fft[0]);
av_fft_end(s->fft[1]);
s->fft[0] = av_fft_init(log2(s->n_fft), 0);
s->fft[1] = av_fft_init(log2(s->n_fft), 0);
av_fft_end(s->ifft[0]);
av_fft_end(s->ifft[1]);
s->ifft[0] = av_fft_init(log2(s->n_fft), 1);
s->ifft[1] = av_fft_init(log2(s->n_fft), 1);
if (!s->fft[0] || !s->fft[1] || !s->ifft[0] || !s->ifft[1]) {
av_log(ctx, AV_LOG_ERROR, "Unable to create FFT contexts of size %d.\n", s->n_fft);
ret = AVERROR(ENOMEM);
goto fail;
} }
}
if (s->type == TIME_DOMAIN) {
s->ringbuffer[0] = av_calloc(s->buffer_length, sizeof(float) * nb_input_channels);
s->ringbuffer[1] = av_calloc(s->buffer_length, sizeof(float) * nb_input_channels);
} else { } else {
/* get temporary HRTF memory for L and R channel */ /* get temporary HRTF memory for L and R channel */
data_hrtf_l = av_malloc_array(n_fft, sizeof(*data_hrtf_l) * n_conv); data_hrtf_l = av_malloc_array(n_fft, sizeof(*data_hrtf_l) * n_conv);
data_hrtf_r = av_malloc_array(n_fft, sizeof(*data_hrtf_r) * n_conv); data_hrtf_r = av_malloc_array(n_fft, sizeof(*data_hrtf_r) * n_conv);
if (!data_hrtf_r || !data_hrtf_l) { if (!data_hrtf_r || !data_hrtf_l) {
av_free(data_hrtf_l); ret = AVERROR(ENOMEM);
av_free(data_hrtf_r); goto fail;
return AVERROR(ENOMEM); }
s->ringbuffer[0] = av_calloc(s->buffer_length, sizeof(float));
s->ringbuffer[1] = av_calloc(s->buffer_length, sizeof(float));
s->temp_fft[0] = av_malloc_array(s->n_fft, sizeof(FFTComplex));
s->temp_fft[1] = av_malloc_array(s->n_fft, sizeof(FFTComplex));
if (!s->temp_fft[0] || !s->temp_fft[1]) {
ret = AVERROR(ENOMEM);
goto fail;
}
}
if (!s->ringbuffer[0] || !s->ringbuffer[1]) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (s->type == FREQUENCY_DOMAIN) {
fft_in_l = av_calloc(n_fft, sizeof(*fft_in_l));
fft_in_r = av_calloc(n_fft, sizeof(*fft_in_r));
if (!fft_in_l || !fft_in_r) {
ret = AVERROR(ENOMEM);
goto fail;
} }
} }
for (i = 0; i < s->n_conv; i++) { for (i = 0; i < s->n_conv; i++) {
/* load and store IRs and corresponding delays */ float *lir, *rir;
azim = (int)(s->speaker_azim[i] + azim_orig) % 360;
elev = (int)(s->speaker_elev[i] + elev_orig) % 90;
/* get id of IR closest to desired position */
m[i] = find_m(s, azim, elev, radius);
/* load the delays associated with the current IRs */ offset = i * FFALIGN(n_samples, 32); /* no. samples already written */
delay_l[i] = *(s->sofa.data_delay + 2 * m[i]);
delay_r[i] = *(s->sofa.data_delay + 2 * m[i] + 1); lir = data_ir_l + offset;
rir = data_ir_r + offset;
if (s->type == TIME_DOMAIN) { if (s->type == TIME_DOMAIN) {
offset = i * FFALIGN(n_samples, 16); /* no. samples already written */
for (j = 0; j < n_samples; j++) { for (j = 0; j < n_samples; j++) {
/* load reversed IRs of the specified source position /* load reversed IRs of the specified source position
* sample-by-sample for left and right ear; and apply gain */ * sample-by-sample for left and right ear; and apply gain */
*(data_ir_l + offset + j) = /* left channel */ s->data_ir[0][offset + j] = lir[n_samples - 1 - j] * gain_lin;
*(s->sofa.data_ir + 2 * m[i] * n_samples + n_samples - 1 - j) * gain_lin; s->data_ir[1][offset + j] = rir[n_samples - 1 - j] * gain_lin;
*(data_ir_r + offset + j) = /* right channel */
*(s->sofa.data_ir + 2 * m[i] * n_samples + n_samples - 1 - j + n_samples) * gain_lin;
} }
} else { } else {
fft_in_l = av_calloc(n_fft, sizeof(*fft_in_l)); memset(fft_in_l, 0, n_fft * sizeof(*fft_in_l));
fft_in_r = av_calloc(n_fft, sizeof(*fft_in_r)); memset(fft_in_r, 0, n_fft * sizeof(*fft_in_r));
if (!fft_in_l || !fft_in_r) {
av_free(data_hrtf_l);
av_free(data_hrtf_r);
av_free(fft_in_l);
av_free(fft_in_r);
return AVERROR(ENOMEM);
}
offset = i * n_fft; /* no. samples already written */ offset = i * n_fft; /* no. samples already written */
for (j = 0; j < n_samples; j++) { for (j = 0; j < n_samples; j++) {
@ -961,10 +738,8 @@ static int load_data(AVFilterContext *ctx, int azim, int elev, float radius)
* sample-by-sample and apply gain, * sample-by-sample and apply gain,
* L channel is loaded to real part, R channel to imag part, * L channel is loaded to real part, R channel to imag part,
* IRs ared shifted by L and R delay */ * IRs ared shifted by L and R delay */
fft_in_l[delay_l[i] + j].re = /* left channel */ fft_in_l[s->delay[0][i] + j].re = lir[j] * gain_lin;
*(s->sofa.data_ir + 2 * m[i] * n_samples + j) * gain_lin; fft_in_r[s->delay[1][i] + j].re = rir[j] * gain_lin;
fft_in_r[delay_r[i] + j].re = /* right channel */
*(s->sofa.data_ir + (2 * m[i] + 1) * n_samples + j) * gain_lin;
} }
/* actually transform to frequency domain (IRs -> HRTFs) */ /* actually transform to frequency domain (IRs -> HRTFs) */
@ -975,45 +750,33 @@ static int load_data(AVFilterContext *ctx, int azim, int elev, float radius)
av_fft_calc(s->fft[0], fft_in_r); av_fft_calc(s->fft[0], fft_in_r);
memcpy(data_hrtf_r + offset, fft_in_r, n_fft * sizeof(*fft_in_r)); memcpy(data_hrtf_r + offset, fft_in_r, n_fft * sizeof(*fft_in_r));
} }
av_log(ctx, AV_LOG_DEBUG, "Index: %d, Azimuth: %f, Elevation: %f, Radius: %f of SOFA file.\n",
m[i], *(s->sofa.sp_a + m[i]), *(s->sofa.sp_e + m[i]), *(s->sofa.sp_r + m[i]));
} }
if (s->type == TIME_DOMAIN) { if (s->type == FREQUENCY_DOMAIN) {
/* copy IRs and delays to allocated memory in the SOFAlizerContext struct: */
memcpy(s->data_ir[0], data_ir_l, sizeof(float) * n_conv * FFALIGN(n_samples, 16));
memcpy(s->data_ir[1], data_ir_r, sizeof(float) * n_conv * FFALIGN(n_samples, 16));
av_freep(&data_ir_l); /* free temporary IR memory */
av_freep(&data_ir_r);
} else {
s->data_hrtf[0] = av_malloc_array(n_fft * s->n_conv, sizeof(FFTComplex)); s->data_hrtf[0] = av_malloc_array(n_fft * s->n_conv, sizeof(FFTComplex));
s->data_hrtf[1] = av_malloc_array(n_fft * s->n_conv, sizeof(FFTComplex)); s->data_hrtf[1] = av_malloc_array(n_fft * s->n_conv, sizeof(FFTComplex));
if (!s->data_hrtf[0] || !s->data_hrtf[1]) { if (!s->data_hrtf[0] || !s->data_hrtf[1]) {
av_freep(&data_hrtf_l); ret = AVERROR(ENOMEM);
av_freep(&data_hrtf_r); goto fail;
av_freep(&fft_in_l);
av_freep(&fft_in_r);
return AVERROR(ENOMEM); /* memory allocation failed */
} }
memcpy(s->data_hrtf[0], data_hrtf_l, /* copy HRTF data to */ memcpy(s->data_hrtf[0], data_hrtf_l, /* copy HRTF data to */
sizeof(FFTComplex) * n_conv * n_fft); /* filter struct */ sizeof(FFTComplex) * n_conv * n_fft); /* filter struct */
memcpy(s->data_hrtf[1], data_hrtf_r, memcpy(s->data_hrtf[1], data_hrtf_r,
sizeof(FFTComplex) * n_conv * n_fft); sizeof(FFTComplex) * n_conv * n_fft);
}
av_freep(&data_hrtf_l); /* free temporary HRTF memory */ fail:
av_freep(&data_hrtf_r); av_freep(&data_hrtf_l); /* free temporary HRTF memory */
av_freep(&data_hrtf_r);
av_freep(&fft_in_l); /* free temporary FFT memory */ av_freep(&data_ir_l); /* free temprary IR memory */
av_freep(&fft_in_r); av_freep(&data_ir_r);
}
memcpy(s->delay[0], &delay_l[0], sizeof(int) * s->n_conv); av_freep(&fft_in_l); /* free temporary FFT memory */
memcpy(s->delay[1], &delay_r[0], sizeof(int) * s->n_conv); av_freep(&fft_in_r);
return 0; return ret;
} }
static av_cold int init(AVFilterContext *ctx) static av_cold int init(AVFilterContext *ctx)
@ -1026,12 +789,8 @@ static av_cold int init(AVFilterContext *ctx)
return AVERROR(EINVAL); return AVERROR(EINVAL);
} }
/* load SOFA file, */ /* preload SOFA file, */
/* initialize file IDs to 0 before attempting to load SOFA files, ret = preload_sofa(ctx, s->filename, &s->sample_rate);
* this assures that in case of error, only the memory of already
* loaded files is free'd */
s->sofa.ncid = 0;
ret = load_sofa(ctx, s->filename, &s->sample_rate);
if (ret) { if (ret) {
/* file loading error */ /* file loading error */
av_log(ctx, AV_LOG_ERROR, "Error while loading SOFA file: '%s'\n", s->filename); av_log(ctx, AV_LOG_ERROR, "Error while loading SOFA file: '%s'\n", s->filename);
@ -1055,10 +814,6 @@ static int config_input(AVFilterLink *inlink)
{ {
AVFilterContext *ctx = inlink->dst; AVFilterContext *ctx = inlink->dst;
SOFAlizerContext *s = ctx->priv; SOFAlizerContext *s = ctx->priv;
int nb_input_channels = inlink->channels; /* no. input channels */
int n_max_ir = 0;
int n_current;
int n_max = 0;
int ret; int ret;
if (s->type == FREQUENCY_DOMAIN) { if (s->type == FREQUENCY_DOMAIN) {
@ -1070,85 +825,14 @@ static int config_input(AVFilterLink *inlink)
/* gain -3 dB per channel, -6 dB to get LFE on a similar level */ /* gain -3 dB per channel, -6 dB to get LFE on a similar level */
s->gain_lfe = expf((s->gain - 3 * inlink->channels - 6 + s->lfe_gain) / 20 * M_LN10); s->gain_lfe = expf((s->gain - 3 * inlink->channels - 6 + s->lfe_gain) / 20 * M_LN10);
s->n_conv = nb_input_channels; s->n_conv = inlink->channels;
/* get size of ringbuffer (longest IR plus max. delay) */
/* then choose next power of 2 for performance optimization */
n_current = s->sofa.n_samples + max_delay(&s->sofa);
if (n_current > n_max) {
/* length of longest IR plus max. delay (in all SOFA files) */
n_max = n_current;
/* length of longest IR (without delay, in all SOFA files) */
n_max_ir = s->sofa.n_samples;
}
/* buffer length is longest IR plus max. delay -> next power of 2
(32 - count leading zeros gives required exponent) */
s->buffer_length = 1 << (32 - ff_clz(n_max));
s->n_fft = 1 << (32 - ff_clz(n_max + inlink->sample_rate));
if (s->type == FREQUENCY_DOMAIN) {
av_fft_end(s->fft[0]);
av_fft_end(s->fft[1]);
s->fft[0] = av_fft_init(log2(s->n_fft), 0);
s->fft[1] = av_fft_init(log2(s->n_fft), 0);
av_fft_end(s->ifft[0]);
av_fft_end(s->ifft[1]);
s->ifft[0] = av_fft_init(log2(s->n_fft), 1);
s->ifft[1] = av_fft_init(log2(s->n_fft), 1);
if (!s->fft[0] || !s->fft[1] || !s->ifft[0] || !s->ifft[1]) {
av_log(ctx, AV_LOG_ERROR, "Unable to create FFT contexts of size %d.\n", s->n_fft);
return AVERROR(ENOMEM);
}
}
/* Allocate memory for the impulse responses, delays and the ringbuffers */
/* size: (longest IR) * (number of channels to convolute) */
s->data_ir[0] = av_calloc(FFALIGN(n_max_ir, 16), sizeof(float) * s->n_conv);
s->data_ir[1] = av_calloc(FFALIGN(n_max_ir, 16), sizeof(float) * s->n_conv);
/* length: number of channels to convolute */
s->delay[0] = av_malloc_array(s->n_conv, sizeof(float));
s->delay[1] = av_malloc_array(s->n_conv, sizeof(float));
/* length: (buffer length) * (number of input channels),
* OR: buffer length (if frequency domain processing)
* calloc zero-initializes the buffer */
if (s->type == TIME_DOMAIN) {
s->ringbuffer[0] = av_calloc(s->buffer_length, sizeof(float) * nb_input_channels);
s->ringbuffer[1] = av_calloc(s->buffer_length, sizeof(float) * nb_input_channels);
} else {
s->ringbuffer[0] = av_calloc(s->buffer_length, sizeof(float));
s->ringbuffer[1] = av_calloc(s->buffer_length, sizeof(float));
s->temp_fft[0] = av_malloc_array(s->n_fft, sizeof(FFTComplex));
s->temp_fft[1] = av_malloc_array(s->n_fft, sizeof(FFTComplex));
if (!s->temp_fft[0] || !s->temp_fft[1])
return AVERROR(ENOMEM);
}
/* length: number of channels to convolute */
s->speaker_azim = av_calloc(s->n_conv, sizeof(*s->speaker_azim));
s->speaker_elev = av_calloc(s->n_conv, sizeof(*s->speaker_elev));
/* memory allocation failed: */
if (!s->data_ir[0] || !s->data_ir[1] || !s->delay[1] ||
!s->delay[0] || !s->ringbuffer[0] || !s->ringbuffer[1] ||
!s->speaker_azim || !s->speaker_elev)
return AVERROR(ENOMEM);
compensate_volume(ctx);
/* get speaker positions */
if ((ret = get_speaker_pos(ctx, s->speaker_azim, s->speaker_elev)) < 0) {
av_log(ctx, AV_LOG_ERROR, "Couldn't get speaker positions. Input channel configuration not supported.\n");
return ret;
}
/* load IRs to data_ir[0] and data_ir[1] for required directions */ /* load IRs to data_ir[0] and data_ir[1] for required directions */
if ((ret = load_data(ctx, s->rotation, s->elevation, s->radius)) < 0) if ((ret = load_data(ctx, s->rotation, s->elevation, s->radius, inlink->sample_rate)) < 0)
return ret; return ret;
av_log(ctx, AV_LOG_DEBUG, "Samplerate: %d Channels to convolute: %d, Length of ringbuffer: %d x %d\n", av_log(ctx, AV_LOG_DEBUG, "Samplerate: %d Channels to convolute: %d, Length of ringbuffer: %d x %d\n",
inlink->sample_rate, s->n_conv, nb_input_channels, s->buffer_length); inlink->sample_rate, s->n_conv, inlink->channels, s->buffer_length);
return 0; return 0;
} }
@ -1157,13 +841,7 @@ static av_cold void uninit(AVFilterContext *ctx)
{ {
SOFAlizerContext *s = ctx->priv; SOFAlizerContext *s = ctx->priv;
if (s->sofa.ncid) { close_sofa(&s->sofa);
av_freep(&s->sofa.sp_a);
av_freep(&s->sofa.sp_e);
av_freep(&s->sofa.sp_r);
av_freep(&s->sofa.data_delay);
av_freep(&s->sofa.data_ir);
}
av_fft_end(s->ifft[0]); av_fft_end(s->ifft[0]);
av_fft_end(s->ifft[1]); av_fft_end(s->ifft[1]);
av_fft_end(s->fft[0]); av_fft_end(s->fft[0]);

Loading…
Cancel
Save