feat(preferences): choose MIDI devices and persistent channel defaults (phase 102)
This commit is contained in:
parent
75dee16b6a
commit
e9d0c62fd6
9 changed files with 101 additions and 17 deletions
|
|
@ -32,3 +32,20 @@ the MIDI-drop counter. Panic and stop send CC123 on all channels. Immediate stop
|
|||
waits up to 100 ms for callback acknowledgement before deactivation; delivery
|
||||
cannot be guaranteed after server/device loss or to devices ignoring CC123.
|
||||
No device is connected unless a user chooses it or an external patchbay routes it.
|
||||
|
||||
## MIDI preferences
|
||||
|
||||
Preferences → midi has editable dropdowns for a default input source and output
|
||||
destination, plus a Refresh MIDI devices button. Names are enumerated from the
|
||||
current JACK/PipeWire server, excluding xtc's own ports. Select None or clear a
|
||||
field for no automatic connection. Typed offline names remain saved. Hardware
|
||||
ports are visible only when the host exposes them through its MIDI bridge.
|
||||
|
||||
Input channel `-1` accepts all channels; `0..15` selects MIDI channel 1..16.
|
||||
Output channel `-1` preserves note channels; `0..15` forces a channel. Thru defaults
|
||||
off. Apply saves these choices; the next Play/Record restores the selected routes.
|
||||
A missing port rejects that start and stops the backend, preserving the saved
|
||||
choice for retry. Reconnect audio after server loss, then retry Play. Defaults
|
||||
also apply when playback resumes after a loop edit. MIDI input/thru currently
|
||||
requires active transport. Existing editing and project_defaults tabs contain
|
||||
note length, velocity, channel, filters and new-project controller mappings.
|
||||
|
|
|
|||
|
|
@ -2256,12 +2256,12 @@ Implemented: public JACK MIDI discovery/naming/routing/options APIs, a prepared
|
|||
Provide device dropdowns and refresh in Preferences, persistent default input/output ports, live channel filters and MIDI thru; retain editing and controller defaults in ~/.xtc.conf.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Implement and document the workflow with bounded validation and safe ownership.
|
||||
- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo.
|
||||
- [ ] Commit this phase separately after validation.
|
||||
- [x] Implement and document the workflow with bounded validation and safe ownership.
|
||||
- [x] Pass meaningful core and GUI checks, including errors and persistence or undo.
|
||||
- [x] Commit this phase separately after validation.
|
||||
|
||||
### Implementation status
|
||||
Planned.
|
||||
Implemented: a MIDI preferences tab provides editable source/destination dropdowns, device refresh, input filtering, output channel defaults and optional thru. All choices use the strict shared configuration schema and ~/.xtc.conf. Play/Record restores selected routes; missing devices reject startup without losing choices. Settings roundtrip/invalid-value, GUI discovery/refresh/persistence/missing-device checks and the full GUI suite passed. MIDI help explains host bridge exposure, channel numbering and existing note/CC defaults.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ extern "C" {
|
|||
typedef struct {
|
||||
char client_name[64], clap_paths[2048], vst2_paths[2048], vst3_paths[2048], ladspa_paths[2048];
|
||||
int64_t max_frames, channels, transition_ms, offline_rate, recording_seconds;
|
||||
char midi_input_port[256],midi_output_port[256];
|
||||
int64_t midi_input_channel,midi_output_channel,midi_thru;
|
||||
int64_t main_x, main_y, inspector_visible;
|
||||
int64_t width, height, piano_width, piano_height, refresh_ms;
|
||||
int64_t transport_detached,mixer_detached,sequencer_detached,graph_detached,piano_detached;
|
||||
|
|
|
|||
|
|
@ -905,9 +905,9 @@ void MainWindow::start_engine()
|
|||
xtc_result result = configure_loop(project_.get(),settings_);
|
||||
if(result==XTC_OK) result = xtc_backend_prepare_project(backend_, project_.get());
|
||||
if (result == XTC_OK)
|
||||
result = xtc_backend_start(backend_);
|
||||
result = start_configured_backend();
|
||||
if (result != XTC_OK)
|
||||
report_error("Start engine failed", result);
|
||||
report_error("Start failed; check audio and saved MIDI devices in Preferences", result);
|
||||
else {
|
||||
set_label(error_, "");
|
||||
const char *issue = xtc_backend_asset_issue(backend_);
|
||||
|
|
@ -1659,7 +1659,7 @@ xtc_result MainWindow::record_audio(const char *path)
|
|||
recording_track_ = sequencer_->selected_track();
|
||||
r = xtc_backend_set_recorder(backend_, recorder_);
|
||||
if (r == XTC_OK)
|
||||
r = xtc_backend_start(backend_);
|
||||
r = start_configured_backend();
|
||||
if (r != XTC_OK) {
|
||||
(void)xtc_backend_set_recorder(backend_, nullptr);
|
||||
xtc_recorder_destroy(recorder_);
|
||||
|
|
@ -2462,7 +2462,7 @@ xtc_result MainWindow::set_loop(uint64_t start,uint64_t end,bool enabled)
|
|||
bool resume=running_;xtc_result r=ready_ ? xtc_backend_stop(backend_) : XTC_OK;
|
||||
if(r==XTC_OK)r=configure_loop(project_.get(),candidate);
|
||||
if(r==XTC_OK) {settings_=candidate;sequencer_->loop_region(start,end,enabled);}
|
||||
if(ready_ && resume && r==XTC_OK) {xtc_result restarted=xtc_backend_start(backend_);if(r==XTC_OK)r=restarted;}
|
||||
if(ready_ && resume && r==XTC_OK) {xtc_result restarted=start_configured_backend();if(r==XTC_OK)r=restarted;}
|
||||
refresh_status();return r;
|
||||
}
|
||||
void MainWindow::collect_preferences()
|
||||
|
|
@ -2524,10 +2524,27 @@ xtc_result MainWindow::save_preferences()
|
|||
if(configuration_.empty() || !configuration_valid_)return XTC_ERR_INVALID_STATE;
|
||||
collect_preferences();return xtc_settings_save(&settings_,configuration_.c_str());
|
||||
}
|
||||
std::vector<std::string> MainWindow::midi_devices(bool input)
|
||||
{
|
||||
std::vector<std::string> ports;
|
||||
if(ready_)(void)xtc_backend_visit_midi_ports(backend_,input ? XTC_PORT_OUTPUT : XTC_PORT_INPUT,
|
||||
[](const char *name,void *c){static_cast<std::vector<std::string> *>(c)->emplace_back(name);return 0;},&ports);
|
||||
return ports;
|
||||
}
|
||||
xtc_result MainWindow::start_configured_backend()
|
||||
{
|
||||
xtc_result r=xtc_backend_set_midi_options(backend_,static_cast<int>(settings_.midi_input_channel),static_cast<int>(settings_.midi_output_channel),settings_.midi_thru!=0);
|
||||
if(r==XTC_OK)r=xtc_backend_start(backend_);
|
||||
if(r!=XTC_OK)return r;
|
||||
if(settings_.midi_input_port[0])r=xtc_backend_route_midi(backend_,XTC_PORT_INPUT,settings_.midi_input_port,0);
|
||||
if(settings_.midi_output_port[0]){auto out=xtc_backend_route_midi(backend_,XTC_PORT_OUTPUT,settings_.midi_output_port,0);if(r==XTC_OK)r=out;}
|
||||
if(r!=XTC_OK){(void)xtc_backend_stop(backend_);return r;}
|
||||
return XTC_OK;
|
||||
}
|
||||
void MainWindow::show_preferences()
|
||||
{
|
||||
if(recorder_) {report_error("Stop recording before preferences",XTC_ERR_INVALID_STATE);return;}
|
||||
collect_preferences();PreferencesDialog dialog(settings_,configuration_,[this](const xtc_settings &s){return apply_preferences(s);});dialog.run();
|
||||
collect_preferences();PreferencesDialog dialog(settings_,configuration_,[this](const xtc_settings &s){return apply_preferences(s);},[this](bool input){return midi_devices(input);});dialog.run();
|
||||
}
|
||||
|
||||
xtc_result MainWindow::set_tempo(unsigned int milli_bpm)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ class MainWindow : public Fl_Double_Window {
|
|||
const xtc_settings &settings() const { return settings_; }
|
||||
xtc_result apply_preferences(const xtc_settings &settings);
|
||||
void show_preferences();
|
||||
std::vector<std::string> midi_devices(bool input);
|
||||
xtc_result start_configured_backend();
|
||||
xtc_result save_preferences();
|
||||
xtc_result set_loop(uint64_t start,uint64_t end,bool enabled);
|
||||
Fl_Widget *velocity_lane() const { return velocity_lane_; }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "theme.hpp"
|
||||
#include <FL/Fl_Double_Window.H>
|
||||
#include <FL/Fl_Input.H>
|
||||
#include <FL/Fl_Input_Choice.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/Fl_Button.H>
|
||||
#include <functional>
|
||||
|
|
@ -15,6 +16,18 @@ class PreferencesDialog : public Fl_Double_Window {
|
|||
std::function<xtc_result(const xtc_settings &)> apply_;
|
||||
std::vector<Fl_Input *> inputs_;
|
||||
Fl_Box *message_;
|
||||
std::function<std::vector<std::string>(bool)> devices_;
|
||||
Fl_Input_Choice *midi_input_=nullptr,*midi_output_=nullptr;
|
||||
void refresh_devices() {
|
||||
for(auto pair:{std::pair<Fl_Input_Choice *,bool>{midi_input_,true},{midi_output_,false}}){
|
||||
if(!pair.first)continue;
|
||||
std::string value=pair.first->value();std::vector<std::string> names=devices_ ? devices_(pair.second) : std::vector<std::string>{};
|
||||
names.insert(names.begin(),"");std::vector<Fl_Menu_Item> items(names.size()+1);
|
||||
for(size_t i=0;i<names.size();++i)items[i].text=i ? names[i].c_str() : "(None)";
|
||||
pair.first->menubutton()->copy(items.data());pair.first->value(value.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_callback(Fl_Widget *,void *context) {
|
||||
auto *d=static_cast<PreferencesDialog *>(context);
|
||||
xtc_settings candidate=d->original_; size_t count=0;
|
||||
|
|
@ -36,8 +49,9 @@ class PreferencesDialog : public Fl_Double_Window {
|
|||
}
|
||||
public:
|
||||
PreferencesDialog(const xtc_settings &settings,const std::string &path,
|
||||
std::function<xtc_result(const xtc_settings &)> apply)
|
||||
: Fl_Double_Window(1050,660,"xtc — Preferences"),original_(settings),apply_(std::move(apply)) {
|
||||
std::function<xtc_result(const xtc_settings &)> apply,
|
||||
std::function<std::vector<std::string>(bool)> devices={})
|
||||
: Fl_Double_Window(1050,660,"xtc — Preferences"),original_(settings),apply_(std::move(apply)),devices_(std::move(devices)) {
|
||||
set_modal(); begin();
|
||||
auto *location=new Fl_Box(16,12,1018,30); location->copy_label(path.c_str()); location->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
|
||||
auto *tabs=new Fl_Tabs(12,50,1026,510); tabs->begin();
|
||||
|
|
@ -50,12 +64,23 @@ class PreferencesDialog : public Fl_Double_Window {
|
|||
group=new Fl_Group(12,82,1026,478); group->copy_label(section.c_str()); group->begin();
|
||||
scroll=new Fl_Scroll(22,94,1000,450); scroll->begin(); row=0;
|
||||
}
|
||||
auto *input=new Fl_Input(510,108+row*48,480,30,schema[i].label);
|
||||
Fl_Input *input;
|
||||
if(!strcmp(schema[i].key,"midi_input_port") || !strcmp(schema[i].key,"midi_output_port")){
|
||||
auto *choice=new Fl_Input_Choice(510,108+row*48,480,30,schema[i].label);
|
||||
choice->callback([](Fl_Widget *w,void *){auto *c=static_cast<Fl_Input_Choice *>(w);auto *item=c->menubutton()->mvalue();if(item && !strcmp(item->label(),"(None)"))c->value("");});
|
||||
input=choice->input();if(!strcmp(schema[i].key,"midi_input_port"))midi_input_=choice;else midi_output_=choice;
|
||||
}else input=new Fl_Input(510,108+row*48,480,30,schema[i].label);
|
||||
input->align(FL_ALIGN_LEFT); input->maximum_size(schema[i].capacity ? static_cast<int>(schema[i].capacity-1) : 22);
|
||||
char value[4096]; xtc_settings_get(&settings,i,value,sizeof(value)); input->value(value);
|
||||
inputs_[i]=input; ++row;
|
||||
}
|
||||
if(scroll) { scroll->end(); group->end(); } tabs->end();
|
||||
tabs->begin();auto *midi_info=new Fl_Group(12,82,1026,478,"MIDI help");
|
||||
auto *help=new Fl_Box(42,110,950,190,"Choose JACK/PipeWire MIDI ports on the midi tab. Refresh to see newly connected devices.\nYou can also type a remembered port name while it is offline; blank means no automatic route.\nRouting is restored on Play/Record; missing ports are reported without deleting preferences.\nInput/output channels use 0..15 for MIDI channels 1..16. MIDI thru defaults off.\nNote length/velocity/channel defaults are on editing; CC defaults are on project_defaults.\nHardware MIDI devices must be exposed by your host JACK/PipeWire MIDI bridge.");
|
||||
help->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);midi_info->end();tabs->end();
|
||||
refresh_devices();
|
||||
auto *refresh=new Fl_Button(16,614,200,30,"Refresh MIDI devices");
|
||||
refresh->callback([](Fl_Widget *,void *c){static_cast<PreferencesDialog *>(c)->refresh_devices();},this);
|
||||
message_=new Fl_Box(16,568,1018,36,"Apply saves every tab. Existing project contents are not overwritten by defaults.");
|
||||
message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);
|
||||
auto *apply_button=new Fl_Button(812,614,100,30,"Apply"); apply_button->callback(apply_callback,this);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ static const xtc_setting_info schema[] = {
|
|||
STR("plugins",vst2_paths,"VST2 search paths (colon-separated)"),
|
||||
STR("plugins",vst3_paths,"VST3 search paths (colon-separated)"),
|
||||
STR("plugins",ladspa_paths,"LADSPA search paths (colon-separated)"),
|
||||
STR("midi",midi_input_port,"Default MIDI input source (next Play)"),
|
||||
STR("midi",midi_output_port,"Default MIDI output destination (next Play)"),
|
||||
NUM("midi",midi_input_channel,"Live input channel (-1 all, 0..15)",-1,15),
|
||||
NUM("midi",midi_output_channel,"Output channel (-1 original, 0..15)",-1,15),
|
||||
NUM("midi",midi_thru,"Forward live MIDI to output: 0 or 1",0,1),
|
||||
NUM("interface",main_x,"Main window X",-32768,32768), NUM("interface",main_y,"Main window Y",-32768,32768),
|
||||
NUM("interface",inspector_visible,"Track inspector visible: 0 or 1",0,1),
|
||||
NUM("interface",width,"Main window width",1220,3840), NUM("interface",height,"Main window height",720,2160),
|
||||
|
|
@ -77,6 +82,7 @@ void xtc_settings_defaults(xtc_settings *s)
|
|||
s->mixer_x=s->mixer_y=100;s->mixer_width=1056;s->mixer_height=600;
|
||||
s->sequencer_snap=s->piano_snap=240; s->sequencer_zoom=48; s->clip_ticks=3840; s->note_ticks=960;
|
||||
s->note_velocity=100; s->filter_channel=-1; s->filter_high=127; s->tempo=120000;
|
||||
s->midi_input_channel=s->midi_output_channel=-1;
|
||||
s->volume_cc=7; s->pan_cc=10; s->sustain_cc=64; s->loop_end=3840;s->tail_ms=3000;
|
||||
}
|
||||
static int snap(int64_t v) { return v==1 || v==120 || v==160 || v==240 || v==320 || v==480 || v==960 || v==1920 || v==3840; }
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|||
++engine_status.submitted_revision;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_backend_set_midi_options(xtc_backend *,int input,int output,int thru)
|
||||
{CHECK(input>=-1 && input<16 && output>=-1 && output<16 && thru>=0 && thru<2);return handle.running ? XTC_ERR_INVALID_STATE : XTC_OK;}
|
||||
xtc_result xtc_backend_visit_midi_ports(xtc_backend *,xtc_port_direction d,xtc_backend_port_visitor visit,void *c)
|
||||
{visit(d==XTC_PORT_OUTPUT ? "Keyboard:out" : "Synth:in",c);return XTC_OK;}
|
||||
xtc_result xtc_backend_route_midi(xtc_backend *,xtc_port_direction,const char *name,int)
|
||||
{return !strcmp(name,"missing:port") ? XTC_ERR_NOT_FOUND : XTC_OK;}
|
||||
xtc_result xtc_backend_panic(xtc_backend *) { xtc_midi_plan_panic(handle.midi, engine_status.sample_position); return XTC_OK; }
|
||||
xtc_result xtc_backend_read_meters(xtc_backend *, xtc_meter_snapshot *out)
|
||||
{
|
||||
|
|
@ -351,10 +357,19 @@ int midi_workflow_tests()
|
|||
char dir[]="/tmp/xtc-midi-settings-XXXXXX";CHECK(mkdtemp(dir));std::string config=std::string(dir)+"/.xtc.conf";
|
||||
reset();{
|
||||
MainWindow window(false,config.c_str());window.show();auto *s=timeline(&window);
|
||||
auto settings=window.settings();strcpy(settings.midi_input_port,"Keyboard:out");strcpy(settings.midi_output_port,"Synth:in");settings.midi_input_channel=2;settings.midi_output_channel=4;settings.midi_thru=1;
|
||||
CHECK(window.apply_preferences(settings)==XTC_OK);
|
||||
CHECK(window.midi_devices(true)==std::vector<std::string>{"Keyboard:out"} && window.midi_devices(false)==std::vector<std::string>{"Synth:in"});
|
||||
CHECK(window.start_configured_backend()==XTC_OK && handle.running);CHECK(xtc_backend_stop(&handle)==XTC_OK);
|
||||
PreferencesDialog dialog(settings,config,[&](const xtc_settings &v){return window.apply_preferences(v);},[&](bool input){return window.midi_devices(input);});
|
||||
CHECK(find(&dialog,"Refresh MIDI devices"));press(find(&dialog,"Refresh MIDI devices"));
|
||||
CHECK(window.settings().midi_output_channel==4);
|
||||
auto missing=settings;strcpy(missing.midi_input_port,"missing:port");CHECK(window.apply_preferences(missing)==XTC_OK);
|
||||
CHECK(window.start_configured_backend()==XTC_ERR_NOT_FOUND && !handle.running);CHECK(window.apply_preferences(settings)==XTC_OK);
|
||||
s->grid(480);window.show_track_inspector(true);window.resize(20,30,1300,820);
|
||||
CHECK(window.set_loop(960,3840,true)==XTC_OK);window.do_callback();CHECK(!window.shown());
|
||||
}
|
||||
xtc_settings saved{};CHECK(xtc_settings_load(&saved,config.c_str())==XTC_OK && saved.sequencer_snap==480 && saved.inspector_visible && saved.loop_start==960 && saved.width==1300);
|
||||
xtc_settings saved{};CHECK(xtc_settings_load(&saved,config.c_str())==XTC_OK && saved.sequencer_snap==480 && saved.inspector_visible && saved.loop_start==960 && saved.width==1300 && saved.midi_thru && !strcmp(saved.midi_input_port,"Keyboard:out"));
|
||||
reset();{
|
||||
MainWindow window(false,config.c_str());CHECK(window.settings().sequencer_snap==480 && window.settings().inspector_visible && window.settings().loop_enabled);
|
||||
window.do_callback();
|
||||
|
|
@ -409,9 +424,9 @@ int main(int argc, char **argv)
|
|||
CHECK(engine_status.sample_position == 0 && timeline(&window)->position() == 0);
|
||||
start_result = XTC_ERR_IO;
|
||||
press(play);
|
||||
CHECK(contains(&window, "Start engine failed: I/O error"));
|
||||
CHECK(contains(&window, "Start failed; check audio and saved MIDI devices in Preferences: I/O error"));
|
||||
pump(); // A refresh must not erase a command error.
|
||||
CHECK(contains(&window, "Start engine failed: I/O error"));
|
||||
CHECK(contains(&window, "Start failed; check audio and saved MIDI devices in Preferences: I/O error"));
|
||||
start_result = XTC_OK;
|
||||
press(play);
|
||||
CHECK(!contains(&window, "Start engine failed"));
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ int main(void)
|
|||
CHECK(fd>=0);close(fd);unlink(path);
|
||||
xtc_settings_defaults(&a);CHECK(xtc_settings_validate(&a)==XTC_OK);
|
||||
b=a;CHECK(xtc_settings_load(&b,path)==XTC_ERR_NOT_FOUND);
|
||||
strcpy(a.clap_paths,"~/space folder:/tmp/quoted\"name\\path");a.loop_enabled=1;a.follow_playhead=1;a.transport_detached=1;a.mixer_detached=1;a.transport_x=-30000;a.transport_width=400;a.note_velocity=87;
|
||||
strcpy(a.clap_paths,"~/space folder:/tmp/quoted\"name\\path");a.loop_enabled=1;a.follow_playhead=1;a.transport_detached=1;a.mixer_detached=1;a.transport_x=-30000;a.transport_width=400;a.note_velocity=87;strcpy(a.midi_input_port,"Keyboard:out");strcpy(a.midi_output_port,"Synth:in");a.midi_input_channel=2;a.midi_output_channel=4;a.midi_thru=1;
|
||||
CHECK(xtc_settings_save(&a,path)==XTC_OK);CHECK(stat(path,&st)==0 && (st.st_mode&0777)==0600);
|
||||
CHECK(xtc_settings_load(&b,path)==XTC_OK);CHECK(memcmp(&a,&b,sizeof(a))==0);
|
||||
xtc_settings_schema(&n);
|
||||
for(i=0;i<n;++i){CHECK(xtc_settings_get(&a,i,value,sizeof(value))==XTC_OK);CHECK(xtc_settings_set(&b,i,value)==XTC_OK);}
|
||||
CHECK(memcmp(&a,&b,sizeof(a))==0);
|
||||
{const char *bad[]={"[unknown]\n","[interface]\ntransport_width=399\n","[interface]\nmixer_detached=2\n","[audio]\nchannels=3\n","[audio]\nchannels=1\nchannels=2\n","[audio]\nchannels=9999999999999999999999\n","[plugins]\nclap_paths=\"unterminated\n","[transport]\nloop_start=4000\nloop_end=2\n"};
|
||||
{const char *bad[]={"[midi]\nmidi_input_channel=16\n","[midi]\nmidi_thru=2\n","[unknown]\n","[interface]\ntransport_width=399\n","[interface]\nmixer_detached=2\n","[audio]\nchannels=3\n","[audio]\nchannels=1\nchannels=2\n","[audio]\nchannels=9999999999999999999999\n","[plugins]\nclap_paths=\"unterminated\n","[transport]\nloop_start=4000\nloop_end=2\n"};
|
||||
for(i=0;i<sizeof(bad)/sizeof(bad[0]);++i){f=fopen(path,"wb");CHECK(f);CHECK(fputs(bad[i],f)>=0);CHECK(fclose(f)==0);before=b;CHECK(xtc_settings_load(&b,path)==XTC_ERR_FORMAT);CHECK(memcmp(&before,&b,sizeof(b))==0);}}
|
||||
f=fopen(path,"wb");CHECK(f);CHECK(fwrite(";x\0hidden",1,9,f)==9);fclose(f);CHECK(xtc_settings_load(&b,path)==XTC_ERR_FORMAT);
|
||||
CHECK(xtc_settings_save(&a,path)==XTC_OK);b=a;b.loop_end=b.loop_start;CHECK(xtc_settings_save(&b,path)==XTC_ERR_INVALID_ARGUMENT);CHECK(xtc_settings_load(&b,path)==XTC_OK && b.loop_end==a.loop_end);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue