You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
563 lines
19 KiB
563 lines
19 KiB
/*
|
|
Calf Box, an open source musical instrument.
|
|
Copyright (C) 2010-2011 Krzysztof Foltman
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "cmd.h"
|
|
#include "config.h"
|
|
#include "config-api.h"
|
|
#include "dspmath.h"
|
|
#include "errors.h"
|
|
#include "tarfile.h"
|
|
#include "wavebank.h"
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <glib.h>
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define STD_WAVEFORM_FRAMES 1024
|
|
#define STD_WAVEFORM_BITS 10
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Sine table
|
|
static complex float euler_table[STD_WAVEFORM_FRAMES];
|
|
|
|
// Bit reversal table
|
|
static int map_table[STD_WAVEFORM_FRAMES];
|
|
|
|
// Initialise tables using for FFT
|
|
static void init_tables(void)
|
|
{
|
|
int rev = 1 << (STD_WAVEFORM_BITS - 1);
|
|
for (int i = 0; i < STD_WAVEFORM_FRAMES; i++)
|
|
{
|
|
euler_table[i] = cos(i * 2 * M_PI / STD_WAVEFORM_FRAMES) + I * sin(i * 2 * M_PI / STD_WAVEFORM_FRAMES);
|
|
int ni = 0;
|
|
for (int j = 0; j < STD_WAVEFORM_BITS; j++)
|
|
{
|
|
if (i & (rev >> j))
|
|
ni = ni | (1 << j);
|
|
}
|
|
map_table[i] = ni;
|
|
}
|
|
}
|
|
|
|
// Trivial implementation of Cooley-Tukey, only works for even values of ANALYSIS_BUFFER_BITS
|
|
static void my_fft_main(complex float output[STD_WAVEFORM_FRAMES])
|
|
{
|
|
complex float temp[STD_WAVEFORM_FRAMES];
|
|
|
|
for (int i = 0; i < STD_WAVEFORM_BITS; i++)
|
|
{
|
|
complex float *src = (i & 1) ? temp : output;
|
|
complex float *dst = (i & 1) ? output : temp;
|
|
int invi = STD_WAVEFORM_BITS - i - 1;
|
|
int disp = 1 << i;
|
|
int mask = disp - 1;
|
|
|
|
for (int j = 0; j < STD_WAVEFORM_FRAMES / 2; j++)
|
|
{
|
|
int jj1 = (j & mask) + ((j & ~mask) << 1); // insert 0 at i'th bit to get the left arm of the butterfly
|
|
int jj2 = jj1 + disp; // insert 1 at i'th bit to get the right arm
|
|
assert((jj1 + disp) == (jj1 | disp));
|
|
|
|
// e^iw
|
|
complex float eiw1 = euler_table[(jj1 << invi) & (STD_WAVEFORM_FRAMES - 1)];
|
|
complex float eiw2 = euler_table[(jj2 << invi) & (STD_WAVEFORM_FRAMES - 1)];
|
|
|
|
// printf("%d -> %d, %d\n", j, jj, jj + disp);
|
|
butterfly(&dst[jj1], &dst[jj2], src[jj1], src[jj2], eiw1, eiw2);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void my_fft_r2c(complex float output[STD_WAVEFORM_FRAMES], int16_t input[STD_WAVEFORM_FRAMES])
|
|
{
|
|
assert(!(STD_WAVEFORM_BITS&1));
|
|
// Copy + bit reversal addressing
|
|
for (int i = 0; i < STD_WAVEFORM_FRAMES; i++)
|
|
output[i] = input[map_table[i]] * (1.0 / STD_WAVEFORM_FRAMES);
|
|
|
|
my_fft_main(output);
|
|
|
|
}
|
|
|
|
static void my_ifft_c2r(int16_t output[STD_WAVEFORM_FRAMES], complex float input[STD_WAVEFORM_FRAMES])
|
|
{
|
|
complex float temp2[STD_WAVEFORM_FRAMES];
|
|
for (int i = 0; i < STD_WAVEFORM_FRAMES; i++)
|
|
temp2[i] = input[map_table[i]];
|
|
assert(!(STD_WAVEFORM_BITS&1));
|
|
|
|
my_fft_main(temp2);
|
|
// Copy + bit reversal addressing
|
|
float maxv = 0;
|
|
for (int i = 0; i < STD_WAVEFORM_FRAMES; i++)
|
|
{
|
|
float value = creal(temp2[i]);
|
|
if (value < -32768) value = -32768;
|
|
if (value > 32767) value = 32767;
|
|
if (fabs(value) > maxv)
|
|
maxv = fabs(value);
|
|
output[i] = (int16_t)value;
|
|
}
|
|
}
|
|
|
|
struct wave_bank
|
|
{
|
|
int64_t bytes, maxbytes, serial_no;
|
|
GHashTable *waveforms_by_name, *waveforms_by_id;
|
|
GSList *std_waveforms;
|
|
uint32_t streaming_prefetch_size;
|
|
};
|
|
|
|
static struct wave_bank bank;
|
|
|
|
GQuark cbox_waveform_error_quark(void)
|
|
{
|
|
return g_quark_from_string("cbox-waveform-error-quark");
|
|
}
|
|
|
|
float func_sine(float v, void *user_data)
|
|
{
|
|
return sin(2 * M_PI * v);
|
|
}
|
|
|
|
float func_silence(float v, void *user_data)
|
|
{
|
|
return 0.f;
|
|
}
|
|
|
|
float func_sqr(float v, void *user_data)
|
|
{
|
|
return v < 0.5 ? -1 : 1;
|
|
}
|
|
|
|
float func_saw(float v, void *user_data)
|
|
{
|
|
return 2 * v - 1;
|
|
}
|
|
|
|
float func_tri(float v, void *user_data)
|
|
{
|
|
if (v <= 0.25f)
|
|
return v * 4;
|
|
if (v <= 0.75f)
|
|
return 1 - (v - 0.25f) * 4;
|
|
return -1 + 4 * (v - 0.75f);
|
|
}
|
|
|
|
void cbox_waveform_generate_levels(struct cbox_waveform *waveform, int levels, double ratio)
|
|
{
|
|
complex float output[STD_WAVEFORM_FRAMES], bandlimited[STD_WAVEFORM_FRAMES];
|
|
my_fft_r2c(output, waveform->data);
|
|
int N = STD_WAVEFORM_FRAMES;
|
|
|
|
waveform->levels = calloc(levels, sizeof(struct cbox_waveform_level));
|
|
double rate = 65536.0 * 65536.0; // / waveform->info.frames;
|
|
double orig_rate = 65536.0 * 65536.0; // / waveform->info.frames;
|
|
for (int i = 0; i < levels; i++)
|
|
{
|
|
int harmonics = N / 2 / (rate / orig_rate);
|
|
bandlimited[0] = 0;
|
|
|
|
if (harmonics > 0)
|
|
{
|
|
for (int j = 1; j <= harmonics; j++)
|
|
{
|
|
bandlimited[j] = output[j];
|
|
bandlimited[N - j] = output[N - j];
|
|
}
|
|
for (int j = harmonics; j <= N / 2; j++)
|
|
bandlimited[j] = bandlimited [N - j] = 0;
|
|
}
|
|
|
|
waveform->levels[i].data = calloc(N + MAX_INTERPOLATION_ORDER, sizeof(int16_t));
|
|
my_ifft_c2r(waveform->levels[i].data, bandlimited);
|
|
memcpy(waveform->levels[i].data + N, waveform->levels[i].data, MAX_INTERPOLATION_ORDER * sizeof(int16_t));
|
|
waveform->levels[i].max_rate = (uint64_t)(rate);
|
|
rate *= ratio;
|
|
}
|
|
waveform->level_count = levels;
|
|
}
|
|
|
|
void cbox_wavebank_add_std_waveform(const char *name, float (*getfunc)(float v, void *user_data), void *user_data, int levels)
|
|
{
|
|
int nsize = STD_WAVEFORM_FRAMES;
|
|
int16_t *wave = calloc(nsize, sizeof(int16_t));
|
|
for (int i = 0; i < nsize; i++)
|
|
{
|
|
float v = getfunc(i * 1.0 / nsize, user_data);
|
|
if (fabs(v) > 1)
|
|
v = (v < 0) ? -1 : 1;
|
|
// cannot use full scale here, because bandlimiting will introduce
|
|
// some degree of overshoot
|
|
wave[i] = (int16_t)(25000 * v);
|
|
}
|
|
struct cbox_waveform *waveform = calloc(1, sizeof(struct cbox_waveform));
|
|
waveform->data = wave;
|
|
waveform->info.channels = 1;
|
|
waveform->preloaded_frames = waveform->info.frames = nsize;
|
|
waveform->info.samplerate = (int)(nsize * 261.6255);
|
|
waveform->id = ++bank.serial_no;
|
|
waveform->bytes = waveform->info.channels * 2 * (waveform->info.frames + 1);
|
|
waveform->refcount = 1;
|
|
waveform->canonical_name = g_strdup(name);
|
|
waveform->display_name = g_strdup(name);
|
|
waveform->has_loop = TRUE;
|
|
waveform->loop_start = 0;
|
|
waveform->loop_end = nsize;
|
|
waveform->levels = NULL;
|
|
waveform->level_count = 0;
|
|
|
|
if (levels)
|
|
cbox_waveform_generate_levels(waveform, levels, 2);
|
|
|
|
g_hash_table_insert(bank.waveforms_by_name, waveform->canonical_name, waveform);
|
|
g_hash_table_insert(bank.waveforms_by_id, &waveform->id, waveform);
|
|
bank.std_waveforms = g_slist_prepend(bank.std_waveforms, waveform);
|
|
// These waveforms are not included in the bank size, I don't think it has
|
|
// much value for the user.
|
|
}
|
|
|
|
void cbox_wavebank_init()
|
|
{
|
|
init_tables();
|
|
|
|
bank.bytes = 0;
|
|
bank.maxbytes = 0;
|
|
bank.serial_no = 0;
|
|
bank.waveforms_by_name = g_hash_table_new(g_str_hash, g_str_equal);
|
|
bank.waveforms_by_id = g_hash_table_new(g_int_hash, g_int_equal);
|
|
bank.std_waveforms = NULL;
|
|
bank.streaming_prefetch_size = cbox_config_get_int("streaming", "prefetch_size", 65536);
|
|
|
|
cbox_wavebank_add_std_waveform("*sine", func_sine, NULL, 0);
|
|
// XXXKF this should not be a real waveform
|
|
cbox_wavebank_add_std_waveform("*silence", func_silence, NULL, 0);
|
|
cbox_wavebank_add_std_waveform("*saw", func_saw, NULL, 11);
|
|
cbox_wavebank_add_std_waveform("*square", func_sqr, NULL, 11);
|
|
cbox_wavebank_add_std_waveform("*triangle", func_tri, NULL, 11);
|
|
}
|
|
|
|
struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struct cbox_tarfile *tarfile, const char *sample_dir, const char *filename, GError **error)
|
|
{
|
|
if (!filename)
|
|
{
|
|
g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED, "%s: no filename specified", context_name);
|
|
return NULL;
|
|
}
|
|
|
|
// Built in waveforms don't go through path canonicalization
|
|
if (filename[0] == '*')
|
|
{
|
|
if (!strcmp(filename + 1, "sqr"))
|
|
filename = "*square";
|
|
else if (!strcmp(filename + 1, "tri"))
|
|
filename = "*triangle";
|
|
gpointer value = g_hash_table_lookup(bank.waveforms_by_name, filename);
|
|
if (value)
|
|
{
|
|
struct cbox_waveform *waveform = value;
|
|
cbox_waveform_ref(waveform);
|
|
return waveform;
|
|
}
|
|
}
|
|
|
|
gchar *value_copy = g_strdup(filename);
|
|
for (int i = 0; value_copy[i]; i++)
|
|
{
|
|
if (value_copy[i] == '\\')
|
|
value_copy[i] = '/';
|
|
}
|
|
gchar *pathname = value_copy[0] == '/' ? g_strdup(value_copy) : g_build_filename(sample_dir, value_copy, NULL);
|
|
g_free(value_copy);
|
|
|
|
char *canonical = NULL;
|
|
if (tarfile)
|
|
canonical = g_strdup_printf("sbtar:%s;%s", tarfile->file_pathname, pathname);
|
|
else
|
|
{
|
|
// make sure canonical is always allocated on the same (glib) heap
|
|
char *p = realpath(pathname, NULL);
|
|
if (p)
|
|
{
|
|
canonical = g_strdup(p);
|
|
free(p);
|
|
}
|
|
}
|
|
if (!canonical)
|
|
{
|
|
g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED, "%s: cannot find a real path for '%s': %s", context_name, pathname, strerror(errno));
|
|
g_free(pathname);
|
|
return NULL;
|
|
}
|
|
gpointer value = g_hash_table_lookup(bank.waveforms_by_name, canonical);
|
|
if (value)
|
|
{
|
|
g_free(pathname);
|
|
g_free(canonical);
|
|
|
|
struct cbox_waveform *waveform = value;
|
|
cbox_waveform_ref(waveform);
|
|
return waveform;
|
|
}
|
|
|
|
struct cbox_waveform *waveform = calloc(1, sizeof(struct cbox_waveform));
|
|
SNDFILE *sndfile = NULL;
|
|
struct cbox_taritem *taritem = NULL;
|
|
if (tarfile)
|
|
{
|
|
if (strcmp(sample_dir, ".") == 0)
|
|
{
|
|
//Potential path lookup problem:
|
|
|
|
//This is a sample without sfz default_path opcode inside a tar.
|
|
//We need to set the sample dir to the position of the .sfz file within the .tar
|
|
//because we also assume that the sample paths in regions are relative to the .sfz path.
|
|
|
|
//If the sfz is in the tar root this is a redundant action, but if the sfz is itself
|
|
//in a subdirectoy we need to adjust the path now.
|
|
// XXXNH sample_dir will not be updated in the struct itself and thus reported as "." in python etc.
|
|
|
|
//context_name is the sfz file, filename the sample file without leading ./ and sample_dir just a dot.
|
|
|
|
gchar *sfz_dir = g_path_get_dirname(context_name); //take the path of the sfz file...
|
|
pathname = g_build_filename(sfz_dir, filename, NULL); //... and prefix the sample filename with it.
|
|
g_free(sfz_dir);
|
|
}
|
|
taritem = cbox_tarfile_get_item_by_name(tarfile, pathname, TRUE);
|
|
if (taritem)
|
|
sndfile = cbox_tarfile_opensndfile(tarfile, taritem, &waveform->sndstream, &waveform->info);
|
|
}
|
|
else
|
|
sndfile = sf_open(pathname, SFM_READ, &waveform->info);
|
|
if (!sndfile)
|
|
{
|
|
g_set_error(error, G_FILE_ERROR, g_file_error_from_errno (errno), "%s: cannot open '%s'", context_name, pathname);
|
|
g_free(pathname);
|
|
g_free(canonical);
|
|
free(waveform);
|
|
return NULL;
|
|
}
|
|
|
|
SF_INSTRUMENT instrument;
|
|
uint32_t nshorts;
|
|
if (waveform->info.channels != 1 && waveform->info.channels != 2)
|
|
{
|
|
g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED,
|
|
"%s: cannot open file '%s': unsupported channel count %d", context_name, pathname, (int)waveform->info.channels);
|
|
sf_close(sndfile);
|
|
free(canonical);
|
|
g_free(pathname);
|
|
return NULL;
|
|
}
|
|
g_free(pathname);
|
|
uint32_t preloaded_frames = waveform->info.frames;
|
|
// If sample is larger than 2x prefetch buffer size, then load only
|
|
// a prefetch buffer worth of data, and stream the rest.
|
|
if (preloaded_frames > 2 * bank.streaming_prefetch_size)
|
|
preloaded_frames = bank.streaming_prefetch_size;
|
|
waveform->id = ++bank.serial_no;
|
|
waveform->bytes = waveform->info.channels * 2 * preloaded_frames;
|
|
waveform->data = malloc(waveform->bytes);
|
|
waveform->refcount = 1;
|
|
waveform->canonical_name = canonical;
|
|
waveform->display_name = g_filename_display_name(canonical);
|
|
waveform->has_loop = FALSE;
|
|
waveform->levels = NULL;
|
|
waveform->level_count = 0;
|
|
waveform->preloaded_frames = preloaded_frames;
|
|
waveform->tarfile = tarfile;
|
|
waveform->taritem = taritem;
|
|
|
|
if (sf_command(sndfile, SFC_GET_INSTRUMENT, &instrument, sizeof(SF_INSTRUMENT)))
|
|
{
|
|
for (int i = 0; i < instrument.loop_count; i++)
|
|
{
|
|
if (instrument.loops[i].mode == SF_LOOP_FORWARD)
|
|
{
|
|
waveform->loop_start = instrument.loops[i].start;
|
|
waveform->loop_end = instrument.loops[i].end;
|
|
waveform->has_loop = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
nshorts = waveform->info.channels * preloaded_frames;
|
|
for (uint32_t i = 0; i < nshorts; i++)
|
|
waveform->data[i] = 0;
|
|
sf_readf_short(sndfile, waveform->data, preloaded_frames);
|
|
sf_close(sndfile);
|
|
bank.bytes += waveform->bytes;
|
|
if (bank.bytes > bank.maxbytes)
|
|
bank.maxbytes = bank.bytes;
|
|
g_hash_table_insert(bank.waveforms_by_name, waveform->canonical_name, waveform);
|
|
g_hash_table_insert(bank.waveforms_by_id, &waveform->id, waveform);
|
|
|
|
return waveform;
|
|
}
|
|
|
|
int64_t cbox_wavebank_get_bytes()
|
|
{
|
|
return bank.bytes;
|
|
}
|
|
|
|
int64_t cbox_wavebank_get_maxbytes()
|
|
{
|
|
return bank.maxbytes;
|
|
}
|
|
|
|
int cbox_wavebank_get_count()
|
|
{
|
|
return g_hash_table_size(bank.waveforms_by_id);
|
|
}
|
|
|
|
struct cbox_waveform *cbox_wavebank_peek_waveform_by_id(int id)
|
|
{
|
|
return g_hash_table_lookup(bank.waveforms_by_id, &id);
|
|
}
|
|
|
|
void cbox_wavebank_foreach(void (*cb)(void *, struct cbox_waveform *), void *user_data)
|
|
{
|
|
GHashTableIter iter;
|
|
gpointer key, value;
|
|
|
|
g_hash_table_iter_init (&iter, bank.waveforms_by_id);
|
|
while (g_hash_table_iter_next (&iter, &key, &value))
|
|
{
|
|
(*cb)(user_data, value);
|
|
}
|
|
}
|
|
|
|
void cbox_wavebank_close()
|
|
{
|
|
if (bank.bytes > 0)
|
|
g_warning("Warning: %lld bytes in unfreed samples", (long long int)bank.bytes);
|
|
while(bank.std_waveforms)
|
|
{
|
|
cbox_waveform_unref((struct cbox_waveform *)bank.std_waveforms->data);
|
|
bank.std_waveforms = g_slist_delete_link(bank.std_waveforms, bank.std_waveforms);
|
|
}
|
|
g_hash_table_destroy(bank.waveforms_by_id);
|
|
g_hash_table_destroy(bank.waveforms_by_name);
|
|
bank.waveforms_by_id = NULL;
|
|
bank.waveforms_by_name = NULL;
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void cbox_waveform_ref(struct cbox_waveform *waveform)
|
|
{
|
|
++waveform->refcount;
|
|
}
|
|
|
|
void cbox_waveform_unref(struct cbox_waveform *waveform)
|
|
{
|
|
if (--waveform->refcount > 0)
|
|
return;
|
|
|
|
g_hash_table_remove(bank.waveforms_by_name, waveform->canonical_name);
|
|
g_hash_table_remove(bank.waveforms_by_id, &waveform->id);
|
|
bank.bytes -= waveform->bytes;
|
|
|
|
g_free(waveform->display_name);
|
|
g_free(waveform->canonical_name);
|
|
for (int i = 0; i < waveform->level_count; i++)
|
|
free(waveform->levels[i].data);
|
|
free(waveform->levels);
|
|
free(waveform->data);
|
|
free(waveform);
|
|
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct waves_foreach_data
|
|
{
|
|
struct cbox_command_target *fb;
|
|
GError **error;
|
|
gboolean success;
|
|
};
|
|
|
|
void wave_list_cb(void *user_data, struct cbox_waveform *waveform)
|
|
{
|
|
struct waves_foreach_data *wfd = user_data;
|
|
|
|
wfd->success = wfd->success && cbox_execute_on(wfd->fb, NULL, "/waveform", "i", wfd->error, (int)waveform->id);
|
|
}
|
|
|
|
static gboolean waves_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error)
|
|
{
|
|
if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, ""))
|
|
{
|
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
|
return FALSE;
|
|
|
|
// XXXKF this only supports 4GB - not a big deal for now yet?
|
|
return cbox_execute_on(fb, NULL, "/bytes", "i", error, (int)cbox_wavebank_get_bytes()) &&
|
|
cbox_execute_on(fb, NULL, "/max_bytes", "i", error, (int)cbox_wavebank_get_maxbytes()) &&
|
|
cbox_execute_on(fb, NULL, "/count", "i", error, (int)cbox_wavebank_get_count())
|
|
;
|
|
}
|
|
else if (!strcmp(cmd->command, "/list") && !strcmp(cmd->arg_types, ""))
|
|
{
|
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
|
return FALSE;
|
|
|
|
struct waves_foreach_data wfd = { fb, error, TRUE };
|
|
cbox_wavebank_foreach(wave_list_cb, &wfd);
|
|
return wfd.success;
|
|
}
|
|
else if (!strcmp(cmd->command, "/info") && !strcmp(cmd->arg_types, "i"))
|
|
{
|
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
|
return FALSE;
|
|
|
|
int id = CBOX_ARG_I(cmd, 0);
|
|
struct cbox_waveform *waveform = cbox_wavebank_peek_waveform_by_id(id);
|
|
if (waveform == NULL)
|
|
{
|
|
g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Waveform %d not found", id);
|
|
return FALSE;
|
|
}
|
|
assert(id == waveform->id);
|
|
if (!cbox_execute_on(fb, NULL, "/filename", "s", error, waveform->canonical_name)) // XXXKF convert to utf8
|
|
return FALSE;
|
|
if (!cbox_execute_on(fb, NULL, "/name", "s", error, waveform->display_name))
|
|
return FALSE;
|
|
if (!cbox_execute_on(fb, NULL, "/bytes", "i", error, (int)waveform->bytes))
|
|
return FALSE;
|
|
if (waveform->has_loop && !cbox_execute_on(fb, NULL, "/loop", "ii", error, (int)waveform->loop_start, (int)waveform->loop_end))
|
|
return FALSE;
|
|
return TRUE;
|
|
}
|
|
else
|
|
{
|
|
g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
struct cbox_command_target cbox_waves_cmd_target =
|
|
{
|
|
.process_cmd = waves_process_cmd,
|
|
.user_data = NULL
|
|
};
|
|
|