diff options
Diffstat (limited to 'audio/filter/af_scaletempo.c')
-rw-r--r-- | audio/filter/af_scaletempo.c | 577 |
1 files changed, 344 insertions, 233 deletions
diff --git a/audio/filter/af_scaletempo.c b/audio/filter/af_scaletempo.c index 0499631ea9..4e48e6168b 100644 --- a/audio/filter/af_scaletempo.c +++ b/audio/filter/af_scaletempo.c @@ -35,14 +35,32 @@ #include <limits.h> #include <assert.h> +#include "audio/aframe.h" +#include "audio/format.h" #include "common/common.h" - -#include "af.h" +#include "filters/f_autoconvert.h" +#include "filters/filter_internal.h" +#include "filters/user_filters.h" #include "options/m_option.h" -// Data for specific instances of this filter -typedef struct af_scaletempo_s -{ +struct f_opts { + float scale_nominal; + float ms_stride; + float ms_search; + float percent_overlap; +#define SCALE_TEMPO 1 +#define SCALE_PITCH 2 + int speed_opt; +}; + +struct priv { + struct f_opts *opts; + + struct mp_pin *in_pin; + struct mp_aframe *cur_format; + struct mp_aframe_pool *out_pool; + double current_pts; + // stride float scale; float speed; @@ -62,28 +80,21 @@ typedef struct af_scaletempo_s int bytes_standing; void *buf_overlap; void *table_blend; - void (*output_overlap)(struct af_scaletempo_s *s, void *out_buf, + void (*output_overlap)(struct priv *s, void *out_buf, int bytes_off); // best overlap int frames_search; int num_channels; void *buf_pre_corr; void *table_window; - int (*best_overlap_offset)(struct af_scaletempo_s *s); - // command line - float scale_nominal; - float ms_stride; - float percent_overlap; - float ms_search; -#define SCALE_TEMPO 1 -#define SCALE_PITCH 2 - int speed_opt; -} af_scaletempo_t; + int (*best_overlap_offset)(struct priv *s); +}; -static int fill_queue(struct af_instance *af, struct mp_audio *data, int offset) +static bool reinit(struct mp_filter *f, struct mp_aframe *in); + +static int fill_queue(struct priv *s, struct mp_aframe *in, int offset) { - af_scaletempo_t *s = af->priv; - int bytes_in = (data ? mp_audio_psize(data) : 0) - offset; + int bytes_in = in ? mp_aframe_get_size(in) * s->bytes_per_frame - offset : 0; int offset_unchanged = offset; if (s->bytes_to_slide > 0) { @@ -106,8 +117,8 @@ static int fill_queue(struct af_instance *af, struct mp_audio *data, int offset) if (bytes_in > 0) { int bytes_copy = MPMIN(s->bytes_queue - s->bytes_queued, bytes_in); assert(bytes_copy >= 0); - memcpy(s->buf_queue + s->bytes_queued, - (int8_t *)data->planes[0] + offset, bytes_copy); + uint8_t **planes = mp_aframe_get_data_ro(in); + memcpy(s->buf_queue + s->bytes_queued, planes[0] + offset, bytes_copy); s->bytes_queued += bytes_copy; offset += bytes_copy; } @@ -117,7 +128,7 @@ static int fill_queue(struct af_instance *af, struct mp_audio *data, int offset) #define UNROLL_PADDING (4 * 4) -static int best_overlap_offset_float(af_scaletempo_t *s) +static int best_overlap_offset_float(struct priv *s) { float best_corr = INT_MIN; int best_off = 0; @@ -146,7 +157,7 @@ static int best_overlap_offset_float(af_scaletempo_t *s) return best_off * 4 * s->num_channels; } -static int best_overlap_offset_s16(af_scaletempo_t *s) +static int best_overlap_offset_s16(struct priv *s) { int64_t best_corr = INT64_MIN; int best_off = 0; @@ -183,7 +194,7 @@ static int best_overlap_offset_s16(af_scaletempo_t *s) return best_off * 2 * s->num_channels; } -static void output_overlap_float(af_scaletempo_t *s, void *buf_out, +static void output_overlap_float(struct priv *s, void *buf_out, int bytes_off) { float *pout = buf_out; @@ -196,7 +207,7 @@ static void output_overlap_float(af_scaletempo_t *s, void *buf_out, } } -static void output_overlap_s16(af_scaletempo_t *s, void *buf_out, +static void output_overlap_s16(struct priv *s, void *buf_out, int bytes_off) { int16_t *pout = buf_out; @@ -209,28 +220,65 @@ static void output_overlap_s16(af_scaletempo_t *s, void *buf_out, } } -static int filter(struct af_instance *af, struct mp_audio *data) +static void process(struct mp_filter *f) { - af_scaletempo_t *s = af->priv; + struct priv *s = f->priv; + + if (!mp_pin_can_transfer_data(f->ppins[1], s->in_pin)) + return; - if (s->scale == 1.0) { - af->delay = 0; - af_add_output_frame(af, data); - return 0; + struct mp_aframe *in = NULL, *out = NULL; + + struct mp_frame frame = mp_pin_out_read(s->in_pin); + if (frame.type != MP_FRAME_AUDIO && frame.type != MP_FRAME_EOF) { + MP_ERR(f, "unexpected frame type\n"); + goto error; + } + + in = frame.type == MP_FRAME_AUDIO ? frame.data : NULL; + bool is_eof = !in; + + // EOF before it was even initialized once. + if (is_eof && !mp_aframe_config_is_valid(s->cur_format)) { + mp_pin_in_write(f->ppins[1], MP_EOF_FRAME); + return; + } + + if (in && !mp_aframe_config_equals(in, s->cur_format)) { + if (s->bytes_queued) { + // Drain remaining data before executing the format change. + MP_VERBOSE(f, "draining\n"); + mp_pin_out_unread(s->in_pin, frame); + in = NULL; + } else { + if (!reinit(f, in)) { + MP_ERR(f, "initialization failed\n"); + goto error; + } + } } - int in_samples = data ? data->samples : 0; - struct mp_audio *out = mp_audio_pool_get(af->out_pool, af->data, - ((int)(in_samples / s->frames_stride_scaled) + 1) * s->frames_stride); - if (!out) { - talloc_free(data); - return -1; + int in_samples = in ? mp_aframe_get_size(in) : 0; + + int max_out_samples = + ((int)(in_samples / s->frames_stride_scaled) + 1) * s->frames_stride; + if (!in) + max_out_samples += s->bytes_queued; + out = mp_aframe_new_ref(s->cur_format); + if (mp_aframe_pool_allocate(s->out_pool, out, max_out_samples) < 0) + goto error; + + if (in) { + mp_aframe_copy_attributes(out, in); + s->current_pts = mp_aframe_end_pts(in); } - if (data) - mp_audio_copy_attributes(out, data); - int offset_in = fill_queue(af, data, 0); - int8_t *pout = out->planes[0]; + int offset_in = fill_queue(s, in, 0); + uint8_t **out_planes = mp_aframe_get_data_rw(out); + if (!out_planes) + goto error; + int8_t *pout = out_planes[0]; + int out_offset = 0; while (s->bytes_queued >= s->bytes_queue) { int ti; float tf; @@ -240,12 +288,12 @@ static int filter(struct af_instance *af, struct mp_audio *data) if (s->output_overlap) { if (s->best_overlap_offset) bytes_off = s->best_overlap_offset(s); - s->output_overlap(s, pout, bytes_off); + s->output_overlap(s, pout + out_offset, bytes_off); } - memcpy(pout + s->bytes_overlap, + memcpy(pout + out_offset + s->bytes_overlap, s->buf_queue + bytes_off + s->bytes_overlap, s->bytes_standing); - pout += s->bytes_stride; + out_offset += s->bytes_stride; // input stride memcpy(s->buf_overlap, @@ -256,239 +304,302 @@ static int filter(struct af_instance *af, struct mp_audio *data) s->frames_stride_error = tf - ti; s->bytes_to_slide = ti * s->bytes_per_frame; - offset_in += fill_queue(af, data, offset_in); + offset_in += fill_queue(s, in, offset_in); } + // Drain remaining buffered data. + if (!in && s->bytes_queued) { + memcpy(pout + out_offset, s->buf_queue, s->bytes_queued); + out_offset += s->bytes_queued; + s->bytes_queued = 0; + } + mp_aframe_set_size(out, out_offset / s->bytes_per_frame); // This filter can have a negative delay when scale > 1: // output corresponding to some length of input can be decided and written // after receiving only a part of that input. - af->delay = (s->bytes_queued - s->bytes_to_slide) / s->scale - / out->sstride / out->rate; + double delay = (out_offset * s->speed + s->bytes_queued - s->bytes_to_slide) / + s->bytes_per_frame / mp_aframe_get_effective_rate(out); - out->samples = (pout - (int8_t *)out->planes[0]) / out->sstride; - talloc_free(data); - if (out->samples) { - af_add_output_frame(af, out); - } else { - talloc_free(out); + if (s->current_pts != MP_NOPTS_VALUE) + mp_aframe_set_pts(out, s->current_pts - delay); + + mp_aframe_mul_speed(out, s->speed); + + if (!mp_aframe_get_size(out)) + TA_FREEP(&out); + + if (is_eof && out) { + mp_pin_out_repeat_eof(s->in_pin); + } else if (is_eof && !out) { + mp_pin_in_write(f->ppins[1], MP_EOF_FRAME); + } else if (!is_eof && !out) { + mp_pin_out_request_data(s->in_pin); } - return 0; + + if (out) + mp_pin_in_write(f->ppins[1], MAKE_FRAME(MP_FRAME_AUDIO, out)); + + talloc_free(in); + return; + +error: + talloc_free(in); + talloc_free(out); + mp_filter_internal_mark_failed(f); } -static void update_speed(struct af_instance *af, float speed) +static void update_speed(struct priv *s, float speed) { - af_scaletempo_t *s = af->priv; - s->speed = speed; - double factor = (s->speed_opt & SCALE_PITCH) ? 1.0 / s->speed : s->speed; - s->scale = factor * s->scale_nominal; + double factor = (s->opts->speed_opt & SCALE_PITCH) ? 1.0 / s->speed : s->speed; + s->scale = factor * s->opts->scale_nominal; s->frames_stride_scaled = s->scale * s->frames_stride; s->frames_stride_error = MPMIN(s->frames_stride_error, s->frames_stride_scaled); } -// Initialization and runtime control -static int control(struct af_instance *af, int cmd, void *arg) +static bool reinit(struct mp_filter *f, struct mp_aframe *in) { - af_scaletempo_t *s = af->priv; - switch (cmd) { - case AF_CONTROL_REINIT: { - struct mp_audio *data = (struct mp_audio *)arg; - float srate = data->rate / 1000.0; - int nch = data->nch; - int use_int = 0; - - mp_audio_force_interleaved_format(data); - mp_audio_copy_config(af->data, data); - - if (data->format == AF_FORMAT_S16) { - use_int = 1; - } else { - mp_audio_set_format(af->data, AF_FORMAT_FLOAT); - } - int bps = af->data->bps; + struct priv *s = f->priv; - s->frames_stride = srate * s->ms_stride; - s->bytes_stride = s->frames_stride * bps * nch; - af->delay = 0; + mp_aframe_reset(s->cur_format); - update_speed(af, s->speed); + float srate = mp_aframe_get_rate(in) / 1000.0; + int nch = mp_aframe_get_channels(in); + int format = mp_aframe_get_format(in); - int frames_overlap = s->frames_stride * s->percent_overlap; - if (frames_overlap <= 0) { - s->bytes_standing = s->bytes_stride; - s->samples_standing = s->bytes_standing / bps; - s->output_overlap = NULL; - s->bytes_overlap = 0; - } else { - s->samples_overlap = frames_overlap * nch; - s->bytes_overlap = frames_overlap * nch * bps; - s->bytes_standing = s->bytes_stride - s->bytes_overlap; - s->samples_standing = s->bytes_standing / bps; - s->buf_overlap = realloc(s->buf_overlap, s->bytes_overlap); - s->table_blend = realloc(s->table_blend, s->bytes_overlap * 4); - if (!s->buf_overlap || !s->table_blend) { - MP_FATAL(af, "Out of memory\n"); - return AF_ERROR; + int use_int = 0; + if (format == AF_FORMAT_S16) { + use_int = 1; + } else if (format != AF_FORMAT_FLOAT) { + return false; + } + int bps = use_int ? 2 : 4; + + s->frames_stride = srate * s->opts->ms_stride; + s->bytes_stride = s->frames_stride * bps * nch; + + update_speed(s, s->speed); + + int frames_overlap = s->frames_stride * s->opts->percent_overlap; + if (frames_overlap <= 0) { + s->bytes_standing = s->bytes_stride; + s->samples_standing = s->bytes_standing / bps; + s->output_overlap = NULL; + s->bytes_overlap = 0; + } else { + s->samples_overlap = frames_overlap * nch; + s->bytes_overlap = frames_overlap * nch * bps; + s->bytes_standing = s->bytes_stride - s->bytes_overlap; + s->samples_standing = s->bytes_standing / bps; + s->buf_overlap = realloc(s->buf_overlap, s->bytes_overlap); + s->table_blend = realloc(s->table_blend, s->bytes_overlap * 4); + if (!s->buf_overlap || !s->table_blend) { + MP_FATAL(f, "Out of memory\n"); + return false; + } + memset(s->buf_overlap, 0, s->bytes_overlap); + if (use_int) { + int32_t *pb = s->table_blend; + int64_t blend = 0; + for (int i = 0; i < frames_overlap; i++) { + int32_t v = blend / frames_overlap; + for (int j = 0; j < nch; j++) + *pb++ = v; + blend += 65536; // 2^16 } - memset(s->buf_overlap, 0, s->bytes_overlap); - if (use_int) { - int32_t *pb = s->table_blend; - int64_t blend = 0; - for (int i = 0; i < frames_overlap; i++) { - int32_t v = blend / frames_overlap; - for (int j = 0; j < nch; j++) - *pb++ = v; - blend += 65536; // 2^16 - } - s->output_overlap = output_overlap_s16; - } else { - float *pb = s->table_blend; - for (int i = 0; i < frames_overlap; i++) { - float v = i / (float)frames_overlap; - for (int j = 0; j < nch; j++) - *pb++ = v; - } - s->output_overlap = output_overlap_float; + s->output_overlap = output_overlap_s16; + } else { + float *pb = s->table_blend; + for (int i = 0; i < frames_overlap; i++) { + float v = i / (float)frames_overlap; + for (int j = 0; j < nch; j++) + *pb++ = v; } + s->output_overlap = output_overlap_float; } + } - s->frames_search = (frames_overlap > 1) ? srate * s->ms_search : 0; - if (s->frames_search <= 0) - s->best_overlap_offset = NULL; - else { - if (use_int) { - int64_t t = frames_overlap; - int32_t n = 8589934588LL / (t * t); // 4 * (2^31 - 1) / t^2 - s->buf_pre_corr = realloc(s->buf_pre_corr, - s->bytes_overlap * 2 + UNROLL_PADDING); - s->table_window = realloc(s->table_window, - s->bytes_overlap * 2 - nch * bps * 2); - if (!s->buf_pre_corr || !s->table_window) { - MP_FATAL(af, "Out of memory\n"); - return AF_ERROR; - } - memset((char *)s->buf_pre_corr + s->bytes_overlap * 2, 0, - UNROLL_PADDING); - int32_t *pw = s->table_window; - for (int i = 1; i < frames_overlap; i++) { - int32_t v = (i * (t - i) * n) >> 15; - for (int j = 0; j < nch; j++) - *pw++ = v; - } - s->best_overlap_offset = best_overlap_offset_s16; - } else { - s->buf_pre_corr = realloc(s->buf_pre_corr, s->bytes_overlap); - s->table_window = realloc(s->table_window, - s->bytes_overlap - nch * bps); - if (!s->buf_pre_corr || !s->table_window) { - MP_FATAL(af, "Out of memory\n"); - return AF_ERROR; - } - float *pw = s->table_window; - for (int i = 1; i < frames_overlap; i++) { - float v = i * (frames_overlap - i); - for (int j = 0; j < nch; j++) - *pw++ = v; - } - s->best_overlap_offset = best_overlap_offset_float; + s->frames_search = (frames_overlap > 1) ? srate * s->opts->ms_search : 0; + if (s->frames_search <= 0) + s->best_overlap_offset = NULL; + else { + if (use_int) { + int64_t t = frames_overlap; + int32_t n = 8589934588LL / (t * t); // 4 * (2^31 - 1) / t^2 + s->buf_pre_corr = realloc(s->buf_pre_corr, + s->bytes_overlap * 2 + UNROLL_PADDING); + s->table_window = realloc(s->table_window, + s->bytes_overlap * 2 - nch * bps * 2); + if (!s->buf_pre_corr || !s->table_window) { + MP_FATAL(f, "Out of memory\n"); + return false; + } + memset((char *)s->buf_pre_corr + s->bytes_overlap * 2, 0, + UNROLL_PADDING); + int32_t *pw = s->table_window; + for (int i = 1; i < frames_overlap; i++) { + int32_t v = (i * (t - i) * n) >> 15; + for (int j = 0; j < nch; j++) + *pw++ = v; } + s->best_overlap_offset = best_overlap_offset_s16; + } else { + s->buf_pre_corr = realloc(s->buf_pre_corr, s->bytes_overlap); + s->table_window = realloc(s->table_window, + s->bytes_overlap - nch * bps); + if (!s->buf_pre_corr || !s->table_window) { + MP_FATAL(f, "Out of memory\n"); + return false; + } + float *pw = s->table_window; + for (int i = 1; i < frames_overlap; i++) { + float v = i * (frames_overlap - i); + for (int j = 0; j < nch; j++) + *pw++ = v; + } + s->best_overlap_offset = best_overlap_offset_float; } + } - s->bytes_per_frame = bps * nch; - s->num_channels = nch; - - s->bytes_queue = (s->frames_search + s->frames_stride + frames_overlap) - * bps * nch; - s->buf_queue = realloc(s->buf_queue, s->bytes_queue + UNROLL_PADDING); - if (!s->buf_queue) { - MP_FATAL(af, "Out of memory\n"); - return AF_ERROR; - } + s->bytes_per_frame = bps * nch; + s->num_channels = nch; - s->bytes_queued = 0; - s->bytes_to_slide = 0; - - MP_DBG(af, "" - "%.2f stride_in, %i stride_out, %i standing, " - "%i overlap, %i search, %i queue, %s mode\n", - s->frames_stride_scaled, - (int)(s->bytes_stride / nch / bps), - (int)(s->bytes_standing / nch / bps), - (int)(s->bytes_overlap / nch / bps), - s->frames_search, - (int)(s->bytes_queue / nch / bps), - (use_int ? "s16" : "float")); - - return af_test_output(af, (struct mp_audio *)arg); + s->bytes_queue = (s->frames_search + s->frames_stride + frames_overlap) + * bps * nch; + s->buf_queue = realloc(s->buf_queue, s->bytes_queue + UNROLL_PADDING); + if (!s->buf_queue) { + MP_FATAL(f, "Out of memory\n"); + return false; } - case AF_CONTROL_SET_PLAYBACK_SPEED: { - double speed = *(double *)arg; - if (s->speed_opt & SCALE_TEMPO) { - if (s->speed_opt & SCALE_PITCH) - break; - update_speed(af, speed); - } else if (s->speed_opt & SCALE_PITCH) { - update_speed(af, speed); - break; // do not signal OK + + s->bytes_queued = 0; + s->bytes_to_slide = 0; + + MP_DBG(f, "" + "%.2f stride_in, %i stride_out, %i standing, " + "%i overlap, %i search, %i queue, %s mode\n", + s->frames_stride_scaled, + (int)(s->bytes_stride / nch / bps), + (int)(s->bytes_standing / nch / bps), + (int)(s->bytes_overlap / nch / bps), + s->frames_search, + (int)(s->bytes_queue / nch / bps), + (use_int ? "s16" : "float")); + + mp_aframe_config_copy(s->cur_format, in); + + return true; +} + +static bool command(struct mp_filter *f, struct mp_filter_command *cmd) +{ + struct priv *s = f->priv; + + if (cmd->type == MP_FILTER_COMMAND_SET_SPEED) { + if (s->opts->speed_opt & SCALE_TEMPO) { + if (s->opts->speed_opt & SCALE_PITCH) + return false; + update_speed(s, cmd->speed); + return true; + } else if (s->opts->speed_opt & SCALE_PITCH) { + update_speed(s, cmd->speed); + return false; // do not signal OK } - return AF_OK; - } - case AF_CONTROL_RESET: - s->bytes_queued = 0; - s->bytes_to_slide = 0; - s->frames_stride_error = 0; - memset(s->buf_overlap, 0, s->bytes_overlap); } - return AF_UNKNOWN; + + return false; } -// Deallocate memory -static void uninit(struct af_instance *af) +static void reset(struct mp_filter *f) { - af_scaletempo_t *s = af->priv; + struct priv *s = f->priv; + + s->current_pts = MP_NOPTS_VALUE; + s->bytes_queued = 0; + s->bytes_to_slide = 0; + s->frames_stride_error = 0; + memset(s->buf_overlap, 0, s->bytes_overlap); +} + +static void destroy(struct mp_filter *f) +{ + struct priv *s = f->priv; free(s->buf_queue); free(s->buf_overlap); free(s->buf_pre_corr); free(s->table_blend); free(s->table_window); + mp_filter_free_children(f); } -// Allocate memory and set function pointers -static int af_open(struct af_instance *af) +static const struct mp_filter_info af_scaletempo_filter = { + .name = "scaletempo", + .priv_size = sizeof(struct priv), + .process = process, + .command = command, + .reset = reset, + .destroy = destroy, +}; + +static struct mp_filter *af_scaletempo_create(struct mp_filter *parent, + void *options) { - af->control = control; - af->uninit = uninit; - af->filter_frame = filter; - return AF_OK; -} + struct mp_filter *f = mp_filter_create(parent, &af_scaletempo_filter); + if (!f) { + talloc_free(options); + return NULL; + } -#define OPT_BASE_STRUCT af_scaletempo_t + mp_filter_add_pin(f, MP_PIN_IN, "in"); + mp_filter_add_pin(f, MP_PIN_OUT, "out"); -const struct af_info af_info_scaletempo = { - .info = "Scale audio tempo while maintaining pitch", - .name = "scaletempo", - .open = af_open, - .priv_size = sizeof(af_scaletempo_t), - .priv_defaults = &(const af_scaletempo_t) { - .ms_stride = 60, - .percent_overlap = .20, - .ms_search = 14, - .speed_opt = SCALE_TEMPO, - .speed = 1.0, - .scale_nominal = 1.0, - }, - .options = (const struct m_option[]) { - OPT_FLOAT("scale", scale_nominal, M_OPT_MIN, .min = 0.01), - OPT_FLOAT("stride", ms_stride, M_OPT_MIN, .min = 0.01), - OPT_FLOAT("overlap", percent_overlap, M_OPT_RANGE, .min = 0, .max = 1), - OPT_FLOAT("search", ms_search, M_OPT_MIN, .min = 0), - OPT_CHOICE("speed", speed_opt, 0, - ({"pitch", SCALE_PITCH}, - {"tempo", SCALE_TEMPO}, - {"none", 0}, - {"both", SCALE_TEMPO | SCALE_PITCH})), - {0} + struct priv *s = f->priv; + s->opts = talloc_steal(s, options); + s->speed = 1.0; + s->cur_format = talloc_steal(s, mp_aframe_create()); + s->out_pool = mp_aframe_pool_create(s); + + struct mp_autoconvert *conv = mp_autoconvert_create(f); + if (!conv) + abort(); + + mp_autoconvert_add_afmt(conv, AF_FORMAT_S16); + mp_autoconvert_add_afmt(conv, AF_FORMAT_FLOAT); + + mp_pin_connect(conv->f->pins[0], f->ppins[0]); + s->in_pin = conv->f->pins[1]; + + return f; +} + +#define OPT_BASE_STRUCT struct f_opts + +const struct mp_user_filter_entry af_scaletempo = { + .desc = { + .description = "Scale audio tempo while maintaining pitch", + .name = "scaletempo", + .priv_size = sizeof(OPT_BASE_STRUCT), + .priv_defaults = &(const OPT_BASE_STRUCT) { + .ms_stride = 60, + .percent_overlap = .20, + .ms_search = 14, + .speed_opt = SCALE_TEMPO, + .scale_nominal = 1.0, + }, + .options = (const struct m_option[]) { + OPT_FLOAT("scale", scale_nominal, M_OPT_MIN, .min = 0.01), + OPT_FLOAT("stride", ms_stride, M_OPT_MIN, .min = 0.01), + OPT_FLOAT("overlap", percent_overlap, M_OPT_RANGE, .min = 0, .max = 1), + OPT_FLOAT("search", ms_search, M_OPT_MIN, .min = 0), + OPT_CHOICE("speed", speed_opt, 0, + ({"pitch", SCALE_PITCH}, + {"tempo", SCALE_TEMPO}, + {"none", 0}, + {"both", SCALE_TEMPO | SCALE_PITCH})), + {0} + }, }, + .create = af_scaletempo_create, }; |