Phase 22: add non-destructive audio splitting, clip gain and fades, and cached waveforms
This commit is contained in:
parent
73fc5eea8d
commit
01d8c1d661
23 changed files with 650 additions and 18 deletions
5
tests/fixtures/README.md
vendored
5
tests/fixtures/README.md
vendored
|
|
@ -2,3 +2,8 @@
|
|||
|
||||
Store small, redistributable fixtures here. Document their origin and license.
|
||||
Do not commit large recordings or assets with unclear licensing.
|
||||
|
||||
`project-v1.xtc` through `project-v7.xtc` are hand-authored xtc metadata fixtures
|
||||
created for this repository, under the project license. They contain no audio
|
||||
or third-party resources. The v5/v6 plugin fixtures intentionally reference a
|
||||
missing module to exercise metadata migration without loading code.
|
||||
|
|
|
|||
20
tests/fixtures/project-v7.xtc
vendored
Normal file
20
tests/fixtures/project-v7.xtc
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
XTC_PROJECT 7
|
||||
APP 0 1 0
|
||||
TEMPO 120000
|
||||
IDS 1 1
|
||||
GRAPH 1 2 2 1
|
||||
NODE 0 1 3f000000
|
||||
NODE 1 4 00000000
|
||||
EDGE 0 1
|
||||
TRACKS 1
|
||||
TRACK 0 4b657973
|
||||
MIX 3f800000 00000000 0 0
|
||||
CLIPS 1
|
||||
CLIP 0 0 0 960 0 960 5665727365
|
||||
ASSET -
|
||||
MIDI 1
|
||||
NOTES 1 1
|
||||
NOTE 0 0 0 480 69 100 0
|
||||
AUTOMATION 1
|
||||
POINT 0 0 0 3f800000
|
||||
END
|
||||
|
|
@ -332,6 +332,31 @@ int main(int argc, char **argv)
|
|||
press(stop);
|
||||
CHECK(handle.recorder == nullptr);
|
||||
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 2);
|
||||
CHECK(sequence->waveform_count() == 1);
|
||||
// Short clips use their four-pixel draw width for hit testing too.
|
||||
xtc_clip_info audio_clip{};
|
||||
CHECK(xtc_project_get_clip(sequence->project(), 1, &audio_clip) == XTC_OK && audio_clip.asset_path[0]);
|
||||
size_t audio_row = 0;
|
||||
for (; audio_row < track_count; ++audio_row) {
|
||||
xtc_track_info t{}; xtc_project_get_track(sequence->project(), audio_row, &t);
|
||||
if (t.id == audio_clip.track_id) break;
|
||||
}
|
||||
gesture(sequence, 121 + static_cast<int>(audio_clip.start_tick*48/960), 50 + static_cast<int>(audio_row)*56, 0, 0);
|
||||
CHECK(sequence->selected_clip() == audio_clip.id);
|
||||
CHECK(sequence->audio_properties(0.5f, 1, 1) == XTC_OK);
|
||||
sequence->position(audio_clip.start_tick+1);
|
||||
int split_key = Fl::e_keysym, split_state = Fl::e_state;
|
||||
Fl::e_keysym = 's'; Fl::e_state = 0;
|
||||
CHECK(sequence->handle(FL_KEYDOWN) == 1);
|
||||
Fl::e_keysym = split_key; Fl::e_state = split_state;
|
||||
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 3);
|
||||
CHECK(sequence->duplicate_selection() == XTC_OK && sequence->waveform_count() == 1);
|
||||
CHECK(sequence->delete_selection() == XTC_OK);
|
||||
CHECK(window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 4);
|
||||
CHECK(window.undo() == XTC_OK && window.undo() == XTC_OK && window.undo() == XTC_OK);
|
||||
CHECK(xtc_project_find_clip(sequence->project(), audio_clip.id, &audio_clip) == XTC_OK && audio_clip.gain == 1 && audio_clip.fade_in_tick == 0 && audio_clip.length_tick == 3);
|
||||
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 2);
|
||||
CHECK(window.save_project(filename.c_str()) == XTC_OK);
|
||||
CHECK(window.open_project(filename.c_str()) == XTC_OK);
|
||||
float mix[64]; CHECK(xtc_audio_plan_render(handle.audio, 0, mix, 64) == XTC_OK && mix[0] == 0.25f);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def main():
|
|||
assert os.access(root / 'bin/xtc-check', os.X_OK)
|
||||
run([str(root / 'bin/xtc-check')])
|
||||
diagnose = subprocess.check_output([str(root / 'bin/xtc-check'), '--diagnose'], text=True)
|
||||
assert 'Project formats: 1..7' in diagnose and 'JACK compiled:' in diagnose
|
||||
assert 'Project formats: 1..8' in diagnose and 'JACK compiled:' in diagnose
|
||||
# Upgrade/repair repeats the same owned-file install and keeps unrelated data.
|
||||
(root / 'include/xtc/xtc.h').write_text('old installation marker\n')
|
||||
run(make + ['install'] + variables, env=env)
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ int main(void)
|
|||
assert(mkdtemp(root));
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
snprintf(file,sizeof(file),"%s/migration.xtc",root);
|
||||
for (version = 1; version <= 6; ++version) {
|
||||
for (version = 1; version <= 7; ++version) {
|
||||
xtc_track_info t; xtc_clip_info c; size_t notes, points;
|
||||
snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version);
|
||||
assert(xtc_project_load(p,old) == XTC_OK);
|
||||
assert(xtc_project_get_track(p,0,&t) == XTC_OK && !strcmp(t.name,"Keys") && t.gain == 1);
|
||||
assert(xtc_project_get_clip(p,0,&c) == XTC_OK && c.midi == (version >= 3));
|
||||
assert(c.gain == 1 && c.fade_in_tick == 0 && c.fade_out_tick == 0);
|
||||
assert(xtc_project_note_count(p,¬es) == XTC_OK && notes == (size_t)(version >= 3));
|
||||
assert(xtc_project_automation_count(p,&points) == XTC_OK && points == (size_t)(version >= 4));
|
||||
assert(xtc_project_save(p,file) == XTC_OK && xtc_project_load(p,file) == XTC_OK);
|
||||
|
|
@ -83,6 +84,6 @@ int main(void)
|
|||
assert(xtc_project_load(p,file) == XTC_ERR_FORMAT);
|
||||
{ xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } }
|
||||
xtc_project_destroy(p); unlink(file); rmdir(moved);
|
||||
puts("v1-v6 migration, v7 folder relocation, cwd independence and missing-media preservation passed.");
|
||||
puts("v1-v7 migration, current folder relocation, cwd independence and missing-media preservation passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
159
tests/unit/test_audio_editing.c
Normal file
159
tests/unit/test_audio_editing.c
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char directory[] = "/tmp/xtc-audio-edits-XXXXXX", path[256], song[256], missing[256];
|
||||
unsigned char original[5000], final[5000]; size_t bytes;
|
||||
xtc_project *p = NULL, *q = NULL, *undo = NULL;
|
||||
xtc_history *history = NULL;
|
||||
xtc_audio_plan *a = NULL, *b = NULL;
|
||||
xtc_recorder *rec = NULL;
|
||||
xtc_waveform *wave = NULL;
|
||||
xtc_clip_info left, right, copy;
|
||||
uint64_t track, clip, split, duplicate, midi, unchanged = UINT64_MAX;
|
||||
size_t i, frames, bins, tracks, clips;
|
||||
unsigned int rate;
|
||||
float source[1003], before[1100], after[1100], blocks[1100], low, high;
|
||||
FILE *file;
|
||||
assert(mkdtemp(directory));
|
||||
snprintf(path, sizeof(path), "%s/source.wav", directory);
|
||||
snprintf(song, sizeof(song), "%s/song.xtc", directory);
|
||||
snprintf(missing, sizeof(missing), "%s/missing.wav", directory);
|
||||
for (i = 0; i < 1003; ++i) source[i] = (float)(i % 31) / 32;
|
||||
assert(xtc_recorder_create(path, 48000, 1003, &rec) == XTC_OK);
|
||||
xtc_recorder_push(rec, source, 1003);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec);
|
||||
file = fopen(path, "rb"); assert(file); bytes = fread(original, 1, sizeof(original), file); assert(fclose(file) == 0);
|
||||
assert(bytes == 4056);
|
||||
assert(xtc_waveform_create(path, &wave) == XTC_OK);
|
||||
assert(xtc_waveform_info(wave, &rate, &frames, &bins) == XTC_OK && rate == 48000 && frames == 1003 && bins == 1003);
|
||||
for (i = 0; i < bins; ++i) {
|
||||
assert(xtc_waveform_peak(wave, i, &low, &high) == XTC_OK && low == source[i] && high == source[i]);
|
||||
}
|
||||
low = 123;
|
||||
assert(xtc_waveform_peak(wave, bins, &low, &high) == XTC_ERR_INVALID_ARGUMENT && low == 123);
|
||||
assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
|
||||
assert(xtc_project_add_track(p, "Audio", &track) == XTC_OK);
|
||||
assert(xtc_project_import_audio(p, track, path, 2, &clip) == XTC_OK);
|
||||
assert(xtc_project_find_clip(p, clip, &left) == XTC_OK && left.gain == 1 && left.length_tick == 41);
|
||||
assert(xtc_audio_plan_create(p, 48000, &a) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(a, 0, before, 1100) == XTC_OK);
|
||||
assert(xtc_history_create(p, &history) == XTC_OK);
|
||||
assert(xtc_project_split_audio(p, clip, 2, &unchanged) == XTC_ERR_INVALID_ARGUMENT && unchanged == UINT64_MAX);
|
||||
assert(xtc_project_split_audio(p, clip, 43, &unchanged) == XTC_ERR_INVALID_ARGUMENT && unchanged == UINT64_MAX);
|
||||
assert(xtc_project_split_audio(p, clip, 22, &split) == XTC_OK);
|
||||
assert(xtc_project_find_clip(p, clip, &left) == XTC_OK && left.length_tick == 20 && left.offset_tick == 0);
|
||||
assert(xtc_project_find_clip(p, split, &right) == XTC_OK && right.offset_tick == 20 && right.length_tick == 21 && right.source_length_tick == 41);
|
||||
assert(!strcmp(left.asset_path, right.asset_path) && split != clip);
|
||||
assert(xtc_audio_plan_create(p, 48000, &b) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(b, 0, after, 1100) == XTC_OK && !memcmp(before, after, sizeof(before)));
|
||||
xtc_audio_plan_destroy(b);
|
||||
assert(xtc_history_record(history, p) == XTC_OK && xtc_history_checkout(history, 0, &undo) == XTC_OK);
|
||||
assert(xtc_project_find_clip(undo, split, ©) == XTC_ERR_NOT_FOUND);
|
||||
assert(xtc_project_find_clip(undo, clip, ©) == XTC_OK && copy.length_tick == 41);
|
||||
xtc_project_destroy(undo);
|
||||
assert(xtc_project_set_clip_audio(p, split, 0.5f, 2, 3) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(p, 48000, &b) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(b, 0, after, 1100) == XTC_OK);
|
||||
/* Start 550, media end 1053; fade-in 50 frames, fade-out 75. */
|
||||
assert(after[550] == 0 && after[1052] == 0 && after[1053] == 0);
|
||||
assert(after[600] == source[550] * 0.5f);
|
||||
assert(fabsf(after[551] - source[501]*0.5f/50) < 0.000001f);
|
||||
assert(fabsf(after[1051] - source[1001]*0.5f/75) < 0.000001f);
|
||||
for (i = 0; i < 1100;) {
|
||||
size_t n = i % 61 + 1; if (n > 1100-i) n = 1100-i;
|
||||
assert(xtc_audio_plan_render(b, i, blocks+i, n) == XTC_OK); i += n;
|
||||
}
|
||||
assert(!memcmp(after, blocks, sizeof(after)));
|
||||
assert(xtc_audio_plan_render(b, 551, blocks, 17) == XTC_OK && !memcmp(after+551, blocks, 17*sizeof(float)));
|
||||
xtc_audio_plan_destroy(b);
|
||||
assert(xtc_project_set_clip_audio(p, split, NAN, 0, 0) == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_set_clip_audio(p, split, 5, 0, 0) == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_set_clip_audio(p, split, 1, 42, 0) == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_duplicate_clip(p, split, track, 100, &duplicate) == XTC_OK);
|
||||
assert(xtc_project_find_clip(p, duplicate, ©) == XTC_OK && copy.gain == 0.5f && copy.fade_in_tick == 2 && copy.fade_out_tick == 3 && copy.offset_tick == 20);
|
||||
/* Splits retain outer fades and clear only inner explicit fades. */
|
||||
assert(xtc_project_split_audio(p, duplicate, 110, &unchanged) == XTC_OK);
|
||||
assert(xtc_project_find_clip(p, duplicate, ©) == XTC_OK && copy.fade_in_tick == 2 && copy.fade_out_tick == 0);
|
||||
assert(xtc_project_find_clip(p, unchanged, ©) == XTC_OK && copy.fade_in_tick == 0 && copy.fade_out_tick == 3);
|
||||
assert(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
|
||||
assert(xtc_project_find_clip(q, split, ©) == XTC_OK && copy.gain == 0.5f && copy.fade_in_tick == 2 && copy.fade_out_tick == 3);
|
||||
/* A bad v8 AUDIO record rejects the entire load. */
|
||||
{
|
||||
char text[10000], *record; size_t length;
|
||||
file = fopen(song, "rb"); assert(file); length = fread(text, 1, sizeof(text)-1, file); assert(fclose(file) == 0);
|
||||
text[length] = 0; record = strstr(text, "AUDIO "); assert(record);
|
||||
memcpy(record+6, "7fc00000", 8); /* NaN */
|
||||
file = fopen(song, "wb"); assert(file); assert(fwrite(text, 1, length, file) == length && fclose(file) == 0);
|
||||
assert(xtc_project_load(q, song) == XTC_ERR_FORMAT);
|
||||
assert(xtc_project_find_clip(q, split, ©) == XTC_OK && copy.gain == 0.5f);
|
||||
}
|
||||
/* Last source tick, very short audible tail and oversized overlapping fades. */
|
||||
assert(xtc_project_edit_clip(q, split, track, 0, 1, 40) == XTC_OK);
|
||||
assert(xtc_project_set_clip_audio(q, split, 1, 41, 41) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(q, 48000, &b) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(b, 0, blocks, 25) == XTC_OK && blocks[0] == 0 && blocks[2] == 0 && blocks[3] == 0);
|
||||
xtc_audio_plan_destroy(b);
|
||||
assert(xtc_project_edit_clip(q, split, track, 0, 2, 40) == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_edit_clip(q, split, track, XTC_PROJECT_MAX_TICK, 1, 40) == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_add_midi_clip(p, track, 1000, 960, &midi) == XTC_OK);
|
||||
assert(xtc_project_split_audio(p, midi, 1001, &unchanged) == XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_set_clip_audio(p, midi, 1, 0, 0) == XTC_ERR_INVALID_STATE);
|
||||
assert(xtc_project_get_counts(p, &tracks, &clips) == XTC_OK);
|
||||
for (i = clips; i < XTC_PROJECT_MAX_CLIPS; ++i) assert(xtc_project_add_clip(p, track, "Full", 0, 1, &midi) == XTC_OK);
|
||||
unchanged = UINT64_MAX;
|
||||
assert(xtc_project_split_audio(p, clip, 3, &unchanged) == XTC_ERR_CAPACITY && unchanged == UINT64_MAX);
|
||||
assert(xtc_project_find_clip(p, clip, ©) == XTC_OK && copy.length_tick == 20);
|
||||
file = fopen(path, "rb"); assert(file); assert(fread(final, 1, sizeof(final), file) == bytes && fclose(file) == 0);
|
||||
assert(!memcmp(original, final, bytes)); /* All editing leaves media byte-identical. */
|
||||
assert(unlink(path) == 0);
|
||||
assert(xtc_waveform_peak(wave, 7, &low, &high) == XTC_OK && low == source[7]);
|
||||
assert(xtc_audio_plan_render(a, 0, blocks, 1100) == XTC_OK && !memcmp(before, blocks, sizeof(before)));
|
||||
xtc_waveform_destroy(wave);
|
||||
/* Grouped bins retain extrema, including the final odd-sized bin. */
|
||||
{
|
||||
float long_source[2049];
|
||||
for (i = 0; i < 2049; ++i) long_source[i] = (float)((int)(i % 17)-8) / 8;
|
||||
assert(xtc_recorder_create(path, 48000, 2049, &rec) == XTC_OK);
|
||||
xtc_recorder_push(rec, long_source, 2049);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec);
|
||||
assert(xtc_waveform_create(path, &wave) == XTC_OK);
|
||||
assert(xtc_waveform_info(wave, &rate, &frames, &bins) == XTC_OK && bins == 1024);
|
||||
for (i = 0; i < bins; ++i) {
|
||||
size_t j, first = i*2049/bins, end = (i+1)*2049/bins;
|
||||
float expected_low = long_source[first], expected_high = expected_low;
|
||||
for (j = first+1; j < end; ++j) {
|
||||
if (long_source[j] < expected_low) expected_low = long_source[j];
|
||||
if (long_source[j] > expected_high) expected_high = long_source[j];
|
||||
}
|
||||
assert(xtc_waveform_peak(wave, i, &low, &high) == XTC_OK && low == expected_low && high == expected_high);
|
||||
}
|
||||
xtc_waveform_destroy(wave); assert(unlink(path) == 0);
|
||||
assert(xtc_recorder_create(path, 48000, 1, &rec) == XTC_OK);
|
||||
source[0] = 1;
|
||||
xtc_recorder_push(rec, source, 1);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec);
|
||||
assert(xtc_waveform_create(path, &wave) == XTC_OK);
|
||||
assert(xtc_waveform_info(wave, &rate, &frames, &bins) == XTC_OK && bins == 1 && frames == 1);
|
||||
xtc_waveform_destroy(wave);
|
||||
assert(xtc_project_set_clip_asset(q, split, path) == XTC_OK);
|
||||
assert(xtc_project_edit_clip(q, split, track, 0, 1, 0) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(q, 48000, &b) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(b, 0, blocks, 25) == XTC_OK && blocks[0] == 0 && blocks[1] == 0);
|
||||
xtc_audio_plan_destroy(b); assert(unlink(path) == 0);
|
||||
}
|
||||
assert(xtc_waveform_create(missing, &wave) != XTC_OK && wave == NULL);
|
||||
xtc_audio_plan_destroy(a); xtc_history_destroy(history); xtc_project_destroy(p); xtc_project_destroy(q);
|
||||
assert(unlink(song) == 0 && rmdir(directory) == 0);
|
||||
puts("Non-destructive audio split/gain/fades/cache, bounds, persistence and undo passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -197,7 +197,7 @@ int main(void)
|
|||
fputc('\n',legacy); continue;
|
||||
}
|
||||
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
|
||||
strncmp(line, "POINT ", 6))
|
||||
strncmp(line, "POINT ", 6) && strncmp(line, "AUDIO ", 6))
|
||||
assert(fputs(line, legacy) >= 0);
|
||||
}
|
||||
assert(fclose(f) == 0 && fclose(legacy) == 0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue