#include "sequencer_widget.hpp" #include "theme.hpp" #include "snap.hpp" #include #include "xtc/xtc_midi.h" #include #include #include #include #include #include #include #include #include /** * @brief Constructs the timeline widget with a borrowed project and seek callback. * * @param left Left coordinate in pixels. * @param top Top coordinate in pixels. * @param project Borrowed project; must outlive the widget's use of it. * @param seek Callback used to request a transport seek. * @param context Opaque callback context. */ SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek seek, void *context) : Fl_Group(left, top, 6300, 3700), project_(project), seek_(seek), context_(context) { resizable(nullptr); extent(); end(); } /** * @brief Replaces the borrowed project and refreshes the widget's displayed state. * * @param project Borrowed project; must outlive the widget's use of it. * @param reset Whether to clear selection, cursor, and drag state. */ bool SequencerWidget::select(uint64_t track, uint64_t clip) { size_t tracks = 0, clips = 0; xtc_project_get_counts(project_,&tracks,&clips); bool found = false; for (size_t i = 0; i < tracks; ++i) { xtc_track_info info{}; if (xtc_project_get_track(project_,i,&info) == XTC_OK && info.id == track) found = true; } if (!found) return false; if (clip != UINT64_MAX) { xtc_clip_info info{}; if (xtc_project_find_clip(project_,clip,&info) != XTC_OK || info.track_id != track) return false; } track_ = track; clip_ = clip; mode_ = 0; redraw(); return true; } void SequencerWidget::set_project(xtc_project *project, bool reset) { project_ = project; if (reset) { waves_.clear(); track_ = clip_ = UINT64_MAX; cursor_ = 0; mode_ = 0; } refresh_waveforms(); extent(); redraw(); } /** * @brief Converts a screen x coordinate to a bounded timeline tick snapped to the selected grid. * * @param px Screen x coordinate in pixels. * @return Snapped tick within the supported project range. */ uint64_t SequencerWidget::tick(int px) const { int64_t raw = static_cast(std::max(0,px-x()-180))*960/scale_; return std::min(XTC_PROJECT_MAX_TICK,static_cast(xtc_ui::snap_position(raw,grid_))); } /** * @brief Resizes the timeline's scrollable area to fit its tracks and clip ends. */ void SequencerWidget::extent() { size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); uint64_t end = 32U * 3840U; for (size_t i = 0; i < clips; ++i) { xtc_clip_info c{}; xtc_project_get_clip(project_, i, &c); end = std::max(end, c.start_tick + c.length_tick + 3840U); } size(static_cast(end * static_cast(scale_) / 960U + 200U), std::max(400, static_cast(tracks) * 56 + 100)); refresh_instruments(); if (parent()) parent()->redraw(); } /** * @brief Reports an edit result, refreshes the widget, and notifies listeners on success. * * @param result Result of the attempted operation. */ void SequencerWidget::changed(xtc_result result) { if (result != XTC_OK) fl_alert("%s", xtc_result_string(result)); else { refresh_waveforms(); extent(); do_callback(); } redraw(); } /** * @brief Creates and selects a track through the arrangement edit hook. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result SequencerWidget::add_track() { if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } uint64_t id; xtc_result r = xtc_project_add_track(project_, "Track", &id); if (r == XTC_OK) { track_ = id; clip_ = UINT64_MAX; } changed(r); return r; } /** * @brief Creates and selects a one-bar silent region at the timeline cursor. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result SequencerWidget::add_region() { if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); if (track_ == UINT64_MAX && tracks) { xtc_track_info t{}; xtc_project_get_track(project_, 0, &t); track_ = t.id; } uint64_t id; xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, new_clip_ticks, &id); if (r == XTC_OK) clip_ = id; changed(r); return r; } /** * @brief Creates and selects a one-bar MIDI clip at the timeline cursor. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result SequencerWidget::add_midi_clip() { xtc_result r = before_edit ? before_edit(context_) : XTC_OK; uint64_t id; if (r == XTC_OK) { if (track_ == UINT64_MAX) { xtc_track_info t{}; r = xtc_project_get_track(project_, 0, &t); track_ = r == XTC_OK ? t.id : UINT64_MAX; } if (r == XTC_OK) r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id); if (r == XTC_OK) clip_ = id; } changed(r); return r; } /** * @brief Removes the selected clip, or the selected track when no clip is selected. * @return XTC_OK on success, or an error code describing the failure. */ xtc_result SequencerWidget::delete_selection() { if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } xtc_result r = clip_ != UINT64_MAX ? xtc_project_remove_clip(project_, clip_) : xtc_project_remove_track(project_, track_); if (r == XTC_OK) clip_ = track_ = UINT64_MAX; changed(r); return r; } /** * @brief Adjusts timeline zoom within its supported bounds and updates the scroll extent. * * @param direction Zoom direction; positive zooms in, nonpositive zooms out. */ void SequencerWidget::zoom(int direction) { scale_ = std::clamp(direction > 0 ? scale_ * 2 : scale_ / 2, 24, 384); extent(); redraw(); } /** * @brief Draws the widget's current model, selection, and interaction feedback. */ void SequencerWidget::draw() { fl_push_clip(x(),y(),w(),h()); int cx, cy, cw, ch; fl_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); fl_color(xtc_ui::background()); fl_rectf(cx,cy,cw,ch); size_t tracks = 0, clips = 0, notes = 0; xtc_project_get_counts(project_,&tracks,&clips); xtc_project_note_count(project_,¬es); for (size_t i = 0; i < tracks; ++i) { int py = y()+28+static_cast(i)*56; if (py+56 < cy || py > cy+ch) continue; fl_color(i%2 ? fl_rgb_color(30,34,40) : fl_rgb_color(33,37,43)); fl_rectf(x()+180,py,w()-180,55); } fl_font(FL_HELVETICA,11); uint64_t visual = grid_ == 1 ? 960 : grid_; while (visual*static_cast(scale_)/960 < 6) visual *= 2; int spacing = static_cast(visual*static_cast(scale_)/960); int first = std::max(0,(cx-x()-180)/spacing); int last = (cx+cw-x()-180)/spacing+1; for (int step = first; step <= last; ++step) { uint64_t tick = static_cast(step)*visual; int px = x()+180+static_cast(tick*static_cast(scale_)/960); fl_color(tick%3840 == 0 ? fl_rgb_color(76,84,96) : tick%960 == 0 ? fl_rgb_color(56,63,73) : fl_rgb_color(42,48,56)); fl_line(px,y()+28,px,y()+h()); } first = std::max(0,(cx-x()-180)/(scale_*4)); last = (cx+cw-x()-180)/(scale_*4)+1; for (int bar = first; bar <= last; ++bar) { char label[32]; std::snprintf(label,sizeof(label),"%d",bar+1); fl_color(xtc_ui::muted()); fl_draw(label,x()+180+bar*scale_*4+6,y()+19); } fl_color(xtc_ui::muted()); fl_draw("TRACKS / 4:4",x()+12,y()+19); for (size_t i = 0; i < tracks; ++i) { xtc_track_info t{}; xtc_project_get_track(project_,i,&t); int py = y()+28+static_cast(i)*56; if (py+56 < cy || py > cy+ch) continue; Fl_Color color = xtc_ui::track(t.id); fl_color(t.id == track_ ? fl_rgb_color(49,62,70) : xtc_ui::panel()); fl_rectf(x(),py,178,55); fl_color(color); fl_rectf(x(),py,3,55); fl_push_clip(x()+9,py,162,55); fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12); fl_draw(t.name,x()+12,py+18); char detail[64]; std::snprintf(detail,sizeof(detail),"%02zu %s%s%s",i+1,t.is_bus ? "BUS" : "TRACK", t.mute ? " MUTE" : "",t.solo ? " SOLO" : ""); if (t.is_bus) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,x()+12,py+42); } if (t.mute || t.solo) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(t.mute ? "M" : "S",x()+164,py+18); } fl_pop_clip(); for (size_t j = 0; j < clips; ++j) { xtc_clip_info c{}; xtc_project_get_clip(project_,j,&c); if (c.track_id != t.id) continue; int left = x()+180+static_cast(c.start_tick*static_cast(scale_)/960U); int right = left+std::max(4,static_cast(c.length_tick*static_cast(scale_)/960U)); int a = std::max(left,cx), b = std::min(right,cx+cw); if (b <= a) continue; fl_color(fl_color_average(color,xtc_ui::background(),c.id == clip_ ? .75f : .45f)); fl_rectf(a,py+5,b-a,46); fl_color(c.id == clip_ ? xtc_ui::text() : color); fl_rect(a,py+5,b-a,46); fl_push_clip(a+1,py+6,std::max(0,b-a-2),44); if (c.midi) { /* Real note positions, pitch and duration; no placeholder waveform. */ unsigned int low = 127, high = 0; for (size_t n = 0; n < notes; ++n) { xtc_note_info note{}; xtc_project_get_note(project_,n,¬e); if (note.clip_id == c.id) { low = std::min(low,note.pitch); high = std::max(high,note.pitch); } } unsigned int range = high >= low ? std::max(12U,high-low) : 12U; fl_color(fl_color_average(color,FL_WHITE,.55f)); for (size_t n = 0; n < notes; ++n) { xtc_note_info note{}; xtc_project_get_note(project_,n,¬e); if (note.clip_id != c.id) continue; uint64_t begin = std::max(note.start_tick,c.offset_tick); uint64_t end = std::min(note.start_tick+note.length_tick,c.offset_tick+c.length_tick); if (begin >= end) continue; int nx = left+static_cast((begin-c.offset_tick)*static_cast(scale_)/960U); int nw = std::max(2,static_cast((end-begin)*static_cast(scale_)/960U)); fl_rectf(nx,py+26+static_cast((high-note.pitch)*18/range),nw,2); } } else draw_waveform(c,left,py); fl_color(fl_color_average(color,xtc_ui::background(),.35f)); fl_rectf(a+1,py+6,std::max(0,b-a-2),15); fl_font(FL_HELVETICA,11); fl_color(xtc_ui::text()); fl_draw(c.name,left+7,py+18); fl_pop_clip(); } } if (!tracks) { fl_font(FL_HELVETICA_BOLD,18); fl_color(xtc_ui::text()); fl_draw("Your next track starts here",x()+220,y()+105); fl_font(FL_HELVETICA,13); fl_color(xtc_ui::muted()); fl_draw("Create a MIDI track or import audio from the panel on the left.",x()+220,y()+135); fl_draw("Arrange clips here. Open the piano roll to draw and edit MIDI notes.",x()+220,y()+161); } { uint64_t begin=mode_==4 ? std::min(loop_anchor_,loop_preview_) : loop_begin_; uint64_t end=mode_==4 ? std::max(loop_anchor_,loop_preview_) : loop_end_; int left=x()+180+static_cast(begin*static_cast(scale_)/960); int right=x()+180+static_cast(end*static_cast(scale_)/960); fl_color(loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); fl_rectf(left,y()+23,std::max(0,right-left),4); } int playhead = x()+180+static_cast(cursor_*static_cast(scale_)/960U); if (playhead >= cx && playhead <= cx+cw) { fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h()); fl_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6); } draw_children(); fl_pop_clip(); } /** * @brief Handles FLTK input events and delegates unhandled events to the base widget. * * @param event FLTK event code. * @return Nonzero if the event was handled, zero otherwise. */ int SequencerWidget::handle(int event) { for (const auto &choice : track_choices_) { if (!choice.widget || !choice.widget->visible()) continue; if ((event == FL_PUSH && Fl::event_inside(choice.widget)) || ((event == FL_DRAG || event == FL_RELEASE) && Fl::pushed() == choice.widget) || (event == FL_KEYDOWN && Fl::focus() == choice.widget)) return Fl_Group::handle(event); } if (event == FL_PUSH && (Fl::event_button() == FL_LEFT_MOUSE || Fl::event_button() == FL_RIGHT_MOUSE)) { take_focus(); mode_ = 0; if (Fl::event_y() < y() + 28 && Fl::event_x() >= x()+180 && Fl::event_button()==FL_LEFT_MOUSE && Fl::event_state(FL_SHIFT)) { loop_anchor_=loop_preview_=tick(Fl::event_x()); mode_=4; redraw(); return 1; } if (Fl::event_y() < y() + 28) { uint64_t target = tick(Fl::event_x()); xtc_result r = seek_(context_, target); if (r == XTC_OK) cursor_ = target; else fl_alert("Stop playback before seeking: %s", xtc_result_string(r)); redraw(); return 1; } size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); int row = (Fl::event_y() - y() - 28) / 56; if (row < 0 || static_cast(row) >= tracks) return 1; xtc_track_info t{}; xtc_project_get_track(project_, static_cast(row), &t); track_ = t.id; clip_ = UINT64_MAX; for (size_t j = clips; j > 0; --j) { xtc_clip_info c{}; xtc_project_get_clip(project_, j - 1, &c); int left = x() + 180 + static_cast(c.start_tick * static_cast(scale_) / 960U); int right = left + std::max(4, static_cast(c.length_tick * static_cast(scale_) / 960U)); if (c.track_id == track_ && Fl::event_x() >= left && Fl::event_x() < right) { clip_ = c.id; original_ = c; drag_x_ = Fl::event_x(); mode_ = Fl::event_x() - left < 7 ? 2 : right - Fl::event_x() < 7 ? 3 : 1; break; } } if (Fl::event_button() == FL_LEFT_MOUSE && Fl::event_clicks() && clip_ != UINT64_MAX && original_.midi) { mode_ = 0; if (open_clip) open_clip(context_); return 1; } if (Fl::event_button() == FL_RIGHT_MOUSE) { mode_ = 0; Fl_Menu_Item items[6]{}; items[0].text = "Duplicate clip (Ctrl+D)"; items[1].text = "Rename (F2)"; items[2].text = "Delete (undoable)"; items[3].text = "Split audio at cursor (S)"; items[4].text = "Audio gain and fades (A)"; const Fl_Menu_Item *picked = items->popup(Fl::event_x(),Fl::event_y()); if (picked == items) duplicate_selection(); else if (picked == items+1) { const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); } else if (picked == items+2) delete_selection(); else if (picked == items+3) split_selection(); else if (picked == items+4) audio_properties_dialog(); } redraw(); return 1; } if (event==FL_DRAG && mode_==4) {loop_preview_=tick(Fl::event_x());redraw();return 1;} if (event==FL_RELEASE && mode_==4) { loop_preview_=tick(Fl::event_x()); mode_=0; uint64_t begin=std::min(loop_anchor_,loop_preview_),end=std::max(loop_anchor_,loop_preview_); if(end>begin && loop_select) { xtc_result r=loop_select(context_,begin,end); if(r!=XTC_OK) fl_alert("Loop unchanged: %s",xtc_result_string(r)); } redraw();return 1; } if (event == FL_DRAG) return mode_ != 0; if (event == FL_RELEASE && mode_) { int mode = mode_; mode_ = 0; int64_t delta = static_cast(Fl::event_x() - drag_x_) * 960 / scale_; int64_t start = static_cast(original_.start_tick); int64_t length = static_cast(original_.length_tick); int64_t offset = static_cast(original_.offset_tick); uint64_t target = original_.track_id; if (mode == 1) { start = xtc_ui::snap_position(start+delta,grid_); int row = (Fl::event_y() - y() - 28) / 56; xtc_track_info t{}; if (row >= 0 && xtc_project_get_track(project_, static_cast(row), &t) == XTC_OK) target = t.id; } else if (mode == 2) { delta = xtc_ui::snap_position(start+delta,grid_)-start; start += delta; offset += delta; length -= delta; } else length = xtc_ui::snap_position(start+length+delta,grid_)-start; if (start < 0 || offset < 0 || length <= 0) return 1; if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return 1; } } changed(xtc_project_edit_clip(project_, clip_, target, static_cast(start), static_cast(length), static_cast(offset))); return 1; } if (event == FL_KEYDOWN && !Fl::event_state(FL_CTRL | FL_ALT)) { if (Fl::event_key() == 's') { split_selection(); return 1; } if (Fl::event_key() == 'a') { audio_properties_dialog(); return 1; } } if (event == FL_KEYDOWN && Fl::event_state(FL_CTRL) && Fl::event_key() == 'd') { duplicate_selection(); return 1; } if (event == FL_KEYDOWN && Fl::event_key() == FL_F+2) { const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); return 1; } if (event == FL_KEYDOWN && Fl::event_state(FL_ALT) && (Fl::event_key() == FL_Up || Fl::event_key() == FL_Down)) { move_track(Fl::event_key() == FL_Up ? -1 : 1); return 1; } if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) { mode_ = 0; return 1; } if (event == FL_KEYDOWN && Fl::event_key() == FL_Delete) { delete_selection(); return 1; } if (event == FL_FOCUS || event == FL_UNFOCUS) return 1; return Fl_Group::handle(event); } xtc_result SequencerWidget::grid(uint64_t ticks) { if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT; grid_ = ticks; redraw(); return XTC_OK; } xtc_result SequencerWidget::duplicate_selection() { xtc_clip_info c{}; uint64_t id; xtc_result r = xtc_project_find_clip(project_,clip_,&c); if (r == XTC_OK && before_edit) r = before_edit(context_); if (r == XTC_OK) r = xtc_project_duplicate_clip(project_,clip_,c.track_id,c.start_tick+c.length_tick,&id); if (r == XTC_OK) clip_ = id; changed(r); return r; } xtc_result SequencerWidget::rename_selection(const char *name) { xtc_result r = before_edit ? before_edit(context_) : XTC_OK; if (r == XTC_OK) r = clip_ != UINT64_MAX ? xtc_project_rename_clip(project_,clip_,name) : xtc_project_rename_track(project_,track_,name); changed(r); return r; } xtc_result SequencerWidget::move_track(int direction) { size_t tracks, clips, i; xtc_project_get_counts(project_,&tracks,&clips); xtc_track_info track{}; for (i = 0; i < tracks; ++i) { xtc_project_get_track(project_,i,&track); if (track.id == track_) break; } if (i == tracks || (direction < 0 ? i == 0 : i+1 == tracks)) return XTC_ERR_NOT_FOUND; xtc_result r = before_edit ? before_edit(context_) : XTC_OK; if (r == XTC_OK) r = xtc_project_move_track(project_,track_,direction < 0 ? i-1 : i+1); changed(r); return r; } xtc_result SequencerWidget::split_selection() { uint64_t right; const uint64_t split_tick = cursor_; // Stop/status refresh may move the playhead. xtc_result result = before_edit ? before_edit(context_) : XTC_OK; if (result == XTC_OK) result = xtc_project_split_audio(project_, clip_, split_tick, &right); if (result == XTC_OK) clip_ = right; changed(result); return result; } xtc_result SequencerWidget::audio_properties(float gain, uint64_t in, uint64_t out) { xtc_result result = before_edit ? before_edit(context_) : XTC_OK; if (result == XTC_OK) result = xtc_project_set_clip_audio(project_, clip_, gain, in, out); changed(result); return result; } void SequencerWidget::audio_properties_dialog() { xtc_clip_info clip{}; if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || clip.midi) { fl_alert("Select an audio clip or silent region first."); return; } char defaults[128]; std::snprintf(defaults, sizeof(defaults), "%.9g %llu %llu", static_cast(clip.gain), static_cast(clip.fade_in_tick), static_cast(clip.fade_out_tick)); const char *text = fl_input("Linear gain (0..4), fade-in ticks, fade-out ticks (0 disables)", defaults); if (!text) return; std::istringstream input(text); float gain; uint64_t in, out; if (!(input >> gain >> in >> out) || !(input >> std::ws).eof()) { fl_alert("Enter gain and two fade durations in ticks."); return; } audio_properties(gain, in, out); } void SequencerWidget::refresh_waveforms() try { // Control-thread loading only. Keep cached failures as well as successes; // removed/relinked paths are evicted. Reopening rebuilds externally changed files. size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); std::vector next; for (size_t i = 0; i < clips && next.size() < 64; ++i) { xtc_clip_info c{}; xtc_project_get_clip(project_, i, &c); if (!c.asset_path[0]) continue; bool present = false; for (const auto &wave : next) if (wave.path == c.asset_path) { present = true; break; } if (present) continue; Wave entry; entry.path = c.asset_path; for (auto &wave : waves_) if (wave.path == entry.path) { entry.data = std::move(wave.data); present = true; break; } if (!present) { xtc_waveform *summary = nullptr; (void)xtc_waveform_create(c.asset_path, &summary); entry.data.reset(summary); } next.push_back(std::move(entry)); } waves_ = std::move(next); } catch (const std::bad_alloc &) { waves_.clear(); // Preview failure must not interrupt the model/history edit. } void SequencerWidget::draw_waveform(const xtc_clip_info &c, int left, int top) { const xtc_waveform *summary = nullptr; for (const auto &wave : waves_) if (wave.path == c.asset_path) { summary = wave.data.get(); break; } if (!summary) return; unsigned int rate; size_t frames, bins; if (xtc_waveform_info(summary, &rate, &frames, &bins) != XTC_OK) return; double ticks_per_frame = static_cast(xtc_project_tempo(project_)) * 960 / (rate * 60000.0); fl_color(fl_rgb_color(30, 65, 80)); for (size_t bin = 0; bin < bins; ++bin) { double start = static_cast(bin * frames / bins) * ticks_per_frame; double end = static_cast((bin+1) * frames / bins) * ticks_per_frame; double offset = static_cast(c.offset_tick); if (end <= offset || start >= offset + static_cast(c.length_tick)) continue; int px = left + static_cast((std::max(start, offset) - offset) * scale_ / 960); int width = std::max(1, static_cast((end - std::max(start, offset)) * scale_ / 960)); float low, high; if (xtc_waveform_peak(summary, bin, &low, &high) != XTC_OK) continue; int y1 = top + 28 - static_cast(std::clamp(high, -1.0f, 1.0f) * 19); int y2 = top + 28 - static_cast(std::clamp(low, -1.0f, 1.0f) * 19); fl_rectf(px, y1, width, std::max(1, y2-y1+1)); } } Fl_Choice *SequencerWidget::instrument_choice(uint64_t track) const { for (const auto &entry : track_choices_) if (entry.widget && entry.track == track) return entry.widget; return nullptr; } void SequencerWidget::refresh_instruments() try { size_t tracks = 0, clips = 0; xtc_project_get_counts(project_,&tracks,&clips); catalog_.clear(); for (size_t i = 0; i < tracks; ++i) { xtc_track_info t{}; xtc_plugin_spec spec{}; xtc_project_get_track(project_,i,&t); if (xtc_project_get_instrument(project_,t.id,&spec) != XTC_OK) continue; bool present = false; for (const auto &item : catalog_) if (!std::strcmp(item.path,spec.path) && item.index == spec.index) present = true; if (!present) catalog_.push_back(spec); } for (size_t i = 0; i < track_choices_.size(); ++i) { auto &entry = track_choices_[i]; xtc_track_info t{}; if (i >= tracks || xtc_project_get_track(project_,i,&t) != XTC_OK || t.is_bus) { if (entry.widget) entry.widget->hide(); entry.track = UINT64_MAX; continue; } if (!entry.widget) { struct GroupScope { Fl_Group *previous; explicit GroupScope(Fl_Group *group) : previous(Fl_Group::current()) { Fl_Group::current(group); } ~GroupScope() { Fl_Group::current(previous); } } scope(this); entry.widget = new Fl_Choice(x()+10,y()+58+static_cast(i)*56,158,20); xtc_ui::style(entry.widget); entry.widget->textsize(10); entry.widget->textcolor(xtc_ui::text()); entry.widget->tooltip("Track instrument: built-in sine, an instrument used in this project, or Load plugin..."); entry.widget->callback(instrument_callback,this); } entry.track = t.id; entry.widget->resize(x()+10,y()+58+static_cast(i)*56,158,20); entry.widget->clear(); entry.widget->add("Built-in sine"); xtc_plugin_spec current{}; int selected = 0; bool assigned = xtc_project_get_instrument(project_,t.id,¤t) == XTC_OK; for (size_t j = 0; j < catalog_.size(); ++j) { std::string name = std::string(xtc_plugin_format_name(catalog_[j].format))+": "+catalog_[j].label; int item = entry.widget->add("Instrument"); entry.widget->replace(item,name.c_str()); if (assigned && !std::strcmp(current.path,catalog_[j].path) && current.index == catalog_[j].index) selected = static_cast(j)+1; } entry.widget->add("Load plugin..."); entry.widget->value(selected); entry.widget->show(); } } catch (const std::bad_alloc &) { catalog_.clear(); for (auto &entry : track_choices_) if (entry.widget) entry.widget->hide(); } void SequencerWidget::instrument_callback(Fl_Widget *widget, void *context) { auto *owner = static_cast(context); auto *choice = static_cast(widget); for (const auto &entry : owner->track_choices_) if (entry.widget == choice) { uint64_t track = entry.track; int index = choice->value(); bool browse = index == static_cast(owner->catalog_.size())+1; xtc_plugin_spec spec{}; if (index > 0 && !browse) { if (static_cast(index) > owner->catalog_.size()) return; spec = owner->catalog_[static_cast(index)-1]; } if (owner->instrument_select) owner->instrument_select(owner->context_,track,index > 0 && !browse ? &spec : nullptr,browse); owner->refresh_instruments(); owner->redraw(); return; } } xtc_result SequencerWidget::set_zoom(int scale) { if(scale!=24 && scale!=48 && scale!=96 && scale!=192 && scale!=384) return XTC_ERR_INVALID_ARGUMENT; scale_=scale;extent();redraw();return XTC_OK; }