├── .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 res = ExtractPropertyData(property, tokens, noImplicitCast); 58 | if (!res.Success) 59 | { 60 | return (false, res.ErrorMessage); 61 | } 62 | 63 | property.SetMethod?.Invoke(result, res.Result); 64 | } 65 | } 66 | 67 | return (true, ""); 68 | } 69 | 70 | private static ProcessResult ExtractPropertyData(PropertyInfo property, IReadOnlyList tokens, bool noImplicitCast = false) 71 | { 72 | if (tokens.Count == 0) 73 | { 74 | if (property.PropertyType == typeof(bool)) 75 | { 76 | // handle simple bool flag 77 | return new ProcessResult(new object[] {true}); 78 | } 79 | 80 | return new ProcessResult($"Missing property value for non-bool parameter: {property.Name}"); 81 | } 82 | 83 | var type = tokens[0].Type; 84 | 85 | var rangeInfo = property 86 | .GetCustomAttributes(false) 87 | .Select(a => (OptionInfo) a) 88 | .FirstOrDefault(a => a.MaxNumberValue != null || a.MinNumberValue != null || a.MinDecimalValue != null || a.MaxDecimalValue != null); 89 | 90 | switch (type) 91 | { 92 | // todo: add some kind of safe guard here, for instance an upper limit on number sizes 93 | // handle single value 94 | case Number when property.PropertyType == typeof(decimal) && noImplicitCast: 95 | return new ProcessResult(new object[] {ClampIfSpecified(decimal.Parse(tokens[0].Value, CultureInfo.InvariantCulture), rangeInfo)}); 96 | case MusicalDivision when property.PropertyType == typeof(decimal) && !noImplicitCast: 97 | case Number when property.PropertyType == typeof(decimal) && !noImplicitCast: 98 | return new ProcessResult(new object[] {Utilities.MusicalDivisionToDecimal(tokens[0].Value)}); 99 | case BarsBeatsSixteenths when property.PropertyType == typeof(decimal) && !noImplicitCast: 100 | return new ProcessResult(new object[] {Utilities.BarsBeatsSixteenthsToDecimal(tokens[0].Value)}); 101 | case TokenType.Decimal when property.PropertyType == typeof(decimal): 102 | return new ProcessResult(new object[] {ClampIfSpecified(decimal.Parse(tokens[0].Value, CultureInfo.InvariantCulture), rangeInfo)}); 103 | case TokenType.Decimal when property.PropertyType == typeof(int) && !noImplicitCast: 104 | return new ProcessResult(new object[] {ClampIfSpecified((decimal) int.Parse(tokens[0].Value), rangeInfo)}); 105 | case InlineClip when property.PropertyType == typeof(Clip): 106 | return new ProcessResult(new object[] {tokens[0].Clip}); 107 | case Number when property.PropertyType == typeof(int): 108 | { 109 | if (int.TryParse(tokens[0].Value, out int value)) 110 | { 111 | return new ProcessResult(new object[] 112 | { 113 | ClampIfSpecified(value, rangeInfo) 114 | }); 115 | } 116 | return new ProcessResult($"Unable to parse value {tokens[0].Value} for parameter {property.Name}"); 117 | } 118 | 119 | default: 120 | { 121 | if (property.PropertyType.IsEnum) 122 | { 123 | if (Enum.TryParse(property.PropertyType, tokens[0].Value, true, out object result)) 124 | { 125 | return new ProcessResult(new[] {result}); 126 | } 127 | 128 | return new ProcessResult($"Enum {property.Name} does not support value {tokens[0].Value}"); 129 | } 130 | 131 | if (property.PropertyType == typeof(decimal[]) && !noImplicitCast && 132 | tokens.All(t => t.Type == Number || t.Type == MusicalDivision || t.Type == TokenType.Decimal || t.Type == BarsBeatsSixteenths)) 133 | { 134 | // handle implicit cast from number or MusicalDivision to decimal 135 | decimal[] values = tokens.Select(t => 136 | { 137 | if (t.Type == MusicalDivision || t.Type == Number) return Utilities.MusicalDivisionToDecimal(t.Value); 138 | if (t.Type == BarsBeatsSixteenths) return Utilities.BarsBeatsSixteenthsToDecimal(t.Value); 139 | return ClampIfSpecified(decimal.Parse(t.Value, CultureInfo.InvariantCulture), rangeInfo); 140 | }).ToArray(); 141 | return new ProcessResult(new object[] {values}); 142 | } 143 | 144 | if (tokens.Any(t => t.Type != type)) 145 | { 146 | return new ProcessResult("Invalid option values: Values for a single option need to be of the same type."); 147 | } 148 | 149 | switch (type) 150 | { 151 | case MusicalDivision when property.PropertyType == typeof(decimal[]) && !noImplicitCast: 152 | { 153 | decimal[] values = tokens.Select(t => Utilities.MusicalDivisionToDecimal(t.Value)).ToArray(); 154 | return new ProcessResult(new object[] {values}); 155 | } 156 | case BarsBeatsSixteenths when property.PropertyType == typeof(decimal[]) && !noImplicitCast: 157 | { 158 | decimal[] values = tokens.Select(t => Utilities.BarsBeatsSixteenthsToDecimal(t.Value)).ToArray(); 159 | return new ProcessResult(new object[] {values}); 160 | } 161 | case TokenType.Decimal when property.PropertyType == typeof(decimal[]): 162 | { 163 | decimal[] values = tokens.Select(t => ClampIfSpecified(decimal.Parse(t.Value, CultureInfo.InvariantCulture), rangeInfo)).ToArray(); 164 | return new ProcessResult(new object[] {values}); 165 | } 166 | case Number when property.PropertyType == typeof(int[]): 167 | { 168 | int[] values = tokens.Select(t => ClampIfSpecified(int.Parse(t.Value), rangeInfo)).ToArray(); 169 | return new ProcessResult(new object[] {values}); 170 | } 171 | default: 172 | return new ProcessResult($"Invalid combination. Token of type {type.ToString()} and property of type {property.PropertyType.Name} are not compatible."); 173 | } 174 | } 175 | } 176 | } 177 | 178 | private static int ClampIfSpecified(int value, OptionInfo rangeInfo) 179 | { 180 | if (value > rangeInfo?.MaxNumberValue) 181 | { 182 | value = (int) rangeInfo.MaxNumberValue; 183 | } 184 | 185 | if (value < rangeInfo?.MinNumberValue) 186 | { 187 | value = (int) rangeInfo.MinNumberValue; 188 | } 189 | 190 | return value; 191 | } 192 | 193 | private static decimal ClampIfSpecified(decimal value, OptionInfo rangeInfo) 194 | { 195 | if (rangeInfo?.MinDecimalValue == null && rangeInfo?.MaxDecimalValue == null) return value; 196 | 197 | if (rangeInfo.MinDecimalValue != null) 198 | { 199 | var minValue = (decimal) rangeInfo.MinDecimalValue; 200 | if (value < minValue) value = minValue; 201 | } 202 | 203 | if (rangeInfo.MaxDecimalValue != null) 204 | { 205 | var maxValue = (decimal) rangeInfo.MaxDecimalValue; 206 | if (value > maxValue) value = maxValue; 207 | } 208 | 209 | return value; 210 | } 211 | } -------------------------------------------------------------------------------- /src/Mutateful/Compiler/Token.cs: -------------------------------------------------------------------------------- 1 | using static Mutateful.Compiler.TokenType; 2 | 3 | namespace Mutateful.Compiler; 4 | 5 | public class Token 6 | { 7 | public TokenType Type { get; set; } 8 | public string Value { get; } 9 | public int Position { get; } 10 | public Clip Clip { get; set; } 11 | 12 | public Token(TokenType type, string value, int position) 13 | { 14 | Clip = Clip.Empty; 15 | Type = type; 16 | Value = value; 17 | Position = position; 18 | } 19 | 20 | public Token(Token token) 21 | { 22 | Clip = token.Clip; 23 | Type = token.Type; 24 | Value = token.Value; 25 | Position = token.Position; 26 | } 27 | 28 | public Token(TreeToken treeToken) 29 | { 30 | Clip = treeToken.Clip; 31 | Type = treeToken.Type; 32 | Value = treeToken.Value; 33 | Position = treeToken.Position; 34 | } 35 | 36 | public Token(TokenType type, string value, Clip clip, int position) : this(type, value, position) 37 | { 38 | Clip = clip; 39 | } 40 | 41 | public bool IsClipReference => Type == TokenType.ClipReference; 42 | public bool IsOption => Type > _OptionsBegin && Type < _OptionsEnd && Value.StartsWith('-'); 43 | public bool IsCommand => Type > _CommandsBegin && Type < _CommandsEnd && !Value.StartsWith('-'); 44 | public bool IsOperatorToken => Type > _OperatorsBegin && Type < _OperatorsEnd; 45 | public bool IsOptionValue => IsEnumValue || IsValue; 46 | public bool IsEnumValue => Type > _EnumValuesBegin && Type < _EnumValuesEnd; 47 | public bool IsValue => Type > _ValuesBegin && Type < _ValuesEnd; 48 | public bool IsPureValue => Type > _ValuesBegin && Type < _PureValuesEnd; 49 | } -------------------------------------------------------------------------------- /src/Mutateful/Compiler/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Compiler; 2 | 3 | public enum TokenType 4 | { 5 | NoToken, 6 | Root, 7 | 8 | _CommandsBegin, 9 | Arpeggiate, 10 | Concat, 11 | Crop, 12 | Echo, 13 | Extract, 14 | Filter, 15 | Interleave, 16 | InterleaveEvent, 17 | Legato, 18 | Loop, 19 | Mask, 20 | Monophonize, 21 | Padding, 22 | Quantize, 23 | Ratchet, 24 | Relength, 25 | Remap, 26 | Resize, 27 | Scan, 28 | SetLength, 29 | SetPitch, 30 | SetRhythm, 31 | Scale, 32 | Shuffle, 33 | Slice, 34 | Sustain, 35 | Take, 36 | Transpose, 37 | VelocityScale, 38 | 39 | _OptionsBegin, 40 | Invert, 41 | Skip, // Need to find a better way of supporting token names that can signify both options and commands. Time to stop using an enum for this I guess. For now we have this quick fix though. 42 | _CommandsEnd, 43 | Amount, 44 | AutoScale, 45 | By, 46 | ChunkChords, 47 | ControlMax, 48 | ControlMin, 49 | Count, 50 | Divisions, 51 | Duration, 52 | Echoes, 53 | EnableMask, 54 | Factor, 55 | HighPitch, 56 | HighVelocity, 57 | Length, 58 | Lengths, 59 | LowPitch, 60 | LowVelocity, 61 | Magnetic, 62 | Max, 63 | Min, 64 | Mode, 65 | PadAmount, 66 | Pitch, 67 | PitchValues, 68 | Position, 69 | PositionAware, 70 | Post, 71 | Ranges, 72 | RatchetValues, 73 | RemoveOffset, 74 | Repeats, 75 | Rescale, 76 | Shape, 77 | ShuffleValues, 78 | SkipCounts, 79 | Solo, 80 | Start, 81 | Strength, 82 | Strict, 83 | TakeCounts, 84 | Thin, 85 | TransposeValues, 86 | Threshold, 87 | To, 88 | VelocityToStrength, 89 | Window, 90 | With, 91 | _OptionsEnd, 92 | 93 | _EnumValuesBegin, 94 | Absolute, 95 | Both, 96 | EaseIn, 97 | EaseInOut, 98 | EaseOut, 99 | Event, 100 | Linear, 101 | Overwrite, 102 | Velocity, 103 | Pitches, // todo: Quickfix to avoid conflict with Pitch. Need to find a better solution here... 104 | Relative, 105 | Rhythm, 106 | Time, 107 | Major, 108 | Minor, 109 | Ionian, 110 | Dorian, 111 | Phrygian, 112 | Lydian, 113 | Mixolydian, 114 | Aeolian, 115 | Locrian, 116 | _EnumValuesEnd, 117 | 118 | _ValuesBegin, 119 | BarsBeatsSixteenths, 120 | Decimal, 121 | MusicalDivision, 122 | Number, 123 | _PureValuesEnd, 124 | ClipReference, 125 | InlineClip, 126 | _ValuesEnd, 127 | 128 | _OperatorsBegin, 129 | RangeOperator, 130 | AlternationOperator, 131 | EmptyOperator, 132 | RepeatOperator, 133 | FillOperator, 134 | _OperatorsEnd, 135 | 136 | _TestOptionsBegin, 137 | DecimalValue, 138 | DecimalValues, 139 | IntValue, 140 | IntValues, 141 | EnumValue, 142 | SimpleBoolFlag, 143 | _TestOptionsEnd, 144 | 145 | _TestEnumValuesBegin, 146 | EnumValue1, 147 | EnumValue2, 148 | _TestEnumValuesEnd, 149 | 150 | LeftParen, 151 | RightParen, 152 | Nested 153 | } -------------------------------------------------------------------------------- /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/Mutateful/Core/ChainedCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class ChainedCommand 4 | { 5 | public List Commands { get; } 6 | public Clip[] SourceClips { get; } 7 | public ClipMetaData TargetMetaData { get; } 8 | 9 | public static readonly ChainedCommand Empty = new ChainedCommand(); 10 | 11 | public ChainedCommand(List commands, Clip[] sourceClips, ClipMetaData targetMetadata) 12 | { 13 | Commands = commands; 14 | SourceClips = sourceClips; 15 | TargetMetaData = targetMetadata; 16 | } 17 | 18 | public ChainedCommand() 19 | { 20 | Commands = new List(); 21 | SourceClips = new Clip[0]; 22 | TargetMetaData = new ClipMetaData(); 23 | } 24 | } -------------------------------------------------------------------------------- /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/Core/ClipMetaData.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public struct ClipMetaData 4 | { 5 | public ushort Id; 6 | public byte TrackNumber; 7 | 8 | public ClipMetaData(ushort id, byte trackNumber) 9 | { 10 | Id = id; 11 | TrackNumber = trackNumber; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/ClipReference.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public record struct ClipReference(int Track, int Clip) 4 | { 5 | public static ClipReference FromString(string clipRef) 6 | { 7 | if (TryParse(clipRef, out var result)) 8 | { 9 | return result; 10 | } 11 | throw new ArgumentException($"Unable to parse given ClipReference: {clipRef}"); 12 | } 13 | 14 | private static bool TryParse(string clipRef, out ClipReference clipReference) 15 | { 16 | clipReference = new ClipReference(0, 0); 17 | var val = clipRef.ToUpperInvariant(); 18 | int lastAlphaIx = 0; 19 | 20 | while (lastAlphaIx < val.Length && val[lastAlphaIx] >= 'A' && val[lastAlphaIx] <= 'Z') lastAlphaIx++; 21 | 22 | if (lastAlphaIx == 0) return false; 23 | clipReference.Track = FromSpreadshimal(val[..lastAlphaIx]); 24 | var clipParsedSuccessfully = int.TryParse(val[lastAlphaIx..], out var clip); 25 | clipReference.Clip = clip; 26 | return clipParsedSuccessfully; 27 | } 28 | 29 | public static string ToSpreadshimal(int val) 30 | { 31 | if (val <= 0) return ""; 32 | var digits = new List(); 33 | 34 | while (val-- > 0) 35 | { 36 | var remainder = val % 26; 37 | val /= 26; 38 | digits.Add(ToSpreadshimalDigit(remainder)); 39 | } 40 | digits.Reverse(); 41 | return new string(digits.ToArray()); 42 | } 43 | 44 | public static int FromSpreadshimal(string val) 45 | { 46 | if (val.Length == 0) return 0; 47 | var digits = val.ToCharArray(); 48 | var decimalVal = 0; 49 | 50 | for (var i = 1; i <= digits.Length; i++) 51 | { 52 | decimalVal += FromSpreadshimalDigit(digits[^i]) * (int) Math.Pow(26, i - 1); 53 | } 54 | return decimalVal; 55 | } 56 | 57 | private static char ToSpreadshimalDigit(int val) 58 | { 59 | return (char)(65 + val); 60 | } 61 | 62 | private static int FromSpreadshimalDigit(char val) 63 | { 64 | return val switch 65 | { 66 | >= 'A' and <= 'Z' => val - 64, 67 | >= 'a' and <= 'z' => val - 96, 68 | _ => 0 69 | }; 70 | } 71 | 72 | public override string ToString() 73 | { 74 | return $"{ToSpreadshimal(Track)}{Clip}"; 75 | } 76 | 77 | public static readonly ClipReference Empty = new ClipReference(0, 0); 78 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/Command.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class Command 4 | { 5 | public TokenType Id { get; set; } 6 | public Dictionary> Options { get; set; } = new Dictionary>(); 7 | public List DefaultOptionValues { get; set; } = new List(); 8 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/DecimalCounter.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class DecimalCounter 4 | { 5 | public decimal Value { get; private set; } 6 | 7 | private decimal Max { get; } 8 | 9 | public bool Overflow { get; private set; } 10 | 11 | public DecimalCounter(decimal max) 12 | { 13 | Max = max; 14 | } 15 | 16 | public void Inc(decimal amount) 17 | { 18 | Value += amount; 19 | if (Value >= Max) 20 | { 21 | Value = 0; 22 | Overflow = true; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/Formula.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class Formula 4 | { 5 | public Dictionary ClipSlotsByClipReference { get; } 6 | public List Commands { get; } 7 | public List SourceClipReferences { get; } 8 | public List AllReferencedClips { get; } 9 | 10 | public static readonly Formula Empty = new Formula(); 11 | 12 | public Formula() : this(new List(), new List(), new List(), new Dictionary()) { } 13 | 14 | public Formula(List commands, List sourceClipReferences, List allReferencedClips, Dictionary clipSlotsByClipReference) 15 | { 16 | Commands = commands; 17 | SourceClipReferences = sourceClipReferences; 18 | AllReferencedClips = allReferencedClips; 19 | ClipSlotsByClipReference = clipSlotsByClipReference; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/IntCounter.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class IntCounter 4 | { 5 | public int Value { get; private set; } 6 | 7 | private int Max { get; } 8 | 9 | public bool Overflow { get; private set; } 10 | 11 | public IntCounter(int max) 12 | { 13 | Max = max; 14 | } 15 | 16 | public void Inc() 17 | { 18 | Inc(1); 19 | } 20 | 21 | public void Inc(int amount) 22 | { 23 | Value += amount; 24 | if (Value >= Max) 25 | { 26 | Value = 0; 27 | Overflow = true; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/NoteEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public record NoteEvent(float VelocityDeviation = 0, float ReleaseVelocity = 64, float Probability = 1) : IComparable 4 | { 5 | private int PitchField; 6 | private float VelocityField; 7 | private decimal StartField; 8 | 9 | public int Pitch 10 | { 11 | get => PitchField & 0x7F; 12 | set { 13 | if (HasChildren) 14 | { 15 | var delta = value - PitchField; 16 | Children.ForEach(c => c.Pitch += delta); 17 | } 18 | PitchField = value; 19 | } 20 | } 21 | 22 | public decimal Duration { get; set; } 23 | 24 | public float Velocity 25 | { 26 | get => Math.Clamp(VelocityField, 0f, 127f); 27 | set => VelocityField = value; 28 | } 29 | 30 | public decimal End => Start + Duration; 31 | 32 | public List Children = EmptyList; 33 | 34 | private static readonly List EmptyList = new(); 35 | 36 | public bool HasChildren => Children?.Count > 0; 37 | 38 | public decimal Start 39 | { 40 | get => StartField; 41 | set 42 | { 43 | StartField = value; 44 | if (HasChildren) 45 | { 46 | Children.ForEach(c => c.Start = StartField); 47 | } 48 | } 49 | } 50 | 51 | public NoteEvent Parent { get; set; } 52 | 53 | public NoteEvent(int pitch, decimal start, decimal duration, float velocity, float probability = 1, 54 | float velocityDeviation = 0, float releaseVelocity = 64) : this(velocityDeviation, releaseVelocity, probability) 55 | { 56 | Pitch = pitch; 57 | Start = start; 58 | Velocity = velocity; 59 | Duration = duration; 60 | } 61 | 62 | public int CompareTo(NoteEvent b) 63 | { 64 | if (Start < b.Start) 65 | { 66 | return -1; 67 | } 68 | if (Start > b.Start) 69 | { 70 | return 1; 71 | } 72 | if (Pitch < b.Pitch) 73 | { 74 | return -1; 75 | } 76 | if (Pitch > b.Pitch) 77 | { 78 | return 1; 79 | } 80 | return 0; 81 | } 82 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/OptionInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public enum OptionType 4 | { 5 | //AllOrSpecified, // If none specified, all are active. Otherwise, only specified options are active. 6 | Value, // Option that takes one or more values. 7 | Default // If values are given without an option header, use them for this parameter. 8 | } 9 | 10 | [AttributeUsage(AttributeTargets.Property)] 11 | public class OptionInfo : Attribute 12 | { 13 | public OptionType Type { get; } = OptionType.Value; 14 | public int? MinNumberValue { get; } 15 | public int? MaxNumberValue { get; } 16 | public float? MinDecimalValue { get; } 17 | public float? MaxDecimalValue { get; } 18 | public bool NoImplicitCast { get; } // Decimal values are usually assumed to be interchangeable with MusicalDivision and Number (i.e. numbers and musical divisions are converted to decimal values). Specify this to avoid this implicit casting, useful for some decimal parameters such as scale factors and such 19 | 20 | public OptionInfo(OptionType type) 21 | { 22 | Type = type; 23 | } 24 | 25 | public OptionInfo(OptionType type, bool noImplicitCast) : this(type) 26 | { 27 | NoImplicitCast = noImplicitCast; 28 | } 29 | 30 | public OptionInfo(int min, int max) 31 | { 32 | MinNumberValue = min; 33 | MaxNumberValue = max; 34 | } 35 | 36 | public OptionInfo(int min) 37 | { 38 | MinNumberValue = min; 39 | } 40 | 41 | public OptionInfo(OptionType type, int min, int max, bool noImplicitCast = false) : this(type, noImplicitCast) 42 | { 43 | MinNumberValue = min; 44 | MaxNumberValue = max; 45 | } 46 | 47 | public OptionInfo(OptionType type, int min, bool noImplicitCast = false) : this(type, noImplicitCast) 48 | { 49 | MinNumberValue = min; 50 | } 51 | 52 | public OptionInfo(float min, float max) 53 | { 54 | MinDecimalValue = min; 55 | MaxDecimalValue = max; 56 | } 57 | 58 | public OptionInfo(float min) 59 | { 60 | MinDecimalValue = min; 61 | } 62 | 63 | public OptionInfo(OptionType type, float min, float max, bool noImplicitCast = false) : this(type, noImplicitCast) 64 | { 65 | MinDecimalValue = min; 66 | MaxDecimalValue = max; 67 | } 68 | 69 | public OptionInfo(OptionType type, float min, bool noImplicitCast = false) : this(type, noImplicitCast) 70 | { 71 | MinDecimalValue = min; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/OptionsDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class OptionsDefinition 4 | { 5 | public OptionGroup[] OptionGroups; 6 | } 7 | 8 | public class OptionGroup 9 | { 10 | public TokenType[] Options; 11 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/ProcessResult.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class ProcessResult 4 | { 5 | public bool Success { get; } 6 | public T Result { get; } 7 | public string ErrorMessage { get; } 8 | 9 | public string WarningMessage { get; } 10 | 11 | public ProcessResult(bool success, T result, string errorMessage, string warningMessage = "") 12 | { 13 | Success = success; 14 | Result = result; 15 | ErrorMessage = errorMessage; 16 | WarningMessage = warningMessage; 17 | } 18 | 19 | public ProcessResult(string errorMessage) : this(false, default, $"Error: {errorMessage}") { } 20 | 21 | public ProcessResult(string errorMessage, string header) : this(false, default, $"{header}: {errorMessage}") { } 22 | 23 | public ProcessResult(T result) : this(true, result, "") { } 24 | 25 | public ProcessResult(T result, string warningMessage) : this(true, result, "", warningMessage) { } 26 | 27 | public void Deconstruct(out bool success, out T result, out string errorMessage) 28 | { 29 | success = Success; 30 | result = Result; 31 | errorMessage = ErrorMessage; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/Result.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class Result 4 | { 5 | public bool Success { get; } 6 | public string ErrorMessage { get; } 7 | 8 | public Result(bool success, string errorMessage) 9 | { 10 | Success = success; 11 | ErrorMessage = errorMessage; 12 | } 13 | 14 | public Result(string errorMessage) : this(false, $"Error: {errorMessage}") { } 15 | 16 | public Result(string errorMessage, string header) : this(false, $"{header}: {errorMessage}") { } 17 | } -------------------------------------------------------------------------------- /src/Mutateful/Core/SortedList.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Core; 2 | 3 | public class SortedList : IEnumerable, IEquatable 4 | { 5 | private readonly List ListField; 6 | private readonly IComparer ComparerField; 7 | 8 | public int Count => ListField.Count; 9 | 10 | public bool IsReadOnly => ((IList)ListField).IsReadOnly; 11 | 12 | public T this[int index] => ListField[index]; 13 | 14 | public static readonly SortedList Empty = new(); 15 | 16 | public SortedList(IComparer comparer = null) 17 | { 18 | ComparerField = comparer ?? Comparer.Default; 19 | ListField = new List(); 20 | } 21 | 22 | public SortedList(IEnumerable items) : this(comparer: null) 23 | { 24 | AddRange(items); 25 | } 26 | 27 | public void Add(T item) 28 | { 29 | if (ListField.Contains(item)) return; 30 | var index = ListField.BinarySearch(item); 31 | ListField.Insert(index < 0 ? ~index : index, item); 32 | } 33 | 34 | public void AddRange(List items) 35 | { 36 | items.ForEach(x => Add(x)); 37 | } 38 | 39 | public void AddRange(IEnumerable items) 40 | { 41 | foreach (var item in items) 42 | { 43 | Add(item); 44 | } 45 | } 46 | 47 | public int IndexOf(T item) 48 | { 49 | return ListField.IndexOf(item); 50 | } 51 | 52 | public void RemoveAt(int index) 53 | { 54 | ListField.RemoveAt(index); 55 | } 56 | 57 | public void Clear() 58 | { 59 | ListField.Clear(); 60 | } 61 | 62 | public bool Contains(T item) 63 | { 64 | return ListField.Contains(item); 65 | } 66 | 67 | public void CopyTo(T[] array, int arrayIndex) 68 | { 69 | ListField.CopyTo(array, arrayIndex); 70 | } 71 | 72 | public bool Remove(T item) 73 | { 74 | return ListField.Remove(item); 75 | } 76 | 77 | public IEnumerator GetEnumerator() 78 | { 79 | return ((IList)ListField).GetEnumerator(); 80 | } 81 | 82 | IEnumerator IEnumerable.GetEnumerator() 83 | { 84 | return ((IEnumerable)ListField).GetEnumerator(); 85 | } 86 | 87 | protected bool Equals(SortedList other) 88 | { 89 | if (ListField.Count != other.Count) return false; 90 | for (var i = 0; i < ListField.Count; i++) 91 | { 92 | if (!ListField[i].Equals(other[i])) 93 | { 94 | return false; 95 | } 96 | } 97 | return true; 98 | } 99 | 100 | public bool Equals(T other) 101 | { 102 | throw new NotImplementedException(); 103 | } 104 | 105 | public override bool Equals(object obj) 106 | { 107 | if (ReferenceEquals(null, obj)) return false; 108 | if (ReferenceEquals(this, obj)) return true; 109 | if (obj.GetType() != this.GetType()) return false; 110 | return Equals((SortedList)obj); 111 | } 112 | 113 | public override int GetHashCode() 114 | { 115 | return (ListField != null ? ListField.GetHashCode() : 0); 116 | } 117 | } -------------------------------------------------------------------------------- /src/Mutateful/GlobalImports.cs: -------------------------------------------------------------------------------- 1 | global using Mutateful; 2 | global using Mutateful.Hubs; 3 | global using Mutateful.IO; 4 | global using Mutateful.State; 5 | global using Mutateful.Compiler; 6 | global using Mutateful.Core; 7 | global using Mutateful.Utility; 8 | global using System.Text; 9 | global using System.Collections; 10 | global using Microsoft.AspNetCore.SignalR; 11 | -------------------------------------------------------------------------------- /src/Mutateful/Hubs/IMutatefulHub.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Hubs; 2 | 3 | public interface IMutatefulHub 4 | { 5 | Task SetClipDataOnClient(bool isLive11, byte[] data); 6 | Task SetClipDataOnWebUI(string clipRef, byte[] data); 7 | Task SetFormulaOnWebUI(int trackNo, int clipNo, string formula); 8 | } -------------------------------------------------------------------------------- /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/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/IO/CommandHandlerResult.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.IO; 2 | 3 | public record CommandHandlerResult(bool RanToCompletion, List SuccessfulClips, List Errors, List Warnings) 4 | { 5 | public static readonly CommandHandlerResult CompletedResult = 6 | new CommandHandlerResult(true, new List(), new List(), new List()); 7 | 8 | public static readonly CommandHandlerResult AbortedResult = 9 | new CommandHandlerResult(false, new List(), new List(), new List()); 10 | }; -------------------------------------------------------------------------------- /src/Mutateful/IO/Decoder.cs: -------------------------------------------------------------------------------- 1 | using static Mutateful.State.InternalCommandType; 2 | 3 | namespace Mutateful.IO; 4 | 5 | public static class Decoder 6 | { 7 | public const byte TypedDataFirstByte = 127; 8 | public const byte TypedDataSecondByte = 126; 9 | public const byte TypedDataThirdByte = 125; 10 | public const byte TypedDataThirdByteLive11Mode = 128; 11 | public const int SizeOfOneNoteInBytes = 10; 12 | public const int SizeOfOneNoteInBytesLive11 = 25; 13 | 14 | public const byte StringDataSignifier = 124; 15 | public const byte SetClipDataOnServerSignifier = 255; 16 | public const byte SetFormulaOnServerSignifier = 254; 17 | public const byte EvaluateFormulasSignifier = 253; 18 | public const byte SetAndEvaluateClipDataOnServerSignifier = 252; 19 | public const byte SetAndEvaluateFormulaOnServerSignifier = 251; 20 | 21 | public static bool IsStringData(byte[] result) 22 | { 23 | return result.Length > 4 && result[0] == TypedDataFirstByte && result[1] == TypedDataSecondByte && 24 | result[2] == TypedDataThirdByte && result[3] == StringDataSignifier; 25 | } 26 | 27 | public static bool IsTypedCommand(byte[] result) 28 | { 29 | return result.Length >= 4 && result[0] == TypedDataFirstByte && result[1] == TypedDataSecondByte && 30 | (result[2] == TypedDataThirdByte || result[2] == TypedDataThirdByteLive11Mode); 31 | } 32 | 33 | public static InternalCommandType GetCommandType(byte dataSignifier) 34 | { 35 | return dataSignifier switch 36 | { 37 | StringDataSignifier => OutputString, 38 | SetClipDataOnServerSignifier => SetClipDataOnServer, 39 | SetFormulaOnServerSignifier => SetFormulaOnServer, 40 | EvaluateFormulasSignifier => EvaluateFormulas, 41 | SetAndEvaluateFormulaOnServerSignifier => SetAndEvaluateFormulaOnServer, 42 | SetAndEvaluateClipDataOnServerSignifier => SetAndEvaluateClipDataOnServer, 43 | _ => UnknownCommand 44 | }; 45 | } 46 | 47 | public static (int trackNo, int clipNo, string formula) GetFormula(byte[] data) 48 | { 49 | int trackNo = data[0] + 1; 50 | int clipNo = data[1] + 1; 51 | var formula = Encoding.UTF8.GetString(data[2..]); 52 | return (trackNo, clipNo, formula); 53 | } 54 | 55 | public static string GetText(byte[] data) 56 | { 57 | return Encoding.UTF8.GetString(data); 58 | } 59 | 60 | /*public static void HandleTypedCommand(byte[] data, ClipSet clipSet, ChannelWriter writer) 61 | { 62 | switch (GetCommandType(data[3])) 63 | { 64 | case OutputString: 65 | CommandHandler.OutputString(data); 66 | break; 67 | case SetFormulaOnServer: 68 | CommandHandler.SetFormula(data, clipSet); 69 | break; 70 | case SetClipDataOnServer: 71 | CommandHandler.SetClipData(data, clipSet); 72 | break; 73 | case EvaluateFormulas: 74 | CommandHandler.EvaluateFormulas(data, clipSet, writer); 75 | break; 76 | case SetAndEvaluateClipDataOnServer: 77 | CommandHandler.SetAndEvaluateClipData(data, clipSet, writer); 78 | break; 79 | case SetAndEvaluateFormulaOnServer: 80 | CommandHandler.SetAndEvaluateFormula(data, clipSet, writer); 81 | break; 82 | case UnknownCommand: 83 | break; 84 | } 85 | }*/ 86 | 87 | public static Clip GetSingleClip(byte[] data) 88 | { 89 | var offset = 0; 90 | var clipReference = new ClipReference(data[offset] + 1, data[offset += 1] + 1); 91 | decimal length = (decimal)BitConverter.ToSingle(data, offset += 1); 92 | bool isLooping = data[offset += 4] == 1; 93 | var clip = new Clip(length, isLooping) 94 | { 95 | ClipReference = clipReference 96 | }; 97 | ushort numNotes = BitConverter.ToUInt16(data, offset += 1); 98 | offset += 2; 99 | for (var i = 0; i < numNotes; i++) 100 | { 101 | clip.Notes.Add(new NoteEvent( 102 | data[offset], 103 | (decimal)BitConverter.ToSingle(data, offset += 1), 104 | (decimal)BitConverter.ToSingle(data, offset += 4), 105 | data[offset += 4]) 106 | ); 107 | offset++; 108 | } 109 | 110 | return clip; 111 | } 112 | 113 | public static Clip GetSingleLive11Clip(byte[] data) 114 | { 115 | var offset = 0; 116 | var clipReference = new ClipReference(data[offset] + 1, data[offset += 1] + 1); 117 | decimal length = (decimal)BitConverter.ToSingle(data, offset += 1); 118 | bool isLooping = data[offset += 4] == 1; 119 | var clip = new Clip(length, isLooping) 120 | { 121 | ClipReference = clipReference 122 | }; 123 | ushort numNotes = BitConverter.ToUInt16(data, offset += 1); 124 | offset += 2; 125 | 126 | for (var i = 0; i < numNotes; i++) 127 | { 128 | clip.Notes.Add(new NoteEvent( 129 | pitch: data[offset], 130 | start: (decimal)BitConverter.ToSingle(data, offset + 1), 131 | duration: (decimal)BitConverter.ToSingle(data, offset + 5), 132 | velocity: BitConverter.ToSingle(data, offset + 9), 133 | probability: BitConverter.ToSingle(data, offset + 13), 134 | velocityDeviation: BitConverter.ToSingle(data, offset + 17), 135 | releaseVelocity: BitConverter.ToSingle(data, offset + 21) 136 | )); 137 | offset += SizeOfOneNoteInBytesLive11; 138 | } 139 | 140 | return clip; 141 | } 142 | 143 | public static (List Clips, string Formula, ushort Id, byte TrackNo) DecodeData(byte[] data) 144 | { 145 | var clips = new List(); 146 | ushort id = BitConverter.ToUInt16(data, 0); 147 | byte trackNo = data[2]; 148 | byte numClips = data[3]; 149 | int dataOffset = 4; 150 | 151 | // Decode clipdata 152 | while (clips.Count < numClips) 153 | { 154 | ClipReference clipReference = new ClipReference(data[dataOffset], data[dataOffset += 1]); 155 | decimal length = (decimal)BitConverter.ToSingle(data, dataOffset += 1); 156 | bool isLooping = data[dataOffset += 4] == 1; 157 | var clip = new Clip(length, isLooping) 158 | { 159 | ClipReference = clipReference 160 | }; 161 | ushort numNotes = BitConverter.ToUInt16(data, dataOffset += 1); 162 | dataOffset += 2; 163 | for (var i = 0; i < numNotes; i++) 164 | { 165 | clip.Notes.Add(new NoteEvent( 166 | data[dataOffset], 167 | (decimal)BitConverter.ToSingle(data, dataOffset += 1), 168 | (decimal)BitConverter.ToSingle(data, dataOffset += 4), 169 | data[dataOffset += 4]) 170 | ); 171 | dataOffset++; 172 | } 173 | 174 | clips.Add(clip); 175 | } 176 | 177 | // Convert remaining bytes to text containing the formula 178 | string formula = Encoding.ASCII.GetString(data, dataOffset, data.Length - dataOffset); 179 | 180 | return (clips, formula, id, trackNo); 181 | } 182 | } -------------------------------------------------------------------------------- /src/Mutateful/LiveConnector/mutateful-connector-l11.amxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrierdown/mutateful/15f52eb0598c39d00d91344eb758599eeafbbe0b/src/Mutateful/LiveConnector/mutateful-connector-l11.amxd -------------------------------------------------------------------------------- /src/Mutateful/LiveConnector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LiveConnector", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sockets.js", 6 | "dependencies": { 7 | "@microsoft/signalr": "^5.0.6", 8 | "@microsoft/signalr-protocol-msgpack": "^5.0.6" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC" 17 | } 18 | -------------------------------------------------------------------------------- /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/Mutateful.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | <_ContentIncludedByDefault Remove="Pages\Error.cshtml" /> 10 | <_ContentIncludedByDefault Remove="Pages\Index.cshtml" /> 11 | <_ContentIncludedByDefault Remove="Pages\Privacy.cshtml" /> 12 | <_ContentIncludedByDefault Remove="Pages\Shared\_Layout.cshtml" /> 13 | <_ContentIncludedByDefault Remove="Pages\Shared\_ValidationScriptsPartial.cshtml" /> 14 | <_ContentIncludedByDefault Remove="Pages\_ViewImports.cshtml" /> 15 | <_ContentIncludedByDefault Remove="Pages\_ViewStart.cshtml" /> 16 | <_ContentIncludedByDefault Remove="wwwroot\css\site.css" /> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Mutateful/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | builder.Services.AddConnections(); 4 | builder.Services.AddSignalR().AddMessagePackProtocol(); 5 | builder.Services.AddSingleton(); 6 | builder.Services.AddSingleton(); 7 | // builder.Services.AddHostedService(); 8 | 9 | var app = builder.Build(); 10 | 11 | app.UseDefaultFiles(); 12 | app.UseStaticFiles(); 13 | app.UseRouting(); 14 | 15 | app.UseEndpoints(endpoints => 16 | { 17 | endpoints.MapHub("/mutatefulHub"); 18 | }); 19 | 20 | app.Run(); 21 | -------------------------------------------------------------------------------- /src/Mutateful/State/ClipSlot.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.State; 2 | 3 | public class ClipSlot 4 | { 5 | public Clip Clip { get; set; } 6 | public string Name { get; } 7 | public Formula Formula { get; } 8 | 9 | public ClipReference ClipReference => Clip.ClipReference; 10 | 11 | public static readonly ClipSlot Empty = new ClipSlot("", Clip.Empty, Formula.Empty); 12 | 13 | public ClipSlot(string name, Clip clip, Formula formula) 14 | { 15 | Name = name; 16 | Clip = clip; 17 | Formula = formula; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Mutateful/State/GuiCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.State; 2 | 3 | public enum GuiCommandType 4 | { 5 | SetName, 6 | Delete, 7 | UpdateFormula 8 | } 9 | 10 | public class GuiCommand 11 | { 12 | public GuiCommandType Type { get; } 13 | public ClipReference ClipReference { get; } 14 | public ChainedCommand Command { get; } 15 | } -------------------------------------------------------------------------------- /src/Mutateful/State/InternalCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.State; 2 | 3 | public enum InternalCommandType 4 | { 5 | UnknownCommand, 6 | OutputString, 7 | LegacyProcessClips, 8 | SetFormulaOnServer, // set a single clip slot without triggering evaluation 9 | SetClipDataOnServer, // set and trigger evaluation of a single clip slot 10 | SetAndEvaluateFormulaOnServer, 11 | SetAndEvaluateClipDataOnServer, 12 | EvaluateFormulas, // trigger evaluation of one or more clipslots - arguments are a list of clipslot coordinates 13 | SetFormulaOnClient, 14 | SetClipDataOnClient, 15 | SetClipDataOnClientLive11 16 | } 17 | 18 | public class InternalCommand 19 | { 20 | public InternalCommandType Type { get; } 21 | public ClipSlot ClipSlot { get; } 22 | public ClipReference[] ClipReferences { get; } 23 | 24 | public InternalCommand(InternalCommandType type, ClipSlot clipSlot, ClipReference[] clipReferences = null) 25 | { 26 | Type = type; 27 | ClipSlot = clipSlot; 28 | ClipReferences = clipReferences ?? new ClipReference[0]; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Mutateful/State/LegacyClipSlot.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.State; 2 | 3 | public class LegacyClipSlot 4 | { 5 | public Clip Clip { get; set; } 6 | public string Name { get; } 7 | public ChainedCommand ChainedCommand { get; } 8 | public ushort Id { get; } 9 | public ClipReference ClipReference => Clip.ClipReference; 10 | 11 | public static readonly LegacyClipSlot Empty = new LegacyClipSlot("", Clip.Empty, ChainedCommand.Empty, ushort.MaxValue); 12 | 13 | public LegacyClipSlot(string name, Clip clip, ChainedCommand chainedCommand, ushort id) 14 | { 15 | Name = name; 16 | Clip = clip; 17 | ChainedCommand = chainedCommand; 18 | Id = id; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Mutateful/TestService.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful; 2 | 3 | public class TestService : IHostedService 4 | { 5 | private Timer Timer; 6 | private readonly IHubContext MutatefulHub; 7 | 8 | public TestService(IHubContext mutatefulHub) 9 | { 10 | MutatefulHub = mutatefulHub; 11 | } 12 | 13 | public Task StartAsync(CancellationToken cancellationToken) 14 | { 15 | Timer = new Timer(SendTestMessage, null, TimeSpan.Zero, TimeSpan.FromSeconds(15)); 16 | return Task.CompletedTask; 17 | } 18 | 19 | public Task StopAsync(CancellationToken cancellationToken) 20 | { 21 | Timer?.Change(Timeout.Infinite, 0); 22 | return Task.CompletedTask; 23 | } 24 | 25 | private void SendTestMessage(object state) 26 | { 27 | /*Console.WriteLine("Sending test message..."); 28 | MutatefulHub.Clients.All.SendAsync("SetClipDataOnClient", "A1", new byte[] 29 | { 30 | 0,0,0,0,128,64,1,4,0,36,0,0,0,0,0,0,128,62,0,0,200,66,0,0,128,63,0,0,0,0,0,0,128,66,38,0,0,0,63,154,153,25,62,0,0,200,66,0,0,128,63,0,0,0,0,0,0,128,66,40,0,0,128,63,154,153,153,62,0,0,200,66,0,0,128,63,0,0,0,0,0,0,128,66,42,0,0,0,64,0,0,0,64,0,0,180,66,0,0,128,63,0,0,0,0,0,0,128,66 31 | });*/ 32 | } 33 | 34 | public void Dispose() 35 | { 36 | Timer?.Dispose(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Mutateful/Utility/ClipExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Utility; 2 | 3 | public static class ClipExtensions 4 | { 5 | public static bool HasNestedNotes(this Clip clip) 6 | { 7 | return clip.Notes.Any(n => n.Children.Count > 0); 8 | } 9 | 10 | public static void Flatten(this Clip clip) 11 | { 12 | var flattenedNotes = new SortedList(); 13 | 14 | foreach (var note in clip.Notes) 15 | { 16 | flattenedNotes.Add(note); 17 | if (note.HasChildren) 18 | { 19 | foreach (var child in note.Children) 20 | { 21 | child.Parent = null; 22 | } 23 | flattenedNotes.AddRange(note.Children); 24 | note.Children = null; 25 | } 26 | } 27 | clip.Notes = flattenedNotes; 28 | } 29 | 30 | public static void GroupSimultaneousNotes(this Clip clip) 31 | { 32 | var groupedNotes = new SortedList(); 33 | var i = 0; 34 | 35 | do 36 | { 37 | var note = clip.Notes[i]; 38 | var simultaneousNotes = clip.Notes.Skip(i).Where(x => x.Start == note.Start && x.Pitch != note.Pitch).ToList(); 39 | if (simultaneousNotes.Count > 0) 40 | { 41 | foreach (var simultaneousNote in simultaneousNotes) 42 | { 43 | simultaneousNote.Parent = note; 44 | } 45 | note.Children = simultaneousNotes; 46 | i += note.Children.Count; 47 | } 48 | groupedNotes.Add(note); 49 | } while (++i < clip.Count); 50 | 51 | clip.Notes = groupedNotes; 52 | } 53 | } -------------------------------------------------------------------------------- /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/Mutateful/Utility/NoteExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Utility; 2 | 3 | public static class NoteExtensions 4 | { 5 | // [ --- ] 6 | public static bool InsideIntervalInclusive(this NoteEvent note, decimal start, decimal end) 7 | { 8 | return note.Start >= start && note.Start + note.Duration <= end; 9 | } 10 | 11 | // --[--------]--- 12 | public static bool CoversInterval(this NoteEvent note, decimal start, decimal end) 13 | { 14 | return note.Start < start && note.Start + note.Duration > end; 15 | } 16 | 17 | // --[------ ] 18 | public static bool CrossesStartOfIntervalInclusive(this NoteEvent note, decimal start, decimal end) 19 | { 20 | return note.Start < start && (note.Start + note.Duration) > start && (note.Start + note.Duration) <= end; 21 | } 22 | 23 | // [ ----]---- 24 | public static bool CrossesEndOfIntervalInclusive(this NoteEvent note, decimal start, decimal end) 25 | { 26 | return note.Start >= start && note.Start < end && (note.Start + note.Duration) > end; 27 | } 28 | 29 | // [--------]?? 30 | public static bool StartsInsideIntervalInclusive(this NoteEvent note, decimal start, decimal end) 31 | { 32 | return note.Start >= start && note.Start < end; 33 | } 34 | 35 | public static bool StartsInsideInterval(this NoteEvent note, decimal start, decimal end) 36 | { 37 | return note.Start > start && note.Start < end; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Mutateful/Utility/ScaleUtilities.cs: -------------------------------------------------------------------------------- 1 | using static Mutateful.Utility.Scales; 2 | 3 | namespace Mutateful.Utility; 4 | 5 | public enum Scales 6 | { 7 | None = -1, 8 | Ionian, 9 | Dorian, 10 | Phrygian, 11 | Lydian, 12 | Mixolydian, 13 | Aeolian, 14 | Locrian, 15 | Major, 16 | Minor 17 | } 18 | 19 | public static class ScaleUtilities 20 | { 21 | public static readonly int[] IonianNoteIndexes = new[] {0, 2, 4, 5, 7, 9, 11}; 22 | 23 | public static Clip GetGuideClipFromScale(Scales scale, int root, decimal length) 24 | { 25 | Clip clip = new Clip(length, true); 26 | root &= 0x7F; 27 | 28 | foreach (var ix in GetIndexesFromScale(scale)) 29 | { 30 | clip.Add(new NoteEvent(root + ix, 0, length, 127)); 31 | } 32 | return clip; 33 | } 34 | 35 | public static int[] GetIndexesFromScale(Scales scale) 36 | { 37 | int[] indexes = new int[7]; 38 | int scaleRootIx = 0; // Ionian/major 39 | if (scale >= Ionian && scale <= Locrian) 40 | { 41 | scaleRootIx = (int) scale; 42 | } 43 | else if (scale == Minor) 44 | { 45 | scaleRootIx = (int) Aeolian; 46 | } 47 | 48 | for (var i = 0; i < IonianNoteIndexes.Length; i++) 49 | { 50 | indexes[i] = IonianNoteIndexes[(scaleRootIx + i) % IonianNoteIndexes.Length]; 51 | } 52 | return indexes; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Mutateful/Utility/SvgUtilities.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Utility; 2 | 3 | public static class SvgUtilities 4 | { 5 | private const string NoteFill = "#55d400"; 6 | private const string NoteStroke = "#233450"; 7 | private const string PianoRollBackgroundMainFill = "#cbcbd3"; 8 | private const string PianoRollBackgroundSharpFill = "#b3b3b9"; 9 | private const string PianoRollVerticalGuideStroke = "#e6e6e6"; 10 | private const string PianoRollHorizontalGuideStroke = "#bbbbbb"; 11 | private const string PianoKeyWhiteFill = "#ffffff"; 12 | private const string PianoKeyBlackFill = "#000000"; 13 | private const string PianoKeyStroke = "#8e8e8e"; 14 | private const string FontFamily = "Helvetica, Consolas, Arial, Sans"; 15 | private const int HeaderHeight = 20; 16 | private const int TextBaselineOffset = 4; 17 | private const int PianoRollWidth = 30; 18 | 19 | public static void GenerateSvgDoc(string formula, List clips, Clip resultClip, int width, int height) 20 | { 21 | var contentHeight = height - HeaderHeight; 22 | var padding = 8; 23 | var resultClipWidth = (width - padding) / 2; 24 | var sourceClipWidth = resultClipWidth; 25 | var sourceClipHeight = contentHeight; 26 | 27 | if (clips.Count > 1) 28 | { 29 | sourceClipHeight -= padding; 30 | sourceClipHeight /= 2; 31 | } 32 | if (clips.Count > 2) 33 | { 34 | sourceClipWidth = resultClipWidth / 2; 35 | } 36 | 37 | var output = new StringBuilder($"" + Environment.NewLine); 39 | 40 | output.Append($"Source clips" + 41 | $"Result"); 42 | 43 | var x = 0; 44 | var y = HeaderHeight; 45 | var highestNote = (clips.Max(c => c.Notes.Max(d => d.Pitch)) + 4) & 0x7F; // Leave 3 notes on each side 46 | var lowestNote = (clips.Min(c => c.Notes.Min(d => d.Pitch)) - 3) & 0x7F; // as padding, and clamp to 0-127 range 47 | var numNotes = highestNote - lowestNote + 1; 48 | 49 | for (var i = 0; i < Math.Min(4, clips.Count); i++) 50 | { 51 | var clip = clips[i]; 52 | output.Append(ClipToSvg(clip, x, y, sourceClipWidth, sourceClipHeight, numNotes, lowestNote, highestNote)); 53 | y += sourceClipHeight + padding; 54 | if (i == 1) 55 | { 56 | x += sourceClipWidth + padding; 57 | y = HeaderHeight; 58 | } 59 | } 60 | 61 | y = HeaderHeight; 62 | highestNote = (resultClip.Notes.Max(c => c.Pitch) + 4) & 0x7F; 63 | lowestNote = (resultClip.Notes.Min(c => c.Pitch) - 3) & 0x7F; 64 | numNotes = highestNote - lowestNote + 1; 65 | output.Append(ClipToSvg(resultClip, width - resultClipWidth, y, resultClipWidth, contentHeight, numNotes, lowestNote, highestNote)); 66 | output.Append(""); 67 | 68 | using (var file = File.AppendText($"Generated{DateTime.Now.Ticks}-clip.svg")) 69 | { 70 | file.Write(output.ToString()); 71 | } 72 | } 73 | 74 | public static string ClipToSvg(Clip clip, int x, int y, int width, int height, int numNotes, int lowestNote, int highestNote) 75 | { 76 | var output = new StringBuilder( 77 | $"" + Environment.NewLine); 79 | 80 | var yDelta = (decimal) height / numNotes; 81 | // piano + horizontal guides 82 | var j = 0; 83 | for (var i = lowestNote; i <= highestNote; i++) 84 | { 85 | var pianoColour = (i % 12 == 0 || i % 12 == 2 || i % 12 == 4 || i % 12 == 5 || i % 12 == 7 || i % 12 == 9 || i % 12 == 11) == true 86 | ? PianoKeyWhiteFill 87 | : PianoKeyBlackFill; 88 | output.Append($"" + Environment.NewLine); 91 | if (i % 12 == 1 || i % 12 == 3 || i % 12 == 6 || i % 12 == 8 || i % 12 == 10) 92 | { 93 | output.Append($"" + Environment.NewLine); 96 | } 97 | 98 | output.Append($"" + Environment.NewLine); 101 | j++; 102 | } 103 | 104 | // vertical guides 105 | var xDelta = (width - PianoRollWidth) / clip.Length; 106 | for (decimal i = 0; i < clip.Length; i += .25m) 107 | { 108 | output.Append($"" + Environment.NewLine); 110 | } 111 | 112 | foreach (var note in clip.Notes) 113 | { 114 | if (note.Pitch >= lowestNote && note.Pitch <= highestNote) 115 | { 116 | output.Append($"" + Environment.NewLine); 119 | } 120 | } 121 | 122 | if (!string.IsNullOrEmpty(clip.RawClipReference)) 123 | { 124 | output.Append($"{clip.RawClipReference.ToUpper()}"); 125 | } 126 | 127 | return output.ToString(); 128 | } 129 | } -------------------------------------------------------------------------------- /src/Mutateful/Utility/TestUtilities.cs: -------------------------------------------------------------------------------- 1 | using Mutateful.Cli; 2 | 3 | namespace Mutateful.Utility; 4 | 5 | public static class TestUtilities 6 | { 7 | public static void AppendUnitTest(string formula, byte[] inputData, byte[] outputData) 8 | { 9 | using (var file = File.AppendText(@"GeneratedUnitTests.txt")) 10 | { 11 | file.Write($"// Test generated by mutate4l from formula: {formula}{Environment.NewLine}" + 12 | $"[TestMethod]{Environment.NewLine}" + 13 | $"public void NameThisMethod(){Environment.NewLine}" + 14 | $"{{{Environment.NewLine}" + 15 | $" byte[] input = {{ {Utilities.GetByteArrayContentsAsString(inputData.Take(inputData.Length - CliHandler.UnitTestDirective.Length).ToArray())} }};{Environment.NewLine}" + 16 | $" byte[] output = {{ {Utilities.GetByteArrayContentsAsString(outputData)} }};{Environment.NewLine}" + 17 | $"{Environment.NewLine}" + 18 | $" TestUtilities.InputShouldProduceGivenOutput(input, output);{Environment.NewLine}" + 19 | $"}}{Environment.NewLine}{Environment.NewLine}"); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Mutateful/Utility/Utilities.cs: -------------------------------------------------------------------------------- 1 | namespace Mutateful.Utility; 2 | 3 | public static class Utilities 4 | { 5 | public static Dictionary> GetValidOptions(Dictionary> options, TokenType[] validOptions) 6 | { 7 | var cleanedOptions = new Dictionary>(); 8 | foreach (var key in options.Keys) 9 | { 10 | if (validOptions.Contains(key)) 11 | { 12 | cleanedOptions.Add(key, options[key]); 13 | } 14 | } 15 | return cleanedOptions; 16 | } 17 | 18 | public static decimal MusicalDivisionToDecimal(string value) 19 | { 20 | // TODO - refinement: support triplet or dotted divisions like 1/16t or 1/8d as well. For triplets, the denominator is divided by two and multiplied with 3. 21 | if (value.IndexOf('/') >= 0) 22 | { 23 | return 4m / int.Parse(value.Substring(value.IndexOf('/') + 1)) * int.Parse(value.Substring(0, value.IndexOf('/'))); 24 | } 25 | return int.Parse(value) * 4m; 26 | } 27 | 28 | public static decimal BarsBeatsSixteenthsToDecimal(string value) 29 | { 30 | var parts = value.Split('.'); 31 | var parsedParts = new int[3]; 32 | for (var i = 0; i < parts.Length; i++) 33 | { 34 | parsedParts[i] = int.Parse(parts[i]); 35 | if (i > 0 && parsedParts[i] > 3) parsedParts[i] = Math.Clamp(parsedParts[i], 0, 3); 36 | } 37 | return parsedParts[0] * 4m + parsedParts[1] + parsedParts[2] * 0.25m; 38 | } 39 | 40 | public static SortedList ToSortedList(this IEnumerable list) 41 | { 42 | return new SortedList(list); 43 | } 44 | 45 | public static string GetByteArrayContentsAsString(byte[] result) 46 | { 47 | var sb = new StringBuilder(); 48 | for (var i = 0; i < result.Length; i++) 49 | { 50 | var res = result[i]; 51 | sb.Append($"{res}{(i == result.Length - 1 ? "" : ", ")}"); 52 | } 53 | return sb.ToString(); 54 | } 55 | 56 | public static decimal RoundUpToNearestSixteenth(decimal val) 57 | { 58 | var numSixteenths = val / 0.25m; 59 | var numSixteenthsFloored = (int) Math.Floor(numSixteenths); 60 | if (numSixteenths - numSixteenthsFloored > 0) 61 | { 62 | numSixteenthsFloored += 1; 63 | } 64 | return numSixteenthsFloored * 0.25m; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Mutateful/WebUI/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^17.0.0", 13 | "@rollup/plugin-node-resolve": "^11.0.0", 14 | "@rollup/plugin-typescript": "^8.0.0", 15 | "@tsconfig/svelte": "^2.0.0", 16 | "less": "^4.1.3", 17 | "rollup": "^2.3.4", 18 | "rollup-plugin-css-only": "^3.1.0", 19 | "rollup-plugin-livereload": "^2.0.0", 20 | "rollup-plugin-svelte": "^7.0.0", 21 | "rollup-plugin-terser": "^7.0.0", 22 | "svelte": "^3.0.0", 23 | "svelte-check": "^2.0.0", 24 | "svelte-preprocess": "^4.10.7", 25 | "tslib": "^2.0.0", 26 | "typescript": "^4.0.0" 27 | }, 28 | "dependencies": { 29 | "sirv-cli": "^1.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import sveltePreprocess from 'svelte-preprocess'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | import css from 'rollup-plugin-css-only'; 9 | 10 | const production = !process.env.ROLLUP_WATCH; 11 | 12 | function serve() { 13 | let server; 14 | 15 | function toExit() { 16 | if (server) server.kill(0); 17 | } 18 | 19 | return { 20 | writeBundle() { 21 | if (server) return; 22 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 23 | stdio: ['ignore', 'inherit', 'inherit'], 24 | shell: true 25 | }); 26 | 27 | process.on('SIGTERM', toExit); 28 | process.on('exit', toExit); 29 | } 30 | }; 31 | } 32 | 33 | export default { 34 | input: 'src/main.ts', 35 | output: { 36 | sourcemap: true, 37 | format: 'iife', 38 | name: 'app', 39 | globals: { 40 | signalR: '@microsoft/signalr' 41 | }, 42 | file: '../wwwroot/index.js' 43 | }, 44 | plugins: [ 45 | svelte({ 46 | preprocess: sveltePreprocess({ sourceMap: !production, less: true }), 47 | compilerOptions: { 48 | // enable run-time checks when not in production 49 | dev: !production 50 | } 51 | }), 52 | // we'll extract any component CSS out into 53 | // a separate file - better for performance 54 | css({ output: 'index.css' }), 55 | 56 | // If you have external dependencies installed from 57 | // npm, you'll most likely need these plugins. In 58 | // some cases you'll need additional configuration - 59 | // consult the documentation for details: 60 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 61 | resolve({ 62 | browser: true, 63 | dedupe: ['svelte'] 64 | }), 65 | commonjs(), 66 | typescript({ 67 | sourceMap: !production, 68 | inlineSources: !production 69 | }), 70 | 71 | // In dev mode, call `npm run start` once 72 | // the bundle has been generated 73 | !production && serve(), 74 | 75 | // Watch the `public` directory and refresh the 76 | // browser on changes when not in production 77 | !production && livereload('src'), 78 | 79 | // If we're building for production (npm run build 80 | // instead of npm run dev), minify 81 | production && terser() 82 | ], 83 | watch: { 84 | clearScreen: false 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 9 |
10 | 11 | 23 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/ClipSet.svelte: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | {#each clips as clipRow} 13 | 14 | {#each clipRow as clip} 15 | 16 | {/each} 17 | 18 | {/each} 19 |
20 | 21 | 37 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/ClipSlot.svelte: -------------------------------------------------------------------------------- 1 |  53 | 54 |
55 |
56 | {clipRef.toUpperCase()}{formula} 57 |
58 | {#if $clipDataStore.clipRef === clipRef}{getClip($clipDataStore.data)}{/if} 59 |
60 | 61 | 107 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/FormulaInput.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | M 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/clip.ts: -------------------------------------------------------------------------------- 1 | export class Clip { 2 | length: number; 3 | notes: NoteEvent[] = []; 4 | 5 | constructor(length: number) { 6 | this.length = length; 7 | } 8 | } 9 | 10 | export class NoteEvent { 11 | pitch: number; 12 | start: number; 13 | duration: number; 14 | velocity: number; 15 | 16 | constructor(pitch: number, start: number, duration: number, velocity: number) { 17 | this.pitch = pitch; 18 | this.start = start; 19 | this.duration = duration; 20 | this.velocity = velocity; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/dataHelpers.ts: -------------------------------------------------------------------------------- 1 | import {Clip, NoteEvent} from "./clip"; 2 | 3 | const sizeOfOneNoteInBytesLive11 = 25; 4 | 5 | export function decodeClip(data: Uint8Array): Clip 6 | { 7 | let length = getFloat32FromByteArray(data, 2); 8 | let clip = new Clip(length); 9 | let numNotes = getUint16FromByteArray(data, 7); 10 | let offset = 9; 11 | for (let i = 0; i < numNotes; i++) 12 | { 13 | clip.notes.push( 14 | new NoteEvent( 15 | data[offset], 16 | getFloat32FromByteArray(data, offset + 1), 17 | getFloat32FromByteArray(data, offset + 5), 18 | getFloat32FromByteArray(data, offset + 9) 19 | ) 20 | ); 21 | offset += sizeOfOneNoteInBytesLive11; 22 | } 23 | return clip; 24 | } 25 | 26 | function getFloat32FromByteArray(bytes: Uint8Array, start = 0): number { 27 | let temp = new Uint8Array(4); 28 | for (let i = 0; i < 4; i++) { 29 | temp[i] = bytes[i + start]; 30 | } 31 | let value = new Float32Array(temp.buffer); 32 | return value[0]; 33 | } 34 | 35 | function getUint16FromByteArray(bytes: Uint8Array, start = 0): number { 36 | let temp = new Uint8Array(2); 37 | temp[0] = bytes[start]; 38 | temp[1] = bytes[start + 1]; 39 | let value = new Uint16Array(temp.buffer); 40 | return value[0]; 41 | } -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | name: 'world' 7 | } 8 | }); 9 | 10 | export default app; -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/stores.ts: -------------------------------------------------------------------------------- 1 | import {Subscriber, writable} from "svelte/store"; 2 | 3 | declare const signalR: any; 4 | 5 | const connection = new signalR.HubConnectionBuilder() 6 | .withUrl("http://localhost:5000/mutatefulHub?username=webui") 7 | .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol()) 8 | .build(); 9 | 10 | let clipDataSetter: Subscriber<{clipRef: string, data: Uint8Array}> | null = null; 11 | 12 | connection.on("SetClipDataOnWebUI", (clipRef: string, data: Uint8Array) => { 13 | console.log("DebugMessage - received data: ", data, "clipRef", clipRef); 14 | updateClipData(clipRef, data); 15 | }); 16 | 17 | connection.start() 18 | .then(async () => { 19 | console.log("Connection established, got", connection.connectionId); 20 | }, () => { 21 | console.log("Failed to connect :("); 22 | }); 23 | 24 | const createClipDataStore = () => { 25 | const { subscribe, set } = writable({ clipRef: "", data: new Uint8Array() }); 26 | 27 | clipDataSetter = set; 28 | 29 | return { 30 | subscribe, 31 | set: (clipRef: string, data: Uint8Array) => { 32 | set({ clipRef, data }); 33 | }, 34 | }; 35 | }; 36 | 37 | export const clipDataStore = createClipDataStore(); 38 | 39 | export const updateClipData = (clipRef: string, data: Uint8Array) => { 40 | clipDataSetter?.({ clipRef, data }); 41 | }; 42 | 43 | /* 44 | connection.on("SetClipDataOnClient", (clipRef: string, data: Uint8Array) => { 45 | console.log("Received data", data); 46 | }); 47 | 48 | return () => { 49 | console.log("readableClip end called"); 50 | // connection.stop(); 51 | }; 52 | });*/ 53 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/src/variables.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | gridBackground: "#202020", 3 | clipPreviewBackground: "#353535", 4 | clipPreviewForeground: "#B4B1B0", 5 | formulaBackground: "#5A5A5A", 6 | formulaForeground: "#DFDFDF" 7 | } -------------------------------------------------------------------------------- /src/Mutateful/WebUI/svelte.config.js: -------------------------------------------------------------------------------- 1 | // svelte.config.js 2 | const sveltePreprocess = require('svelte-preprocess'); 3 | 4 | module.exports = { 5 | preprocess: sveltePreprocess({ 6 | less: true, 7 | }), 8 | }; 9 | -------------------------------------------------------------------------------- /src/Mutateful/WebUI/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | 4 | "include": ["src/**/*"], 5 | "exclude": ["node_modules/*", "__sapper__/*"], 6 | "compilerOptions": { 7 | "types": ["@microsoft/signalr", "@microsoft/signalr-protocol-msgpack"] 8 | } 9 | } -------------------------------------------------------------------------------- /src/Mutateful/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft": "Warning", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Mutateful/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/Mutateful/wwwroot/index.css: -------------------------------------------------------------------------------- 1 | body{font-family:sans-serif;margin:0;background-color:#202020;--scrollbar-width:calc(100vw - 100%)}.container.svelte-1y12bcm{overflow-x:auto;width:calc(100vw - var(--scrollbar-width))}table.svelte-8k2hbi{border-collapse:collapse;table-layout:fixed;width:100%;background-color:#202020}td.svelte-8k2hbi{width:150px;overflow:hidden;white-space:nowrap}.char-width-reference.svelte-170irtv{padding:0;margin:0;width:auto;border-width:0;position:absolute;left:-100px;top:-100px}input.svelte-170irtv{box-sizing:border-box;width:100%;font-family:Consolas, "Lucida Console", monospace}.autocomplete-list.svelte-170irtv{display:block;position:absolute;width:150px;height:4rem;background-color:#2f4f4f;color:#ffffff}.clip-slot.svelte-1hayvt1.svelte-1hayvt1{border:none;background-color:#353535}.empty.svelte-1hayvt1.svelte-1hayvt1{transform:scale(0)}.clip-slot--header.svelte-1hayvt1.svelte-1hayvt1{display:flex;line-height:2rem;white-space:nowrap;overflow:hidden}.clip-slot--header.svelte-1hayvt1 span.svelte-1hayvt1{padding:0 0.4rem}.clip-slot--ref.svelte-1hayvt1.svelte-1hayvt1{background-color:#7562C9;color:black}.clip-slot--title.svelte-1hayvt1.svelte-1hayvt1{flex-grow:1;background-color:#5A5A5A;color:#DFDFDF}.clip-slot--ref.svelte-1hayvt1.svelte-1hayvt1,.clip-slot--title.svelte-1hayvt1.svelte-1hayvt1{padding:0.1rem 0.2rem}canvas.svelte-1hayvt1.svelte-1hayvt1{background-color:#353535;width:100%;height:50px;padding:1px} -------------------------------------------------------------------------------- /src/Mutateful/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mutateful 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Mutateful/wwwroot/index.js: -------------------------------------------------------------------------------- 1 | var app=function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function l(t){t.forEach(e)}function o(t){return"function"==typeof t}function r(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function c(e,n,l){e.$$.on_destroy.push(function(e,...n){if(null==e)return t;const l=e.subscribe(...n);return l.unsubscribe?()=>l.unsubscribe():l}(n,l))}function i(t,e){t.appendChild(e)}function f(t,e,n){t.insertBefore(e,n||null)}function u(t){t.parentNode.removeChild(t)}function a(t,e){for(let n=0;n{H.delete(t),l&&(n&&t.d(1),l())})),t.o(e)}}function G(t){t&&t.c()}function J(t,n,r,c){const{fragment:i,on_mount:f,on_destroy:u,after_update:a}=t.$$;i&&i.m(n,r),c||A((()=>{const n=f.map(e).filter(o);u?u.push(...n):l(n),t.$$.on_mount=[]})),a.forEach(A)}function O(t,e){const n=t.$$;null!==n.fragment&&(l(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function j(t,e){-1===t.$$.dirty[0]&&(v.push(t),C||(C=!0,k.then(B)),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const o=l.length?l[0]:n;return m.ctx&&i(m.ctx[t],m.ctx[t]=o)&&(!m.skip_bound&&m.bound[t]&&m.bound[t](o),h&&j(e,t)),n})):[],m.update(),h=!0,l(m.before_update),m.fragment=!!c&&c(m.ctx),o.target){if(o.hydrate){const t=function(t){return Array.from(t.childNodes)}(o.target);m.fragment&&m.fragment.l(t),t.forEach(u)}else m.fragment&&m.fragment.c();o.intro&&P(e.$$.fragment),J(e,o.target,o.anchor,o.customElement),B()}b(p)}class L{$destroy(){O(this,1),this.$destroy=t}$on(t,e){const n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}$set(t){var e;this.$$set&&(e=t,0!==Object.keys(e).length)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const N=[];const q=(new signalR.HubConnectionBuilder).withUrl("http://localhost:5000/mutatefulHub?username=webui").withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol).build();let T=null;q.on("SetClipDataOnWebUI",((t,e)=>{console.log("DebugMessage - received data: ",e,"clipRef",t),W(t,e)})),q.start().then((async()=>{console.log("I'm updated. Connection established, got",q.connectionId)}),(()=>{console.log("Failed to connect :(")}));const V=(()=>{const{subscribe:e,set:n}=function(e,n=t){let l;const o=new Set;function c(t){if(r(e,t)&&(e=t,l)){const t=!N.length;for(const t of o)t[1](),N.push(t,e);if(t){for(let t=0;t{o.delete(f),0===o.size&&(l(),l=null)}}}}({clipRef:"",data:new Uint8Array});return T=n,{subscribe:e,set:(t,e)=>{n({clipRef:t,data:e})}}})(),W=(t,e)=>{null==T||T({clipRef:t,data:e})};class K{constructor(t){this.notes=[],this.length=t}}class Q{constructor(t,e,n,l){this.pitch=t,this.start=e,this.duration=n,this.velocity=l}}function X(t){let e=Y(t,2),n=new K(e),l=function(t,e=0){let n=new Uint8Array(2);return n[0]=t[e],n[1]=t[e+1],new Uint16Array(n.buffer)[0]}(t,7),o=9;for(let e=0;en(6,l=t)));let o,{clipRef:r}=e,{formula:i=""}=e,f=300,u=150,a=!0;y((()=>{let t=getComputedStyle(o);n(3,f=parseInt(t.getPropertyValue("width"),10)),n(4,u=parseInt(t.getPropertyValue("height"),10))}));return t.$$set=t=>{"clipRef"in t&&n(0,r=t.clipRef),"formula"in t&&n(1,i=t.formula)},[r,i,o,f,u,a,l,t=>((t=>{n(5,a=!1);const e=o.getContext("2d");e.clearRect(0,0,f,u),e.fillStyle="#B4B1B0";let l=Math.min(t.length,16),r=t.notes.map((t=>t.pitch)),c=Math.max(...r),i=Math.min(...r),s=Math.max(c-i,5),p=f/l,m=u/(s+1);for(let n of t.notes){if(n.start>=l)return void console.log("skipping");e.fillRect(Math.floor(p*n.start),Math.floor(m*(s-(n.pitch-i))),Math.floor(p*n.duration),Math.floor(m))}})(X(t)),""),function(t){w[t?"unshift":"push"]((()=>{o=t,n(2,o)}))}]}class nt extends L{constructor(t){super(),z(this,t,et,tt,r,{clipRef:0,formula:1})}}function lt(t,e,n){const l=t.slice();return l[1]=e[n],l}function ot(t,e,n){const l=t.slice();return l[4]=e[n],l}function rt(e){let n,l,o;return l=new nt({props:{clipRef:e[4].clipRef,formula:e[4].formula}}),{c(){n=s("td"),G(l.$$.fragment),h(n,"class","svelte-8k2hbi")},m(t,e){f(t,n,e),J(l,n,null),o=!0},p:t,i(t){o||(P(l.$$.fragment,t),o=!0)},o(t){D(l.$$.fragment,t),o=!1},d(t){t&&u(n),O(l)}}}function ct(t){let e,n,l,o=t[1],r=[];for(let e=0;eD(r[t],1,1,(()=>{r[t]=null}));return{c(){e=s("tr");for(let t=0;tD(r[t],1,1,(()=>{r[t]=null}));return{c(){e=m(),n=s("table");for(let t=0;ts.removeEventListener(p,m,h),i=!0)},p(t,[e]){1&e&&g(c,"left",t[0]+"px")},i:t,o:t,d(t){t&&u(n),t&&u(l),t&&u(o),t&&u(r),t&&u(c),i=!1,a()}}}function st(t,e,n){let l=0,o=8;return y((()=>{o=document.querySelector(".char-width-reference").offsetWidth})),[l,function(t){n(0,l=o*t.target.selectionStart),l>0&&n(0,l-=4)}]}class pt extends L{constructor(t){super(),z(this,t,st,at,r,{})}}function mt(e){let n,l,o,r,c;return l=new pt({}),r=new ut({}),{c(){n=s("div"),G(l.$$.fragment),o=m(),G(r.$$.fragment),h(n,"class","container svelte-1y12bcm")},m(t,e){f(t,n,e),J(l,n,null),i(n,o),J(r,n,null),c=!0},p:t,i(t){c||(P(l.$$.fragment,t),P(r.$$.fragment,t),c=!0)},o(t){D(l.$$.fragment,t),D(r.$$.fragment,t),c=!1},d(t){t&&u(n),O(l),O(r)}}}return new class extends L{constructor(t){super(),z(this,t,null,mt,r,{})}}({target:document.body,props:{name:"world"}})}(); 2 | //# sourceMappingURL=index.js.map 3 | -------------------------------------------------------------------------------- /src/MutatefulTests/CommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Mutateful.Core; 2 | using Mutateful.IO; 3 | using NUnit.Framework; 4 | 5 | namespace MutatefulTests; 6 | 7 | [TestFixture] 8 | public class CommandHandlerTests 9 | { 10 | [Test] 11 | public void TestEvaluationOfClipIsAbortedWhenContentsUnchanged() 12 | { 13 | var commandHandler = new CommandHandler(); 14 | var result = commandHandler.SetAndEvaluateClipData(new Clip(4, true) 15 | { 16 | ClipReference = ClipReference.FromString("A1"), 17 | Notes = new SortedList { 18 | new NoteEvent(36, 0, .5m, 100), 19 | new NoteEvent(38, 1, .5m, 100), 20 | new NoteEvent(40, 2, .75m, 100) 21 | } 22 | }); 23 | Assert.IsTrue(result.RanToCompletion); 24 | 25 | result = commandHandler.SetAndEvaluateClipData(new Clip(4, true) 26 | { 27 | ClipReference = ClipReference.FromString("A1"), 28 | Notes = new SortedList { 29 | new NoteEvent(36, 0, .75m, 100), 30 | new NoteEvent(38, 1, .5m, 100), 31 | new NoteEvent(40, 2, .5m, 100) 32 | } 33 | }); 34 | Assert.IsTrue(result.RanToCompletion); 35 | 36 | result = commandHandler.SetAndEvaluateClipData(new Clip(4, true) 37 | { 38 | ClipReference = ClipReference.FromString("A1"), 39 | Notes = new SortedList { 40 | new NoteEvent(36, 0, .75m, 100), 41 | new NoteEvent(38, 1, .5m, 100), 42 | new NoteEvent(40, 2, .5m, 100) 43 | } 44 | }); 45 | Assert.IsFalse(result.RanToCompletion); 46 | Assert.AreEqual($"Aborted evaluation of clip at A1 since it was unchanged.", result.Warnings.FirstOrDefault()); 47 | } 48 | 49 | [Test] 50 | public void TestEvaluationOfFormulaIsAbortedWhenContentsUnchanged() 51 | { 52 | var commandHandler = new CommandHandler(); 53 | var result = commandHandler.SetAndEvaluateFormula("a1 slice 1/8 ratchet 4 8 12 2 1 6", 1, 1); 54 | Assert.IsTrue(result.RanToCompletion); 55 | result = commandHandler.SetAndEvaluateFormula("a1 slice 1/8 ratchet 4 8 12 2 1 5", 1, 1); 56 | Assert.IsTrue(result.RanToCompletion); 57 | result = commandHandler.SetAndEvaluateFormula("a1 slice 1/8 ratchet 4 8 12 2 1 5", 1, 1); 58 | Assert.IsFalse(result.RanToCompletion); 59 | } 60 | } -------------------------------------------------------------------------------- /src/MutatefulTests/MutatefulTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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/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 { Clip.Empty, Clip.Empty }); 120 | var result = lexer.GetTokens(); 121 | Assert.IsTrue(result.Success); 122 | var sTokens = Parser.CreateSyntaxTree(result.Result); 123 | TestUtilities.PrintSyntaxTree(sTokens.Result.Children); 124 | } 125 | 126 | [Test] 127 | public void TestParseNestedOperators() 128 | { 129 | var command = Parser.ParseFormulaToChainedCommand("[0] shuffle 1 2|3|9x6 1 2 4x3 5|6|7 8", 130 | new List { Clip1 }, new ClipMetaData(100, 0)); 131 | Assert.IsTrue(command.Success); 132 | Assert.AreEqual(1, command.Result.Commands.Count); 133 | 134 | var flattenedValues = command.Result.Commands[0].DefaultOptionValues.Select(x => int.Parse(x.Value)).ToList(); 135 | var expectedValues = new List 136 | { 1, 2, 1, 2, 4, 4, 4, 5, 8, 1, 3, 1, 2, 4, 4, 4, 6, 8, 1, 9, 9, 9, 9, 9, 9, 1, 2, 4, 4, 4, 7, 8 }; 137 | Assert.IsTrue(flattenedValues.SequenceEqual(expectedValues)); 138 | 139 | command = Parser.ParseFormulaToChainedCommand("[0] loop 4 rat 2 3|4|5x2 6", new List { Clip1 }, 140 | new ClipMetaData(100, 0)); 141 | Assert.IsTrue(command.Success); 142 | } 143 | 144 | [Test] 145 | public void TestParseNestedCommands() 146 | { 147 | var lexer = new Lexer("[0] (([0] transpose 12)([0] transpose 24) il) il", new List { Clip.Empty }); 148 | var result = lexer.GetTokens(); 149 | Assert.IsTrue(result.Success); 150 | var (success, errorMessage) = Parser.AreTokensValid(result.Result); 151 | if (!success) Console.WriteLine($"Failed with: {errorMessage}"); 152 | Assert.IsTrue(success); 153 | var sTokens = Parser.CreateSyntaxTree(result.Result); 154 | TestUtilities.PrintSyntaxTree(sTokens.Result.Children); 155 | /*var clip1 = new Clip(4, true) 156 | { 157 | Notes = new SortedList 158 | { 159 | new (60, 0, .5m, 100) 160 | } 161 | }; 162 | 163 | var command = Parser.ParseFormulaToChainedCommand("[0] ([0] transpose 12) il", new List { Clip1 }, new ClipMetaData(100, 0)); 164 | Assert.IsTrue(command.Success);*/ 165 | } 166 | } -------------------------------------------------------------------------------- /src/MutatefulTests/TestUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Mutateful; 3 | using Mutateful.Compiler; 4 | using Mutateful.Core; 5 | using Mutateful.Utility; 6 | using NUnit.Framework; 7 | using Decoder = Mutateful.IO.Decoder; 8 | 9 | namespace MutatefulTests; 10 | 11 | public static class TestUtilities 12 | { 13 | public static void InputShouldProduceGivenOutput(byte[] input, byte[] output) 14 | { 15 | var (clips, formula, id, trackNo) = Decoder.DecodeData(input); 16 | var chainedCommandWrapper = Parser.ParseFormulaToChainedCommand(formula, clips, new ClipMetaData(id, trackNo)); 17 | Assert.IsTrue(chainedCommandWrapper.Success); 18 | 19 | var processedClipWrapper = ClipProcessor.ProcessChainedCommand(chainedCommandWrapper.Result); 20 | Assert.IsTrue(processedClipWrapper.Success); 21 | Assert.IsTrue(processedClipWrapper.Result.Length > 0); 22 | 23 | var processedClip = processedClipWrapper.Result[0]; 24 | byte[] clipData = IOUtilities.GetClipAsBytes(chainedCommandWrapper.Result.TargetMetaData.Id, processedClip).ToArray(); 25 | 26 | Assert.IsTrue(output.Length == clipData.Length); 27 | Assert.IsTrue(output.SequenceEqual(clipData)); 28 | } 29 | 30 | public static void PrintSyntaxTree(List treeTokens, int indent = 0) 31 | { 32 | foreach (var treeToken in treeTokens) 33 | { 34 | Console.WriteLine($"{GetIndent(indent)}{treeToken.Value}"); 35 | if (treeToken.HasChildren) PrintSyntaxTree(treeToken.Children, indent + 1); 36 | } 37 | } 38 | 39 | public static string GetIndent(int indent) 40 | { 41 | StringBuilder sb = new StringBuilder(); 42 | for (var i = 0; i < indent; i++) 43 | { 44 | sb.Append(" "); 45 | } 46 | return sb.ToString(); 47 | } 48 | } -------------------------------------------------------------------------------- /src/mutate4l.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocGeneration", "DocGeneration\DocGeneration.csproj", "{5D1EB757-1464-4295-BB08-8900AACB550A}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9FE9B675-A765-4FCA-94C9-D4C2E8D2257E}" 6 | ProjectSection(SolutionItems) = preProject 7 | ..\README.md = ..\README.md 8 | EndProjectSection 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mutateful", "Mutateful\Mutateful.csproj", "{E98B8F1F-44F0-4460-961B-9A160538307D}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MutatefulTests", "MutatefulTests\MutatefulTests.csproj", "{DF1AB469-2B43-4490-A87F-6946356BA528}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {5D1EB757-1464-4295-BB08-8900AACB550A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5D1EB757-1464-4295-BB08-8900AACB550A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5D1EB757-1464-4295-BB08-8900AACB550A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5D1EB757-1464-4295-BB08-8900AACB550A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {E98B8F1F-44F0-4460-961B-9A160538307D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {E98B8F1F-44F0-4460-961B-9A160538307D}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {E98B8F1F-44F0-4460-961B-9A160538307D}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {E98B8F1F-44F0-4460-961B-9A160538307D}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {DF1AB469-2B43-4490-A87F-6946356BA528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {DF1AB469-2B43-4490-A87F-6946356BA528}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {DF1AB469-2B43-4490-A87F-6946356BA528}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {DF1AB469-2B43-4490-A87F-6946356BA528}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(SolutionProperties) = preSolution 34 | HideSolutionNode = FALSE 35 | EndGlobalSection 36 | GlobalSection(ExtensibilityGlobals) = postSolution 37 | SolutionGuid = {31C39135-CD9B-43BD-876F-55EB9FEFE607} 38 | EndGlobalSection 39 | EndGlobal 40 | --------------------------------------------------------------------------------