Music via piano roll via C#

Melanchall - Sep 10 - - Dev Community

Are you a composer? Is the music a thing you express your creativity with? The article shows how you, as a programmer, can create musical compositions via C# and piano roll notation provided by the DryWetMIDI library.


Piano roll is a masterpiece of the digital music industry. I’m pretty sure all of you who create music know that great tool provided by DAWs. But wait… Digital music? DAW? In fact, piano roll is a thing with a much longer history then digital audio workstations and computers at all. It’s pretty interesting how the music storage medium for a player piano has been taken as an idea for visual representation of music on computers.

I’m a developer of the DryWetMIDI library. It has a rich set of features related to MIDI and MIDI-based music creation and editing. You can examine the README file or read help articles on the documentation site. Starting from the 7.0.2 version the library provides a way to create simple music parts via a piano roll string. It’s the subject of this article.

Table of contents

Pattern

Before we dive into the piano roll magic provided by the DryWetMIDI, let’s take a look at the Pattern API which allows to create a MIDI file in a more "musical" manner. The key class here is the PatternBuilder which makes it easy to build a musical composition step by step. Using the PatternBuilder, you work with a fluent interface. Small example:

var patternBuilder = new PatternBuilder()
    .Note(Octave.Get(4).A, new MusicalTimeSpan(5, 17), (SevenBitNumber)68)
    .Note(Note.Get(NoteName.GSharp, 3), new MetricTimeSpan(0, 0, 2))
    .Note(F#4);
Enter fullscreen mode Exit fullscreen mode

We add three notes here:

  • A4 with length of 5/17 and velocity of 68;
  • G#3 with a length of 2 seconds;
  • and F#4.

The last Note method call inserts the note with default length and velocity that can be altered via corresponding methods at any moment:

var patternBuilder = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Half)
    .SetVelocity((SevenBitNumber)50)

    // All the following notes will have length of 1/2 and velocity of 50

    .Note(Octave.Get(4).A)
    .Note(Note.Get(NoteName.B, 1))
    .Note(-Interval.Two)

    .SetNoteLength(MusicalTimeSpan.Quarter)

    // All the following notes will have length of 1/4

    .Note(Octave.Get(2).A);
Enter fullscreen mode Exit fullscreen mode

Notes (and other actions) are added after each other. So where a note ends, the following one starts.

You can also add chords, markers, control changes, move the current position back and forth and do many other actions. Please take a look at the entire API provided by the PatternBuilder.

What about something more interesting? Following example shows how to create first four bars of the Beethoven's "Moonlight Sonata":

// Define a chord for the bass part which is just an octave
var bassChord = new[] { Interval.Twelve };

// Build the composition
var pattern = new PatternBuilder()

    // The length of all main theme's notes within four first bars is
    // triplet eighth so set it as a note length which will free us from necessity to specify
    // the length of each note explicitly

    .SetNoteLength(MusicalTimeSpan.Eighth.Triplet())

    // Anchor current time (start of the pattern) to jump to it
    // when we'll start to program bass part

    .Anchor()

    // We will add notes relative to G#3.
    // Instead of Octave.Get(3).GSharp it is possible to use Note.Get(NoteName.GSharp, 3)

    .SetRootNote(Octave.Get(3).GSharp)

    // Add first three notes and repeat them seven times which will
    // give us two bars of the main theme

                           // G#3
    .Note(Interval.Zero)   // +0  (G#3)
    .Note(Interval.Five)   // +5  (C#4)
    .Note(Interval.Eight)  // +8  (E4)

    .Repeat(3, 7)          // repeat three previous notes seven times

    // Add notes of the next two bars

                           // G#3
    .Note(Interval.One)    // +1  (A3)
    .Note(Interval.Five)   // +5  (C#4)
    .Note(Interval.Eight)  // +8  (E4)

    .Repeat(3, 1)          // repeat three previous notes
                           // one time

    .Note(Interval.One)    // +1  (A3)
    .Note(Interval.Six)    // +6  (D4)
    .Note(Interval.Ten)    // +10 (F#4)

    .Repeat(3, 1)          // repeat three previous notes
                           // one time reaching the end of
                           // third bar

    .Note(Interval.Zero)   // +0  (G#3)
    .Note(Interval.Four)   // +4  (C4)
    .Note(Interval.Ten)    // +10 (F#4)
    .Note(Interval.Zero)   // +0  (G#3)
    .Note(Interval.Five)   // +5  (C#4)
    .Note(Interval.Eight)  // +8  (E4)
    .Note(Interval.Zero)   // +0  (G#3)
    .Note(Interval.Five)   // +5  (C#4)
    .Note(Interval.Seven) // +7  (D#4)
    .Note(-Interval.Two)   // -2  (F#3)
    .Note(Interval.Four)   // +4  (C4)
    .Note(Interval.Seven)  // +7  (D#4)

    // Now we will program the bass part. To start adding notes from the
    // beginning of the pattern we need to move to the anchor we set
    // above

    .MoveToFirstAnchor()

    // First two chords have the whole length

    .SetNoteLength(MusicalTimeSpan.Whole)

                                            // insert a chord relative to
    .Chord(bassChord, Octave.Get(2).CSharp) // C#2 (C#2, C#3)
    .Chord(bassChord, Octave.Get(1).B)      // B1  (B1, B2)

    // Remaining four chords has half length

    .SetNoteLength(MusicalTimeSpan.Half)

    .Chord(bassChord, Octave.Get(1).A)      // A1  (A1, A2)
    .Chord(bassChord, Octave.Get(1).FSharp) // F#1 (F#1, F#2)
    .Chord(bassChord, Octave.Get(1).GSharp) // G#1 (G#1, G#2)

    .Repeat()                               // repeat the previous chord

    // Build a pattern that can be then saved to a MIDI file

    .Build();
