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.
358 lines
12 KiB
358 lines
12 KiB
3 years ago
|
/*
|
||
|
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 "app.h"
|
||
|
#include "engine.h"
|
||
|
#include "errors.h"
|
||
|
#include "song.h"
|
||
|
#include "track.h"
|
||
|
#include <assert.h>
|
||
|
#include <stdlib.h>
|
||
|
|
||
|
CBOX_CLASS_DEFINITION_ROOT(cbox_song)
|
||
|
|
||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
void cbox_master_track_item_destroy(struct cbox_master_track_item *item)
|
||
|
{
|
||
|
free(item);
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
gboolean cbox_song_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error)
|
||
|
{
|
||
|
struct cbox_song *song = ct->user_data;
|
||
|
if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, ""))
|
||
|
{
|
||
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
||
|
return FALSE;
|
||
|
|
||
|
for(GList *p = song->tracks; p; p = g_list_next(p))
|
||
|
{
|
||
|
struct cbox_track *trk = p->data;
|
||
|
if (!cbox_execute_on(fb, NULL, "/track", "sio", error, trk->name, g_list_length(trk->items), trk))
|
||
|
return FALSE;
|
||
|
}
|
||
|
for(GList *p = song->patterns; p; p = g_list_next(p))
|
||
|
{
|
||
|
struct cbox_midi_pattern *pat = p->data;
|
||
|
if (!cbox_execute_on(fb, NULL, "/pattern", "sio", error, pat->name, pat->loop_end, pat))
|
||
|
return FALSE;
|
||
|
}
|
||
|
uint32_t pos = 0;
|
||
|
for(GList *p = song->master_track_items; p; p = g_list_next(p))
|
||
|
{
|
||
|
struct cbox_master_track_item *mti = p->data;
|
||
|
// Omit dummy item at 0 position.
|
||
|
if (pos || (mti->timesig_num && mti->timesig_denom) || mti->tempo)
|
||
|
{
|
||
|
if (!cbox_execute_on(fb, NULL, "/mti", "ifii", error, pos, mti->tempo, mti->timesig_num, mti->timesig_denom))
|
||
|
return FALSE;
|
||
|
}
|
||
|
pos += mti->duration_ppqn;
|
||
|
}
|
||
|
return cbox_execute_on(fb, NULL, "/loop_start", "i", error, (int)song->loop_start_ppqn) &&
|
||
|
cbox_execute_on(fb, NULL, "/loop_end", "i", error, (int)song->loop_end_ppqn) &&
|
||
|
CBOX_OBJECT_DEFAULT_STATUS(song, fb, error);
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/set_loop") && !strcmp(cmd->arg_types, "ii"))
|
||
|
{
|
||
|
song->loop_start_ppqn = CBOX_ARG_I(cmd, 0);
|
||
|
song->loop_end_ppqn = CBOX_ARG_I(cmd, 1);
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/set_mti") && !strcmp(cmd->arg_types, "ifii"))
|
||
|
{
|
||
|
cbox_song_set_mti(song, CBOX_ARG_I(cmd, 0), CBOX_ARG_F(cmd, 1), CBOX_ARG_I(cmd, 2), CBOX_ARG_I(cmd, 3));
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/clear") && !strcmp(cmd->arg_types, ""))
|
||
|
{
|
||
|
cbox_song_clear(song);
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/add_track") && !strcmp(cmd->arg_types, ""))
|
||
|
{
|
||
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
||
|
return FALSE;
|
||
|
|
||
|
struct cbox_track *track = cbox_track_new(CBOX_GET_DOCUMENT(song));
|
||
|
cbox_song_add_track(song, track);
|
||
|
if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, track))
|
||
|
{
|
||
|
CBOX_DELETE(track);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/load_pattern") && !strcmp(cmd->arg_types, "si"))
|
||
|
{
|
||
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
||
|
return FALSE;
|
||
|
|
||
|
struct cbox_midi_pattern *pattern = cbox_midi_pattern_load(song, CBOX_ARG_S(cmd, 0), CBOX_ARG_I(cmd, 1), app.engine->master->ppqn_factor);
|
||
|
if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern))
|
||
|
{
|
||
|
CBOX_DELETE(pattern);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/load_track") && !strcmp(cmd->arg_types, "si"))
|
||
|
{
|
||
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
||
|
return FALSE;
|
||
|
|
||
|
struct cbox_midi_pattern *pattern = cbox_midi_pattern_load_track(song, CBOX_ARG_S(cmd, 0), CBOX_ARG_I(cmd, 1), app.engine->master->ppqn_factor);
|
||
|
if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern))
|
||
|
{
|
||
|
CBOX_DELETE(pattern);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/load_metronome") && !strcmp(cmd->arg_types, "i"))
|
||
|
{
|
||
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
||
|
return FALSE;
|
||
|
|
||
|
struct cbox_midi_pattern *pattern = cbox_midi_pattern_new_metronome(song, CBOX_ARG_I(cmd, 0), app.engine->master->ppqn_factor);
|
||
|
if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern))
|
||
|
{
|
||
|
CBOX_DELETE(pattern);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
if (!strcmp(cmd->command, "/load_blob") && !strcmp(cmd->arg_types, "bi"))
|
||
|
{
|
||
|
if (!cbox_check_fb_channel(fb, cmd->command, error))
|
||
|
return FALSE;
|
||
|
|
||
|
struct cbox_midi_pattern *pattern = cbox_midi_pattern_new_from_blob(song, CBOX_ARG_B(cmd, 0), CBOX_ARG_I(cmd, 1), app.engine->master->ppqn_factor);
|
||
|
if (!cbox_execute_on(fb, NULL, "/uuid", "o", error, pattern))
|
||
|
{
|
||
|
CBOX_DELETE(pattern);
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
else
|
||
|
return cbox_object_default_process_cmd(ct, fb, cmd, error);
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
struct cbox_song *cbox_song_new(struct cbox_document *document)
|
||
|
{
|
||
|
struct cbox_song *p = calloc(1, sizeof(struct cbox_song));
|
||
|
CBOX_OBJECT_HEADER_INIT(p, cbox_song, document);
|
||
|
|
||
|
// Create the first, dummy tempo map item
|
||
|
struct cbox_master_track_item *mti = calloc(1, sizeof(struct cbox_master_track_item));
|
||
|
mti->timesig_num = 0;
|
||
|
mti->timesig_denom = 0;
|
||
|
mti->tempo = 0;
|
||
|
mti->duration_ppqn = 0;
|
||
|
|
||
|
p->master_track_items = g_list_append(NULL, mti);
|
||
|
p->tracks = NULL;
|
||
|
p->patterns = NULL;
|
||
|
p->lyrics_sheet = NULL;
|
||
|
p->chord_sheet = NULL;
|
||
|
p->loop_start_ppqn = 0;
|
||
|
p->loop_end_ppqn = 0;
|
||
|
cbox_command_target_init(&p->cmd_target, cbox_song_process_cmd, p);
|
||
|
CBOX_OBJECT_REGISTER(p);
|
||
|
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
void cbox_song_set_mti(struct cbox_song *song, uint32_t pos, double tempo, int timesig_num, int timesig_denom)
|
||
|
{
|
||
|
uint32_t tstart = 0, tend = 0;
|
||
|
GList *prev = NULL;
|
||
|
// A full no-op
|
||
|
if (tempo < 0 && timesig_num < 0)
|
||
|
return;
|
||
|
gboolean is_noop = tempo == 0 && timesig_num == 0;
|
||
|
|
||
|
struct cbox_master_track_item *mti = NULL;
|
||
|
for(GList *p = song->master_track_items; p; p = g_list_next(p))
|
||
|
{
|
||
|
mti = p->data;
|
||
|
tend = tstart + mti->duration_ppqn;
|
||
|
// printf("range %d-%d %f %d\n", tstart, tend, mti->tempo, mti->timesig_num);
|
||
|
if (pos == tstart)
|
||
|
{
|
||
|
double new_tempo = tempo >= 0 ? tempo : mti->tempo;
|
||
|
int new_timesig_num = timesig_num >= 0 ? timesig_num : mti->timesig_num;
|
||
|
// Is this operation going to become a no-op after the change?
|
||
|
gboolean is_noop_here = new_tempo <= 0 && new_timesig_num <= 0;
|
||
|
// If the new item is a no-op and not the first item, delete it
|
||
|
// and extend the previous item by deleted item's duration
|
||
|
if (is_noop_here)
|
||
|
{
|
||
|
uint32_t deleted_duration = mti->duration_ppqn;
|
||
|
if (prev) {
|
||
|
song->master_track_items = g_list_remove(song->master_track_items, mti);
|
||
|
mti = prev->data;
|
||
|
mti->duration_ppqn += deleted_duration;
|
||
|
} else {
|
||
|
// Instead of deleting the first item, make it a dummy one.
|
||
|
mti->tempo = 0;
|
||
|
mti->timesig_num = 0;
|
||
|
mti->timesig_denom = 0;
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
goto set_values;
|
||
|
}
|
||
|
if (pos >= tstart && pos < tend)
|
||
|
{
|
||
|
if (is_noop || (tempo <= 0 && timesig_num <= 0))
|
||
|
return;
|
||
|
// Split old item's duration
|
||
|
mti->duration_ppqn = pos - tstart;
|
||
|
mti = calloc(1, sizeof(struct cbox_master_track_item));
|
||
|
mti->duration_ppqn = tend - pos;
|
||
|
p = g_list_next(p);
|
||
|
song->master_track_items = g_list_insert_before(song->master_track_items, p, mti);
|
||
|
goto set_values;
|
||
|
}
|
||
|
prev = p;
|
||
|
tstart = tend;
|
||
|
}
|
||
|
// The new item is a no-op and it's not deleting any of the current MTIs.
|
||
|
// Ignore it then.
|
||
|
if (is_noop)
|
||
|
return;
|
||
|
// The add position is past the end of the current MTIs.
|
||
|
if (pos > tend)
|
||
|
{
|
||
|
// Either extend the previous item, if there's any
|
||
|
if (prev)
|
||
|
{
|
||
|
mti = prev->data;
|
||
|
mti->duration_ppqn += pos - tend;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// ... or add a dummy 'pad' item
|
||
|
mti = calloc(1, sizeof(struct cbox_master_track_item));
|
||
|
mti->duration_ppqn = pos;
|
||
|
assert(!song->master_track_items);
|
||
|
song->master_track_items = g_list_append(song->master_track_items, mti);
|
||
|
prev = song->master_track_items;
|
||
|
}
|
||
|
}
|
||
|
// Add the new item at the end
|
||
|
mti = calloc(1, sizeof(struct cbox_master_track_item));
|
||
|
song->master_track_items = g_list_append(song->master_track_items, mti);
|
||
|
set_values:
|
||
|
// No effect if -1
|
||
|
if (tempo >= 0)
|
||
|
mti->tempo = tempo;
|
||
|
if ((timesig_num > 0 && timesig_denom > 0) ||
|
||
|
(timesig_num == 0 && timesig_denom == 0))
|
||
|
{
|
||
|
mti->timesig_num = timesig_num;
|
||
|
mti->timesig_denom = timesig_denom;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void cbox_song_add_track(struct cbox_song *song, struct cbox_track *track)
|
||
|
{
|
||
|
track->owner = song;
|
||
|
song->tracks = g_list_append(song->tracks, track);
|
||
|
}
|
||
|
|
||
|
void cbox_song_remove_track(struct cbox_song *song, struct cbox_track *track)
|
||
|
{
|
||
|
assert(track->owner == song);
|
||
|
song->tracks = g_list_remove(song->tracks, track);
|
||
|
track->owner = NULL;
|
||
|
}
|
||
|
|
||
|
void cbox_song_add_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern)
|
||
|
{
|
||
|
pattern->owner = song;
|
||
|
song->patterns = g_list_append(song->patterns, pattern);
|
||
|
}
|
||
|
|
||
|
void cbox_song_remove_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern)
|
||
|
{
|
||
|
assert(pattern->owner == song);
|
||
|
pattern->owner = NULL;
|
||
|
song->patterns = g_list_remove(song->patterns, pattern);
|
||
|
}
|
||
|
|
||
|
void cbox_song_clear(struct cbox_song *song)
|
||
|
{
|
||
|
while(song->tracks)
|
||
|
cbox_object_destroy(song->tracks->data);
|
||
|
while(song->patterns)
|
||
|
cbox_object_destroy(song->patterns->data);
|
||
|
while(song->master_track_items)
|
||
|
{
|
||
|
struct cbox_master_track_item *mti = song->master_track_items->data;
|
||
|
song->master_track_items = g_list_remove(song->master_track_items, mti);
|
||
|
cbox_master_track_item_destroy(mti);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void cbox_song_use_looped_pattern(struct cbox_song *song, struct cbox_midi_pattern *pattern)
|
||
|
{
|
||
|
assert(pattern->owner == song);
|
||
|
song->patterns = g_list_remove(song->patterns, pattern);
|
||
|
pattern->owner = NULL;
|
||
|
|
||
|
cbox_song_clear(song);
|
||
|
struct cbox_track *trk = cbox_track_new(CBOX_GET_DOCUMENT(song));
|
||
|
cbox_song_add_track(song, trk);
|
||
|
cbox_song_add_pattern(song, pattern);
|
||
|
song->loop_start_ppqn = 0;
|
||
|
song->loop_end_ppqn = pattern->loop_end;
|
||
|
cbox_track_add_item(trk, 0, pattern, 0, pattern->loop_end);
|
||
|
cbox_engine_update_song_playback(app.engine);
|
||
|
}
|
||
|
|
||
|
void cbox_song_destroyfunc(struct cbox_objhdr *objhdr)
|
||
|
{
|
||
|
struct cbox_song *song = CBOX_H2O(objhdr);
|
||
|
cbox_song_clear(song);
|
||
|
free(song);
|
||
|
}
|
||
|
|