MAIN GOAL: Zone-in and planning for [midi-editor] copy/paste
OK, so MIDI recording is built and it seems to be working well. On to the next feature! And that happens to be copy/paste.
I think this one is going to be a cinch and I'll probably come back here to plan the next feature. What we want to do is take all selected notes and record their positions and line numbers relative to the start of the earliest note when the copy button is pressed. Then, when we paste, we take the cursor position and treat it as the start point and paste all of the notes in. After we paste, we change the selection to the set of all notes that were just pasted. The only thing we really have to do is figure out how to handle note conflicts. I like the idea of just not pasting notes that have conflicts but continuing with the paste for ones that do. I don't see much downside; if the user wants to undo, they can just hit delete since the selection will be changed to the set of all just-pasted notes.
When we paste, we need to move the cursor to largest end point of all notes that were just pasted to facilitate pasting the same selection multiple times in a row.
And... I think that's it! I don't think that there's anything else that needs to be done for this. Let's knock it out.
Oh one thing I just thought of is that we'll probably want to add a "cut" as well as "copy" but that's literally just "copy + delete" so no problem there.
Yep we're here again; what's next? We can do "quantize notes" button. That might consist of some stuff.
So the premise is simple - you just need to snap the starts and ends of all notes to the nearest beat snap interval. The only thing that needs to be handled, as always, is conflict handling. We snap notes starting from earliest to latest. The snap is going to involve either lengthening or shortening notes. If we shorten, conflicts are not possible unless we a note is so small it basically snaps out of existence. In that case we'll just not touch them, I think.
OK so how do we handle this? What if we perform the "optimal snap" for a given note, basically figure out what it would look like if there were no conflicts. What if we pre-compute that for all notes? Then, we manage conflicts on the desired shapes as a second step. Are there situations where we'd get two notes that want to snap to the exact same place? I don't think so; there is one exact middle point between snap points, meaning that unless a note has zero width (which isn't possible in our implementation) it's not possible for two notes want to snap to the same place unless they're smaller than half the snap interval or whatever in which case we won't bother snapping.
Once we have our optimal snapped forms, we try to insert them from left to right. If there is a conflict, we re-insert the original note. I believe that it's not possible for a re-insert of an original note to fail -- nevermind, that is possible. For the case of small notes < 1/2 snap interval, we should re-insert them immediately and they will serve as --
I'm re-considering. We should probably try to snap those small notes, but only on one end but only if no other notes want to move to the same place. I really think this note quantization should be best-effort; it really doesn't need to be perfect for everything. Users can easily adjust the notes manually to handle weird edge-cases however they want.
Also I don't care if this algorithm isn't perfect efficiency-wise. This is a rarely used operation that is usually going to be running on seletions of dozens/hundreds of notes. Whatever makes the code easiest to write is best.
OK how about this - we go through notes and actually apply all snap transformations that will make them smaller first. This will make as much space as possible and facilitate other snap transformations.
After we do that, we can apply all small-note movements where possible. For each small note move, we check to see if there are other small notes that want to move to the same place and do nothing if they do, leaving them where they are. If any conflict happens (which might not be possible but I'm not sure), we do nothing and leave it as-is.
Finally, we perform extensions of all other notes where possible, in order from first to last. If any conflicts or issues happen, we fall back to leaving notes as-is. I ike this in-place editing a lot better than removing all notes and re-inserting. It simplifies things a lot.
I've just realized that we can perform this snapping on a line-by-line basis since all snap operations are only happening to notes on the same line. That's nice. Let's expose a Wasm method that --- HAHA jk we're not going to do that. We'd need to update the UI; we have no way of updating the frontend from the backend currently, and that's OK. We implement the snap code in JS and use the existing note operations to perform the necessary modifications.
OK, so to summarize: 1) We perform all note shortening operations 2) We perform all small note move operations possible, bailing out on any kind of conflict or potential conflict. 3) We perform note extension operations as we can from left to right, leaving notes as-is if there are any conflicts.
I like this a lot, and I honestly think it's going to work. We handle the case where one note needs to be shortened and another lengthened to make them touch exactly by splitting the shortening/lengthening into separate steps, we handle small notes smaller than 1/2 the snap interval, and we ensure that it's not possible to get into a bad state or break things by doing everything in-place and avoiding any conflicts statically.
Let's build it.
OK that's built, on to the next thing.
I want to build a metronome that can be enabled during playback/recording. For looping playback, it's pretty simple - we can schedule extra metronome events alongside the normally normal note events. For oneshot playback, we could do the lame thing and schedule a few thousand events. That really doesn't fly for super high-BPM use cases. Honestly, I think we should schedule metronome separately rather than trying to piggy-back it on the timing loop for notes.
Yeah OK. So we just schedule the metronome separately using its own loop and handle re-scheduling automatically. We cancel it when playback stops. OK let's build it.