1use crate::midi::PianoNote;
2use iced::{
3 Color, Event, Point, Rectangle, Renderer, Size, Theme, mouse,
4 widget::canvas::{Action as CanvasAction, Frame, Geometry, Path, Program},
5};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum DrumMessage {
10 NoteSelected(usize),
11 ClearSelection,
12 NoteCreate {
13 start_sample: usize,
14 end_sample: usize,
15 pitch: u8,
16 repeat: bool,
17 },
18 NoteDelete(usize),
19 NoteMove {
20 note_index: usize,
21 delta_samples: i64,
22 target_pitch: u8,
23 },
24 AdjustVelocity {
25 note_index: usize,
26 delta: i8,
27 },
28 SelectRectStart {
29 position: Point,
30 },
31 SelectRectDrag {
32 position: Point,
33 },
34 SelectRectEnd,
35}
36
37#[derive(Default, Debug, Clone, Copy, PartialEq)]
38pub enum DraggingMode {
39 #[default]
40 None,
41 SelectingRect,
42 DraggingNote,
43 CreatingNote,
44}
45
46#[derive(Debug)]
47pub struct DrumRollInteraction {
48 pub notes: Vec<PianoNote>,
49 pub pixels_per_sample: f32,
50 pub zoom_x: f32,
51 pub drum_rows: Vec<u8>,
52 pub row_height: f32,
53 pub selecting_rect: Option<(Point, Point)>,
54 pub selected_notes: HashSet<usize>,
55 pub repeat_create: bool,
56}
57
58#[derive(Default, Debug)]
59pub struct DrumRollInteractionState {
60 pub dragging_mode: DraggingMode,
61 pub drag_start: Option<Point>,
62 pub drag_current: Option<Point>,
63 pub drag_note_index: Option<usize>,
64 pub hover_note_index: Option<usize>,
65 pub creating_dragged: bool,
66 pub creating_start_sample: Option<usize>,
67 pub creating_pitch: Option<u8>,
68}
69
70impl DrumRollInteraction {
71 pub fn new(
72 notes: Vec<PianoNote>,
73 pixels_per_sample: f32,
74 zoom_x: f32,
75 drum_rows: Vec<u8>,
76 row_height: f32,
77 selecting_rect: Option<(Point, Point)>,
78 selected_notes: HashSet<usize>,
79 ) -> Self {
80 Self {
81 notes,
82 pixels_per_sample,
83 zoom_x,
84 drum_rows,
85 row_height,
86 selecting_rect,
87 selected_notes,
88 repeat_create: false,
89 }
90 }
91
92 fn note_at_position(&self, position: Point, pps: f32, notes: &[PianoNote]) -> Option<usize> {
93 for (idx, note) in notes.iter().enumerate() {
94 let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch) else {
95 continue;
96 };
97 let y = row_idx as f32 * self.row_height + 1.0;
98 let x = note.start_sample as f32 * pps;
99 let w = (note.length_samples as f32 * pps).max(2.0);
100 let h = (self.row_height - 2.0).max(2.0);
101 if position.x >= x && position.x <= x + w && position.y >= y && position.y <= y + h {
102 return Some(idx);
103 }
104 }
105 None
106 }
107
108 fn pitch_at_y(&self, y: f32) -> u8 {
109 let row_idx = (y / self.row_height)
110 .floor()
111 .clamp(0.0, (self.drum_rows.len().saturating_sub(1)) as f32)
112 as usize;
113 self.drum_rows.get(row_idx).copied().unwrap_or(60)
114 }
115
116 fn sample_at_x(&self, x: f32, pps: f32) -> usize {
117 (x / pps).max(0.0) as usize
118 }
119
120 fn local_position(bounds: Rectangle, position: Point) -> Point {
121 Point::new(position.x - bounds.x, position.y - bounds.y)
122 }
123}
124
125impl Program<DrumMessage> for DrumRollInteraction {
126 type State = DrumRollInteractionState;
127
128 fn update(
129 &self,
130 state: &mut Self::State,
131 event: &Event,
132 bounds: Rectangle,
133 cursor: mouse::Cursor,
134 ) -> Option<CanvasAction<DrumMessage>> {
135 let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
136 let notes = &self.notes;
137
138 match event {
139 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
140 if let Some(position) = cursor.position_in(bounds) {
141 if let Some(note_idx) = self.note_at_position(position, pps, notes) {
142 state.drag_start = Some(position);
143 state.drag_current = Some(position);
144 state.drag_note_index = Some(note_idx);
145 state.dragging_mode = DraggingMode::DraggingNote;
146 return Some(
147 CanvasAction::publish(DrumMessage::NoteSelected(note_idx))
148 .and_capture(),
149 );
150 } else {
151 state.drag_start = Some(position);
152 state.drag_current = Some(position);
153 state.drag_note_index = None;
154 state.dragging_mode = DraggingMode::SelectingRect;
155 return Some(
156 CanvasAction::publish(DrumMessage::SelectRectStart { position })
157 .and_capture(),
158 );
159 }
160 }
161 }
162 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
163 if let Some(position) = cursor.position_in(bounds) {
164 state.drag_start = Some(position);
165 state.drag_current = Some(position);
166 state.drag_note_index = None;
167 state.creating_dragged = false;
168 state.creating_start_sample = Some(self.sample_at_x(position.x, pps));
169 state.creating_pitch = Some(self.pitch_at_y(position.y));
170 state.dragging_mode = DraggingMode::CreatingNote;
171 return Some(CanvasAction::request_redraw().and_capture());
172 }
173 }
174 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => {
175 if let Some(position) = cursor.position_in(bounds)
176 && let Some(note_idx) = self.note_at_position(position, pps, notes)
177 {
178 return Some(
179 CanvasAction::publish(DrumMessage::NoteDelete(note_idx)).and_capture(),
180 );
181 }
182 }
183 Event::Mouse(mouse::Event::CursorMoved { position }) => {
184 let position = Self::local_position(bounds, *position);
185 if state.drag_start.is_some() {
186 state.drag_current = Some(position);
187 }
188 match state.dragging_mode {
189 DraggingMode::SelectingRect => {
190 return Some(CanvasAction::publish(DrumMessage::SelectRectDrag {
191 position,
192 }));
193 }
194 DraggingMode::DraggingNote => {
195 return Some(CanvasAction::request_redraw());
196 }
197 DraggingMode::CreatingNote => {
198 if let Some(drag_start) = state.drag_start
199 && (position.x - drag_start.x).hypot(position.y - drag_start.y) < 3.0
200 {
201 return Some(CanvasAction::request_redraw().and_capture());
202 }
203 state.creating_dragged = true;
204 return Some(CanvasAction::request_redraw().and_capture());
205 }
206 DraggingMode::None => {}
207 }
208 state.hover_note_index = cursor
209 .position_in(bounds)
210 .and_then(|position| self.note_at_position(position, pps, notes));
211 }
212 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
213 let mode = state.dragging_mode;
214
215 match mode {
216 DraggingMode::SelectingRect => {
217 state.drag_start = None;
218 state.drag_current = None;
219 state.drag_note_index = None;
220 state.dragging_mode = DraggingMode::None;
221 return Some(CanvasAction::publish(DrumMessage::SelectRectEnd));
222 }
223 DraggingMode::DraggingNote => {
224 if let (Some(drag_start), Some(note_idx)) =
225 (state.drag_start.take(), state.drag_note_index.take())
226 {
227 let drag_current = state.drag_current.take();
228 state.dragging_mode = DraggingMode::None;
229 let position = cursor
230 .position_in(bounds)
231 .or(drag_current)
232 .unwrap_or(drag_start);
233 if let Some(original_note) = notes.get(note_idx) {
234 let delta_x = position.x - drag_start.x;
235 let delta_samples = (delta_x / pps) as i64;
236 let target_pitch = self.pitch_at_y(position.y);
237 if delta_samples != 0 || target_pitch != original_note.pitch {
238 return Some(
239 CanvasAction::publish(DrumMessage::NoteMove {
240 note_index: note_idx,
241 delta_samples,
242 target_pitch,
243 })
244 .and_capture(),
245 );
246 }
247 }
248 }
249 }
250 DraggingMode::CreatingNote => {}
251 DraggingMode::None => {}
252 }
253 }
254 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => {
255 if state.dragging_mode == DraggingMode::CreatingNote {
256 let start = state.drag_start.take();
257 let current = state.drag_current.take();
258 let start_sample = state.creating_start_sample;
259 let pitch = state.creating_pitch;
260 state.drag_note_index = None;
261 state.creating_dragged = false;
262 state.creating_start_sample = None;
263 state.creating_pitch = None;
264 state.dragging_mode = DraggingMode::None;
265 if let Some(position) = cursor.position_in(bounds).or(current).or(start) {
266 let Some(start_sample) = start_sample else {
267 return Some(CanvasAction::request_redraw().and_capture());
268 };
269 let Some(pitch) = pitch else {
270 return Some(CanvasAction::request_redraw().and_capture());
271 };
272 let end_sample = self.sample_at_x(position.x, pps);
273 return Some(
274 CanvasAction::publish(DrumMessage::NoteCreate {
275 start_sample,
276 end_sample,
277 pitch,
278 repeat: self.repeat_create,
279 })
280 .and_capture(),
281 );
282 }
283 return Some(CanvasAction::request_redraw().and_capture());
284 }
285 }
286 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
287 if let Some(position) = cursor.position_in(bounds) {
288 let raw = match delta {
289 mouse::ScrollDelta::Lines { y, .. } => *y,
290 mouse::ScrollDelta::Pixels { y, .. } => *y / 16.0,
291 };
292 let steps = raw.round() as i32;
293 if steps != 0
294 && let Some(note_idx) = self.note_at_position(position, pps, notes)
295 {
296 let delta = steps.clamp(-24, 24) as i8;
297 return Some(
298 CanvasAction::publish(DrumMessage::AdjustVelocity {
299 note_index: note_idx,
300 delta,
301 })
302 .and_capture(),
303 );
304 }
305 }
306 }
307 _ => {}
308 }
309 None
310 }
311
312 fn draw(
313 &self,
314 state: &Self::State,
315 renderer: &Renderer,
316 _theme: &Theme,
317 bounds: Rectangle,
318 cursor: mouse::Cursor,
319 ) -> Vec<Geometry> {
320 let mut frame = Frame::new(renderer, bounds.size());
321
322 if state.dragging_mode == DraggingMode::DraggingNote
323 && let (Some(drag_start), Some(cursor_pos)) =
324 (state.drag_start, cursor.position_in(bounds))
325 {
326 let pps = (self.pixels_per_sample * self.zoom_x).max(0.0001);
327 let delta_x = cursor_pos.x - drag_start.x;
328 let row_delta = self
329 .note_at_position(drag_start, pps, &self.notes)
330 .and_then(|idx| self.notes.get(idx))
331 .and_then(|note| {
332 let start_row = self.drum_rows.iter().position(|&p| p == note.pitch)?;
333 let target_pitch = self.pitch_at_y(cursor_pos.y);
334 let target_row = self.drum_rows.iter().position(|&p| p == target_pitch)?;
335 Some(target_row as isize - start_row as isize)
336 })
337 .unwrap_or(0);
338 for ¬e_idx in &self.selected_notes {
339 if let Some(note) = self.notes.get(note_idx)
340 && let Some(row_idx) = self.drum_rows.iter().position(|&p| p == note.pitch)
341 {
342 let x = note.start_sample as f32 * pps + delta_x;
343 let target_row = (row_idx as isize + row_delta)
344 .clamp(0, self.drum_rows.len().saturating_sub(1) as isize)
345 as usize;
346 let y = target_row as f32 * self.row_height + 1.0;
347 let w = (note.length_samples as f32 * pps).max(2.0);
348 let h = (self.row_height - 2.0).max(2.0);
349 frame.fill(
350 &Path::rectangle(Point::new(x, y), Size::new(w, h)),
351 Color::from_rgba(0.9, 0.9, 0.95, 0.35),
352 );
353 }
354 }
355 }
356
357 if self.repeat_create
358 && state.dragging_mode == DraggingMode::CreatingNote
359 && let (Some(start), Some(current), Some(pitch)) =
360 (state.drag_start, state.drag_current, state.creating_pitch)
361 && let Some(row_idx) = self
362 .drum_rows
363 .iter()
364 .position(|&row_pitch| row_pitch == pitch)
365 {
366 let x0 = start.x.min(current.x).max(0.0);
367 let x1 = start.x.max(current.x).max(0.0);
368 let y = row_idx as f32 * self.row_height + 1.0;
369 let w = (x1 - x0).max(2.0);
370 let h = (self.row_height - 2.0).max(2.0);
371 let path = Path::rectangle(Point::new(x0, y), Size::new(w, h));
372 frame.fill(&path, Color::from_rgba(0.3, 0.55, 0.95, 0.28));
373 frame.stroke(
374 &path,
375 iced::widget::canvas::Stroke::default()
376 .with_color(Color::from_rgba(0.5, 0.75, 1.0, 0.9))
377 .with_width(1.5),
378 );
379 }
380
381 if let Some((start, end)) = self.selecting_rect {
382 let min_x = start.x.min(end.x);
383 let min_y = start.y.min(end.y);
384 let max_x = start.x.max(end.x);
385 let max_y = start.y.max(end.y);
386
387 let rect = Rectangle {
388 x: min_x,
389 y: min_y,
390 width: max_x - min_x,
391 height: max_y - min_y,
392 };
393
394 frame.fill(
395 &Path::rectangle(
396 Point::new(rect.x, rect.y),
397 Size::new(rect.width, rect.height),
398 ),
399 Color {
400 r: 0.3,
401 g: 0.5,
402 b: 0.8,
403 a: 0.2,
404 },
405 );
406 frame.stroke(
407 &Path::rectangle(
408 Point::new(rect.x, rect.y),
409 Size::new(rect.width, rect.height),
410 ),
411 iced::widget::canvas::Stroke::default()
412 .with_color(Color::from_rgb(0.4, 0.6, 0.9))
413 .with_width(1.5),
414 );
415 }
416
417 vec![frame.into_geometry()]
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use crate::midi::PianoNote;
425 use iced::widget::canvas::Program;
426 use iced::{Event, Point, Rectangle, Size, event, mouse};
427 use std::collections::HashSet;
428
429 fn action_message(action: CanvasAction<DrumMessage>) -> (Option<DrumMessage>, event::Status) {
430 let (message, _redraw, status) = action.into_inner();
431 (message, status)
432 }
433
434 fn drum_note(start_sample: usize, pitch: u8) -> PianoNote {
435 PianoNote {
436 start_sample,
437 length_samples: 20,
438 pitch,
439 velocity: 100,
440 channel: 0,
441 mpe: Default::default(),
442 }
443 }
444
445 #[test]
446 fn drum_roll_click_on_note_selects_and_starts_drag() {
447 let interaction = DrumRollInteraction::new(
448 vec![drum_note(10, 38)],
449 1.0,
450 1.0,
451 vec![36, 38],
452 20.0,
453 None,
454 HashSet::new(),
455 );
456 let mut state = DrumRollInteractionState::default();
457 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
458 let cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
459
460 let action = interaction
461 .update(
462 &mut state,
463 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
464 bounds,
465 cursor,
466 )
467 .expect("action");
468
469 let (message, status) = action_message(action);
470 assert_eq!(message, Some(DrumMessage::NoteSelected(0)));
471 assert_eq!(status, event::Status::Captured);
472 assert_eq!(state.dragging_mode, DraggingMode::DraggingNote);
473 assert_eq!(state.drag_note_index, Some(0));
474 }
475
476 #[test]
477 fn drum_roll_drag_release_publishes_move_with_delta() {
478 let interaction = DrumRollInteraction::new(
479 vec![drum_note(10, 38)],
480 1.0,
481 1.0,
482 vec![36, 38],
483 20.0,
484 None,
485 HashSet::new(),
486 );
487 let mut state = DrumRollInteractionState::default();
488 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
489 let press_cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
490 let release_cursor = mouse::Cursor::Available(Point::new(35.0, 22.0));
491
492 let _ = interaction.update(
493 &mut state,
494 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
495 bounds,
496 press_cursor,
497 );
498
499 let action = interaction
500 .update(
501 &mut state,
502 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
503 bounds,
504 release_cursor,
505 )
506 .expect("release action");
507
508 let (message, status) = action_message(action);
509 assert_eq!(
510 message,
511 Some(DrumMessage::NoteMove {
512 note_index: 0,
513 delta_samples: 20,
514 target_pitch: 38,
515 })
516 );
517 assert_eq!(status, event::Status::Captured);
518 }
519
520 #[test]
521 fn drum_roll_vertical_drag_release_publishes_target_pitch() {
522 let interaction = DrumRollInteraction::new(
523 vec![drum_note(10, 38)],
524 1.0,
525 1.0,
526 vec![36, 38],
527 20.0,
528 None,
529 HashSet::new(),
530 );
531 let mut state = DrumRollInteractionState::default();
532 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
533 let press_cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
534 let release_cursor = mouse::Cursor::Available(Point::new(15.0, 2.0));
535
536 let _ = interaction.update(
537 &mut state,
538 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
539 bounds,
540 press_cursor,
541 );
542
543 let action = interaction
544 .update(
545 &mut state,
546 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
547 bounds,
548 release_cursor,
549 )
550 .expect("release action");
551
552 let (message, status) = action_message(action);
553 assert_eq!(
554 message,
555 Some(DrumMessage::NoteMove {
556 note_index: 0,
557 delta_samples: 0,
558 target_pitch: 36,
559 })
560 );
561 assert_eq!(status, event::Status::Captured);
562 }
563
564 #[test]
565 fn drum_roll_cursor_moved_while_dragging_requests_redraw() {
566 let interaction = DrumRollInteraction::new(
567 vec![drum_note(10, 38)],
568 1.0,
569 1.0,
570 vec![36, 38],
571 20.0,
572 None,
573 HashSet::new(),
574 );
575 let mut state = DrumRollInteractionState::default();
576 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
577 let press_cursor = mouse::Cursor::Available(Point::new(15.0, 22.0));
578 let drag_cursor = mouse::Cursor::Available(Point::new(35.0, 22.0));
579
580 let _ = interaction.update(
581 &mut state,
582 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
583 bounds,
584 press_cursor,
585 );
586
587 let action = interaction
588 .update(
589 &mut state,
590 &Event::Mouse(mouse::Event::CursorMoved {
591 position: Point::new(35.0, 22.0),
592 }),
593 bounds,
594 drag_cursor,
595 )
596 .expect("drag action");
597
598 let (message, _status) = action_message(action);
599 assert!(message.is_none());
600 }
601
602 #[test]
603 fn drum_roll_right_drag_publishes_paint_create_messages() {
604 let interaction = DrumRollInteraction::new(
605 Vec::new(),
606 1.0,
607 1.0,
608 vec![36, 38],
609 20.0,
610 None,
611 HashSet::new(),
612 );
613 let mut state = DrumRollInteractionState::default();
614 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
615 let press_cursor = mouse::Cursor::Available(Point::new(12.0, 2.0));
616 let drag_cursor = mouse::Cursor::Unavailable;
617
618 let press = interaction
619 .update(
620 &mut state,
621 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
622 bounds,
623 press_cursor,
624 )
625 .expect("press action");
626 let (message, status) = action_message(press);
627 assert_eq!(message, None);
628 assert_eq!(status, event::Status::Captured);
629 assert_eq!(state.dragging_mode, DraggingMode::CreatingNote);
630
631 let drag = interaction
632 .update(
633 &mut state,
634 &Event::Mouse(mouse::Event::CursorMoved {
635 position: Point::new(32.0, 22.0),
636 }),
637 bounds,
638 drag_cursor,
639 )
640 .expect("drag action");
641 let (message, _status) = action_message(drag);
642 assert_eq!(message, None);
643
644 let release_cursor = mouse::Cursor::Available(Point::new(32.0, 22.0));
645 let release = interaction
646 .update(
647 &mut state,
648 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)),
649 bounds,
650 release_cursor,
651 )
652 .expect("release action");
653 let (message, status) = action_message(release);
654 assert_eq!(
655 message,
656 Some(DrumMessage::NoteCreate {
657 start_sample: 12,
658 end_sample: 32,
659 pitch: 36,
660 repeat: false,
661 })
662 );
663 assert_eq!(status, event::Status::Captured);
664 }
665
666 #[test]
667 fn drum_roll_right_click_creates_note_on_release() {
668 let interaction = DrumRollInteraction::new(
669 Vec::new(),
670 1.0,
671 1.0,
672 vec![36, 38],
673 20.0,
674 None,
675 HashSet::new(),
676 );
677 let mut state = DrumRollInteractionState::default();
678 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
679 let cursor = mouse::Cursor::Available(Point::new(12.0, 2.0));
680
681 let _ = interaction.update(
682 &mut state,
683 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
684 bounds,
685 cursor,
686 );
687
688 let release = interaction
689 .update(
690 &mut state,
691 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)),
692 bounds,
693 cursor,
694 )
695 .expect("release action");
696 let (message, status) = action_message(release);
697 assert_eq!(
698 message,
699 Some(DrumMessage::NoteCreate {
700 start_sample: 12,
701 end_sample: 12,
702 pitch: 36,
703 repeat: false,
704 })
705 );
706 assert_eq!(status, event::Status::Captured);
707 assert_eq!(state.dragging_mode, DraggingMode::None);
708 }
709
710 #[test]
711 fn drum_roll_shift_right_drag_marks_create_as_repeat() {
712 let mut interaction = DrumRollInteraction::new(
713 Vec::new(),
714 1.0,
715 1.0,
716 vec![36, 38],
717 20.0,
718 None,
719 HashSet::new(),
720 );
721 interaction.repeat_create = true;
722 let mut state = DrumRollInteractionState::default();
723 let bounds = Rectangle::new(Point::ORIGIN, Size::new(200.0, 100.0));
724 let press_cursor = mouse::Cursor::Available(Point::new(12.0, 2.0));
725 let release_cursor = mouse::Cursor::Available(Point::new(32.0, 22.0));
726
727 let _ = interaction.update(
728 &mut state,
729 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)),
730 bounds,
731 press_cursor,
732 );
733
734 let release = interaction
735 .update(
736 &mut state,
737 &Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)),
738 bounds,
739 release_cursor,
740 )
741 .expect("release action");
742 let (message, status) = action_message(release);
743 assert_eq!(
744 message,
745 Some(DrumMessage::NoteCreate {
746 start_sample: 12,
747 end_sample: 32,
748 pitch: 36,
749 repeat: true,
750 })
751 );
752 assert_eq!(status, event::Status::Captured);
753 }
754}