Make a Piano Roll Editor

Let’s break down how to build a piano roll editor with Tracktion Engine.

Piano roll editors are essential tools for MIDI sequencing, allowing precise control over note placement, timing, and other musical parameters. In this blog post, we’ll delve into the process of creating a custom piano roll editor using the powerful Tracktion Engine audio framework.

The Essential Guide: EditViewState

Before we dive into the piano roll itself, let’s understand a crucial component: EditViewState. This class acts as a central hub for storing and managing visual settings and state information for your Tracktion Engine edit. It’s your bridge between the underlying MIDI data and its visual representation on the screen.

  • View Position & Zoom: EditViewState keeps track of the visible area of your edit, both horizontally (time or beats) and vertically (pitch in the piano roll). It also stores zoom levels.
  • Snap Settings: It manages the current snap settings (e.g., snap to grid, beat, bar) used for note quantization.
  • Appearance: EditViewState can store preferences related to the visual appearance of your edit, like track heights, colors, waveform drawing, and more.

The beauty of EditViewState is that it provides methods to seamlessly convert between time/beat positions and pixel coordinates:

  • beatsToX(): Convert a beat position to a pixel coordinate on the horizontal axis.
  • xToBeats(): Convert a pixel coordinate to a beat position.
  • timeToX(): Convert a time position (seconds) to a pixel coordinate.
  • xToTime(): Convert a pixel coordinate to a time position.

These conversion methods are invaluable when drawing notes, interpreting mouse input, and ensuring your piano roll editor stays in sync with Tracktion Engine’s internal data.

I. Understanding the Core Concepts

  • Tracktion Engine’s Midi Data: At its heart, Tracktion Engine stores MIDI data within MidiClip objects. You’ll primarily work with:
    • MidiSequence: Represents the collection of MIDI events within a clip.
    • MidiNote: Individual note events containing pitch, velocity, start time, duration, etc.
  • Visual Representation: Your piano roll needs to visually map note information:
    • Vertical Axis: Represents pitch (typically keyboard keys).
    • Horizontal Axis: Represents time (beats, bars, or timecode).
    • Note Rectangles: The dimensions of these rectangles reflect a note’s pitch, start time, and duration.

II. Building the Piano Roll Editor (Step-by-Step)

  1. Create a Custom Component:
  • Derive a new class (e.g., MyPianoRollEditor) from juce::Component.
  • Add essential data members:
    • EditViewState& evs: To access project settings, zoom, and view positions.
    • te::Track::Ptr track: To reference the track containing the MIDI clips.
    • (Optional) te::SelectedMidiEvents* selectedEvents: To manage note selection.
  1. Drawing the Piano Roll Grid (paint method):
  • Key Lines: Draw horizontal lines for each piano key (white and black). Use juce::MidiMessage::isMidiNoteBlack() to alternate colors.
  • Time Grid: Draw vertical lines for bars, beats, and subdivisions. Use the EditViewState to convert time positions to pixel coordinates (more on this below).
  • Notes:
    • Iterate through the MidiClip objects on the track.
    • For each MidiNote in the clip’s MidiSequence:
    • Calculate the note’s rectangle using its pitch, start time, and duration (again, EditViewState will be your friend).
    • Draw the rectangle using g.fillRect().
    • Consider adding visual cues for velocity, selected notes, and hovered notes.
  1. User Interaction:
  • Mouse Handling: Implement mouse listener methods (mouseDown, mouseDrag, mouseUp) within MyPianoRollEditor.
    • Note Creation (Draw Mode):
    • mouseDown: Determine the pitch and quantized start time from the mouse position.
    • Create a new MidiNote and add it to the appropriate MidiSequence.
    • mouseDrag: Adjust the note’s end time.
    • mouseUp: Finalize the note’s duration.
    • Note Selection (Pointer Mode):
    • mouseDown: Check if a note is under the mouse. Add/remove from selection accordingly.
    • mouseDrag: If a selected note is being dragged, update its position.
    • Lasso Selection: You’ll likely want a tool to select multiple notes within a rectangular region. This might involve a separate component that overlays the piano roll.
    • Vertical/Horizontal Scrolling: Use the mouse wheel to scroll. Update the EditViewState to reflect the new view position.
  1. Time/Beat/Pixel Conversion:
  • Tracktion Engine primarily uses time positions (seconds) and beat positions for internal MIDI data.
  • You’ll need to convert these to pixel coordinates to draw on the screen and vice-versa to interpret mouse events.
  • EditViewState comes to the rescue! It contains methods like:
    • beatsToX(): Converts a beat position to a pixel coordinate.
    • xToBeats(): Converts a pixel coordinate to a beat position.
    • timeToX(): Converts a time position to a pixel coordinate.
    • xToTime(): Converts a pixel coordinate to a time position.
  1. Working with the Selection Manager (te::SelectionManager):
  • It’s crucial to use the SelectionManager to track selected notes. This allows other components (like a velocity editor) to access the selected data.
  • Example: // Add a selected note evs.m_selectionManager.select(note, false); // false = don't add to existing selection // Get selected notes auto selectedNotes = evs.m_selectionManager.getItemsOfType<te::MidiNote>();
  1. Additional Features:
  • Velocity Editor: A common addition is a visual editor that lets you adjust note velocities.
  • Tool Modes: Implement different tools for drawing, selecting, erasing, splitting notes, etc.
  • Quantization: Snap note positions to a grid based on the current project’s time signature.
  • Keyboard Component: A visual keyboard alongside the piano roll enhances user experience.

III. Example Snippets

// Inside MyPianoRollEditor::paint(juce::Graphics& g)
auto area = getLocalBounds();

// Draw key lines
for (int i = lowestVisibleNote; i <= highestVisibleNote; ++i) {
    g.setColour(juce::MidiMessage::isMidiNoteBlack(i) ? colours::black : colours::white);
    auto y = noteToY(i);
    g.drawLine(area.getX(), y, area.getRight(), y);
}

// Draw time grid (bars/beats)
for (double beat = startVisibleBeat; beat <= endVisibleBeat; beat += beatGridSpacing) {
    auto x = evs.beatsToX(beat);
    g.setColour(colours::lightgrey);
    g.drawLine(x, area.getY(), x, area.getBottom());
}

// Draw notes
for (auto clip : getMidiClipsOfTrack()) {
    for (auto note : clip->getSequence().getNotes()) {
        auto noteRect = getNoteRect(note);
        g.setColour(track->getColour()); // Or adjust based on velocity
        g.fillRect(noteRect);
    }
}

IV. Crucial Points

  • EditViewState is your mapping guide! Get comfortable using its time/beat to pixel conversion methods.
  • Leverage the SelectionManager. Properly managing selection is vital for a smooth editing experience.

Leave a Reply

Your email address will not be published. Required fields are marked *