Enter fullscreen mode Exit fullscreen mode

Build method returns an instance of the Pattern. A pattern can be transformed or altered by methods in PatternUtilities.

Pattern can be then saved to MidiFile (via ToFile method) or TrackChunk (via ToTrackChunk method). You need to provide a tempo map. Also you can optionally specify the channel that should be set to events. The default channel is 0. If you want to output the data to the default drum channel, just set (FourBitNumber)9 (channel is a number from 0 to 15 in the DryWetMIDI, so you need to pass 9 instead of 10).

Also please see the Extension methods section of the Pattern API.

Piano roll

And now we’re getting to the subject of the article. There is a way to create simple patterns easily — via the PianoRoll method. The method was introduced in the 7.2.0 version of the library. To quickly dive into the method, just take a look at this example:

var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Eighth)
    .PianoRoll(@"
        F#2   ||||||||
        D2    --|---|-
        C2    |---|---")
    .Repeat(9)
    .Build()
    .ToFile(TempoMap.Default, (FourBitNumber)9);
midiFile.Write("pianoroll-simple.mid", true);
Enter fullscreen mode Exit fullscreen mode

Each line starts with a note. Think about a line as a piano roll lane in your favorite DAW. So notes on the first line will be F#2, on the second line — D2 and on the third one — C2. Each character then except spaces means one cell. The length of a cell is determined by the SetNoteLength method.

'|' symbol means a single-cell note, i.e. the note's length is equal to a cell's length. So each note in the example will be an 8th one. By the way, you can alter this symbol with the SingleCellNoteSymbol property of the PianoRollSettings passed to the PianoRoll method.

Hyphen ('-') means nothing except a step of a cell's length. We will call it a fill symbol. You should keep in mind that spaces will be cut from the piano roll string before processing. So it's required to use a fill symbol to specify an empty space (rest) to get correct results. For example, this pattern:

F2   ||||
D2     |
C2   |
Enter fullscreen mode Exit fullscreen mode

will be transformed by the piano roll processing engine to these strings:

F2||||
D2|
C2|
Enter fullscreen mode Exit fullscreen mode

which is probably not what you want.

Be aware that a fill symbol must not be the same as those used for notes and must not be a part of a collection of custom actions symbols (see Customization section further).

The example above demonstrates how to create a simple drum rhythm — standard 8th note groove — using General MIDI drum map. You can listen to the file produced — pianoroll-simple.mid. By the way, you can use notes numbers instead of letters and octaves (and don't forget about string interpolation along with meaningful variables names):

var bassDrum = 36;
var snareDrum = 38;
var closedHiHat = 42;

var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Eighth)
    .PianoRoll(@$"
        {closedHiHat}   ||||||||
          {snareDrum}   --|---|-
           {bassDrum}   |---|---")
    .Repeat(9)
    .Build()
    .ToFile(TempoMap.Default, (FourBitNumber)9);
midiFile.Write("pianoroll-simple.mid", true);
Enter fullscreen mode Exit fullscreen mode

Or you can create an extension method like this:

public static PatternBuilder GmDrumPattern(
    this PatternBuilder patternBuilder,
    params (GeneralMidiPercussion Instrument, string PianoRollLine)[] pianoRoll)
{
    return patternBuilder.PianoRoll(string.Join(
        Environment.NewLine,
        pianoRoll.Select(l => $"{l.Instrument.AsSevenBitNumber()} {l.PianoRollLine}")));
}
Enter fullscreen mode Exit fullscreen mode

And call it then:

var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Eighth)
    .GmDrumPattern(
        (GeneralMidiPercussion.ClosedHiHat,      "||||||||"),
        (GeneralMidiPercussion.AcousticSnare,    "--|---|-"),
        (GeneralMidiPercussion.AcousticBassDrum, "|---|---"))
    .Repeat(9)
    .Build()
    .ToFile(TempoMap.Default, (FourBitNumber)9);
Enter fullscreen mode Exit fullscreen mode

But let's take a more interesting example which we looked at above — "Moonlight Sonata". The same first four bars of it can be constructed via piano roll like this:

var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Eighth.Triplet())
    .PianoRoll(@"
        F#4   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙|∙∙|   ∙∙|∙∙∙∙∙∙∙∙∙
        E4    ∙∙|∙∙|∙∙|∙∙|   ∙∙|∙∙|∙∙|∙∙|   ∙∙|∙∙|∙∙∙∙∙∙   ∙∙∙∙∙|∙∙∙∙∙∙
        D#4   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙|∙∙|
        C#4   ∙|∙∙|∙∙|∙∙|∙   ∙|∙∙|∙∙|∙∙|∙   ∙|∙∙|∙∙∙∙∙∙∙   ∙∙∙∙|∙∙|∙∙∙∙
        C4    ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙|∙∙∙∙∙∙∙∙|∙
        D4    ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙|∙∙|∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        A3    ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   |∙∙|∙∙|∙∙|∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        G#3   |∙∙|∙∙|∙∙|∙∙   |∙∙|∙∙|∙∙|∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   |∙∙|∙∙|∙∙∙∙∙
        F#3   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙|∙∙

        C#3   [==========]   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        B2    ∙∙∙∙∙∙∙∙∙∙∙∙   [==========]   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        A2    ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   [====]∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        G#2   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   [====][====]
        F#2   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙[====]   ∙∙∙∙∙∙∙∙∙∙∙∙
        C#2   [==========]   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        B1    ∙∙∙∙∙∙∙∙∙∙∙∙   [==========]   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        A1    ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   [====]∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙
        G#1   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   [====][====]
        F#1   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙∙∙∙∙∙∙   ∙∙∙∙∙∙[====]   ∙∙∙∙∙∙∙∙∙∙∙∙")
    .Build()
    .ToFile(TempoMap.Create(Tempo.FromBeatsPerMinute(69)));
midiFile.Write("pianoroll-moonlight-sonata.mid", true);
Enter fullscreen mode Exit fullscreen mode

And here is the file — pianoroll-moonlight-sonata.mid.

Along with usage of spaces to separate bars visually for more readability you can see several new symbols in this example:

Well, we can define long notes (multi-cell) along with single-cell ones. As for length of such notes, let's see at this note:

[====]
Enter fullscreen mode Exit fullscreen mode

The length will be of six cells:

[====]
123456
Enter fullscreen mode Exit fullscreen mode

So if one cell means 8th triplet time span in our example, the length of the note will be 1/2.

Customization

It's time to discuss how you can adjust piano roll processing. First of all, as we said before, you can set custom symbols for a single-cell note, start and end of a multi-cell note:

var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Eighth.Triplet())
    .PianoRoll(@"
        F#4 ∙∙@∙∙@∙∙∙∙∙∙
        E4  ∙∙∙(~~~~~~~)", new PianoRollSettings
        {
            SingleCellNoteSymbol = '@',
            MultiCellNoteStartSymbol = '(',
            MultiCellNoteEndSymbol = ')',
        })
    .Build()
    .ToFile(TempoMap.Default);
