├── .gitignore
├── LICENSE
├── README.md
├── assets
├── Generated637012367962797269-clip.svg
├── Generated637068518186284593-clip.svg
├── Generated637069227069985789-clip.svg
├── Generated637069235238519840-clip.svg
├── mu4l-walkthrough.gif
└── mutateful-logo.png
└── src
├── DocGeneration
├── DocGeneration.csproj
└── Program.cs
├── Mutateful
├── .editorconfig
├── Cli
│ └── CliHandler.cs
├── ClipProcessor.cs
├── Commands
│ ├── Arpeggiate.cs
│ ├── Concat.cs
│ ├── Crop.cs
│ ├── Echo.cs
│ ├── Experimental
│ │ └── Scan.cs
│ ├── Filter.cs
│ ├── Interleave.cs
│ ├── Invert.cs
│ ├── Legato.cs
│ ├── Loop.cs
│ ├── Mask.cs
│ ├── Monophonize.cs
│ ├── Padding.cs
│ ├── Quantize.cs
│ ├── Ratchet.cs
│ ├── Relength.cs
│ ├── Remap.cs
│ ├── Resize.cs
│ ├── Scale.cs
│ ├── SetLength.cs
│ ├── SetPitch.cs
│ ├── SetRhythm.cs
│ ├── Shuffle.cs
│ ├── Skip.cs
│ ├── Slice.cs
│ ├── Take.cs
│ ├── ToBeImplemented
│ │ ├── Magnetify.cs
│ │ ├── Morph.cs
│ │ └── Shift.cs
│ ├── Transpose.cs
│ └── VelocityScale.cs
├── Compiler
│ ├── Lexer.cs
│ ├── OptionParser.cs
│ ├── Parser.cs
│ ├── Token.cs
│ ├── TokenType.cs
│ └── TreeToken.cs
├── Core
│ ├── ChainedCommand.cs
│ ├── Clip.cs
│ ├── ClipMetaData.cs
│ ├── ClipReference.cs
│ ├── Command.cs
│ ├── DecimalCounter.cs
│ ├── Formula.cs
│ ├── IntCounter.cs
│ ├── NoteEvent.cs
│ ├── OptionInfo.cs
│ ├── OptionsDefinition.cs
│ ├── ProcessResult.cs
│ ├── Result.cs
│ └── SortedList.cs
├── GlobalImports.cs
├── Hubs
│ ├── IMutatefulHub.cs
│ └── MutatefulHub.cs
├── IO
│ ├── CommandHandler.cs
│ ├── CommandHandlerResult.cs
│ └── Decoder.cs
├── LiveConnector
│ ├── mutate4l-l11.js
│ ├── mutateful-connector-l11.amxd
│ ├── package-lock.json
│ ├── package.json
│ └── sockets.js
├── Mutateful.csproj
├── Program.cs
├── State
│ ├── ClipSet.cs
│ ├── ClipSlot.cs
│ ├── GuiCommand.cs
│ ├── InternalCommand.cs
│ └── LegacyClipSlot.cs
├── TestService.cs
├── Utility
│ ├── ClipExtensions.cs
│ ├── ClipUtilities.cs
│ ├── IOUtilities.cs
│ ├── NoteExtensions.cs
│ ├── ScaleUtilities.cs
│ ├── SvgUtilities.cs
│ ├── TestUtilities.cs
│ └── Utilities.cs
├── WebUI
│ ├── .gitignore
│ ├── package-lock.json
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── App.svelte
│ │ ├── ClipSet.svelte
│ │ ├── ClipSlot.svelte
│ │ ├── FormulaInput.svelte
│ │ ├── clip.ts
│ │ ├── dataHelpers.ts
│ │ ├── global.d.ts
│ │ ├── main.ts
│ │ ├── stores.ts
│ │ └── variables.ts
│ ├── svelte.config.js
│ └── tsconfig.json
├── appsettings.Development.json
├── appsettings.json
└── wwwroot
│ ├── index.css
│ ├── index.html
│ ├── index.js
│ ├── index.js.map
│ └── lib
│ └── signalr
│ ├── msgpack5.js
│ ├── signalr-protocol-msgpack.min.js
│ └── signalr.min.js
├── MutatefulTests
├── CommandHandlerTests.cs
├── CommandTests.cs
├── CoreTests.cs
├── FormulaTests.cs
├── MutatefulTests.csproj
├── OptionsTest.cs
├── ParserTest.cs
└── TestUtilities.cs
└── mutate4l.sln
/.gitignore:
--------------------------------------------------------------------------------
1 | # User-specific files
2 | *.suo
3 | *.user
4 | *.userosscache
5 | *.sln.docstates
6 |
7 | # User-specific files (MonoDevelop/Xamarin Studio)
8 | *.userprefs
9 |
10 | # Build results
11 | [Dd]ebug/
12 | [Dd]ebugPublic/
13 | [Rr]elease/
14 | [Rr]eleases/
15 | [Xx]64/
16 | [Xx]86/
17 | */[Bb]uild/
18 | bld/
19 | [Bb]in/
20 | [Oo]bj/
21 | Properties/
22 |
23 | .idea/
24 | compiled/
25 | mu4l-test Project/
26 | node_modules/
27 | .vs/
28 |
--------------------------------------------------------------------------------
/assets/mu4l-walkthrough.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carrierdown/mutateful/15f52eb0598c39d00d91344eb758599eeafbbe0b/assets/mu4l-walkthrough.gif
--------------------------------------------------------------------------------
/assets/mutateful-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carrierdown/mutateful/15f52eb0598c39d00d91344eb758599eeafbbe0b/assets/mutateful-logo.png
--------------------------------------------------------------------------------
/src/DocGeneration/DocGeneration.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp5
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Mutateful/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 4
9 | end_of_line = crlf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/src/Mutateful/Cli/CliHandler.cs:
--------------------------------------------------------------------------------
1 | using Decoder = Mutateful.IO.Decoder;
2 |
3 | namespace Mutateful.Cli;
4 |
5 | public static class CliHandler
6 | {
7 | public const string UnitTestDirective = " test";
8 | public const string SvgDocDirective = " doc";
9 |
10 | public static LegacyClipSlot HandleInput(byte[] inputData)
11 | {
12 | var generateUnitTest = false;
13 | var generateSvgDoc = false;
14 |
15 | if (Decoder.IsStringData(inputData))
16 | {
17 | string text = Decoder.GetText(inputData);
18 | Console.WriteLine(text);
19 | return LegacyClipSlot.Empty;
20 | }
21 |
22 | (List clips, string formula, ushort id, byte trackNo) = Decoder.DecodeData(inputData);
23 | formula = formula.Trim(' ');
24 | Console.WriteLine($"Received {clips.Count} clips and formula: {formula}");
25 | if (formula.EndsWith(UnitTestDirective))
26 | {
27 | Console.WriteLine(
28 | $"Saving autogenerated unit test to {Path.Join(Environment.CurrentDirectory, "GeneratedUnitTests.txt")}");
29 | formula = formula.Substring(0, formula.Length - UnitTestDirective.Length);
30 | generateUnitTest = true;
31 | }
32 |
33 | if (formula.EndsWith(SvgDocDirective))
34 | {
35 | Console.WriteLine(
36 | $"Saving autogenerated SVG documentation for this formula to {Path.Join(Environment.CurrentDirectory, "GeneratedDocs.svg")}");
37 | formula = formula.Substring(0, formula.Length - SvgDocDirective.Length);
38 | generateSvgDoc = true;
39 | }
40 |
41 | var chainedCommandWrapper = Parser.ParseFormulaToChainedCommand(formula, clips, new ClipMetaData(id, trackNo));
42 | if (!chainedCommandWrapper.Success)
43 | {
44 | Console.WriteLine(chainedCommandWrapper.ErrorMessage);
45 | return LegacyClipSlot.Empty;
46 | }
47 |
48 | ProcessResult processedClipWrapper;
49 | try
50 | {
51 | processedClipWrapper = ClipProcessor.ProcessChainedCommand(chainedCommandWrapper.Result);
52 | }
53 | catch (Exception e)
54 | {
55 | processedClipWrapper =
56 | new ProcessResult($"{formula}. Please check your syntax. Exception: {e.Message}");
57 | }
58 |
59 | if (processedClipWrapper.WarningMessage.Length > 0)
60 | {
61 | Console.WriteLine($"Warnings were generated:{Environment.NewLine}" +
62 | $"{processedClipWrapper.WarningMessage}");
63 | }
64 |
65 | if (processedClipWrapper.Success && processedClipWrapper.Result.Length > 0)
66 | {
67 | var processedClip = processedClipWrapper.Result[0];
68 | byte[] processedClipData = IOUtilities
69 | .GetClipAsBytes(chainedCommandWrapper.Result.TargetMetaData.Id, processedClip).ToArray();
70 |
71 | if (generateUnitTest)
72 | {
73 | TestUtilities.AppendUnitTest(formula, inputData, processedClipData);
74 | }
75 |
76 | if (generateSvgDoc)
77 | {
78 | SvgUtilities.GenerateSvgDoc(formula, clips, processedClip, 882, 300);
79 | }
80 |
81 | LegacyClipSlot processedLegacyClipSlot =
82 | new LegacyClipSlot(formula, processedClip, chainedCommandWrapper.Result, id);
83 | return processedLegacyClipSlot;
84 | }
85 |
86 | Console.WriteLine($"Error applying formula: {processedClipWrapper.ErrorMessage}");
87 | return LegacyClipSlot.Empty;
88 | }
89 | }
--------------------------------------------------------------------------------
/src/Mutateful/ClipProcessor.cs:
--------------------------------------------------------------------------------
1 | using Mutateful.Commands;
2 | using Mutateful.Commands.Experimental;
3 |
4 | namespace Mutateful;
5 |
6 | public static class ClipProcessor
7 | {
8 | public static ProcessResult ProcessCommand(Command command, Clip[] incomingClips, ClipMetaData targetMetadata)
9 | {
10 | var clips = new Clip[incomingClips.Length];
11 | for (var i = 0; i < incomingClips.Length; i++)
12 | {
13 | clips[i] = new Clip(incomingClips[i]);
14 | }
15 |
16 | return command.Id switch
17 | {
18 | TokenType.Arpeggiate => Arpeggiate.Apply(command, clips),
19 | TokenType.Concat => Concat.Apply(clips),
20 | TokenType.Crop => Crop.Apply(command, clips),
21 | TokenType.Echo => Echo.Apply(command, clips),
22 | TokenType.Extract => Take.Apply(command, clips, true),
23 | TokenType.Filter => Filter.Apply(command, clips),
24 | TokenType.Invert => Invert.Apply(command, clips),
25 | TokenType.Interleave => Interleave.Apply(command, targetMetadata, clips, InterleaveMode.NotSpecified),
26 | TokenType.InterleaveEvent => Interleave.Apply(command, targetMetadata, clips, InterleaveMode.Event),
27 | TokenType.Legato => Legato.Apply(clips),
28 | TokenType.Loop => Loop.Apply(command, clips),
29 | TokenType.Mask => Mask.Apply(command, clips),
30 | TokenType.Monophonize => Monophonize.Apply(clips),
31 | TokenType.Padding => Padding.Apply(command, clips),
32 | TokenType.Quantize => Quantize.Apply(command, clips),
33 | TokenType.Ratchet => Ratchet.Apply(command, clips),
34 | TokenType.Relength => Relength.Apply(command, clips),
35 | TokenType.Remap => Remap.Apply(command, clips),
36 | TokenType.Resize => Resize.Apply(command, clips),
37 | TokenType.Scale => Scale.Apply(command, clips),
38 | TokenType.Scan => Scan.Apply(command, clips),
39 | TokenType.SetLength => SetLength.Apply(command, clips),
40 | TokenType.SetPitch => SetPitch.Apply(command, clips),
41 | TokenType.SetRhythm => SetRhythm.Apply(command, clips),
42 | TokenType.Shuffle => Shuffle.Apply(command, clips),
43 | TokenType.Skip => Skip.Apply(command, clips),
44 | TokenType.Slice => Slice.Apply(command, clips),
45 | TokenType.Take => Take.Apply(command, clips),
46 | TokenType.Transpose => Transpose.Apply(command, clips),
47 | TokenType.VelocityScale => VelocityScale.Apply(command, clips),
48 | _ => new ProcessResult($"Unsupported command {command.Id}")
49 | };
50 | }
51 |
52 | public static ProcessResult ProcessChainedCommand(ChainedCommand chainedCommand)
53 | {
54 | Clip[] sourceClips = chainedCommand.SourceClips.Where(c => c.Notes.Count > 0).ToArray();
55 | if (sourceClips.Length < 1)
56 | {
57 | return new ProcessResult("No clips or empty clips specified. Aborting.");
58 | }
59 |
60 | var currentSourceClips = sourceClips;
61 | var resultContainer = new ProcessResult("No commands specified");
62 | var warnings = new List();
63 | foreach (var command in chainedCommand.Commands)
64 | {
65 | resultContainer = ProcessCommand(command, currentSourceClips, chainedCommand.TargetMetaData);
66 | if (resultContainer.Success)
67 | {
68 | currentSourceClips = resultContainer.Result;
69 | if (resultContainer.WarningMessage.Length > 0)
70 | {
71 | warnings.Add(resultContainer.WarningMessage);
72 | }
73 | }
74 | else
75 | {
76 | break;
77 | }
78 | }
79 |
80 | if (warnings.Count > 0)
81 | {
82 | resultContainer = new ProcessResult(resultContainer.Success, resultContainer.Result, resultContainer.ErrorMessage, string.Join(System.Environment.NewLine, warnings));
83 | }
84 | return resultContainer;
85 | }
86 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Arpeggiate.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class ArpeggiateOptions
4 | {
5 | [OptionInfo(min: 1, max: 32)]
6 | public int Rescale { get; set; } = 2; // todo: make it a percentage of the arpclip instead? With possibility of scaling the number of events further based on the duration of the current event compared to the longest event in the clip.
7 |
8 | public bool RemoveOffset { get; set; } = true;
9 |
10 | public Clip By { get; set; }
11 |
12 | // Could add shaping here as well - easein/out etc..
13 | }
14 |
15 | // # desc: Arpeggiates the given clip using another clip, or itself.
16 | public static class Arpeggiate
17 | {
18 | public static ProcessResult Apply(Command command, params Clip[] clips)
19 | {
20 | var (success, msg) = OptionParser.TryParseOptions(command, out ArpeggiateOptions options);
21 | if (!success)
22 | {
23 | return new ProcessResult(msg);
24 | }
25 | return Apply(options, clips);
26 | }
27 |
28 | // Add option to dynamically set # of events that should be rescaled to another note, probably via velocity.
29 | public static ProcessResult Apply(ArpeggiateOptions options, params Clip[] clips)
30 | {
31 | Clip arpSequence = options.By ?? clips[0];
32 |
33 | foreach (var clip in clips)
34 | {
35 | // ClipUtilities.Monophonize(clip);
36 | }
37 | var processedClips = new List(clips.Length);
38 |
39 | // If arp sequence doesn't start at zero and remove offset is specified, make it start at zero
40 | if (arpSequence.Notes[0].Start != 0 && options.RemoveOffset)
41 | {
42 | foreach (var arpNote in arpSequence.Notes)
43 | {
44 | arpNote.Start -= arpSequence.Notes[0].Start;
45 | }
46 | }
47 |
48 | var count = Math.Min(arpSequence.Notes.Count, options.Rescale);
49 | var arpNotes = arpSequence.Notes.Take(count);
50 | var actualLength = arpNotes.Last().Start + arpNotes.Last().Duration;
51 | // Rescale arp events to the range 0-1
52 | foreach (var arpNote in arpNotes)
53 | {
54 | arpNote.Start = arpNote.Start / actualLength;
55 | arpNote.Duration = arpNote.Duration / actualLength;
56 | }
57 |
58 | foreach (var clip in clips)
59 | {
60 | var resultClip = new Clip(clip.Length, clip.IsLooping);
61 | for (var i = 0; i < clip.Notes.Count; i++)
62 | {
63 | var note = clip.Notes[i];
64 | var processedNotes = new List(count);
65 |
66 | var ix = 0;
67 | foreach (var currentArpNote in arpNotes)
68 | {
69 | var processedNote = currentArpNote with {};
70 | processedNote.Start = note.Start + (processedNote.Start * note.Duration);
71 | processedNote.Duration *= note.Duration;
72 | processedNote.Pitch = note.Pitch + arpSequence.RelativePitch(ix);
73 | processedNotes.Add(processedNote);
74 | ix++;
75 | }
76 | resultClip.Notes.AddRange(processedNotes);
77 | }
78 | processedClips.Add(resultClip);
79 | }
80 | return new ProcessResult(processedClips.ToArray());
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Concat.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public static class Concat
4 | {
5 | // # desc: Concatenates two or more clips together.
6 | public static ProcessResult Apply(params Clip[] clips)
7 | {
8 | Clip resultClip = new Clip(clips.Select(c => c.Length).Sum(), true);
9 | decimal pos = 0;
10 | foreach (var clip in clips)
11 | {
12 | resultClip.Notes.AddRange(ClipUtilities.GetNotesInRangeAtPosition(0, clip.Length, clip.Notes, pos));
13 | pos += clip.Length;
14 | }
15 | return new ProcessResult(new[] { resultClip });
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Crop.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | // Extracts a region of the current clips, effectively cropping them. If two params, start - duration is used, otherwise 0 - duration.
4 | public class CropOptions
5 | {
6 | [OptionInfo(type: OptionType.Default, 1/32f)]
7 | public decimal[] Lengths { get; set; } = { 2 };
8 | }
9 |
10 | // # desc: Crops a clip to the desired length, or within the desired region.
11 | public static class Crop
12 | {
13 | public static ProcessResult Apply(Command command, params Clip[] clips)
14 | {
15 | (var success, var msg) = OptionParser.TryParseOptions(command, out CropOptions options);
16 | if (!success)
17 | {
18 | return new ProcessResult(msg);
19 | }
20 | return Apply(options, clips);
21 | }
22 |
23 | public static ProcessResult Apply(CropOptions options, params Clip[] clips)
24 | {
25 | var processedClips = new Clip[clips.Length];
26 | var start = options.Lengths.Length > 1 ? options.Lengths[0] : 0;
27 | var duration = options.Lengths.Length > 1 ? options.Lengths[1] : options.Lengths[0];
28 | var i = 0;
29 |
30 | foreach (var clip in clips)
31 | {
32 | processedClips[i++] = CropClip(clip, start, duration);
33 | }
34 | return new ProcessResult(processedClips);
35 | }
36 |
37 | public static Clip CropClip(Clip clip, decimal start, decimal duration)
38 | {
39 | var processedClip = new Clip(duration, clip.IsLooping);
40 | processedClip.Notes.AddRange(ClipUtilities.GetSplitNotesInRangeAtPosition(start, start + duration, clip.Notes, 0));
41 | return processedClip;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Echo.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 | public class EchoOptions
3 | {
4 | [OptionInfo(type: OptionType.Default, 1/32f)]
5 | public decimal[] Lengths { get; set; } = { 4m/16 };
6 |
7 | public int[] Echoes { get; set; } = {3};
8 | }
9 |
10 | // # desc: Adds echoes to all notes in a clip, cycling through multiple delay times if more than one delay time is specified.
11 | public static class Echo
12 | {
13 | public static ProcessResult Apply(Command command, params Clip[] clips)
14 | {
15 | var (success, msg) = OptionParser.TryParseOptions(command, out EchoOptions options);
16 | if (!success)
17 | {
18 | return new ProcessResult(msg);
19 | }
20 | return Apply(options, clips);
21 | }
22 |
23 | public static ProcessResult Apply(EchoOptions options, params Clip[] clips)
24 | {
25 | var processedClips = new Clip[clips.Length];
26 |
27 | // naive version
28 | // proper algo needs to also delete notes that are covered by an echoed note
29 | var i = 0;
30 | foreach (var clip in clips)
31 | {
32 | processedClips[i++] = AddEchoes(clip, options.Lengths, options.Echoes);
33 | }
34 | return new ProcessResult(processedClips);
35 | }
36 |
37 | public static Clip AddEchoes(Clip clip, decimal[] lengths, int[] echoes)
38 | {
39 | var lengthIx = 0;
40 | var echoIx = 0;
41 | var newNotes = new List();
42 | foreach (var noteEvent in clip.Notes)
43 | {
44 | var delayTime = lengths[lengthIx++ % lengths.Length];
45 | var echoCount = Math.Max(echoes[echoIx++ % echoes.Length], 2);
46 | var velocityFalloff = (int) Math.Round((noteEvent.Velocity - 10) / (echoCount - 1));
47 | if (noteEvent.Duration > delayTime)
48 | {
49 | noteEvent.Duration = delayTime;
50 | }
51 | for (var i = 0; i < echoCount; i++)
52 | {
53 | var echoedNote = noteEvent with { };
54 | echoedNote.Start += delayTime * i;
55 | echoedNote.Velocity -= velocityFalloff * i;
56 | newNotes.Add(echoedNote);
57 | }
58 | }
59 | foreach (var newNote in newNotes)
60 | {
61 | ClipUtilities.AddNoteCutting(clip, newNote);
62 | }
63 | // todo: handle wrapping echoes outside the length of the clip
64 | return clip;
65 | }
66 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Experimental/Scan.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands.Experimental;
2 |
3 | public class ScanOptions
4 | {
5 | [OptionInfo(min: 1, max: 500)]
6 | public int Count { get; set; } = 8;
7 |
8 | public decimal Window { get; set; } = 1;
9 | }
10 |
11 | // rename to stretch -grainsize 1/16 -factor 2.0
12 | public class Scan
13 | {
14 | public static ProcessResult Apply(Command command, params Clip[] clips)
15 | {
16 | (var success, var msg) = OptionParser.TryParseOptions(command, out ScanOptions options);
17 | if (!success)
18 | {
19 | return new ProcessResult(msg);
20 | }
21 | var processedClips = new Clip[clips.Length];
22 |
23 | for (var c = 0; c < clips.Length; c++)
24 | {
25 | var clip = clips[c];
26 | var processedClip = new Clip(options.Window * options.Count, clip.IsLooping);
27 | decimal delta = clip.Length / options.Count,
28 | curPos = 0;
29 |
30 | for (int i = 0; i < options.Count; i++)
31 | {
32 | processedClip.Notes.AddRange(ClipUtilities.GetSplitNotesInRangeAtPosition(curPos, curPos + options.Window, clip.Notes, options.Window * i));
33 | curPos += delta;
34 | }
35 | processedClips[c] = processedClip;
36 | }
37 |
38 | return new ProcessResult(processedClips);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Filter.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class FilterOptions
4 | {
5 | [OptionInfo(OptionType.Default, 1 / 512f)]
6 | public decimal Duration { get; set; } = 4 / 64m;
7 |
8 | public bool Invert { get; set; }
9 | }
10 |
11 | // # desc: Filters out notes shorter than the length specified (default 1/64). If -invert is specified, notes longer than the specified length are removed.
12 | public static class Filter
13 | {
14 | public static ProcessResult Apply(Command command, params Clip[] clips)
15 | {
16 | var (success, msg) = OptionParser.TryParseOptions(command, out FilterOptions options);
17 | if (!success)
18 | {
19 | return new ProcessResult(msg);
20 | }
21 |
22 | return Apply(options, clips);
23 | }
24 |
25 | public static ProcessResult Apply(FilterOptions options, params Clip[] clips)
26 | {
27 | var processedClips = new Clip[clips.Length];
28 |
29 | for (var c = 0; c < clips.Length; c++)
30 | {
31 | var clip = clips[c];
32 | var processedClip = new Clip(clip.Length, clip.IsLooping);
33 |
34 | processedClip.Notes = (options.Invert)
35 | ? clip.Notes.Where(x => x.Duration < options.Duration).ToSortedList()
36 | : clip.Notes.Where(x => x.Duration > options.Duration).ToSortedList();
37 |
38 | processedClips[c] = processedClip;
39 | }
40 |
41 | return new ProcessResult(processedClips);
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Interleave.cs:
--------------------------------------------------------------------------------
1 | using static Mutateful.Commands.InterleaveMode;
2 |
3 | namespace Mutateful.Commands;
4 |
5 | public class InterleaveOptions
6 | {
7 | public bool ChunkChords { get; set; } = true; // Process notes having the exact same start times as a single event. Only applies to event mode.
8 | // public int[] EnableMask { get; set; } = new int[] { 1 }; // Deprecated. Allows specifying a sequence of numbers to use as a mask for whether the note should be included or omitted. E.g. 1 0 will alternately play and omit every even/odd note. Useful when combining two or more clips but you want to retain only the notes for the current track. In this scenario you would have several formulas that are the same except having different masks.
9 |
10 | public bool Solo { get; set; } = false; // A quicker and more convenient way to control what enablemask is usually made to do - soloing the events from the current channel only.
11 |
12 | public InterleaveMode Mode { get; set; } = Time;
13 |
14 | [OptionInfo(1/512f)]
15 | public decimal[] Ranges { get; set; } = { 1 };
16 |
17 | [OptionInfo(1)]
18 | public int[] Repeats { get; set; } = { 1 };
19 |
20 | public bool Skip { get; set; } = false; // Whether to skip events at the corresponding location in other clips
21 | // todo: public decimal[] ScaleFactors { get; set; } = new decimal[] { 1 }; // Scaling is done after slicing, but prior to interleaving
22 | }
23 |
24 | public enum InterleaveMode
25 | {
26 | NotSpecified,
27 | Event,
28 | Time
29 | }
30 |
31 | // # desc: Combines notes from two or more clips in an interleaved fashion.
32 | public static class Interleave
33 | {
34 | public static ProcessResult Apply(Command command, ClipMetaData metadata, Clip[] clips, InterleaveMode mode)
35 | {
36 | var (success, msg) = OptionParser.TryParseOptions(command, out InterleaveOptions options);
37 | if (!success)
38 | {
39 | return new ProcessResult(msg);
40 | }
41 | if (mode != NotSpecified)
42 | {
43 | options.Mode = mode;
44 | }
45 | return Apply(options, metadata, clips);
46 | }
47 |
48 | public static ProcessResult Apply(InterleaveOptions options, ClipMetaData metadata, params Clip[] clips)
49 | {
50 | if (clips.Length < 2)
51 | {
52 | clips = new[] { clips[0], clips[0] };
53 | }
54 | decimal position = 0;
55 | int repeatsIndex = 0;
56 | Clip resultClip = new Clip(4, true);
57 |
58 | switch (options.Mode)
59 | {
60 | case Event:
61 | if (options.ChunkChords)
62 | {
63 | foreach (var clip in clips)
64 | {
65 | clip.GroupSimultaneousNotes();
66 | }
67 | }
68 | var noteCounters = clips.Select(c => new IntCounter(c.Notes.Count)).ToArray();
69 | position = clips[0].Notes[0].Start;
70 |
71 | while (noteCounters.Any(nc => !nc.Overflow))
72 | {
73 | for (var clipIndex = 0; clipIndex < clips.Length; clipIndex++)
74 | {
75 | var clip = clips[clipIndex];
76 | var currentNoteCounter = noteCounters[clipIndex];
77 |
78 | for (var repeats = 0; repeats < options.Repeats[repeatsIndex % options.Repeats.Length]; repeats++)
79 | {
80 | var note = clip.Notes[currentNoteCounter.Value];
81 |
82 | if (!options.Solo || clip.ClipReference.Track == metadata.TrackNumber)
83 | {
84 | var newNote = note with { };
85 | newNote.Start = position;
86 | resultClip.Notes.Add(newNote);
87 | }
88 | position += clip.DurationUntilNextNote(currentNoteCounter.Value);
89 | }
90 | if (options.Skip)
91 | foreach (var noteCounter in noteCounters) noteCounter.Inc();
92 | else
93 | currentNoteCounter.Inc();
94 | repeatsIndex++;
95 | }
96 | }
97 | if (options.ChunkChords)
98 | {
99 | resultClip.Flatten();
100 | }
101 | break;
102 | case Time:
103 | var srcPositions = clips.Select(c => new DecimalCounter(c.Length)).ToArray();
104 | int timeRangeIndex = 0;
105 |
106 | while (srcPositions.Any(c => !c.Overflow))
107 | {
108 | for (var clipIndex = 0; clipIndex < clips.Length; clipIndex++)
109 | {
110 | var clip = clips[clipIndex];
111 | var currentTimeRange = options.Ranges[timeRangeIndex];
112 | for (var repeats = 0; repeats < options.Repeats[repeatsIndex % options.Repeats.Length]; repeats++)
113 | {
114 | if (!options.Solo || clip.ClipReference.Track == metadata.TrackNumber)
115 | {
116 | resultClip.Notes.AddRange(
117 | ClipUtilities.GetSplitNotesInRangeAtPosition(
118 | srcPositions[clipIndex].Value,
119 | srcPositions[clipIndex].Value + currentTimeRange,
120 | clips[clipIndex].Notes,
121 | position
122 | )
123 | );
124 | }
125 | position += currentTimeRange;
126 | }
127 | if (options.Skip)
128 | {
129 | foreach (var srcPosition in srcPositions)
130 | {
131 | srcPosition.Inc(currentTimeRange);
132 | }
133 | }
134 | else
135 | {
136 | srcPositions[clipIndex].Inc(currentTimeRange);
137 | }
138 | repeatsIndex++;
139 | timeRangeIndex = (timeRangeIndex + 1) % options.Ranges.Length; // this means that you cannot use the Counts parameter to have varying time ranges for each repeat
140 | }
141 | }
142 | break;
143 | }
144 | resultClip.Length = position;
145 | return new ProcessResult(new[] { resultClip });
146 | }
147 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Invert.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class InvertOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 0)]
6 | public int Position { get; set; } = 1;
7 | }
8 |
9 | // # desc: Inverts the contents of the current clip the specified number of times. For each inversion, all notes with the currently lowest pitch value are moved one octave up.
10 | public static class Invert
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | var (success, msg) = OptionParser.TryParseOptions(command, out InvertOptions options);
15 | return !success ? new ProcessResult(msg) : Apply(options, clips);
16 | }
17 |
18 | public static ProcessResult Apply(InvertOptions options, params Clip[] clips)
19 | {
20 | foreach (var clip in clips)
21 | {
22 | for (var i = 0; i < options.Position; i++)
23 | {
24 | DoInvert(clip);
25 | }
26 | }
27 |
28 | return new ProcessResult(clips);
29 | }
30 |
31 | public static void DoInvert(Clip clip)
32 | {
33 | var lowestPitch = clip.Notes.Min(x => x.Pitch);
34 | var notesToInvert = clip.Notes.Where(x => x.Pitch == lowestPitch);
35 | foreach (var note in notesToInvert)
36 | {
37 | note.Pitch += 12;
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Legato.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public static class Legato
4 | {
5 | // # desc: Removes silence between notes. Basically the same as the built-in legato function in Live, but often useful in the context of a mutateful formula as well.
6 | public static ProcessResult Apply(params Clip[] clips)
7 | {
8 | var resultClips = new Clip[clips.Length];
9 | const decimal smallestGap = 4m / 64m;
10 |
11 | for (var x = 0; x < clips.Length; x++)
12 | {
13 | var clip = clips[x];
14 | var resultClip = new Clip(clips[x].Length, clips[x].IsLooping);
15 |
16 | for (var y = 0; y < clip.Notes.Count; y++)
17 | {
18 | var note = clip.Notes[y];
19 | var duration = clip.DurationUntilNextNoteOrEndOfClip(y);
20 | if (duration < smallestGap)
21 | {
22 | var yy = y;
23 | while (++yy < clip.Count && duration < smallestGap)
24 | {
25 | duration = clip.DurationUntilNextNoteOrEndOfClip(yy);
26 | }
27 |
28 | duration = clip.DurationBetweenNotes(y, yy);
29 | }
30 | resultClip.Add(new NoteEvent(note.Pitch, note.Start, duration, note.Velocity));
31 | }
32 | resultClips[x] = resultClip;
33 | }
34 | return new ProcessResult(resultClips);
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Loop.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class LoopOptions
4 | {
5 | [OptionInfo(OptionType.Default, 1, noImplicitCast: true)]
6 | public decimal/*ActualDecimal*/ Length { get; set; } = 1;
7 | }
8 |
9 | // # desc: Lengthens the incoming clips according to the factor specified (e.g. 2 would double the clip length)
10 | public static class Loop
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | var (success, msg) = OptionParser.TryParseOptions(command, out LoopOptions options);
15 | return success ? Apply(options, clips) : new ProcessResult(msg);
16 | }
17 |
18 | public static ProcessResult Apply(LoopOptions options, params Clip[] clips)
19 | {
20 | foreach (var clip in clips)
21 | {
22 | ClipUtilities.EnlargeClipByLooping(clip, options.Length * clip.Length);
23 | }
24 | return new ProcessResult(clips);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Mask.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class MaskOptions
4 | {
5 | public Clip By { get; set; } // If specified, clips passed in are masked (i.e. portions of notes removed) by this clip.
6 | // Otherwise, clips passed in are masked against a dummy clip containing a note as long as
7 | // the clip, effectively inversing the passed in clip.
8 | }
9 |
10 | // # desc: Creates a masking clip which is used to remove or shorten notes not overlapping with the mask clip. If no -by clip is specified, a sustained note is used instead, effectively inversing the clip rhythmically.
11 | public static class Mask
12 | {
13 | public static ProcessResult Apply(Command command, params Clip[] clips)
14 | {
15 | var (success, msg) = OptionParser.TryParseOptions(command, out MaskOptions options);
16 | if (!success)
17 | {
18 | return new ProcessResult(msg);
19 | }
20 |
21 | return Apply(options, clips);
22 | }
23 |
24 | public static ProcessResult Apply(MaskOptions options, params Clip[] clips)
25 | {
26 | var processedClips = new List(clips.Length);
27 | var byClip = options.By != null && options.By.Count > 0;
28 |
29 | if (byClip)
30 | {
31 | foreach (var clip in clips)
32 | {
33 | MaskNotesByClip(clip, options.By);
34 | processedClips.Add(clip);
35 | }
36 | }
37 | else
38 | {
39 | foreach (var clip in clips)
40 | {
41 | var clipToMask = new Clip(clip.Length, clip.IsLooping);
42 | clipToMask.Add(new NoteEvent(60, 0, clipToMask.Length, 100));
43 | MaskNotesByClip(clipToMask, clip);
44 | processedClips.Add(clipToMask);
45 | }
46 | }
47 |
48 | return new ProcessResult(processedClips.ToArray());
49 | }
50 |
51 | private static void MaskNotesByClip(Clip clipToMask, Clip maskClip)
52 | {
53 | // var smallestGap = 1 / 256m;
54 |
55 | var maskClipIx = 0;
56 | while (maskClipIx < maskClip.Notes.Count)
57 | {
58 | var maskNote = maskClip.Notes[maskClipIx];
59 | int i = 0;
60 | while (i < clipToMask.Notes.Count)
61 | {
62 | var note = clipToMask.Notes[i];
63 | var clonedNote = note with { };
64 | if (maskNote.CrossesStartOfIntervalInclusive(clonedNote.Start, clonedNote.End))
65 | {
66 | note.Duration = maskNote.End - note.Start;
67 | note.Start = maskNote.End;
68 | }
69 | else if (maskNote.CrossesEndOfIntervalInclusive(clonedNote.Start, clonedNote.End))
70 | {
71 | note.Duration -= note.End - maskNote.Start;
72 | }
73 | else if (maskNote.InsideIntervalInclusive(clonedNote.Start, clonedNote.End))
74 | {
75 | note.Duration = maskNote.Start - note.Start;
76 | note.Pitch = maskNote.Pitch;
77 | if (clonedNote.End > maskNote.End)
78 | {
79 | clipToMask.Notes.Add(new NoteEvent(note.Pitch, maskNote.End, clonedNote.End - maskNote.End, note.Velocity));
80 | }
81 | }
82 |
83 | i++;
84 | }
85 | maskClipIx++;
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Monophonize.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | // # desc: Makes the clip monophonic by removing any overlapping notes. Lower notes have precedence over higher notes.
4 | public static class Monophonize
5 | {
6 | // TODO: Add option to cut overlapping events, so that more of the original clip is preserved
7 |
8 | public static ProcessResult Apply(params Clip[] clips)
9 | {
10 | var resultClips = ClipUtilities.CreateEmptyPlaceholderClips(clips);
11 |
12 | for (var i = 0; i < clips.Length; i++)
13 | {
14 | var clip = clips[i];
15 | var resultClip = resultClips[i];
16 | foreach (var note in clip.Notes) AddNoteCutting(resultClip, note with {});
17 | }
18 |
19 | return Filter.Apply(new FilterOptions(), resultClips);
20 | }
21 |
22 | private static void AddNoteCutting(Clip clip, NoteEvent noteToAdd)
23 | {
24 | var collidingNotes = clip.Notes.Where(x => noteToAdd.StartsInsideIntervalInclusive(x.Start, x.End)).ToArray();
25 | if (collidingNotes.Length > 0)
26 | {
27 | foreach (var note in collidingNotes)
28 | {
29 | if (note.Start == noteToAdd.Start && noteToAdd.Duration > note.Duration) // largest note wins in the case of a collision
30 | {
31 | clip.Notes.RemoveAt(clip.Notes.IndexOf(note));
32 | }
33 | else
34 | {
35 | // todo: maybe add extra logic to add back previous note if it spans the length of the note being added currently
36 | note.Duration = noteToAdd.Start - note.Start;
37 | }
38 | }
39 | }
40 | clip.Notes.Add(noteToAdd);
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Padding.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class PaddingOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 4 / 128f)]
6 | public decimal PadAmount { get; set; } = 2;
7 |
8 | public decimal Length { get; set; } = -1;
9 |
10 | public bool Post { get; set; } // If specified, adds padding to the end of the clip instead
11 | }
12 |
13 | // # desc: Adds silence (i.e. padding) at the start of a clip, or at the end of a clip if -post is specified. If -length is specified, padding is calculated so that the total length of the clip matches this. If length is shorter than the current clip length, the clip is cropped instead.
14 | public static class Padding
15 | {
16 | public static ProcessResult Apply(Command command, params Clip[] clips)
17 | {
18 | var (success, msg) = OptionParser.TryParseOptions(command, out PaddingOptions options);
19 | if (!success)
20 | {
21 | return new ProcessResult(msg);
22 | }
23 | return Apply(options, clips);
24 | }
25 |
26 | public static ProcessResult Apply(PaddingOptions options, params Clip[] clips)
27 | {
28 | var processedClips = new Clip[clips.Length];
29 |
30 | for (var i = 0; i < clips.Length; i++)
31 | {
32 | var clip = new Clip(clips[i]);
33 | var padAmount = options.PadAmount;
34 | if (options.Length > 0)
35 | {
36 | if (options.Length > clip.Length)
37 | {
38 | padAmount = options.Length - clip.Length;
39 | }
40 | else
41 | {
42 | processedClips[i] = Crop.CropClip(clip, 0, options.Length);
43 | continue;
44 | }
45 | }
46 |
47 | clip.Length += padAmount;
48 | if (!options.Post)
49 | {
50 | foreach (var noteEvent in clip.Notes)
51 | {
52 | noteEvent.Start += padAmount;
53 | }
54 | }
55 |
56 | processedClips[i] = clip;
57 | }
58 |
59 | return new ProcessResult(processedClips);
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Quantize.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class QuantizeOptions
4 | {
5 | [OptionInfo(0f, 1f)]
6 | public decimal/*ActualDecimal*/ Amount { get; set; } = 1.0m;
7 |
8 | [OptionInfo(type: OptionType.Default, 1/64f)]
9 | public decimal[] Divisions { get; set; } = { 4/16m };
10 |
11 | // public decimal Threshold { get; set; } = 0.125m; // to be added
12 |
13 | // public decimal Magnetic { get; set; } = 0; // to be added
14 |
15 | public Clip By { get; set; } // for quantizing based on another clip (possibly very similar to constrain -start?)
16 | }
17 |
18 | // # desc: Quantizes a clip by the specified amount against a regular or irregular set of divisions, or even against the timings of another clip.
19 | public static class Quantize
20 | {
21 | public static ProcessResult Apply(Command command, params Clip[] clips)
22 | {
23 | var (success, msg) = OptionParser.TryParseOptions(command, out QuantizeOptions options);
24 | if (!success)
25 | {
26 | return new ProcessResult(msg);
27 | }
28 | return Apply(options, clips);
29 | }
30 |
31 | public static ProcessResult Apply(QuantizeOptions options, params Clip[] clips)
32 | {
33 | var maxLen = clips.Max(x => x.Length);
34 | if (options.By != null)
35 | {
36 | if (options.By.Length < maxLen)
37 | {
38 | ClipUtilities.EnlargeClipByLooping(options.By, maxLen);
39 | }
40 | options.Divisions = options.By.Notes.Select(x => x.Start).Distinct().ToArray();
41 | }
42 | else
43 | {
44 | var currentPos = 0m;
45 | var quantizePositions = new List();
46 | var i = 0;
47 | while (currentPos <= maxLen)
48 | {
49 | quantizePositions.Add(currentPos);
50 | currentPos += options.Divisions[i % options.Divisions.Length];
51 | i++;
52 | }
53 | options.Divisions = quantizePositions.ToArray();
54 | }
55 | options.Amount = Math.Clamp(options.Amount, 0, 1);
56 | var resultClips = new Clip[clips.Length];
57 |
58 | for (var i = 0; i < clips.Length; i++)
59 | {
60 | var clip = clips[i];
61 | var resultClip = new Clip(clip.Length, clip.IsLooping);
62 |
63 | foreach (var note in clip.Notes)
64 | {
65 | var constrainedNote = note with { };
66 | var newStart = ClipUtilities.FindNearestNoteStartInDecimalSet(note, options.Divisions);
67 | constrainedNote.Start += (newStart - constrainedNote.Start) * options.Amount;
68 | resultClip.Add(constrainedNote);
69 | }
70 | resultClips[i] = resultClip;
71 | }
72 | return new ProcessResult(resultClips);
73 | }
74 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Relength.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class RelengthOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 1/512f, noImplicitCast: true)]
6 | public decimal/*ActualDecimal*/ Factor { get; set; } = 1.0m;
7 | }
8 |
9 | // # desc: Changes the length of all notes in a clip by multiplying their lengths with the specified factor.
10 | public static class Relength
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | (var success, var msg) = OptionParser.TryParseOptions(command, out RelengthOptions options);
15 | if (!success)
16 | {
17 | return new ProcessResult(msg);
18 | }
19 | return Apply(options, clips);
20 | }
21 |
22 | public static ProcessResult Apply(RelengthOptions options, params Clip[] clips)
23 | {
24 | foreach (var clip in clips)
25 | {
26 | foreach (var note in clip.Notes)
27 | {
28 | note.Duration *= options.Factor;
29 | }
30 | }
31 |
32 | return new ProcessResult(clips);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Remap.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class RemapOptions
4 | {
5 | public Clip To { get; set; } = Clip.Empty;
6 | }
7 |
8 | // # desc: Remaps a set of pitches to another set of pitches
9 | public static class Remap
10 | {
11 | public static ProcessResult Apply(Command command, params Clip[] clips)
12 | {
13 | var (success, msg) = OptionParser.TryParseOptions(command, out RemapOptions options);
14 | if (!success)
15 | {
16 | return new ProcessResult(msg);
17 | }
18 | return Apply(options, clips);
19 | }
20 |
21 | public static ProcessResult Apply(RemapOptions options, params Clip[] clips)
22 | {
23 | var resultClips = ClipUtilities.CreateEmptyPlaceholderClips(clips);
24 |
25 | for (var i = 0; i < clips.Length; i++)
26 | {
27 | var clip = clips[i];
28 | var resultClip = resultClips[i];
29 | var sourcePitches = clip.Notes.Select(x => x.Pitch).Distinct().OrderBy(x => x).ToList();
30 | var destPitches = options.To.Count > 0
31 | ? options.To.Notes.Select(x => x.Pitch).Distinct().OrderBy(x => x).ToList()
32 | : Enumerable.Range(36, Math.Min(sourcePitches.Count, 128 - 36)).ToList();
33 | var inc = 1f;
34 |
35 | if (destPitches.Count < sourcePitches.Count)
36 | {
37 | inc = (float) destPitches.Count / sourcePitches.Count;
38 | }
39 |
40 | var map = new Dictionary();
41 | var destIx = 0f;
42 | foreach (var sourcePitch in sourcePitches)
43 | {
44 | map[sourcePitch] = destPitches[(int) Math.Floor(destIx)];
45 | destIx += inc;
46 | }
47 |
48 | foreach (var note in clip.Notes)
49 | {
50 | var remappedNote = note with { Pitch = map[note.Pitch] };
51 | ClipUtilities.AddNoteCutting(resultClip, remappedNote);
52 | }
53 | }
54 | return new ProcessResult(resultClips);
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Resize.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class ResizeOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 1/512f, noImplicitCast: true)]
6 | public decimal/*ActualDecimal*/ Factor { get; set; } = 1.0m;
7 | }
8 |
9 | // # desc: Resizes the current clip based on the specified factor (i.e. 0.5 halves the size of the clip, effectively doubling its tempo)
10 | public static class Resize
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | var (success, msg) = OptionParser.TryParseOptions(command, out ResizeOptions options);
15 | return !success ? new ProcessResult(msg) : Apply(options, clips);
16 | }
17 |
18 | public static ProcessResult Apply(ResizeOptions options, params Clip[] clips)
19 | {
20 | foreach (var clip in clips)
21 | {
22 | foreach (var note in clip.Notes)
23 | {
24 | note.Duration *= options.Factor;
25 | note.Start *= options.Factor;
26 | }
27 | clip.Length *= options.Factor;
28 | }
29 |
30 | return new ProcessResult(clips);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Scale.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class ScaleOptions
4 | {
5 | public Clip By { get; set; }
6 |
7 | public bool Strict { get; set; }
8 |
9 | public bool PositionAware { get; set; } // constrains to pitch based on position, so that events occuring for instance during a chord are constrained to this only. If no events are available in the given span, the entire clip is used instead.
10 | }
11 |
12 | // # desc: Uses a clip passed in via the -by parameter as a scale to which the current clip is made to conform. If -strict is specified, notes are made to follow both the current pitch and octave of the closest matching note.
13 | public static class Scale
14 | {
15 | public static ProcessResult Apply(Command command, params Clip[] clips)
16 | {
17 | var (success, msg) = OptionParser.TryParseOptions(command, out ScaleOptions options);
18 | if (!success)
19 | {
20 | return new ProcessResult(msg);
21 | }
22 | return Apply(options, clips);
23 | }
24 |
25 | public static ProcessResult Apply(ScaleOptions options, params Clip[] clips)
26 | {
27 | if (options.By != null)
28 | {
29 | clips = clips.Prepend(options.By).ToArray();
30 | }
31 | ClipUtilities.NormalizeClipLengths(clips);
32 | if (clips.Length < 2) return new ProcessResult(clips);
33 | var masterClip = clips[0];
34 | var slaveClips = clips.Skip(1).ToArray();
35 | var processedClips = slaveClips.Select(c => new Clip(c.Length, c.IsLooping)).ToArray();
36 |
37 | for (var i = 0; i < slaveClips.Length; i++)
38 | {
39 | var slaveClip = slaveClips[i];
40 | foreach (var note in slaveClip.Notes)
41 | {
42 | var masterNotes = SortedList.Empty;
43 | if (options.PositionAware)
44 | {
45 | masterNotes = masterClip.Notes.Where(x => x.StartsInsideIntervalInclusive(note.Start, note.End) || x.CoversInterval(note.Start, note.End)).ToSortedList();
46 | }
47 | if (masterNotes.Count == 0) masterNotes = masterClip.Notes;
48 |
49 | var constrainedNote = note with {};
50 | constrainedNote.Pitch = options.Strict ?
51 | ClipUtilities.FindNearestNotePitchInSet(note, masterNotes) :
52 | ClipUtilities.FindNearestNotePitchInSetMusical(note, masterNotes);
53 | processedClips[i].Notes.Add(constrainedNote);
54 | }
55 | }
56 | return new ProcessResult(processedClips);
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/SetLength.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class SetLengthOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 1/512f)]
6 | public decimal[] Lengths { get; set; } = { 4/16m };
7 | }
8 |
9 | // # desc: Sets the length of all notes to the specified value(s). When more values are specified, they are cycled through.
10 | public static class SetLength
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | var (success, msg) = OptionParser.TryParseOptions(command, out SetLengthOptions options);
15 | if (!success)
16 | {
17 | return new ProcessResult(msg);
18 | }
19 | return Apply(options, clips);
20 | }
21 |
22 | public static ProcessResult Apply(SetLengthOptions options, params Clip[] clips)
23 | {
24 | var resultClips = ClipUtilities.CreateEmptyPlaceholderClips(clips);
25 | for (var index = 0; index < clips.Length; index++)
26 | {
27 | var clip = clips[index];
28 | var resultClip = resultClips[index];
29 | var lengthCounter = 0;
30 | foreach (var note in clip.Notes)
31 | {
32 | ClipUtilities.AddNoteCutting(resultClip, note with
33 | {
34 | Duration = options.Lengths[lengthCounter++ % options.Lengths.Length]
35 | });
36 | }
37 | }
38 | return new ProcessResult(resultClips);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/SetPitch.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class SetPitchOptions
4 | {
5 | [OptionInfo(OptionType.Default, 0, 127)]
6 | public int[] PitchValues { get; set; } = new int[0];
7 |
8 | public Clip By { get; set; } = Clip.Empty;
9 | }
10 |
11 | // # desc: Sets the pitch of all notes to the specified value(s). When more values are specified, they are cycled through.
12 | public static class SetPitch
13 | {
14 | public static ProcessResult Apply(Command command, params Clip[] clips)
15 | {
16 | var (success, msg) = OptionParser.TryParseOptions(command, out SetPitchOptions options);
17 | if (!success)
18 | {
19 | return new ProcessResult(msg);
20 | }
21 | return Apply(options, clips);
22 | }
23 |
24 | public static ProcessResult Apply(SetPitchOptions options, params Clip[] clips)
25 | {
26 | var resultClips = ClipUtilities.CreateEmptyPlaceholderClips(clips);
27 |
28 | int[] pitches;
29 | if (options.PitchValues.Length > 0)
30 | {
31 | pitches = options.PitchValues;
32 | } else
33 | {
34 | pitches = options.By.Notes.Select(x => x.Pitch).ToArray();
35 | }
36 | if (pitches.Length == 0) return new ProcessResult(clips, "SetPitch did nothing, since neither pitches or -by clip was specified.");
37 |
38 | for (var i = 0; i < clips.Length; i++)
39 | {
40 | var clip = clips[i];
41 | var resultClip = resultClips[i];
42 | var pitchIx = 0;
43 | foreach (var note in clip.Notes)
44 | {
45 | var repitchedNote = note with
46 | {
47 | Pitch = pitches[pitchIx++ % pitches.Length]
48 | };
49 | ClipUtilities.AddNoteCutting(resultClip, repitchedNote);
50 | }
51 | }
52 |
53 | return new ProcessResult(resultClips);
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/SetRhythm.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class SetRhythmOptions
4 | {
5 | public Clip By { get; set; }
6 | }
7 |
8 | // # desc: Retains pitch and velocity from the current clip while changing the timing and duration to match the clip specified in the -by parameter.
9 | public static class SetRhythm
10 | {
11 | public static ProcessResult Apply(Command command, params Clip[] clips)
12 | {
13 | var (success, msg) = OptionParser.TryParseOptions(command, out SetRhythmOptions options);
14 | if (!success)
15 | {
16 | return new ProcessResult(msg);
17 | }
18 | return Apply(options, clips);
19 | }
20 |
21 | public static ProcessResult Apply(SetRhythmOptions options, params Clip[] clips)
22 | {
23 | if (options.By != null)
24 | {
25 | clips = clips.Prepend(options.By).ToArray();
26 | }
27 |
28 | if (clips.Length < 2)
29 | {
30 | return new ProcessResult(clips, $"SetRhythm: Skipped command because it needs 2 clips, and {clips.Length} were passed in.");
31 | }
32 | ClipUtilities.NormalizeClipLengths(clips);
33 |
34 | var resultClips = new Clip[clips.Length - 1];
35 | var byClip = clips[0];
36 | var byIndex = 0;
37 | var resultClipIx = 0;
38 |
39 | for (var i = 1; i < clips.Length; i++)
40 | {
41 | var clip = clips[i];
42 | var resultClip = new Clip(0, clip.IsLooping);
43 |
44 | foreach (var note in clip.Notes)
45 | {
46 | var byNote = byClip.Notes[byIndex % byClip.Count];
47 |
48 | // special case: add silence between start of clip and first note, but only the first time, since subsequent silences are handled by DurationUntilNextNote
49 | if (resultClip.Length == 0 && byIndex == 0)
50 | {
51 | resultClip.Length = byNote.Start;
52 | }
53 |
54 | resultClip.Add(new NoteEvent(note.Pitch, resultClip.Length, byNote.Duration, note.Velocity));
55 | resultClip.Length += byClip.DurationUntilNextNote(byIndex % byClip.Count);
56 | byIndex++;
57 | }
58 |
59 | // stacked/overlapping notes will lead to incorrect final length of clip, so check if this is the case
60 | var latestNoteEnd = resultClip.Notes.Max(x => x.End);
61 | if (latestNoteEnd > resultClip.Length)
62 | {
63 | resultClip.Length = latestNoteEnd;
64 | }
65 |
66 | resultClip.Length = Utilities.RoundUpToNearestSixteenth(resultClip.Length); // quantize clip length to nearest 1/16, or Live won't accept it
67 | resultClips[resultClipIx++] = resultClip;
68 | }
69 |
70 | return new ProcessResult(resultClips);
71 | }
72 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Shuffle.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class ShuffleOptions
4 | {
5 | public Clip By { get; set; } = new Clip(4, true);
6 |
7 | [OptionInfo(type: OptionType.Default, 0)]
8 | public int[] ShuffleValues { get; set; } = new int[0];
9 | }
10 |
11 | // # desc: Shuffles the order of notes by a list of numbers of arbitrary length, or by another clip. When another clip is specified, the relative pitch of each note is used to determine the shuffle order.
12 | public static class Shuffle
13 | {
14 | public static ProcessResult Apply(Command command, params Clip[] clips)
15 | {
16 | var (success, msg) = OptionParser.TryParseOptions(command, out ShuffleOptions options);
17 | return !success ? new ProcessResult(msg) : Apply(options, clips);
18 | }
19 |
20 | public static ProcessResult Apply(ShuffleOptions options, params Clip[] clips)
21 | {
22 | if (options.By.Notes.Count == 0) options.By = clips[0];
23 | if (options.By.Count == 0 && options.ShuffleValues.Length == 0)
24 | {
25 | return new ProcessResult("No -by clip or shuffle values specified.");
26 | }
27 |
28 | ClipUtilities.Monophonize(options.By);
29 | var targetClips = new Clip[clips.Length];
30 |
31 | int[] shuffleValues;
32 | if (options.ShuffleValues.Length == 0)
33 | {
34 | int minPitch = options.By.Notes.Min(x => x.Pitch);
35 | shuffleValues = options.By.Notes.Select(x => x.Pitch - minPitch).ToArray();
36 | }
37 | else
38 | {
39 | shuffleValues = options.ShuffleValues.Select(x => Math.Clamp(x, 1, 100) - 1).ToArray();
40 | }
41 |
42 | var c = 0;
43 | foreach (var clip in clips) // we only support one generated clip since these are tied to a specific clip slot. Maybe support multiple clips under the hood, but discard any additional clips when sending the output is the most flexible approach.
44 | {
45 | clip.GroupSimultaneousNotes();
46 | targetClips[c] = new Clip(clip.Length, clip.IsLooping);
47 |
48 | var numShuffleIndexes = shuffleValues.Length;
49 | if (numShuffleIndexes < clip.Notes.Count) numShuffleIndexes = clip.Notes.Count;
50 | var indexes = new int[numShuffleIndexes];
51 |
52 | for (var i = 0; i < numShuffleIndexes; i++)
53 | {
54 | // Calc shuffle indexes as long as there are notes in the source clip. If the clip to be shuffled contains more events than the source, add zero-indexes so that the rest of the sequence is produced sequentially.
55 | if (i < shuffleValues.Length)
56 | {
57 | indexes[i] = (int)Math.Floor(((float)shuffleValues[i] / clip.Notes.Count) * clip.Notes.Count);
58 | } else
59 | {
60 | indexes[i] = 0;
61 | }
62 | }
63 |
64 | // preserve original durations until next note
65 | var durationUntilNextNote = new List(clip.Notes.Count);
66 | for (var i = 0; i < clip.Notes.Count; i++)
67 | {
68 | durationUntilNextNote.Add(clip.DurationUntilNextNote(i));
69 | }
70 |
71 | // do shuffle
72 | var j = 0;
73 | decimal pos = 0m;
74 | while (clip.Notes.Count > 0)
75 | {
76 | int currentIx = indexes[j++] % clip.Notes.Count;
77 | targetClips[c].Notes.Add(
78 | clip.Notes[currentIx] with {
79 | Start = pos
80 | }
81 | );
82 | pos += durationUntilNextNote[currentIx];
83 | durationUntilNextNote.RemoveAt(currentIx);
84 | clip.Notes.RemoveAt(currentIx);
85 | }
86 | targetClips[c].Flatten();
87 | c++;
88 | }
89 |
90 | return new ProcessResult(targetClips);
91 | }
92 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Skip.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class SkipOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 1)]
6 | public int[] SkipCounts { get; set; } = { 2 };
7 | }
8 |
9 | // # desc: Creates a new clip by skipping every # note from another clip. If more than one skip value is specified, they are cycled through.
10 | public static class Skip
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | var (success, msg) = OptionParser.TryParseOptions(command, out SkipOptions options);
15 | if (!success)
16 | {
17 | return new ProcessResult(msg);
18 | }
19 | return Apply(options, clips);
20 | }
21 |
22 | public static ProcessResult Apply(SkipOptions options, params Clip[] clips)
23 | {
24 | var resultClips = new Clip[clips.Length];
25 |
26 | // Normalize skip values (typical input range: 1 - N, while 0 - N is used internally)
27 | for (var ix = 0; ix < options.SkipCounts.Length; ix++)
28 | {
29 | options.SkipCounts[ix]--;
30 | }
31 |
32 | if (options.SkipCounts.All(x => x == 0))
33 | {
34 | return new ProcessResult("The given input to skip would produce an empty clip. Aborting...");
35 | }
36 |
37 | var i = 0;
38 | foreach (var clip in clips)
39 | {
40 | var resultClip = new Clip(clips[i].Length, clips[i].IsLooping);
41 | decimal currentPos = 0;
42 | var noteIx = 0;
43 | var currentSkip = options.SkipCounts[0];
44 | var skipIx = 0;
45 | // We don't want skipping notes to result in shorter clips, therefore we keep going until we have filled at least
46 | // the same length as the original clip
47 | while (currentPos < resultClip.Length)
48 | {
49 | if (currentSkip > 0)
50 | {
51 | if (noteIx >= clip.Count) noteIx %= clip.Count;
52 | var note = clip.Notes[noteIx] with {Start = currentPos};
53 | currentPos += clip.DurationUntilNextNote(noteIx);
54 | resultClip.Add(note);
55 | currentSkip--;
56 | }
57 | else
58 | {
59 | currentSkip = options.SkipCounts[++skipIx % options.SkipCounts.Length];
60 | }
61 | noteIx++;
62 | }
63 | resultClips[i] = resultClip;
64 | i++;
65 | }
66 | return new ProcessResult(resultClips);
67 | }
68 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Slice.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class SliceOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 1/128f)]
6 | public decimal[] Lengths { get; set; } = { 4/16m };
7 | }
8 |
9 | // # desc: Slices a clip (i.e. cutting any notes) at a regular or irregular set of fractions.
10 | public static class Slice
11 | {
12 | public static ProcessResult Apply(Command command, params Clip[] clips)
13 | {
14 | var (success, msg) = OptionParser.TryParseOptions(command, out SliceOptions options);
15 | if (!success)
16 | {
17 | return new ProcessResult(msg);
18 | }
19 | return Apply(options, clips);
20 | }
21 |
22 | public static ProcessResult Apply(SliceOptions options, params Clip[] clips)
23 | {
24 | var processedClips = new List();
25 | foreach (var clip in clips)
26 | {
27 | processedClips.Add(ClipUtilities.SplitNotesAtEvery(clip, options.Lengths));
28 | }
29 | return new ProcessResult(processedClips.ToArray());
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Take.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public class TakeOptions
4 | {
5 | [OptionInfo(type: OptionType.Default, 1)] public int[] TakeCounts { get; set; } = { 2 };
6 |
7 | public bool Thin { get; set; } // includes silence between skipped notes as well, effectively "thinning out" the clip
8 |
9 | [OptionInfo(0, 127)] public int LowPitch { get; set; } = 0;
10 |
11 | [OptionInfo(0, 127)] public int HighPitch { get; set; } = 127;
12 |
13 | [OptionInfo(0, 127)] public int LowVelocity { get; set; } = 0;
14 |
15 | [OptionInfo(0, 127)] public int HighVelocity { get; set; } = 127;
16 | }
17 |
18 | // # desc: Creates a new clip by taking every # note from another clip. If more than one skip value is specified, they are cycled through.
19 | public static class Take
20 | {
21 | public static ProcessResult Apply(Command command, Clip[] clips, bool doExtract = false)
22 | {
23 | var (success, msg) = OptionParser.TryParseOptions(command, out TakeOptions options);
24 | if (!success)
25 | {
26 | return new ProcessResult(msg);
27 | }
28 | if (doExtract)
29 | {
30 | options.Thin = true;
31 | options.TakeCounts = new[] {1};
32 | }
33 | return Apply(options, clips);
34 | }
35 |
36 | public static ProcessResult Apply(TakeOptions options, params Clip[] clips)
37 | {
38 | var resultClips = new Clip[clips.Length];
39 |
40 | // Normalize take values (typical input range: 1 - N, while 0 - N is used internally)
41 | for (var ix = 0; ix < options.TakeCounts.Length; ix++)
42 | {
43 | options.TakeCounts[ix]--;
44 | }
45 |
46 | var (lowVelocity, highVelocity) = (options.LowVelocity, options.HighVelocity);
47 | if (lowVelocity > highVelocity) (lowVelocity, highVelocity) = (highVelocity, lowVelocity);
48 | var (lowPitch, highPitch) = (options.LowPitch, options.HighPitch);
49 | if (lowPitch > highPitch) (lowPitch, highPitch) = (highPitch, lowPitch);
50 |
51 | var i = 0;
52 | foreach (var clip in clips)
53 | {
54 | var filteredNotes = clip.Notes.Where(x =>
55 | x.Velocity >= lowVelocity && x.Velocity <= highVelocity && x.Pitch >= lowPitch && x.Pitch <= highPitch).ToList();
56 |
57 | var resultClip = new Clip(clips[i].Length, clips[i].IsLooping);
58 | decimal currentPos = 0;
59 | var noteIx = 0;
60 | var currentTake = options.TakeCounts[0];
61 | var takeIx = 0;
62 | // We want to keep the length of the newly created clip approximately equal to the original, therefore we keep
63 | // going until we have filled at least the same length as the original clip
64 | while (currentPos < resultClip.Length)
65 | {
66 | if (currentTake == 0)
67 | {
68 | if (noteIx >= clip.Count) noteIx %= clip.Count;
69 | var note = filteredNotes[noteIx] with {Start = currentPos};
70 | currentPos += clip.DurationUntilNextNote(noteIx);
71 | resultClip.Add(note);
72 | currentTake = options.TakeCounts[++takeIx % options.TakeCounts.Length];
73 | }
74 | else
75 | {
76 | if (options.Thin) currentPos += clip.DurationUntilNextNote(noteIx);
77 | currentTake--;
78 | }
79 | noteIx++;
80 | }
81 | resultClips[i] = resultClip;
82 | i++;
83 | }
84 | return new ProcessResult(resultClips);
85 | }
86 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/ToBeImplemented/Magnetify.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands.ToBeImplemented;
2 |
3 | public class MagnetifyOptions
4 | {
5 | public decimal Radius { get; set; } = 1 / 4;
6 | public Clip By { get; set; }
7 | }
8 |
9 | public class Magnetify
10 | {
11 | // Will probably be replaced by Quantize. Currently not planned for v1.
12 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/ToBeImplemented/Morph.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands.ToBeImplemented;
2 |
3 | public class Morph
4 | {
5 | // Will probably be replaced by Quantize in conjunction with new Loop-functionality. Not planned for v1.
6 |
7 | /* To be implemented...
8 | * Morphs between two clips over a specified number of iterations.
9 | * Notes that only exist in one clip are faded in/out using velocity
10 | * Where several notes of the same pitch exist, if the number of notes do not match the target clip, any extraneous notes are faded in/out using velocity.
11 | * Notes that exist in both clips, i.e. share pitch but not position, are interpolated over the course of the specified number of durations.
12 | */
13 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/ToBeImplemented/Shift.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands.ToBeImplemented;
2 |
3 | public class ShiftOptions
4 | {
5 | [OptionInfo(type:OptionType.Default)]
6 | public decimal[] Amounts { get; set; }
7 | }
8 |
9 | // Simple command to shift contents of clip a specified amount. Notes will be shifted unequally if more values are specified, such that shift values are cycled.
10 | public class Shift
11 | {
12 |
13 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/Transpose.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | public enum TransposeMode
4 | {
5 | Absolute, // pitch is transposed relative to an absolute base pitch of 60 (C4)
6 | Relative // pitch is transposed relative to the first note in the transposing clip
7 | }
8 |
9 | public class TransposeOptions
10 | {
11 | public Clip By { get; set; } // Allows syntax like a1 transpose -by a2 -mode relative. This syntax makes it much clearer which clip is being affected, and which is used as the source.
12 |
13 | public TransposeMode Mode { get; set; } = TransposeMode.Absolute;
14 |
15 | [OptionInfo(type: OptionType.Default)]
16 | public int[] TransposeValues { get; set; } = new int[0];
17 | }
18 |
19 | // also needed: a transpose function (rangetranspose?) transposing all notes contained within the bounds of the respective note in the control clip
20 | // # desc: Transposes the notes in a clip based on either a set of passed-in values, or another clip.
21 | public static class Transpose
22 | {
23 | public static ProcessResult Apply(Command command, params Clip[] clips)
24 | {
25 | var (success, msg) = OptionParser.TryParseOptions(command, out TransposeOptions options);
26 | return !success ? new ProcessResult(msg) : Apply(options, clips);
27 | }
28 |
29 | public static ProcessResult Apply(TransposeOptions options, params Clip[] clips)
30 | {
31 | if (options.By == null) options.By = new Clip(4, true);
32 | if (options.By.Count == 0 && options.TransposeValues.Length == 0)
33 | {
34 | return new ProcessResult("No -by clip or transpose values specified.");
35 | }
36 | int basePitch = 60;
37 | if (options.By.Count > 0)
38 | {
39 | basePitch = options.By.Notes[0].Pitch;
40 | }
41 | if (options.Mode == TransposeMode.Absolute)
42 | {
43 | basePitch -= basePitch % 12;
44 | }
45 |
46 | if (options.Mode == TransposeMode.Relative && options.By.Notes.Count > 0)
47 | {
48 | basePitch = options.By.Notes[0].Pitch;
49 | }
50 |
51 | int[] transposeValues;
52 | if (options.TransposeValues.Length > 0)
53 | {
54 | transposeValues = options.TransposeValues;
55 | } else
56 | {
57 | transposeValues = options.By.Notes.Select(x => x.Pitch - basePitch).ToArray();
58 | }
59 |
60 | foreach (var clip in clips)
61 | {
62 | clip.GroupSimultaneousNotes();
63 | for (var i = 0; i < clip.Count; i++)
64 | {
65 | clip.Notes[i].Pitch += transposeValues[i % transposeValues.Length];
66 | }
67 | clip.Flatten();
68 | }
69 | return new ProcessResult(clips);
70 | }
71 | }
--------------------------------------------------------------------------------
/src/Mutateful/Commands/VelocityScale.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Commands;
2 |
3 | // Scales velocity by value
4 | // TODO: An optional second argument enforces an abritrary minumum which we add back in;
5 | public class VelocityScaleOptions
6 | {
7 | [OptionInfo(type: OptionType.Default, 0)]
8 | public decimal Strength { get; set; }
9 | }
10 |
11 | // # desc: Scale a clips notes' velocities.
12 | public static class VelocityScale
13 | {
14 | const int FullMidiVelocityRange = 127;
15 |
16 | public static ProcessResult Apply(Command command, params Clip[] clips)
17 | {
18 | (var success, var msg) = OptionParser.TryParseOptions(command, out VelocityScaleOptions options);
19 |
20 | if (!success)
21 | {
22 | return new ProcessResult(msg);
23 | }
24 | return Apply(options, clips);
25 | }
26 |
27 | public static ProcessResult Apply(VelocityScaleOptions options, params Clip[] clips)
28 | {
29 | var processedClips = new Clip[clips.Length];
30 |
31 | var i = 0;
32 |
33 | foreach (var clip in clips)
34 | {
35 | var processedClip = new Clip(clip.Length, clip.IsLooping);
36 |
37 | processedClip.Notes.AddRange(ClipUtilities.GetSplitNotesInRangeAtPosition(0, clip.Length, clip.Notes, 0));
38 |
39 | foreach (var item in processedClip.Notes)
40 | {
41 | item.Velocity = System.Math.Min(FullMidiVelocityRange, System.Math.Abs((int)System.Math.Floor(item.Velocity * (float)options.Strength)));
42 | }
43 | processedClips[i++] = processedClip;
44 | }
45 | return new ProcessResult(processedClips);
46 | }
47 | }
--------------------------------------------------------------------------------
/src/Mutateful/Compiler/OptionParser.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Reflection;
3 | using static Mutateful.Compiler.TokenType;
4 |
5 | namespace Mutateful.Compiler;
6 |
7 | public static class OptionParser
8 | {
9 | public static (bool Success, string Message) TryParseOptions(Command command, out T result) where T : new()
10 | {
11 | var options = command.Options;
12 | result = new T();
13 | var props = result.GetType().GetProperties();
14 |
15 | foreach (var property in props)
16 | {
17 | if (Enum.TryParse(property.Name, out TokenType option))
18 | {
19 | if (!(option is > _OptionsBegin and < _OptionsEnd ||
20 | option is > _TestOptionsBegin and < _TestOptionsEnd))
21 | {
22 | return (false, $"Property {property.Name} is not a valid option or test option.");
23 | }
24 | }
25 | else
26 | {
27 | return (false, $"No corresponding entity found for {property.Name}");
28 | }
29 |
30 | bool noImplicitCast = property
31 | .GetCustomAttributes(false)
32 | .Select(a => (OptionInfo) a)
33 | .FirstOrDefault(a => a.NoImplicitCast)?.NoImplicitCast ?? false;
34 |
35 | OptionInfo defaultAttribute = property
36 | .GetCustomAttributes(false)
37 | .Select(a => (OptionInfo) a)
38 | .FirstOrDefault(a => a.Type == OptionType.Default);
39 |
40 | if (defaultAttribute != null && command.DefaultOptionValues.Count > 0)
41 | {
42 | var tokens = command.DefaultOptionValues;
43 | var (success, objects, errorMessage) = ExtractPropertyData(property, tokens, noImplicitCast);
44 | if (!success)
45 | {
46 | return (false, errorMessage);
47 | }
48 |
49 | property.SetMethod?.Invoke(result, objects);
50 | continue;
51 | }
52 |
53 | // handle value properties
54 | if (options.ContainsKey(option))
55 | {
56 | var tokens = options[option];
57 | ProcessResult