(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/WebUI/src/ClipSlot.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
107 |
--------------------------------------------------------------------------------
/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/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/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/Core/Clip.cs:
--------------------------------------------------------------------------------
1 | namespace Mutateful.Core;
2 |
3 | public class Clip : IComparable, IEquatable
4 | {
5 | public SortedList Notes { get; set; }
6 | public int Count => Notes.Count;
7 | public decimal Length { get; set; }
8 | public bool IsLooping { get; set; }
9 | public ClipReference ClipReference { get; set; }
10 | public string RawClipReference { get; set; }
11 | public decimal EndDelta => Length - Math.Clamp(Notes[^1].Start, 0, Length) + Notes[0].Start;
12 | public decimal EndDeltaSilent => Length - Math.Clamp(Notes[^1].End, 0, Length) + Notes[0].Start;
13 | public bool SelectionActive { get; private set; }
14 |
15 | public static readonly Clip Empty = new Clip(4, true);
16 |
17 | public Clip(decimal length, bool isLooping)
18 | {
19 | Notes = new SortedList();
20 | IsLooping = isLooping;
21 | Length = length;
22 | }
23 |
24 | public Clip(ClipReference clipReference) : this(4, true) { ClipReference = clipReference; }
25 |
26 | public Clip(Clip clip) : this(clip.Length, clip.IsLooping)
27 | {
28 | foreach (var note in clip.Notes)
29 | {
30 | Notes.Add(note with {});
31 | }
32 | }
33 |
34 | public void Add(NoteEvent noteEvent)
35 | {
36 | Notes.Add(noteEvent);
37 | }
38 |
39 | public void AddRange(List noteEvents)
40 | {
41 | Notes.AddRange(noteEvents);
42 | }
43 |
44 | public int CompareTo(Clip b)
45 | {
46 | if (Length < b.Length)
47 | {
48 | return -1;
49 | }
50 | if (Length > b.Length)
51 | {
52 | return 1;
53 | }
54 | return 0;
55 | }
56 |
57 | public decimal DurationUntilNextNote(int index)
58 | {
59 | if (index >= Count - 1)
60 | return EndDelta;
61 | else
62 | return Notes[index + 1].Start - Notes[index].Start;
63 | }
64 |
65 | public decimal DurationUntilNextNoteOrEndOfClip(int index)
66 | {
67 | if (index >= Count - 1)
68 | return Length - Math.Clamp(Notes[^1].Start, 0, Length);
69 |
70 | return Notes[index + 1].Start - Notes[index].Start;
71 | }
72 |
73 | public decimal DurationBetweenNotes(int start, int end)
74 | {
75 | var endTime = end >= Count ? Length : Notes[end].Start;
76 | var startTime = start >= Count ? Length : Notes[start].Start;
77 |
78 | return endTime - startTime;
79 | }
80 |
81 | // useful when the length of an event has changed and you want to consider only the interval of silence (if any) preceding the next event
82 | public decimal SilentDurationUntilNextNote(int index)
83 | {
84 | if (index >= Count - 1)
85 | return EndDeltaSilent;
86 | var silentDuration = Notes[index + 1].Start - Notes[index].End;
87 | return silentDuration > 0 ? silentDuration : 0;
88 | }
89 |
90 | // returns pitch relative to first note of clip
91 | public int RelativePitch(int index)
92 | {
93 | if (Notes.Count == 0) return 0;
94 | return Notes[Math.Clamp(index, 0, Count)].Pitch - Notes[0].Pitch;
95 | }
96 |
97 | public override bool Equals(object obj)
98 | {
99 | return Equals(obj as Clip);
100 | }
101 |
102 | public bool Equals(Clip other)
103 | {
104 | if (other == null) return false;
105 | if (!(Length == other.Length && IsLooping == other.IsLooping && ClipReference == other.ClipReference &&
106 | RawClipReference == other.RawClipReference && Count == other.Count)) return false;
107 | for (var i = 0; i < Notes?.Count; i++)
108 | {
109 | if (Notes[i] != other.Notes[i])
110 | {
111 | return false;
112 | }
113 | }
114 | return true;
115 | }
116 |
117 | public override int GetHashCode()
118 | {
119 | return HashCode.Combine(Length, IsLooping, ClipReference, Count, Notes.GetHashCode());
120 | }
121 | }
--------------------------------------------------------------------------------
/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/LiveConnector/sockets.js:
--------------------------------------------------------------------------------
1 | const maxApi = require("max-api");
2 | const signalR = require("@microsoft/signalr");
3 | const signalRMsgPack = require("@microsoft/signalr-protocol-msgpack");
4 | const MESSAGE_TYPES = maxApi.MESSAGE_TYPES;
5 |
6 | const commandSignifiers = {
7 | logMessage: 1,
8 | setClipData: 2,
9 | setFormula: 3,
10 | setAndEvaluateFormula: 4,
11 | setAndEvaluateClipData: 5,
12 | evaluateFormulas: 6,
13 | resetUpdatedInternally: 7,
14 | };
15 |
16 | const live11Flag = 128; // 0x80
17 |
18 | let connection = new signalR.HubConnectionBuilder()
19 | .withUrl("http://localhost:5000/mutatefulHub?username=live")
20 | .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol())
21 | .build();
22 |
23 | connection.on("SetClipDataOnClient", (isLive11, data) => {
24 | isLive11 = isLive11 > 0;
25 | debuglog("SetClipDataOnClient: Received data, Live 11: ", isLive11);
26 | maxApi.outlet(["handleIncomingData", ...data]);
27 | // maxApi.outlet(isLive11, data);
28 | });
29 |
30 | maxApi.addHandler(MESSAGE_TYPES.LIST, async (...args) => {
31 | //maxApi.post(`received list: ${args.join(", ")}`);
32 | if (args.length === 0) return;
33 | let commandSignifier = args[0];
34 | let isLive11 = (commandSignifier & live11Flag) > 0;
35 | try {
36 | switch (commandSignifier & 0x0f) {
37 | case commandSignifiers.setClipData:
38 | await connection.invoke("SetClipData", isLive11, new Uint8Array(args.slice(1)));
39 | break;
40 | case commandSignifiers.setFormula:
41 | await connection.invoke("SetFormula", new Uint8Array(args.slice(1)));
42 | break;
43 | case commandSignifiers.setAndEvaluateClipData:
44 | await connection.invoke("SetAndEvaluateClipData", isLive11, new Uint8Array(args.slice(1)));
45 | break;
46 | case commandSignifiers.setAndEvaluateFormula:
47 | await connection.invoke("SetAndEvaluateFormula", isLive11, new Uint8Array(args.slice(1)));
48 | break;
49 | case commandSignifiers.evaluateFormulas:
50 | await connection.invoke("EvaluateFormulas", isLive11);
51 | break;
52 | case commandSignifiers.logMessage:
53 | await connection.invoke("LogMessage", new Uint8Array(args.slice(1)));
54 | break;
55 | }
56 | } catch (err) {
57 | maxApi.post(`Error occurred when invoking command with id ${commandSignifier & 0x0f}`);
58 | }
59 | });
60 |
61 | connection.start()
62 | .then(() => {
63 | maxApi.post("Connection active.");
64 | // connection.invoke("GetMessageFromClient", "Hello server this is max4l");
65 | // connection.invoke("LogMessage", new Uint8Array([60,61,62,63,64,65]));
66 | }, () => {
67 | maxApi.post("Failed to connect. Make sure mutateful app is running.");
68 | });
69 |
70 | function debuglog(/* ... args */) {
71 | let result = "";
72 | for (let i = 0; i < arguments.length; i++) {
73 | result += (i !== 0 && i < arguments.length ? " " : "") + debugPost(arguments[i], "");
74 | }
75 | maxApi.post(result + "\r\n");
76 | }
77 |
78 | function debugPost(val, res) {
79 | if (Array.isArray(val)) {
80 | res += "[";
81 | for (let i = 0; i < val.length; i++) {
82 | let currentVal = val[i];
83 | if (currentVal === undefined || currentVal === null) {
84 | res += ".";
85 | continue;
86 | }
87 | res = debugPost(currentVal, res);
88 | if (i < val.length - 1) res += ", ";
89 | }
90 | res += "]";
91 | } else if ((typeof val === "object") && (val !== null)) {
92 | let props = Object.getOwnPropertyNames(val);
93 | res += "{";
94 | for (let ii = 0; ii < props.length; ii++) {
95 | res += props[ii] + ": ";
96 | res = debugPost(val[props[ii]], res);
97 | if (ii < props.length - 1) res += ", ";
98 | }
99 | res += "}";
100 | } else {
101 | res += val;
102 | }
103 | return res;
104 | }
105 |
--------------------------------------------------------------------------------
/src/Mutateful/IO/CommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace Mutateful.IO;
4 |
5 | public class CommandHandler
6 | {
7 | private readonly ClipSet ClipSet;
8 |
9 | public CommandHandler()
10 | {
11 | ClipSet = new ClipSet();
12 | }
13 |
14 | public void SetFormula(int trackNo, int clipNo, string formula)
15 | {
16 | var parsedFormula = Parser.ParseFormula(formula);
17 | if (parsedFormula.Success)
18 | {
19 | var clipRef = new ClipReference(trackNo, clipNo);
20 | var clipSlot = new ClipSlot(formula, new Clip(clipRef), parsedFormula.Result);
21 | ClipSet[clipSlot.ClipReference] = clipSlot;
22 | }
23 | }
24 |
25 | public void SetClipData(Clip clip)
26 | {
27 | Console.WriteLine();
28 | if (clip.Equals(Clip.Empty)) return;
29 | var clipSlot = new ClipSlot("", clip, Formula.Empty);
30 | ClipSet[clipSlot.ClipReference] = clipSlot;
31 | }
32 |
33 | public CommandHandlerResult EvaluateFormulas()
34 | {
35 | if (!ClipSet.AllReferencedClipsValid()) return CommandHandlerResult.AbortedResult with {Errors = new List {"Not all referenced clips are valid - aborting evaluation of formulas."}};
36 |
37 | var orderedClipRefs = ClipSet.GetClipReferencesInProcessableOrder();
38 | if (!orderedClipRefs.Success) return CommandHandlerResult.AbortedResult with {Errors = new List {"Unable to fetch clip references in processable order - aborting evaluation of formulas."}};
39 | Console.WriteLine($"Clips to process: {string.Join(", ", orderedClipRefs.Result.Select(x => x.ToString()))}");
40 |
41 | var clipsToProcess = ClipSet.GetClipSlotsFromClipReferences(orderedClipRefs.Result);
42 | var (successfulClips, errors) = ClipSet.ProcessClips(clipsToProcess);
43 | return CommandHandlerResult.CompletedResult with { SuccessfulClips = ClipSet.GetClipsFromClipReferences(successfulClips).ToList(), Errors = errors };
44 | }
45 |
46 | public CommandHandlerResult SetAndEvaluateClipData(Clip clipToEvaluate)
47 | {
48 | if (ClipSet[clipToEvaluate.ClipReference] != ClipSlot.Empty && clipToEvaluate.Equals(ClipSet[clipToEvaluate.ClipReference].Clip))
49 | return CommandHandlerResult.AbortedResult with {Warnings = new List {$"Aborted evaluation of clip at {clipToEvaluate.ClipReference.ToString()} since it was unchanged."}};
50 | var clipSlot = new ClipSlot("", clipToEvaluate, Formula.Empty);
51 | ClipSet[clipSlot.ClipReference] = clipSlot;
52 | var clipReferences = ClipSet.GetAllDependentClipRefsFromClipRef(clipSlot.ClipReference);
53 | var allClipsByClipRef = ClipSet.GetAllReferencedClipsByClipRef();
54 | var orderedClipReferences = ClipSet.GetClipReferencesInProcessableOrder(
55 | clipReferences.Distinct().ToDictionary(
56 | key => key,
57 | key => allClipsByClipRef[key]
58 | .Where(x => clipReferences.Contains(x))
59 | .ToList()));
60 |
61 | Debug.WriteLine($"Found dependencies: {string.Join(' ', clipReferences.Select(x => x.ToString()))}");
62 | Debug.WriteLine($"Found sorted: {string.Join(' ', orderedClipReferences.Result.Select(x => x.ToString()))}");
63 |
64 | var clipsToProcess = ClipSet.GetClipSlotsFromClipReferences(orderedClipReferences.Result);
65 | var (successfulClips, errors) = ClipSet.ProcessClips(clipsToProcess);
66 |
67 | return CommandHandlerResult.CompletedResult with { SuccessfulClips = ClipSet.GetClipsFromClipReferences(successfulClips).ToList(), Errors = errors };
68 | }
69 |
70 | public CommandHandlerResult SetAndEvaluateFormula(string formula, int trackNo, int clipNo)
71 | {
72 | var clipRef = new ClipReference(trackNo, clipNo);
73 | if (ClipSet[clipRef] != ClipSlot.Empty && ClipSet[clipRef].Name == formula) return CommandHandlerResult.AbortedResult with {Warnings = new List {$"Aborted evaluation of formula at {clipRef.ToString()} since it was unchanged."}};
74 | var (success, result, errorMessage) = Parser.ParseFormula(formula);
75 | if (!success) return CommandHandlerResult.AbortedResult with { Errors = new List { errorMessage } };
76 |
77 | var clipSlot = new ClipSlot(formula, new Clip(clipRef), result);
78 | ClipSet[clipSlot.ClipReference] = clipSlot;
79 | var (successfulClipRefs, errors) = ClipSet.ProcessClips(new [] {clipSlot});
80 | return CommandHandlerResult.CompletedResult with { SuccessfulClips = ClipSet.GetClipsFromClipReferences(successfulClipRefs).ToList(), Errors = errors };
81 | }
82 | }
--------------------------------------------------------------------------------
/src/Mutateful/Compiler/TreeToken.cs:
--------------------------------------------------------------------------------
1 | using static Mutateful.Compiler.TokenType;
2 |
3 | namespace Mutateful.Compiler;
4 |
5 | public class TreeToken
6 | {
7 | public TreeToken Parent { get; set; }
8 | public TokenType Type { get; }
9 | public string Value { get; }
10 | public int Position { get; }
11 | public Clip Clip { get; }
12 |
13 | public bool AllValuesFetched => CurrentIndex > 0 && CurrentIndex >= Children.Count && Children.All(x => x.AllValuesFetched);
14 |
15 | public ProcessResult FlattenedValues
16 | {
17 | // todo: test how this behaves with invalid input like e.g. 60|62 61 shuffle 1 2
18 | get {
19 | if (IsCommand)
20 | {
21 | var tokens = new List { new Token(this) };
22 | return ResolveChildren(tokens);
23 | }
24 | if (IsOperatorToken)
25 | {
26 | return Type switch
27 | {
28 | RepeatOperator => ResolveRepeat(),
29 | AlternationOperator => ResolveAlternate(),
30 | _ => ResolveUnsupportedOperator()
31 | };
32 | }
33 |
34 | CurrentIndex++;
35 | return new ProcessResult(new [] { new Token(this) });
36 | }
37 | }
38 |
39 | public readonly List Children = new();
40 | private int CurrentIndex;
41 |
42 | public TreeToken() { }
43 |
44 | public TreeToken(TokenType type, string value, int position)
45 | {
46 | Clip = Clip.Empty;
47 | Type = type;
48 | Value = value;
49 | Position = position;
50 | }
51 |
52 | public TreeToken(Token token)
53 | {
54 | Clip = token.Clip;
55 | Type = token.Type;
56 | Value = token.Value;
57 | Position = token.Position;
58 | }
59 |
60 | public TreeToken SetParent(TreeToken parent)
61 | {
62 | Parent = parent;
63 | return this;
64 | }
65 |
66 | public bool HasChildren => Children.Count > 0;
67 | public bool IsClipReference => Type == TokenType.ClipReference;
68 | public bool IsOption => Type > _OptionsBegin && Type < _OptionsEnd && Value.StartsWith('-');
69 | public bool IsCommand => Type > _CommandsBegin && Type < _CommandsEnd && !Value.StartsWith('-');
70 | public bool IsOperatorToken => Type > _OperatorsBegin && Type < _OperatorsEnd;
71 | public bool IsOptionValue => IsEnumValue || IsValue;
72 | public bool IsEnumValue => Type > _EnumValuesBegin && Type < _EnumValuesEnd;
73 | public bool IsValue => Type > _ValuesBegin && Type < _ValuesEnd;
74 | public bool IsPureValue => Type > _ValuesBegin && Type < _PureValuesEnd;
75 |
76 | private ProcessResult ResolveChildren(List tokens)
77 | {
78 | if (Children.Count == 0)
79 | {
80 | CurrentIndex++;
81 | return new ProcessResult(tokens.ToArray());
82 | }
83 | while (!AllValuesFetched)
84 | {
85 | foreach (var treeToken in Children)
86 | {
87 | var res = treeToken.FlattenedValues;
88 | if (res.Success)
89 | {
90 | tokens.AddRange(res.Result);
91 | CurrentIndex++;
92 | }
93 | else return res;
94 | }
95 | }
96 | return new ProcessResult(tokens.ToArray());
97 | }
98 |
99 | private ProcessResult ResolveRepeat()
100 | {
101 | var result = new List();
102 | if (!Children[0].IsPureValue || Children[1].Type != Number)
103 | {
104 | return new ProcessResult($"Unable to resolve REPEAT expression with values {Children[0].Value} and {Children[1].Value}");
105 | }
106 | var valueToRepeat = Children[0];
107 | var repCountStatus = Children[1].FlattenedValues;
108 | if (!repCountStatus.Success) return repCountStatus;
109 | if (int.TryParse(repCountStatus.Result[0].Value, out var repeatCount))
110 | {
111 | for (var index = 0; index < repeatCount; index++)
112 | {
113 | var res = valueToRepeat.FlattenedValues;
114 | if (res.Success) result.AddRange(res.Result);
115 | else return res;
116 | }
117 | CurrentIndex = Children.Count; // mark this node as completed
118 | }
119 | return new ProcessResult(result.ToArray());
120 | }
121 |
122 | private ProcessResult ResolveAlternate()
123 | {
124 | if (Children.Count < 2) return new ProcessResult("Unable to resolve ALTERNATE expression: At least two values are needed.");
125 | return Children[CurrentIndex++ % Children.Count].FlattenedValues;
126 | }
127 |
128 | private ProcessResult ResolveUnsupportedOperator()
129 | {
130 | return new ProcessResult($"Unsupported operator (value is {Value})");
131 | }
132 | }
--------------------------------------------------------------------------------
/src/MutatefulTests/OptionsTest.cs:
--------------------------------------------------------------------------------
1 | using Mutateful.Compiler;
2 | using Mutateful.Core;
3 | using NUnit.Framework;
4 |
5 | namespace MutatefulTests;
6 |
7 | [TestFixture]
8 | public class OptionsTest
9 | {
10 | class OptionsClassOne
11 | {
12 | public decimal DecimalValue { get; set; }
13 |
14 | public bool SimpleBoolFlag { get; set; }
15 |
16 | [OptionInfo(min: 1, max: 100)] public int IntValue { get; set; }
17 | }
18 |
19 | enum TestEnum
20 | {
21 | EnumValue1,
22 | EnumValue2
23 | }
24 |
25 | class OptionsClassTwo
26 | {
27 | public decimal[] DecimalValue { get; set; }
28 |
29 | public int[] IntValue { get; set; }
30 |
31 | public TestEnum EnumValue { get; set; }
32 | }
33 |
34 | class OptionsClassThree
35 | {
36 | [OptionInfo(min: 0f, max: 1f)] public decimal[] DecimalValues { get; set; }
37 |
38 | [OptionInfo(min: 1, max: 100)] public int[] IntValues { get; set; }
39 | }
40 |
41 | [Test]
42 | public void TestValues()
43 | {
44 | var options = new Dictionary>();
45 | options[TokenType.DecimalValue] = new List() { new Token(TokenType.MusicalDivision, "1/8", 0) };
46 | options[TokenType.IntValue] = new List() { new Token(TokenType.Number, "14", 0) };
47 | options[TokenType.SimpleBoolFlag] = new List();
48 | var (success, msg) =
49 | OptionParser.TryParseOptions(new Command { Options = options }, out OptionsClassOne parsedOptions);
50 | Assert.AreEqual(14, parsedOptions.IntValue);
51 | Assert.AreEqual(.5m, parsedOptions.DecimalValue);
52 | Assert.IsTrue(parsedOptions.SimpleBoolFlag);
53 | }
54 |
55 | [Test]
56 | public void TestMinMaxValue()
57 | {
58 | var options = new Dictionary>();
59 | options[TokenType.IntValue] = new List() { new Token(TokenType.Number, "1000", 0) };
60 | var (success, msg) =
61 | OptionParser.TryParseOptions(new Command { Options = options }, out OptionsClassOne parsedOptions);
62 | Assert.AreEqual(100, parsedOptions.IntValue);
63 | options[TokenType.IntValue] = new List() { new Token(TokenType.Number, "0", 0) };
64 | parsedOptions = new OptionsClassOne();
65 | (success, msg) = OptionParser.TryParseOptions(new Command { Options = options }, out parsedOptions);
66 | Assert.AreEqual(1, parsedOptions.IntValue);
67 | }
68 |
69 | [Test]
70 | public void TestMinMaxValues()
71 | {
72 | var options = new Dictionary>
73 | {
74 | [TokenType.DecimalValues] = new List
75 | {
76 | new Token(TokenType.Decimal, "-1.10", 0),
77 | new Token(TokenType.Decimal, ".5", 0),
78 | new Token(TokenType.Decimal, "1.5", 0)
79 | },
80 | [TokenType.IntValues] = new List
81 | {
82 | new Token(TokenType.Number, "-10", 0),
83 | new Token(TokenType.Number, "50", 0),
84 | new Token(TokenType.Number, "101", 0)
85 | }
86 | };
87 |
88 | var (success, _) =
89 | OptionParser.TryParseOptions(new Command { Options = options }, out OptionsClassThree parsedOptions);
90 | Assert.IsTrue(success);
91 | Assert.AreEqual(0m, parsedOptions.DecimalValues[0]);
92 | Assert.AreEqual(.5m, parsedOptions.DecimalValues[1]);
93 | Assert.AreEqual(1m, parsedOptions.DecimalValues[2]);
94 | Assert.AreEqual(1, parsedOptions.IntValues[0]);
95 | Assert.AreEqual(50, parsedOptions.IntValues[1]);
96 | Assert.AreEqual(100, parsedOptions.IntValues[2]);
97 | }
98 |
99 | [Test]
100 | public void TestListValues()
101 | {
102 | var options = new Dictionary>();
103 | options[TokenType.DecimalValue] = new List()
104 | { new Token(TokenType.MusicalDivision, "1/8", 0), new Token(TokenType.MusicalDivision, "1/16", 0) };
105 | options[TokenType.IntValue] = new List()
106 | {
107 | new Token(TokenType.Number, "14", 0), new Token(TokenType.Number, "2", 0),
108 | new Token(TokenType.Number, "900", 0)
109 | };
110 | var (success, msg) =
111 | OptionParser.TryParseOptions(new Command { Options = options }, out OptionsClassTwo parsedOptions);
112 | Assert.AreEqual(2, parsedOptions.DecimalValue.Length);
113 | Assert.AreEqual(3, parsedOptions.IntValue.Length);
114 | }
115 |
116 | [Test]
117 | public void TestEnumValues()
118 | {
119 | var options = new Dictionary>();
120 | options[TokenType.EnumValue] = new List() { new Token(TokenType.EnumValue, "enumvalue2", 0) };
121 | var (success, msg) =
122 | OptionParser.TryParseOptions(new Command { Options = options }, out OptionsClassTwo parsedOptions);
123 | Assert.AreEqual(TestEnum.EnumValue2, parsedOptions.EnumValue);
124 | }
125 | }
--------------------------------------------------------------------------------
/src/Mutateful/Hubs/MutatefulHub.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using Decoder = Mutateful.IO.Decoder;
3 |
4 | namespace Mutateful.Hubs;
5 |
6 | public class MutatefulHub : Hub
7 | {
8 | private readonly CommandHandler CommandHandler;
9 | private static readonly ConcurrentDictionary Connections = new ();
10 |
11 | public MutatefulHub(CommandHandler commandHandler)
12 | {
13 | CommandHandler = commandHandler;
14 | }
15 |
16 | public override async Task OnConnectedAsync()
17 | {
18 | var httpContext = Context.GetHttpContext();
19 | var username = httpContext?.Request.Query["username"].ToString() ?? "unknown";
20 | var connectionId = Context.ConnectionId;
21 |
22 | Connections.TryAdd(username, connectionId);
23 |
24 | Console.WriteLine($"Client {username} hooked up: {connectionId}");
25 | await base.OnConnectedAsync();
26 | }
27 |
28 | public override async Task OnDisconnectedAsync(Exception exception)
29 | {
30 | string username = Context.User?.Identity?.Name ?? "unknown";
31 |
32 | Connections.TryRemove(username, out _);
33 | Console.WriteLine($"Client {username} disconnected");
34 |
35 | await base.OnDisconnectedAsync(exception);
36 | }
37 |
38 | public Task DoHandshake()
39 | {
40 | return Task.FromResult(Context.ConnectionId);
41 | }
42 |
43 | public Task SetClipData(bool isLive11, byte[] data)
44 | {
45 | var clip = isLive11 ? Decoder.GetSingleLive11Clip(data) : Decoder.GetSingleClip(data);
46 | Console.WriteLine($"{clip.ClipReference.Track}, {clip.ClipReference.Clip} Incoming clip data");
47 | Clients.All.SetClipDataOnWebUI(clip.ClipReference.ToString(), data);
48 | CommandHandler.SetClipData(clip);
49 | return Task.CompletedTask;
50 | }
51 |
52 | public Task SetFormula(byte[] data)
53 | {
54 | var (trackNo, clipNo, formula) = Decoder.GetFormula(data);
55 | Console.WriteLine($"{trackNo}, {clipNo}: Incoming formula {formula}");
56 | Clients.All.SetFormulaOnWebUI(trackNo, clipNo, formula);
57 | CommandHandler.SetFormula(trackNo, clipNo, formula);
58 | return Task.CompletedTask;
59 | }
60 |
61 | public async Task SetAndEvaluateClipData(bool isLive11, byte[] data)
62 | {
63 | var clip = isLive11 ? Decoder.GetSingleLive11Clip(data) : Decoder.GetSingleClip(data);
64 | Console.WriteLine($"{clip.ClipReference.Track}, {clip.ClipReference.Clip} Incoming clip data to evaluate");
65 | await Clients.All.SetClipDataOnWebUI(clip.ClipReference.ToString(), data);
66 | var result = CommandHandler.SetAndEvaluateClipData(clip);
67 |
68 | PrintErrorsAndWarnings(result);
69 | if (result.RanToCompletion == false) return;
70 |
71 | foreach (var successfulClip in result.SuccessfulClips)
72 | {
73 | await Clients.All.SetClipDataOnClient(isLive11,
74 | isLive11
75 | ? IOUtilities.GetClipAsBytesLive11(successfulClip).ToArray()
76 | : IOUtilities.GetClipAsBytesV2(successfulClip).ToArray());
77 | }
78 | }
79 |
80 | public async Task SetAndEvaluateFormula(bool isLive11, byte[] data)
81 | {
82 | var (trackNo, clipNo, formula) = Decoder.GetFormula(data);
83 | Console.WriteLine($"{trackNo}, {clipNo}: Incoming formula {formula}");
84 | await Clients.All.SetFormulaOnWebUI(trackNo, clipNo, formula);
85 | var result = CommandHandler.SetAndEvaluateFormula(formula, trackNo, clipNo);
86 |
87 | PrintErrorsAndWarnings(result);
88 | if (result.RanToCompletion == false) return;
89 |
90 | foreach (var clip in result.SuccessfulClips)
91 | {
92 | await Clients.All.SetClipDataOnClient(isLive11,
93 | isLive11
94 | ? IOUtilities.GetClipAsBytesLive11(clip).ToArray()
95 | : IOUtilities.GetClipAsBytesV2(clip).ToArray());
96 | }
97 | }
98 |
99 | public async Task EvaluateFormulas(bool isLive11)
100 | {
101 | var result = CommandHandler.EvaluateFormulas();
102 | PrintErrorsAndWarnings(result);
103 |
104 | foreach (var clip in result.SuccessfulClips)
105 | {
106 | await Clients.All.SetClipDataOnClient(isLive11,
107 | isLive11
108 | ? IOUtilities.GetClipAsBytesLive11(clip).ToArray()
109 | : IOUtilities.GetClipAsBytesV2(clip).ToArray());
110 | }
111 | }
112 |
113 | public Task LogMessage(byte[] data)
114 | {
115 | var text = Decoder.GetText(data);
116 | Console.WriteLine($"From client: {text}");
117 | return Task.CompletedTask;
118 | }
119 |
120 | private void PrintErrorsAndWarnings(CommandHandlerResult result)
121 | {
122 | if (result.Errors.Count > 0) Console.WriteLine("Evaluation produced errors:");
123 | foreach (var message in result.Errors)
124 | {
125 | Console.WriteLine(message);
126 | }
127 | if (result.Warnings.Count > 0) Console.WriteLine("Evaluation produced warnings:");
128 | foreach (var message in result.Warnings)
129 | {
130 | Console.WriteLine(message);
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Mutateful/Utility/IOUtilities.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using Decoder = Mutateful.IO.Decoder;
3 |
4 | namespace Mutateful.Utility;
5 |
6 | public static class IOUtilities
7 | {
8 | public static Clip StringToClip(string data)
9 | {
10 | if (data.IndexOf('[') >= 0 && data.IndexOf(']') >= 0)
11 | {
12 | data = data.Substring(data.IndexOf('[') + 1).Substring(0, data.IndexOf(']') - 1);
13 | }
14 | var metadataParts = new string[0];
15 | if (data.IndexOf(':') > 0)
16 | {
17 | var metadata = data.Substring(0, data.IndexOf(':'));
18 | metadataParts = metadata.Split(',');
19 | }
20 | var actualData = metadataParts.Length > 0 ? data.Substring(data.IndexOf(':') + 1) : data;
21 | var noteData = actualData.Split(' ');
22 | if (noteData.Length < 2)
23 | {
24 | return null;
25 | }
26 | decimal clipLength = decimal.Parse(noteData[0]);
27 | bool isLooping = noteData[1] == "1";
28 | var notes = new SortedList();
29 | for (var i = 2; i < noteData.Length; i += 4)
30 | {
31 | notes.Add(new NoteEvent(byte.Parse(noteData[i]), decimal.Parse(noteData[i + 1], NumberStyles.Any), decimal.Parse(noteData[i + 2], NumberStyles.Any), byte.Parse(noteData[i + 3])));
32 | }
33 | if (metadataParts.Length > 0)
34 | {
35 | return new Clip(clipLength, isLooping) { Notes = notes, ClipReference = new ClipReference(int.Parse(metadataParts[0]), int.Parse(metadataParts[1])) };
36 | }
37 | return new Clip(clipLength, isLooping) { Notes = notes };
38 | }
39 |
40 | public static string ClipToString(Clip clip)
41 | {
42 | string data = $"{clip.Length} {clip.IsLooping}";
43 | for (var i = 0; i < clip.Notes.Count; i++)
44 | {
45 | var note = clip.Notes[i];
46 | data = string.Join(' ', data, note.Pitch, note.Start.ToString("F5"), note.Duration.ToString("F5"), note.Velocity);
47 | }
48 | return data;
49 | }
50 |
51 | /*
52 | GetClipAsBytes: Convert Clip to array of bytes
53 |
54 | Format:
55 |
56 | 2 bytes (id)
57 | 4 bytes (clip length - float)
58 | 1 byte (loop state - 1/0 for on/off)
59 | 2 bytes (number of notes)
60 | 1 byte (pitch)
61 | 4 bytes (start - float)
62 | 4 bytes (duration - float)
63 | 1 byte (velocity)
64 |
65 | Above block repeated N times
66 | */
67 | public static List GetClipAsBytes(ushort id, Clip clip)
68 | {
69 | var result = new List(2 + 4 + 1 + 2 + (10 * clip.Notes.Count));
70 | result.AddRange(BitConverter.GetBytes(id));
71 | result.AddRange(BitConverter.GetBytes((float)clip.Length));
72 | result.Add((byte)(clip.IsLooping ? 1 : 0));
73 | result.AddRange(BitConverter.GetBytes((ushort)clip.Notes.Count));
74 |
75 | foreach (var note in clip.Notes)
76 | {
77 | if (note.Velocity == 0) continue;
78 | result.Add((byte)note.Pitch);
79 | result.AddRange(BitConverter.GetBytes((float)note.Start));
80 | result.AddRange(BitConverter.GetBytes((float)note.Duration));
81 | result.Add((byte)note.Velocity);
82 | }
83 | return result;
84 | }
85 |
86 | /*
87 | GetClipAsBytes: Convert Clip to array of bytes
88 |
89 | Format:
90 |
91 | 1 byte (track #)
92 | 1 byte (clip #)
93 | 4 bytes (clip length - float)
94 | 1 byte (loop state - on/off)
95 | 2 bytes (number of notes N)
96 | x bytes - note data as chunks of 10 bytes where
97 | 1 byte (pitch)
98 | 4 bytes (start - float)
99 | 4 bytes (duration - float)
100 | 1 byte (velocity)
101 |
102 | Above block repeated N times
103 |
104 | For MPE support: Add another chunk of MPE data for each note. Laying out the data this way
105 | will make it possible to support Live 10 as well, since the Live 10 connector can skip/disregard this data.
106 |
107 | */
108 |
109 | private static List SetClipDataHeader = new() {Decoder.TypedDataFirstByte, Decoder.TypedDataSecondByte, Decoder.TypedDataThirdByte, Decoder.SetClipDataOnServerSignifier};
110 |
111 | public static List GetClipAsBytesV2(Clip clip)
112 | {
113 | // todo: If supporting multiple clients, we need to also send formula (if specified) when sending clip data
114 | var result = new List(1 + 1 + 4 + 1 + 2 + (Decoder.SizeOfOneNoteInBytes * clip.Notes.Count));
115 | // result.AddRange(SetClipDataHeader);
116 | result.Add((byte)clip.ClipReference.Track);
117 | result.Add((byte)clip.ClipReference.Clip);
118 | result.AddRange(BitConverter.GetBytes((float)clip.Length));
119 | result.Add(clip.IsLooping ? (byte)1 : (byte)0);
120 | result.AddRange(BitConverter.GetBytes((ushort)clip.Notes.Count));
121 |
122 | foreach (var note in clip.Notes)
123 | {
124 | if (note.Velocity == 0) continue;
125 | result.Add((byte)note.Pitch);
126 | result.AddRange(BitConverter.GetBytes((float)note.Start));
127 | result.AddRange(BitConverter.GetBytes((float)note.Duration));
128 | result.Add((byte)note.Velocity);
129 | }
130 | return result;
131 | }
132 |
133 | public static List GetClipAsBytesLive11(Clip clip)
134 | {
135 | var result = new List(1 + 1 + 4 + 1 + 2 + (Decoder.SizeOfOneNoteInBytesLive11 * clip.Notes.Count));
136 | // result.AddRange(SetClipDataHeader);
137 | result.Add((byte)clip.ClipReference.Track);
138 | result.Add((byte)clip.ClipReference.Clip);
139 | result.AddRange(BitConverter.GetBytes((float)clip.Length));
140 | result.Add(clip.IsLooping ? (byte)1 : (byte)0);
141 | result.AddRange(BitConverter.GetBytes((ushort)clip.Notes.Count));
142 |
143 | foreach (var note in clip.Notes)
144 | {
145 | if (note.Velocity == 0) continue;
146 | result.Add((byte)note.Pitch);
147 | result.AddRange(BitConverter.GetBytes((float)note.Start));
148 | result.AddRange(BitConverter.GetBytes((float)note.Duration));
149 | result.AddRange(BitConverter.GetBytes(note.Velocity));
150 | result.AddRange(BitConverter.GetBytes(note.Probability));
151 | result.AddRange(BitConverter.GetBytes(note.VelocityDeviation));
152 | result.AddRange(BitConverter.GetBytes(note.ReleaseVelocity));
153 | }
154 | return result;
155 | }
156 | }
--------------------------------------------------------------------------------
/src/MutatefulTests/ParserTest.cs:
--------------------------------------------------------------------------------
1 | using Mutateful.Commands;
2 | using Mutateful.Compiler;
3 | using Mutateful.Core;
4 | using NUnit.Framework;
5 |
6 | namespace MutatefulTests;
7 |
8 | [TestFixture]
9 | public class ParserTest
10 | {
11 | private Clip Clip1 = new Clip(4, true)
12 | {
13 | Notes = new SortedList()
14 | {
15 | new NoteEvent(60, 0, .5m, 100), // C
16 | new NoteEvent(55, 1, .5m, 100), // G
17 | new NoteEvent(62, 2, .5m, 100) // D
18 | }
19 | };
20 |
21 | private Clip Clip2 = new Clip(4, true)
22 | {
23 | Notes = new SortedList()
24 | {
25 | new NoteEvent(47, 0, .5m, 100), // B
26 | new NoteEvent(63, 3, .5m, 100), // D#
27 | new NoteEvent(81, 4, .5m, 100) // A
28 | }
29 | };
30 |
31 | [Test]
32 | public void TestResolveClipReference()
33 | {
34 | Tuple result = Parser.ResolveClipReference("B4");
35 | Assert.AreEqual(result.Item1, 1);
36 | Assert.AreEqual(result.Item2, 3);
37 | }
38 |
39 | [Test]
40 | public void TestParseInvalidInput()
41 | {
42 | var result = Parser.ParseFormulaToChainedCommand("[0] interleave % fisk -by [1] -mode event",
43 | new List { Clip1, Clip2 }, new ClipMetaData(100, 0));
44 | Assert.IsFalse(result.Success);
45 | }
46 |
47 | [Test]
48 | public void TestParseInvalidInput2()
49 | {
50 | var result = Parser.ParseFormulaToChainedCommand("[0] slice /16", new List { Clip1, Clip2 },
51 | new ClipMetaData(100, 0));
52 | Assert.IsFalse(result.Success);
53 | }
54 |
55 | [Test]
56 | public void TestParseInvalidInputDoubleParam()
57 | {
58 | var result = Parser.ParseFormulaToChainedCommand("[0] arp -by [1] -by [2]",
59 | new List { Clip1, Clip2, Clip1 }, new ClipMetaData(100, 0));
60 | Assert.IsTrue(result.Success);
61 | }
62 |
63 | [Test]
64 | public void TestParseMusicalDivision()
65 | {
66 | var result = Parser.ParseFormulaToChainedCommand("[0] interleave -mode time -ranges 1/16 15/16 16/1",
67 | new List { Clip1 }, new ClipMetaData(100, 0));
68 | Assert.IsTrue(result.Success);
69 | }
70 |
71 | [Test]
72 | public void TestCastNumberToMusicalDivision()
73 | {
74 | var command = Parser.ParseFormulaToChainedCommand("[0] interleave -mode time -ranges 1/8 2 1 4",
75 | new List { Clip1 }, new ClipMetaData(100, 0));
76 | Assert.IsTrue(command.Success);
77 | var (success, _) = OptionParser.TryParseOptions(command.Result.Commands.First(), out InterleaveOptions options);
78 | Assert.IsTrue(success);
79 | Assert.AreEqual(0.5m, options.Ranges[0]);
80 | Assert.AreEqual(8, options.Ranges[1]);
81 | Assert.AreEqual(4, options.Ranges[2]);
82 | Assert.AreEqual(16, options.Ranges[3]);
83 | }
84 |
85 | [Test]
86 | public void TestConvertBarsBeatsSixteenths()
87 | {
88 | var command = Parser.ParseFormulaToChainedCommand(
89 | "[0] interleave -mode time -ranges 0.0.2 0.2.0 0.1.0 1.0.0 1.0.1", new List { Clip1 },
90 | new ClipMetaData(100, 0));
91 | Assert.IsTrue(command.Success);
92 | var (success, _) = OptionParser.TryParseOptions(command.Result.Commands.First(), out InterleaveOptions options);
93 | Assert.IsTrue(success);
94 | Assert.AreEqual(0.5m, options.Ranges[0]);
95 | Assert.AreEqual(2, options.Ranges[1]);
96 | Assert.AreEqual(1, options.Ranges[2]);
97 | Assert.AreEqual(4, options.Ranges[3]);
98 | Assert.AreEqual(4.25m, options.Ranges[4]);
99 | }
100 |
101 | [Test]
102 | public void TestCastNumberWhenNoImplicitCastSet()
103 | {
104 | var command =
105 | Parser.ParseFormulaToChainedCommand("[0] resize 1/8", new List { Clip1 }, new ClipMetaData(100, 0));
106 | Assert.IsTrue(command.Success);
107 | var (success, _) = OptionParser.TryParseOptions(command.Result.Commands.First(), out ResizeOptions _);
108 | Assert.IsFalse(success);
109 | command = Parser.ParseFormulaToChainedCommand("[0] resize 0.5", new List { Clip1 },
110 | new ClipMetaData(100, 0));
111 | (success, _) = OptionParser.TryParseOptions(command.Result.Commands.First(), out ResizeOptions _);
112 | Assert.IsTrue(success);
113 | }
114 |
115 | [Test]
116 | public void ShowGeneratedSyntaxTree()
117 | {
118 | var lexer = new Lexer("[0] tp 2 remap -to [0] shuffle 1 2|3|9x6|5|4 tp 12",
119 | new List