Enter fullscreen mode Exit fullscreen mode

So the way to customize the piano roll algorithm is to pass PianoRollSettings. But you can also define your own actions triggered by specified symbols. Let's take a look at the following example (yes, drums again):

var pianoRollSettings = new PianoRollSettings
{
    CustomActions = new Dictionary<char, Action<Melanchall.DryWetMidi.MusicTheory.Note, PatternBuilder>>
    {
        ['*'] = (note, pianoRollBuilder) => pianoRollBuilder
            .Note(note, velocity: (SevenBitNumber)(pianoRollBuilder.Velocity / 2)),
        ['║'] = (note, pianoRollBuilder) => pianoRollBuilder
            .Note(note, pianoRollBuilder.NoteLength.Divide(2))
            .Note(note, pianoRollBuilder.NoteLength.Divide(2), (SevenBitNumber)(pianoRollBuilder.Velocity / 2)),
        ['!'] = (note, pianoRollBuilder) => pianoRollBuilder
            .StepBack(MusicalTimeSpan.ThirtySecond)
            .Note(note, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)(pianoRollBuilder.Velocity / 3))
            .Note(note),
    }
};

var bassDrum = 36;
var snareDrum = 38;
var closedHiHat = 42;

var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Eighth)
    .PianoRoll(@$"
        {closedHiHat}   -------|   ║║|----|
          {snareDrum}   -*|---!-   --|--*!|
           {bassDrum}   |--|---   |-||---",
        pianoRollSettings)
    .Repeat(9)
    .Build()
    .ToFile(TempoMap.Default, (FourBitNumber)9);
midiFile.Write("pianoroll-custom.mid", true);
Enter fullscreen mode Exit fullscreen mode

And here the file — pianoroll-custom.mid. But what we have in the piano roll string:

  • '*' — ghost note (played with half of the current velocity);
  • '║' — double note (two notes, each with length of half of the single-cell note);
  • '!' — flam (ghost thirty-second note right before main beat).

Right now it's possible to specify single-cell actions only. A way to put custom multi-cell actions will be implemented in the next release.

Conclusion

Now you know how you can create simple music patterns. Just install DryWetMIDI into your project, type new PatternBuilder() and dot then and see what you can do. Of course you can ask any questions here in the comments or create an issue or start a discussion in the GitHub.

By the way, modern AI handles text pretty well, so maybe it’s a good idea to try to train a model to generate piano roll strings?... It’s just a thought.

. .
Terabox Video Player