From: Paul Davis Date: Wed, 26 Mar 2008 11:11:47 +0000 (+0000) Subject: hans' patches for MIDI note resizing++ X-Git-Tag: 3.0-alpha5~4302 X-Git-Url: https://git.carlh.net/gitweb/?a=commitdiff_plain;h=6554200e66cc243e92818e6e74d4647d1c34ae9c;p=ardour.git hans' patches for MIDI note resizing++ git-svn-id: svn://localhost/ardour2/branches/3.0@3184 d708f5d6-7413-0410-9779-e7cbd77b26cf --- diff --git a/gtk2_ardour/canvas-midi-event.cc b/gtk2_ardour/canvas-midi-event.cc index 0e509634e0..8b14bea986 100644 --- a/gtk2_ardour/canvas-midi-event.cc +++ b/gtk2_ardour/canvas-midi-event.cc @@ -63,6 +63,7 @@ CanvasMidiEvent::selected(bool yn) bool CanvasMidiEvent::on_event(GdkEvent* ev) { + MidiStreamView *streamview = _region.midi_stream_view(); static uint8_t drag_delta_note = 0; static double drag_delta_x = 0; static double last_x, last_y; @@ -147,16 +148,17 @@ CanvasMidiEvent::on_event(GdkEvent* ev) drag_delta_x += dx; // Snap to note rows - if (abs(dy) < _region.midi_stream_view()->note_height()) { + if (abs(dy) < streamview->note_height()) { dy = 0.0; } else { int8_t this_delta_note; - if (dy > 0) - this_delta_note = (int8_t)ceil(dy / _region.midi_stream_view()->note_height() / 2.0); - else - this_delta_note = (int8_t)floor(dy / _region.midi_stream_view()->note_height() / 2.0); + if (dy > 0) { + this_delta_note = (int8_t)ceil(dy / streamview->note_height() / 2.0); + } else { + this_delta_note = (int8_t)floor(dy / streamview->note_height() / 2.0); + } drag_delta_note -= this_delta_note; - dy = _region.midi_stream_view()->note_height() * this_delta_note; + dy = streamview->note_height() * this_delta_note; last_y = last_y + dy; } diff --git a/gtk2_ardour/canvas-note.cc b/gtk2_ardour/canvas-note.cc index 5e8eb6904f..b39deb0fed 100644 --- a/gtk2_ardour/canvas-note.cc +++ b/gtk2_ardour/canvas-note.cc @@ -12,38 +12,39 @@ bool CanvasNote::on_event(GdkEvent* ev) { double event_x; - static double middle_point, pressed_x, last_x; + static double middle_point, last_x; Gdk::Cursor cursor; static NoteEnd note_end; + Editing::MidiEditMode edit_mode = _region.get_trackview().editor.current_midi_edit_mode(); switch(ev->type) { case GDK_BUTTON_PRESS: - if (ev->button.button == 2) { + if (ev->button.button == 2 || + (ev->button.button == 1 && + edit_mode == Editing::MidiEditResize)) { + double region_start = _region.get_position_pixels(); event_x = ev->button.x; - middle_point = x1() + (x2() - x1()) / 2.0L; + middle_point = region_start + x1() + (x2() - x1()) / 2.0L; if(event_x <= middle_point) { cursor = Gdk::Cursor(Gdk::LEFT_SIDE); - last_x = x1(); note_end = NOTE_ON; } else { cursor = Gdk::Cursor(Gdk::RIGHT_SIDE); - last_x = x2(); note_end = NOTE_OFF; } _item->grab(GDK_POINTER_MOTION_MASK | GDK_BUTTON_RELEASE_MASK, cursor, ev->motion.time); if (_region.mouse_state() == MidiRegionView::SelectTouchDragging) { - _mouse2_state = AbsoluteResize; + _note_state = AbsoluteResize; } else { - _mouse2_state = RelativeResize; + _note_state = RelativeResize; } - pressed_x = event_x; - _region.note_selected(this, true); _region.begin_resizing(note_end); + last_x = event_x; return true; } @@ -51,13 +52,13 @@ CanvasNote::on_event(GdkEvent* ev) case GDK_MOTION_NOTIFY: event_x = ev->motion.x; - if (_mouse2_state == RelativeResize) { + if (_note_state == RelativeResize) { _region.update_resizing(note_end, event_x - last_x, true); last_x = event_x; return true; } - if (_mouse2_state == AbsoluteResize) { + if (_note_state == AbsoluteResize) { _region.update_resizing(note_end, event_x, false); return true; } @@ -65,17 +66,17 @@ CanvasNote::on_event(GdkEvent* ev) case GDK_BUTTON_RELEASE: event_x = ev->button.x; - switch (_mouse2_state) { + switch (_note_state) { case RelativeResize: // Clicked _item->ungrab(ev->button.time); _region.commit_resizing(note_end, event_x, true); - _mouse2_state = None; + _note_state = None; return true; case AbsoluteResize: // Clicked _item->ungrab(ev->button.time); _region.commit_resizing(note_end, event_x, false); - _mouse2_state = None; + _note_state = None; return true; default: diff --git a/gtk2_ardour/canvas-note.h b/gtk2_ardour/canvas-note.h index d6c1d95553..0a1aed8021 100644 --- a/gtk2_ardour/canvas-note.h +++ b/gtk2_ardour/canvas-note.h @@ -55,18 +55,17 @@ public: NOTE_OFF }; - enum Mouse2State { + enum NoteState { None, RelativeResize, AbsoluteResize }; protected: - Mouse2State _mouse2_state; + NoteState _note_state; private: - // single click resizing with mouse-2 - void resize_note(double pressed_x, double event_x, double middle_point); + }; } // namespace Gnome diff --git a/gtk2_ardour/editing_syms.h b/gtk2_ardour/editing_syms.h index 71a7747b47..47017f57bf 100644 --- a/gtk2_ardour/editing_syms.h +++ b/gtk2_ardour/editing_syms.h @@ -63,6 +63,7 @@ MOUSEMODE(MouseNote) MIDIEDITMODE(MidiEditPencil) MIDIEDITMODE(MidiEditSelect) +MIDIEDITMODE(MidiEditResize) MIDIEDITMODE(MidiEditErase) /* Changing this order will break the menu */ diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc index 4f36865905..9138fe392a 100644 --- a/gtk2_ardour/editor.cc +++ b/gtk2_ardour/editor.cc @@ -182,6 +182,7 @@ Gdk::Cursor* Editor::fader_cursor = 0; Gdk::Cursor* Editor::speaker_cursor = 0; Gdk::Cursor* Editor::midi_pencil_cursor = 0; Gdk::Cursor* Editor::midi_select_cursor = 0; +Gdk::Cursor* Editor::midi_resize_cursor = 0; Gdk::Cursor* Editor::midi_erase_cursor = 0; Gdk::Cursor* Editor::wait_cursor = 0; Gdk::Cursor* Editor::timebar_cursor = 0; @@ -1390,6 +1391,7 @@ Editor::build_cursors () timebar_cursor = new Gdk::Cursor(LEFT_PTR); midi_pencil_cursor = new Gdk::Cursor (PENCIL); midi_select_cursor = new Gdk::Cursor (CENTER_PTR); + midi_resize_cursor = new Gdk::Cursor (SIZING); midi_erase_cursor = new Gdk::Cursor (DRAPED_BOX); } @@ -3084,6 +3086,9 @@ Editor::setup_midi_toolbar () midi_tool_select_button.add (*(manage (new Image (::get_icon("midi_tool_select"))))); midi_tool_select_button.set_relief(Gtk::RELIEF_NONE); midi_tool_buttons.push_back (&midi_tool_select_button); + midi_tool_resize_button.add (*(manage (new Image (::get_icon("strip_width"))))); + midi_tool_resize_button.set_relief(Gtk::RELIEF_NONE); + midi_tool_buttons.push_back (&midi_tool_resize_button); midi_tool_erase_button.add (*(manage (new Image (::get_icon("midi_tool_erase"))))); midi_tool_erase_button.set_relief(Gtk::RELIEF_NONE); midi_tool_buttons.push_back (&midi_tool_erase_button); @@ -3093,29 +3098,34 @@ Editor::setup_midi_toolbar () midi_tool_button_set = new GroupedButtons (midi_tool_buttons); midi_tool_button_box.set_border_width (2); - midi_tool_button_box.set_spacing(4); midi_tool_button_box.set_spacing(1); midi_tool_button_box.pack_start(midi_tool_pencil_button, true, true); midi_tool_button_box.pack_start(midi_tool_select_button, true, true); + midi_tool_button_box.pack_start(midi_tool_resize_button, true, true); midi_tool_button_box.pack_start(midi_tool_erase_button, true, true); midi_tool_button_box.set_homogeneous(true); midi_tool_pencil_button.set_name ("MouseModeButton"); midi_tool_select_button.set_name ("MouseModeButton"); + midi_tool_resize_button.set_name ("MouseModeButton"); midi_tool_erase_button.set_name ("MouseModeButton"); ARDOUR_UI::instance()->tooltips().set_tip (midi_tool_pencil_button, _("Add/Move/Stretch Notes")); ARDOUR_UI::instance()->tooltips().set_tip (midi_tool_select_button, _("Select/Move Notes")); + ARDOUR_UI::instance()->tooltips().set_tip (midi_tool_resize_button, _("Resize Notes")); ARDOUR_UI::instance()->tooltips().set_tip (midi_tool_erase_button, _("Erase Notes")); midi_tool_pencil_button.unset_flags (CAN_FOCUS); midi_tool_select_button.unset_flags (CAN_FOCUS); + midi_tool_resize_button.unset_flags (CAN_FOCUS); midi_tool_erase_button.unset_flags (CAN_FOCUS); midi_tool_pencil_button.signal_toggled().connect (bind (mem_fun(*this, &Editor::midi_edit_mode_toggled), Editing::MidiEditPencil)); midi_tool_select_button.signal_toggled().connect (bind (mem_fun(*this, &Editor::midi_edit_mode_toggled), Editing::MidiEditSelect)); + midi_tool_resize_button.signal_toggled().connect (bind (mem_fun(*this, + &Editor::midi_edit_mode_toggled), Editing::MidiEditResize)); midi_tool_erase_button.signal_toggled().connect (bind (mem_fun(*this, &Editor::midi_edit_mode_toggled), Editing::MidiEditErase)); diff --git a/gtk2_ardour/editor.h b/gtk2_ardour/editor.h index 6fe6d0207a..65e64da121 100644 --- a/gtk2_ardour/editor.h +++ b/gtk2_ardour/editor.h @@ -990,6 +990,7 @@ class Editor : public PublicEditor static Gdk::Cursor* speaker_cursor; static Gdk::Cursor* midi_pencil_cursor; static Gdk::Cursor* midi_select_cursor; + static Gdk::Cursor* midi_resize_cursor; static Gdk::Cursor* midi_erase_cursor; static Gdk::Cursor* wait_cursor; static Gdk::Cursor* timebar_cursor; @@ -1642,6 +1643,7 @@ public: Gtkmm2ext::TearOff* midi_tool_tearoff; Gtk::ToggleButton midi_tool_pencil_button; Gtk::ToggleButton midi_tool_select_button; + Gtk::ToggleButton midi_tool_resize_button; Gtk::ToggleButton midi_tool_erase_button; GroupedButtons *midi_tool_button_set; void midi_edit_mode_toggled (Editing::MidiEditMode m); diff --git a/gtk2_ardour/editor_mouse.cc b/gtk2_ardour/editor_mouse.cc index 22215ecb62..0199a3de3c 100644 --- a/gtk2_ardour/editor_mouse.cc +++ b/gtk2_ardour/editor_mouse.cc @@ -414,6 +414,11 @@ Editor::midi_edit_mode_toggled (MidiEditMode m) set_midi_edit_mode (m); break; + case MidiEditResize: + if (midi_tool_resize_button.get_active()) + set_midi_edit_mode (m); + break; + case MidiEditErase: if (midi_tool_erase_button.get_active()) set_midi_edit_mode (m); @@ -453,6 +458,10 @@ Editor::set_midi_edit_mode (MidiEditMode m, bool force) midi_tool_select_button.set_active (true); break; + case MidiEditResize: + midi_tool_resize_button.set_active (true); + break; + case MidiEditErase: midi_tool_erase_button.set_active (true); break; @@ -479,6 +488,10 @@ Editor::set_midi_edit_cursor (MidiEditMode m) current_canvas_cursor = midi_select_cursor; break; + case MidiEditResize: + current_canvas_cursor = midi_resize_cursor; + break; + case MidiEditErase: current_canvas_cursor = midi_erase_cursor; break; diff --git a/gtk2_ardour/midi_region_view.cc b/gtk2_ardour/midi_region_view.cc index 69b97b93d9..86e06d0a3c 100644 --- a/gtk2_ardour/midi_region_view.cc +++ b/gtk2_ardour/midi_region_view.cc @@ -160,6 +160,9 @@ MidiRegionView::canvas_event(GdkEvent* ev) } else if (ev->key.keyval == GDK_Shift_L || ev->key.keyval == GDK_Control_L) { _mouse_state = SelectTouchDragging; return true; + } else if (ev->key.keyval == GDK_Escape) { + clear_selection(); + _mouse_state = None; } return false; @@ -304,6 +307,7 @@ MidiRegionView::canvas_event(GdkEvent* ev) case Pressed: // Clicked switch (trackview.editor.current_midi_edit_mode()) { case MidiEditSelect: + case MidiEditResize: clear_selection(); break; case MidiEditPencil: @@ -382,7 +386,7 @@ MidiRegionView::clear_events() clear_selection(); MidiGhostRegion* gr; - for (vector::iterator g = ghosts.begin(); g != ghosts.end(); ++g) { + for (std::vector::iterator g = ghosts.begin(); g != ghosts.end(); ++g) { if ((gr = dynamic_cast(*g)) != 0) { gr->clear_events(); } @@ -686,7 +690,7 @@ MidiRegionView::add_note(const boost::shared_ptr note) MidiGhostRegion* gr; - for (vector::iterator g = ghosts.begin(); g != ghosts.end(); ++g) { + for (std::vector::iterator g = ghosts.begin(); g != ghosts.end(); ++g) { if ((gr = dynamic_cast(*g)) != 0) { gr->add_note(ev_rect); } @@ -818,6 +822,38 @@ MidiRegionView::note_dropped(CanvasMidiEvent* ev, double dt, uint8_t dnote) { // TODO: This would be faster/nicer with a MoveCommand that doesn't need to copy... if (_selection.find(ev) != _selection.end()) { + uint8_t lowest_note_in_selection = midi_stream_view()->lowest_note(); + uint8_t highest_note_in_selection = midi_stream_view()->highest_note(); + uint8_t highest_note_difference = 0; + + // find highest and lowest notes first + for (Selection::iterator i = _selection.begin(); i != _selection.end() ; ) { + Selection::iterator next = i; + ++next; + + uint8_t pitch = (*i)->note()->note(); + lowest_note_in_selection = std::min(lowest_note_in_selection, pitch); + highest_note_in_selection = std::max(highest_note_in_selection, pitch); + + i = next; + } + + /* + cerr << "dnote: " << (int) dnote << endl; + cerr << "lowest note (streamview): " << int(midi_stream_view()->lowest_note()) + << " highest note (streamview): " << int(midi_stream_view()->highest_note()) << endl; + cerr << "lowest note (selection): " << int(lowest_note_in_selection) << " highest note(selection): " + << int(highest_note_in_selection) << endl; + cerr << "selection size: " << _selection.size() << endl; + cerr << "Highest note in selection: " << (int) highest_note_in_selection << endl; + */ + + // Make sure the note pitch does not exceed the MIDI standard range + if (dnote <= 127 && (highest_note_in_selection + dnote > 127)) { + highest_note_difference = highest_note_in_selection - 127; + cerr << "Highest note difference: " << (int) highest_note_difference; + } + start_delta_command(); for (Selection::iterator i = _selection.begin(); i != _selection.end() ; ) { @@ -828,16 +864,57 @@ MidiRegionView::note_dropped(CanvasMidiEvent* ev, double dt, uint8_t dnote) const boost::shared_ptr copy(new Note(*(*i)->note().get())); copy->set_time((*i)->note()->time() + dt); - copy->set_note((*i)->note()->note() + dnote); + if(copy->time() < 0) { + copy->set_time(0); + } + + uint8_t new_pitch = (*i)->note()->note() + dnote - highest_note_difference; + if(new_pitch > 127) { + new_pitch = 127; + } + lowest_note_in_selection = std::min(lowest_note_in_selection, new_pitch); + highest_note_in_selection = std::max(highest_note_in_selection, new_pitch); + + copy->set_note(new_pitch); + command_add_note(copy); _selection.erase(i); i = next; } + apply_command(); + + //cerr << "new lowest note (selection): " << int(lowest_note_in_selection) << " new highest note(selection): " << int(highest_note_in_selection) << endl; + + // care about notes being moved beyond the upper/lower bounds on the canvas + if(lowest_note_in_selection < midi_stream_view()->lowest_note() || + highest_note_in_selection > midi_stream_view()->highest_note() + ) { + //cerr << "resetting note range" << endl; + midi_stream_view()->set_note_range(MidiStreamView::ContentsRange); } } +} + + +double +MidiRegionView::snap_to(double x) +{ + PublicEditor &editor = trackview.editor; + + nframes_t frame = editor.pixel_to_frame(x); + editor.snap_to(frame); + return (double) editor.frame_to_pixel(frame); +} + +double +MidiRegionView::get_position_pixels(void) +{ + nframes_t region_frame = get_position(); + return trackview.editor.frame_to_pixel(region_frame); +} void MidiRegionView::begin_resizing(CanvasNote::NoteEnd note_end) @@ -852,18 +929,34 @@ MidiRegionView::begin_resizing(CanvasNote::NoteEnd note_end) NoteResizeData *resize_data = new NoteResizeData(); resize_data->canvas_note = note; - SimpleRect *resize_rect = new SimpleRect(*group, note->x1(), note->y1(), note->x2(), note->y2()); - - uint fill_color = UINT_RGBA_CHANGE_A(ARDOUR_UI::config()->canvasvar_MidiNoteSelectedOutline.get(), 128); + // create a new SimpleRect from the note which will be the resize preview + SimpleRect *resize_rect = + new SimpleRect( + *group, + note->x1(), + note->y1(), + note->x2(), + note->y2()); + + // calculate the colors: get the color settings + uint fill_color = + UINT_RGBA_CHANGE_A( + ARDOUR_UI::config()->canvasvar_MidiNoteSelectedOutline.get(), + 128); + + // make the resize preview notes more transparent and bright fill_color = UINT_INTERPOLATE(fill_color, 0xFFFFFF40, 0.5); + // calculate color based on note velocity resize_rect->property_fill_color_rgba() = UINT_INTERPOLATE( note_fill_color(note->note()->velocity()), fill_color, 0.85); - resize_rect->property_outline_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiNoteSelectedOutline.get(); + resize_rect->property_outline_color_rgba() = + ARDOUR_UI::config()->canvasvar_MidiNoteSelectedOutline.get(); + resize_data->resize_rect = resize_rect; if(note_end == CanvasNote::NOTE_ON) { @@ -877,38 +970,33 @@ MidiRegionView::begin_resizing(CanvasNote::NoteEnd note_end) } } -double -MidiRegionView::snap_to(double x) -{ - PublicEditor &editor = trackview.editor; - - nframes_t frame = editor.pixel_to_frame(x); - editor.snap_to(frame); - return (double) editor.frame_to_pixel(frame); -} - void -MidiRegionView::update_resizing(CanvasNote::NoteEnd note_end, double dx, bool relative) +MidiRegionView::update_resizing(CanvasNote::NoteEnd note_end, double x, bool relative) { - - for (std::vector::iterator i = _resize_data.begin(); i != _resize_data.end(); ++i) { SimpleRect *resize_rect = (*i)->resize_rect; CanvasNote *canvas_note = (*i)->canvas_note; + const double region_start = get_position_pixels(); + if(relative) { - (*i)->current_x = (*i)->current_x + dx; + (*i)->current_x = (*i)->current_x + x; } else { - (*i)->current_x = dx; + // x is in track relative, transform it to region relative + (*i)->current_x = x - region_start; } double current_x = (*i)->current_x; if(note_end == CanvasNote::NOTE_ON) { - resize_rect->property_x1() = snap_to(current_x); + // because snapping works on world coordinates we have to transform current_x + // to world coordinates before snapping and transform it back afterwards + resize_rect->property_x1() = snap_to(region_start + current_x) - region_start; resize_rect->property_x2() = canvas_note->x2(); } else { - resize_rect->property_x2() = snap_to(current_x); + // because snapping works on world coordinates we have to transform current_x + // to world coordinates before snapping and transform it back afterwards + resize_rect->property_x2() = snap_to(region_start + current_x) - region_start; resize_rect->property_x1() = canvas_note->x1(); } } @@ -923,14 +1011,18 @@ MidiRegionView::commit_resizing(CanvasNote::NoteEnd note_end, double event_x, bo CanvasNote *canvas_note = (*i)->canvas_note; SimpleRect *resize_rect = (*i)->resize_rect; double current_x = (*i)->current_x; - + const double region_start = get_position_pixels(); if(!relative) { - current_x = event_x; + // event_x is in track relative, transform it to region relative + current_x = event_x - region_start; } - nframes_t current_frame = trackview.editor.pixel_to_frame(current_x); + // because snapping works on world coordinates we have to transform current_x + // to world coordinates before snapping and transform it back afterwards + nframes_t current_frame = trackview.editor.pixel_to_frame(current_x + region_start); trackview.editor.snap_to(current_frame); + current_frame -= get_position(); const boost::shared_ptr copy(new Note(*(canvas_note->note().get()))); diff --git a/gtk2_ardour/midi_region_view.h b/gtk2_ardour/midi_region_view.h index 93dc917629..767d057f57 100644 --- a/gtk2_ardour/midi_region_view.h +++ b/gtk2_ardour/midi_region_view.h @@ -132,8 +132,34 @@ class MidiRegionView : public RegionView void move_selection(double dx, double dy); void note_dropped(ArdourCanvas::CanvasMidiEvent* ev, double dt, uint8_t dnote); + /** + * This function is needed to subtract the region start in pixels + * from world coordinates submitted by the mouse + */ + double get_position_pixels(void); + + /** + * This function is called by CanvasMidiNote when resizing starts, + * i.e. when the user presses mouse-2 on the note + * @param note_end which end of the note, NOTE_ON or NOTE_OFF + */ void begin_resizing(ArdourCanvas::CanvasNote::NoteEnd note_end); - void update_resizing(ArdourCanvas::CanvasNote::NoteEnd note_end, double dx, bool relative); + + /** + * This function is called while the user moves the mouse when resizing notes + * @param note_end which end of the note, NOTE_ON or NOTE_OFF + * @param x the difference in mouse motion, ie the motion difference if relative=true + * or the absolute mouse position (track-relative) if relative is false + * @param relative true if relative resizing is taking place, false if absolute resizing + */ + void update_resizing(ArdourCanvas::CanvasNote::NoteEnd note_end, double x, bool relative); + + /** + * This function is called while the user releases the mouse button when resizing notes + * @param note_end which end of the note, NOTE_ON or NOTE_OFF + * @param event_x the absolute mouse position (track-relative) + * @param relative true if relative resizing is taking place, false if absolute resizing + */ void commit_resizing(ArdourCanvas::CanvasNote::NoteEnd note_end, double event_x, bool relative); enum MouseState { None, Pressed, SelectTouchDragging, SelectRectDragging, AddDragging, EraseTouchDragging }; @@ -147,11 +173,11 @@ class MidiRegionView : public RegionView protected: - /* this constructor allows derived types - to specify their visibility requirements - to the TimeAxisViewItem parent class + /** + * this constructor allows derived types + * to specify their visibility requirements + * to the TimeAxisViewItem parent class */ - MidiRegionView (ArdourCanvas::Group *, RouteTimeAxisView&, boost::shared_ptr, @@ -177,6 +203,11 @@ class MidiRegionView : public RegionView void clear_selection_except(ArdourCanvas::CanvasMidiEvent* ev); void clear_selection() { clear_selection_except(NULL); } void update_drag_selection(double last_x, double x, double last_y, double y); + + /** + * This function provides the snap function for pixel units (double) + * instead of nframes_t + */ double snap_to(double x); double _default_note_length; diff --git a/libs/midi++2/midiparser.cc b/libs/midi++2/midiparser.cc index 3aa6cb9d33..a1f6ce39b4 100644 --- a/libs/midi++2/midiparser.cc +++ b/libs/midi++2/midiparser.cc @@ -143,8 +143,6 @@ Parser::trace_event (Parser &p, byte *msg, size_t len) eventType type; ostream *o; - cerr << "TRACE\n"; - if ((o = trace_stream) == NULL) { /* can be asynchronously removed */ return; }