Skip to content

Commit

Permalink
Piano roll multi-cell custom actions
Browse files Browse the repository at this point in the history
  • Loading branch information
melanchall committed Sep 21, 2024
1 parent f7ae5db commit d7e05eb
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 70 deletions.
64 changes: 54 additions & 10 deletions Docs/articles/composing/Pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,17 +273,17 @@ So the way to customize the piano roll algorithm is to pass [PianoRollSettings](
```csharp
var pianoRollSettings = new PianoRollSettings
{
CustomActions = new Dictionary<char, Action<Melanchall.DryWetMidi.MusicTheory.Note, PatternBuilder>>
CustomActions = new[]
{
['*'] = (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
PianoRollAction.CreateSingleCell('*', (pianoRollBuilder, context) => pianoRollBuilder
.Note(context.Note, velocity: (SevenBitNumber)(pianoRollBuilder.Velocity / 2))),
PianoRollAction.CreateSingleCell('║', (pianoRollBuilder, context) => pianoRollBuilder
.Note(context.Note, pianoRollBuilder.NoteLength.Divide(2))
.Note(context.Note, pianoRollBuilder.NoteLength.Divide(2), (SevenBitNumber)(pianoRollBuilder.Velocity / 2))),
PianoRollAction.CreateSingleCell('!', (pianoRollBuilder, context) => pianoRollBuilder
.StepBack(MusicalTimeSpan.ThirtySecond)
.Note(note, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)(pianoRollBuilder.Velocity / 3))
.Note(note),
.Note(context.Note, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)(pianoRollBuilder.Velocity / 3))
.Note(context.Note)),
}
};

Expand All @@ -310,4 +310,48 @@ And here the file – [pianoroll-custom.mid](files/pianoroll-custom.mid). But wh
* `'║'` – double note (two notes, each with length of half of the single-cell note);
* `'!'` – flam (ghost thirthy-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.
We've used [PianoRollAction.CreateSingleCell](xref:Melanchall.DryWetMidi.Composing.PianoRollAction.CreateSingleCell*) method here to specify single-cell actions. But you can also define multi-cell ones – [PianoRollAction.CreateMultiCell](xref:Melanchall.DryWetMidi.Composing.PianoRollAction.CreateMultiCell*) method is what we need for this purpose. For example, if we want to be able to put quiet notes:

```csharp
var pianoRollSettings = new PianoRollSettings
{
CustomActions = new[]
{
PianoRollAction.CreateMultiCell('{', '}', (pianoRollBuilder, context) => pianoRollBuilder
.Note(context.Note, context.Length, (SevenBitNumber)40)),
}
};

var pattern = new PatternBuilder()
.SetNoteLength(MusicalTimeSpan.Eighth.SingleDotted())
.PianoRoll(@"
A4 ----{==}
B3 -{=}----
G#3 -----{}-",
pianoRollSettings)
.Build();
```

If you want, you can designate start and end of a multi-cell action with the same symbol:

```csharp
var pianoRollSettings = new PianoRollSettings
{
CustomActions = new[]
{
PianoRollAction.CreateMultiCell('/', '/', (pianoRollBuilder, context) => pianoRollBuilder
.Note(context.Note, context.Length, (SevenBitNumber)40)),
}
};

var pattern = new PatternBuilder()
.SetNoteLength(MusicalTimeSpan.Eighth.SingleDotted())
.PianoRoll(@"
A4 ----/==/
B3 -/=/----
G#3 -----//-",
pianoRollSettings)
.Build();
```

So the first `'/'` starts an action and the second one ends it.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Melanchall.DryWetMidi.MusicTheory;
using NUnit.Framework;
using System;
using System.Collections.Generic;

namespace Melanchall.DryWetMidi.Tests.Composing
{
Expand Down Expand Up @@ -146,7 +145,7 @@ public void PianoRoll_CustomSymbols_MultiCellNoteEndSymbolIsSpace() => Assert.Th
}));

[Test]
public void PianoRoll_CustomActions()
public void PianoRoll_CustomActions_1()
{
var step = MusicalTimeSpan.Sixteenth;
var velocity = (SevenBitNumber)90;
Expand All @@ -160,17 +159,17 @@ public void PianoRoll_CustomActions()
", new PianoRollSettings
{
SingleCellNoteSymbol = '+',
CustomActions = new Dictionary<char, Action<DryWetMidi.MusicTheory.Note, PatternBuilder>>
CustomActions = new[]
{
['/'] = (note, builder) => builder
PianoRollAction.CreateSingleCell('/', (builder, context) => builder
.StepBack(MusicalTimeSpan.ThirtySecond)
.Note(note, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)(builder.Velocity * 0.5))
.Note(note),
['#'] = (note, builder) => builder
.Note(context.Note, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)(builder.Velocity * 0.5))
.Note(context.Note)),
PianoRollAction.CreateSingleCell('#', (builder, context) => builder
.StepBack(MusicalTimeSpan.ThirtySecond)
.Note(note, new MusicalTimeSpan(1, 64), (SevenBitNumber)70)
.Note(note, new MusicalTimeSpan(1, 64), (SevenBitNumber)50)
.Note(note),
.Note(context.Note, new MusicalTimeSpan(1, 64), (SevenBitNumber)70)
.Note(context.Note, new MusicalTimeSpan(1, 64), (SevenBitNumber)50)
.Note(context.Note)),
},
})
.Build();
Expand All @@ -190,33 +189,111 @@ public void PianoRoll_CustomActions()
});
}

[Test]
public void PianoRoll_CustomActions_2()
{
var step = MusicalTimeSpan.Sixteenth;
var velocity = (SevenBitNumber)90;

var pattern = new PatternBuilder()
.SetNoteLength(step)
.SetVelocity(velocity)
.PianoRoll(@"
B2 --/- --#- {==}
G#2 +--- +--- ----
", new PianoRollSettings
{
SingleCellNoteSymbol = '+',
CustomActions = new[]
{
PianoRollAction.CreateSingleCell('/', (builder, context) => builder
.StepBack(MusicalTimeSpan.ThirtySecond)
.Note(context.Note, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)(builder.Velocity * 0.5))
.Note(context.Note)),
PianoRollAction.CreateSingleCell('#', (builder, context) => builder
.StepBack(MusicalTimeSpan.ThirtySecond)
.Note(context.Note, new MusicalTimeSpan(1, 64), (SevenBitNumber)70)
.Note(context.Note, new MusicalTimeSpan(1, 64), (SevenBitNumber)50)
.Note(context.Note)),
PianoRollAction.CreateMultiCell('{', '}', (builder, context) => builder
.Note(context.Note, builder.NoteLength.Multiply(context.CellsNumber), (SevenBitNumber)40)),
},
})
.Build();

PatternTestUtilities.TestNotes(pattern, new[]
{
new NoteInfo(NoteName.GSharp, 2, step * 0, step, velocity),

new NoteInfo(NoteName.B, 2, step * 2 - MusicalTimeSpan.ThirtySecond, MusicalTimeSpan.ThirtySecond, (SevenBitNumber)45),
new NoteInfo(NoteName.B, 2, step * 2, step, velocity),

new NoteInfo(NoteName.GSharp, 2, step * 4, step, velocity),

new NoteInfo(NoteName.B, 2, step * 6 - MusicalTimeSpan.ThirtySecond, new MusicalTimeSpan(1, 64), (SevenBitNumber)70),
new NoteInfo(NoteName.B, 2, step * 6 - new MusicalTimeSpan(1, 64), new MusicalTimeSpan(1, 64), (SevenBitNumber)50),
new NoteInfo(NoteName.B, 2, step * 6, step, velocity),

new NoteInfo(NoteName.B, 2, step * 8, step * 4, (SevenBitNumber)40),
});
}

[TestCase('{', '}')]
[TestCase('/', '/')]
public void PianoRoll_CustomActions_MultiCell(char startSymbol, char endSymbol)
{
var step = MusicalTimeSpan.Sixteenth;
var velocity = (SevenBitNumber)90;

var pattern = new PatternBuilder()
.SetNoteLength(step)
.SetVelocity(velocity)
.PianoRoll($@"
B2 {startSymbol}=={endSymbol}
G#2 ----
", new PianoRollSettings
{
CustomActions = new[]
{
PianoRollAction.CreateMultiCell(startSymbol, endSymbol, (builder, context) => builder
.Note(context.Note, context.Length, (SevenBitNumber)40)),
},
})
.Build();

PatternTestUtilities.TestNotes(pattern, new[]
{
new NoteInfo(NoteName.B, 2, step * 0, step * 4, (SevenBitNumber)40),
});
}

[Test]
public void PianoRoll_CustomActions_ContainsSingleCellNoteSymbol() => Assert.Throws<ArgumentOutOfRangeException>(
() => new PatternBuilder().PianoRoll(@"AH3 ----", new PianoRollSettings
{
CustomActions = new Dictionary<char, Action<DryWetMidi.MusicTheory.Note, PatternBuilder>>
CustomActions = new[]
{
['|'] = (note, builder) => { },
PianoRollAction.CreateSingleCell('|', (_, __) => { }),
}
}));

[Test]
public void PianoRoll_CustomActions_ContainsMultiCellNoteStartSymbol() => Assert.Throws<ArgumentOutOfRangeException>(
() => new PatternBuilder().PianoRoll(@"AH3 ----", new PianoRollSettings
{
CustomActions = new Dictionary<char, Action<DryWetMidi.MusicTheory.Note, PatternBuilder>>
CustomActions = new[]
{
['['] = (note, builder) => { },
PianoRollAction.CreateSingleCell('[', (_, __) => { }),
}
}));

[Test]
public void PianoRoll_CustomActions_ContainsMultiCellNoteEndSymbol() => Assert.Throws<ArgumentOutOfRangeException>(
() => new PatternBuilder().PianoRoll(@"AH3 ----", new PianoRollSettings
{
CustomActions = new Dictionary<char, Action<DryWetMidi.MusicTheory.Note, PatternBuilder>>
CustomActions = new[]
{
[']'] = (note, builder) => { },
PianoRollAction.CreateSingleCell(']', (_, __) => { }),
}
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Melanchall.DryWetMidi.Common;
using Melanchall.DryWetMidi.Interaction;
using System;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -67,6 +66,8 @@ private static Pattern BuildPatternFromPianoRoll(
.SetNoteLength(parentPatternBuilder.NoteLength)
.SetStep(parentPatternBuilder.NoteLength)
.SetVelocity(parentPatternBuilder.Velocity)
.SetOctave(parentPatternBuilder.Octave)
.SetRootNote(parentPatternBuilder.RootNote)
.Anchor(pianoRollStartSnchor);

var lines = GetPianoRollLines(pianoRoll);
Expand Down Expand Up @@ -116,47 +117,103 @@ private static void ProcessLine(
MusicTheory.Note note,
int dataStartIndex)
{
var noteStartIndex = 0;
var isNoteBuilding = false;
var multiCellActionStartIndex = 0;
var multiCellActionInProgress = false;

for (var i = dataStartIndex; i < line.Length; i++)
{
var symbol = line[i];

if (symbol == settings.SingleCellNoteSymbol)
{
if (isNoteBuilding)
throw new InvalidOperationException("Single-cell note can't be placed inside a multi-cell one.");

patternBuilder.Note(note);
}
ExecuteSingleCellAction(
multiCellActionInProgress,
() => patternBuilder.Note(note));
else if (symbol == settings.MultiCellNoteStartSymbol)
{
if (isNoteBuilding)
throw new InvalidOperationException("Note can't be started while a previous one is not ended.");

isNoteBuilding = true;
noteStartIndex = i;
}
StartMultiCellAction(
i,
ref multiCellActionStartIndex,
ref multiCellActionInProgress);
else if (symbol == settings.MultiCellNoteEndSymbol)
{
if (!isNoteBuilding)
throw new InvalidOperationException("Note is not started.");

patternBuilder.Note(note, patternBuilder.NoteLength.Multiply(i - noteStartIndex + 1));
isNoteBuilding = false;
}
EndMultiCellAction(
() => patternBuilder.Note(note, patternBuilder.NoteLength.Multiply(i - multiCellActionStartIndex + 1)),
ref multiCellActionInProgress);
else
{
Action<MusicTheory.Note, PatternBuilder> customAction = null;
if (settings.CustomActions?.TryGetValue(symbol, out customAction) == true)
customAction?.Invoke(note, patternBuilder);
else if (!isNoteBuilding)
patternBuilder.StepForward();
var action = settings
.CustomActions
?.FirstOrDefault(a => a.StartSymbol == symbol || a.EndSymbol == symbol);

if (action == null)
{
if (!multiCellActionInProgress)
patternBuilder.StepForward();
}
else if (action.EndSymbol != null)
{
if (action.StartSymbol == symbol)
{
if (action.EndSymbol == symbol && multiCellActionInProgress)
{
var cellsNumber = i - multiCellActionStartIndex + 1;
EndMultiCellAction(
() => action.Action(patternBuilder, new PianoRollActionContext(note, cellsNumber, patternBuilder.NoteLength.Multiply(cellsNumber))),
ref multiCellActionInProgress);
}
else
StartMultiCellAction(
i,
ref multiCellActionStartIndex,
ref multiCellActionInProgress);
}
else
{
var cellsNumber = i - multiCellActionStartIndex + 1;
EndMultiCellAction(
() => action.Action(patternBuilder, new PianoRollActionContext(note, cellsNumber, patternBuilder.NoteLength.Multiply(cellsNumber))),
ref multiCellActionInProgress);
}
}
else
ExecuteSingleCellAction(
multiCellActionInProgress,
() => action.Action(patternBuilder, new PianoRollActionContext(note, 1, patternBuilder.NoteLength)));
}
}
}

private static void ExecuteSingleCellAction(
bool multiCellActionInProgress,
Action action)
{
if (multiCellActionInProgress)
throw new InvalidOperationException("Single-cell note can't be placed inside a multi-cell one.");

action();
}

private static void StartMultiCellAction(
int i,
ref int multiCellActionStartIndex,
ref bool multiCellActionInProgress)
{
if (multiCellActionInProgress)
throw new InvalidOperationException("Note can't be started while a previous one is not ended.");

multiCellActionInProgress = true;
multiCellActionStartIndex = i;
}

private static void EndMultiCellAction(
Action action,
ref bool multiCellActionInProgress)
{
if (!multiCellActionInProgress)
throw new InvalidOperationException("Note is not started.");

action();
multiCellActionInProgress = false;
}

private static string[] GetPianoRollLines(string pianoRoll)
{
return pianoRoll
Expand Down
Loading

0 comments on commit d7e05eb

Please sign in to comment.