Phase 21: add filtered MIDI composition edits and triplet piano-roll grids
This commit is contained in:
parent
830b6e1914
commit
73fc5eea8d
12 changed files with 796 additions and 39 deletions
|
|
@ -351,6 +351,51 @@ int main(int argc, char **argv)
|
|||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 1);
|
||||
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK &&
|
||||
drawn.start_tick == 720 && drawn.length_tick == 1440 && drawn.pitch == 61);
|
||||
// Piano-roll filtered commands use one undo step and preserve durations.
|
||||
uint64_t edited_clip = roll->clip();
|
||||
CHECK(roll->set_grid(160) == XTC_OK && roll->set_grid(320) == XTC_OK);
|
||||
CHECK(roll->set_grid(7) == XTC_ERR_INVALID_ARGUMENT);
|
||||
CHECK(roll->set_filter(0, 60, 72) == XTC_OK);
|
||||
int note_key = Fl::e_keysym, note_state = Fl::e_state;
|
||||
Fl::e_keysym = 'a'; Fl::e_state = FL_CTRL;
|
||||
CHECK(roll->handle(FL_KEYDOWN) == 1);
|
||||
Fl::e_keysym = note_key; Fl::e_state = note_state;
|
||||
xtc_note_edit properties{XTC_NOTES_EDIT, 0, 0, 77, 2, 0, 0};
|
||||
CHECK(roll->apply(properties) == XTC_OK);
|
||||
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.velocity == 77 && drawn.channel == 2);
|
||||
CHECK(roll->set_filter(-1, 0, 127) == XTC_OK);
|
||||
roll->select_filtered();
|
||||
Fl::e_keysym = 'q'; Fl::e_state = 0;
|
||||
CHECK(roll->handle(FL_KEYDOWN) == 1);
|
||||
Fl::e_keysym = note_key; Fl::e_state = note_state;
|
||||
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.start_tick == 640 && drawn.length_tick == 1440);
|
||||
Fl::e_keysym = FL_Up; Fl::e_state = 0;
|
||||
CHECK(roll->handle(FL_KEYDOWN) == 1);
|
||||
Fl::e_keysym = note_key; Fl::e_state = note_state;
|
||||
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.pitch == 62);
|
||||
xtc_note_edit repeat{XTC_NOTES_REPEAT, 0, 0, -1, -1, 1, 960};
|
||||
CHECK(roll->apply(repeat) == XTC_OK);
|
||||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 2);
|
||||
CHECK(roll->delete_selected() == XTC_OK);
|
||||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 0);
|
||||
CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 2);
|
||||
CHECK(window.undo(true) == XTC_OK);
|
||||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 0);
|
||||
for (int step = 0; step < 5; ++step) CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.pitch == 61 && drawn.start_tick == 720 && drawn.velocity == 100 && drawn.channel == 0);
|
||||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 1);
|
||||
roll->set_project(sequence->project(), edited_clip);
|
||||
CHECK(roll->set_grid(160) == XTC_OK && roll->set_filter(5, 60, 60) == XTC_OK);
|
||||
Fl::e_x = roll->x() + 104; Fl::e_y = roll->y() + 1110;
|
||||
Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
|
||||
CHECK(roll->handle(FL_PUSH) == 1); (void)roll->handle(FL_RELEASE);
|
||||
Fl::e_x = saved_x; Fl::e_y = saved_y; Fl::e_keysym = saved_key;
|
||||
CHECK(xtc_project_get_note(sequence->project(), 1, &drawn) == XTC_OK && drawn.start_tick == 320 && drawn.channel == 5 && drawn.pitch == 60);
|
||||
CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 1);
|
||||
CHECK(roll->set_grid(240) == XTC_OK && roll->set_filter(-1, 0, 127) == XTC_OK);
|
||||
roll->set_project(sequence->project(), edited_clip);
|
||||
CHECK(window.save_project(filename.c_str()) == XTC_OK);
|
||||
CHECK(window.open_project(filename.c_str()) == XTC_OK);
|
||||
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.pitch == 61 && drawn.start_tick == 720);
|
||||
|
|
|
|||
137
tests/unit/test_composition.c
Normal file
137
tests/unit/test_composition.c
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void editing(void)
|
||||
{
|
||||
xtc_project *p = NULL, *snapshot = NULL;
|
||||
xtc_history *history = NULL;
|
||||
uint64_t track, clip, second, id, next;
|
||||
xtc_note_info n, original, untouched, foreign;
|
||||
xtc_note_filter f;
|
||||
xtc_note_edit e = {XTC_NOTES_EDIT, 160, 12, -1, -1, 0, 0};
|
||||
size_t count = 999, total;
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
assert(xtc_project_add_track(p, "Keys", &track) == XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p, track, 3840, 3840, &second) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 80, 100, 60, 73, 2, &id) == XTC_OK);
|
||||
assert(xtc_project_get_note(p, 0, &original) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 90, 101, 61, 91, 3, &next) == XTC_OK);
|
||||
assert(xtc_project_get_note(p, 1, &untouched) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, second, 80, 100, 60, 73, 2, &next) == XTC_OK);
|
||||
assert(xtc_project_get_note(p, 2, &foreign) == XTC_OK);
|
||||
f = (xtc_note_filter){clip, UINT64_MAX, 2, 60, 60};
|
||||
assert(xtc_history_create(p, &history) == XTC_OK);
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_OK && count == 1);
|
||||
assert(xtc_project_get_note(p, 0, &n) == XTC_OK);
|
||||
assert(n.id == id && n.start_tick == 160 && n.length_tick == 100 && n.pitch == 72 && n.velocity == 73 && n.channel == 2);
|
||||
assert(xtc_project_get_note(p, 1, &n) == XTC_OK && !memcmp(&n, &untouched, sizeof(n)));
|
||||
assert(xtc_project_get_note(p, 2, &n) == XTC_OK && !memcmp(&n, &foreign, sizeof(n)));
|
||||
assert(xtc_history_record(history, p) == XTC_OK);
|
||||
assert(xtc_history_checkout(history, 0, &snapshot) == XTC_OK);
|
||||
assert(xtc_project_get_note(snapshot, 0, &n) == XTC_OK && !memcmp(&n, &original, sizeof(n)));
|
||||
xtc_project_destroy(snapshot);
|
||||
assert(xtc_history_accept(history, 0) == XTC_OK);
|
||||
assert(xtc_history_checkout(history, 1, &snapshot) == XTC_OK);
|
||||
assert(xtc_project_get_note(snapshot, 0, &n) == XTC_OK && n.start_tick == 160 && n.pitch == 72);
|
||||
xtc_project_destroy(snapshot);
|
||||
f.pitch_min = 0; f.pitch_max = 127;
|
||||
e = (xtc_note_edit){XTC_NOTES_REPEAT, 0, -12, 110, 4, 2, 320};
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_OK && count == 1);
|
||||
assert(xtc_project_get_note(p, 3, &n) == XTC_OK && n.id > foreign.id && n.start_tick == 480 && n.pitch == 60 && n.velocity == 110 && n.channel == 4);
|
||||
next = n.id;
|
||||
assert(xtc_project_get_note(p, 4, &n) == XTC_OK && n.id > next && n.start_tick == 800 && n.length_tick == 100);
|
||||
next = n.id;
|
||||
/* Reject entire batch if a later match overflows pitch/range/capacity. */
|
||||
f.channel = -1;
|
||||
e = (xtc_note_edit){XTC_NOTES_EDIT, 0, 67, -1, -1, 0, 0};
|
||||
count = 999;
|
||||
assert(xtc_project_get_note(p, 0, &original) == XTC_OK);
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_INVALID_ARGUMENT && count == 999);
|
||||
assert(xtc_project_get_note(p, 0, &n) == XTC_OK && !memcmp(&n, &original, sizeof(n)));
|
||||
e = (xtc_note_edit){XTC_NOTES_REPEAT, 0, 0, -1, -1, 2, UINT64_MAX};
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_INVALID_ARGUMENT && count == 999);
|
||||
e.copies = XTC_PROJECT_MAX_NOTES;
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_CAPACITY && count == 999);
|
||||
e = (xtc_note_edit){XTC_NOTES_EDIT, UINT64_MAX, 0, 0, -1, 0, 0};
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_INVALID_ARGUMENT);
|
||||
e.velocity = -1; e.grid_tick = 3840;
|
||||
f.note_id = id;
|
||||
/* Nearest grid preserves duration, including rounding down near zero. */
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_OK);
|
||||
assert(xtc_project_get_note(p, 0, &n) == XTC_OK && n.start_tick == 0 && n.length_tick == 100);
|
||||
assert(xtc_project_add_note(p, clip, 3800, 40, 50, 50, 0, &id) == XTC_OK && id == next + 1);
|
||||
f.note_id = id;
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_INVALID_ARGUMENT);
|
||||
f.note_id = UINT64_MAX; f.channel = 4;
|
||||
e.action = XTC_NOTES_DELETE;
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_OK && count == 2);
|
||||
assert(xtc_project_note_count(p, &total) == XTC_OK && total == 4);
|
||||
assert(xtc_project_get_note(p, 1, &n) == XTC_OK && !memcmp(&n, &untouched, sizeof(n)));
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_NOT_FOUND);
|
||||
f.channel = 16;
|
||||
assert(xtc_project_edit_notes(p, &f, &e, &count) == XTC_ERR_INVALID_ARGUMENT);
|
||||
xtc_history_destroy(history);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
|
||||
static void scheduling(void)
|
||||
{
|
||||
xtc_project *p = NULL;
|
||||
xtc_midi_plan *a = NULL, *b = NULL;
|
||||
uint64_t track, clip, id;
|
||||
float whole[512], split[512], reference[512];
|
||||
xtc_midi_event events[] = {{0, XTC_MIDI_NOTE_ON, 0, 69, 127},
|
||||
{17, XTC_MIDI_NOTE_OFF, 0, 69, 0},
|
||||
{17, XTC_MIDI_NOTE_ON, 0, 69, 127}};
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(p, 48000, &b) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(a, 0, events, 3, whole, 64) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(b, 0, events, 1, split, 17) == XTC_OK);
|
||||
events[1].offset = events[2].offset = 0;
|
||||
assert(xtc_midi_plan_render(b, 17, events+1, 2, split+17, 47) == XTC_OK);
|
||||
assert(!memcmp(whole, split, 64*sizeof(float)) && whole[17] == 0 && whole[18] > 0);
|
||||
/* Equal offsets preserve caller order: on then off must end silent. */
|
||||
events[0].type = XTC_MIDI_NOTE_ON; events[1].type = XTC_MIDI_NOTE_OFF;
|
||||
assert(xtc_midi_plan_render(a, 100, events, 2, whole, 64) == XTC_OK);
|
||||
for (size_t i = 0; i < 64; ++i) assert(whole[i] == 0);
|
||||
events[0].channel = 16;
|
||||
whole[0] = 123;
|
||||
assert(xtc_midi_plan_render(a, 164, events, 1, whole, 64) == XTC_ERR_INVALID_ARGUMENT && whole[0] == 123);
|
||||
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b);
|
||||
assert(xtc_project_add_track(p, "Keys", &track) == XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p, track, 0, 960, &clip) == XTC_OK);
|
||||
/* Adjacent repeated pitch and an overlapping same-pitch note remain independent. */
|
||||
assert(xtc_project_add_note(p, clip, 0, 8, 69, 127, 0, &id) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 8, 8, 69, 127, 0, &id) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 4, 8, 69, 64, 0, &id) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK);
|
||||
assert(xtc_midi_plan_create(p, 48000, &b) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(a, 0, NULL, 0, whole, 512) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(b, 0, NULL, 0, split, 200) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(b, 200, NULL, 0, split+200, 200) == XTC_OK);
|
||||
assert(xtc_midi_plan_render(b, 400, NULL, 0, split+400, 112) == XTC_OK);
|
||||
assert(!memcmp(whole, split, sizeof(whole)));
|
||||
assert(xtc_midi_plan_render(b, 199, NULL, 0, reference, 201) == XTC_OK);
|
||||
assert(!memcmp(whole+199, reference, 201*sizeof(float))); /* seek chase */
|
||||
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b);
|
||||
/* 32 ends before 32 starts at the same timestamp fits voice bound. */
|
||||
assert(xtc_project_remove_clip(p, clip) == XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p, track, 0, 960, &clip) == XTC_OK);
|
||||
for (unsigned int i = 0; i < 32; ++i) {
|
||||
assert(xtc_project_add_note(p, clip, 0, 8, 60, 100, 0, &id) == XTC_OK);
|
||||
assert(xtc_project_add_note(p, clip, 8, 8, 60, 100, 0, &id) == XTC_OK);
|
||||
}
|
||||
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK);
|
||||
xtc_midi_plan_destroy(a); xtc_project_destroy(p);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
editing(); scheduling();
|
||||
puts("Filtered transactional MIDI edits, triplet quantize, repeats, undo and deterministic ordering passed.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue