コード例 #1
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, DeviceController device)
        {
            var kitNumber = context.ParseResult.ValueForOption <int>("kit");
            var file      = context.ParseResult.ValueForOption <string>("file");

            try
            {
                Stopwatch sw = Stopwatch.StartNew();
                // Allow up to 30 seconds in total.
                var token = new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token;
                var kit   = await device.LoadKitAsync(kitNumber, null, token);

                console.WriteLine($"Finished loading in {(int) sw.Elapsed.TotalSeconds} seconds");
                using (var stream = File.Create(file))
                {
                    kit.Save(stream);
                }
                console.WriteLine($"Saved kit to {file}");
            }
            catch (OperationCanceledException)
            {
                console.WriteLine("Data loading from device was cancelled");
                return(1);
            }
            catch (Exception ex)
            {
                console.WriteLine($"Error loading data from device: {ex}");
                return(1);
            }
            return(0);
        }
コード例 #2
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, RolandMidiClient client)
        {
            var schema              = ModuleSchema.KnownSchemas[ModuleIdentifier.AE10].Value;
            var instrumentField     = schema.PhysicalRoot.ResolveField("TemporaryStudioSet/Part[1]/Instrument");
            var instrumentDataField = new EnumDataField((EnumField)instrumentField);

            // Presets
            client.SendData(0x01_00_00_05, new byte[] { 64 });
            for (int i = 1; i <= 128; i++)
            {
                await ShowStudioSet('P', i);
            }

            // Preset 129 is in a different bank...
            client.SendData(0x01_00_00_05, new byte[] { 65 });
            for (int i = 129; i <= 129; i++)
            {
                await ShowStudioSet('P', i);
            }

            // User sets
            client.SendData(0x01_00_00_05, new byte[] { 0 });
            for (int i = 1; i <= 100; i++)
            {
                await ShowStudioSet('U', i);
            }

            async Task ShowStudioSet(char prefix, int index)
            {
                client.SendData(0x01_00_00_06, new byte[] { (byte)((index - 1) & 0x7f) });
                await Task.Delay(50);

                var common = await client.RequestDataAsync(0x18_00_00_00, 16, CancellationToken.None);

                string name = Encoding.UTF8.GetString(common);

                console.WriteLine($"{prefix}:{index:D3}: {name}");
                for (int partIndex = 0; partIndex < 4; partIndex++)
                {
                    var part = await client.RequestDataAsync(0x18_00_20_00 + partIndex * 0x1_00, 9, CancellationToken.None);

                    if (part[1] != 1)
                    {
                        continue;
                    }
                    // The address doesn't matter, as only the offset is passed.
                    var segment = new DataSegment(ModuleAddress.FromDisplayValue(0), part);
                    instrumentDataField.Load(segment);
                    console.WriteLine($"Part {partIndex + 1}: {instrumentDataField.Value}");
                }
                console.WriteLine();
            }

            return(0);
        }
コード例 #3
0
ファイル: DumpDataCommand.cs プロジェクト: marked23/DemoCode
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, RolandMidiClient client)
        {
            var address = ModuleAddress.FromDisplayValue(HexInt32.Parse(context.ParseResult.ValueForOption <string>("address")).Value);
            var size    = HexInt32.Parse(context.ParseResult.ValueForOption <string>("size")).Value;
            var timeout = context.ParseResult.ValueForOption <int>("timeout");

            // Wait up to 10 seconds to receive all the requested data...
            int sizeReceived    = 0;
            var delayTask       = Task.Delay(TimeSpan.FromSeconds(timeout));
            var completeTaskCts = new TaskCompletionSource <int>();

            client.DataSetMessageReceived += DumpMessage;
            client.SendDataRequestMessage(address.DisplayValue, size);

            await Task.WhenAny(delayTask, completeTaskCts.Task);

            return(0);

            void DumpMessage(object sender, DataSetMessage message)
            {
                ModuleAddress address = ModuleAddress.FromDisplayValue(message.Address);

                console.WriteLine($"Address: {address} Length: {message.Length:x4}");
                int index = 0;

                while (index < message.Length)
                {
                    var builder     = new StringBuilder();
                    var textBuilder = new StringBuilder();
                    builder.Append(address);
                    builder.Append(" ");
                    for (int i = 0; i < 16 && index < message.Length; i++)
                    {
                        byte b = message.Data[index];
                        textBuilder.Append(b >= 32 && b < 127 ? (char)b : ' ');
                        builder.Append(b.ToString("x2"));
                        builder.Append(" ");
                        index++;
                    }
                    string text = builder.ToString().PadRight(9 + 16 * 3) + textBuilder;
                    console.WriteLine(text);
                    address = address.PlusLogicalOffset(16);
                }
                console.WriteLine();
                if (Interlocked.Add(ref sizeReceived, message.Length) >= size)
                {
                    completeTaskCts.SetResult(0);
                }
            }
        }
コード例 #4
0
ファイル: DeviceDetection.cs プロジェクト: xiulolz/DemoCode
        internal static async Task <(RolandMidiClient client, ModuleSchema schema)> DetectDeviceAsync(IStandardStreamWriter console)
        {
            var inputDevices  = MidiDevices.ListInputDevices();
            var outputDevices = MidiDevices.ListOutputDevices();
            var commonNames   = inputDevices.Select(input => input.Name)
                                .Intersect(outputDevices.Select(output => output.Name))
                                .OrderBy(x => x)
                                .ToList();

            if (commonNames.Count != 1)
            {
                console.WriteLine("Error: No input and output MIDI ports with the same name detected.");
                return(null, null);
            }
            string name           = commonNames[0];
            var    matchedInputs  = inputDevices.Where(input => input.Name == name).ToList();
            var    matchedOutputs = outputDevices.Where(output => output.Name == name).ToList();

            if (matchedInputs.Count != 1 || matchedOutputs.Count != 1)
            {
                console.WriteLine($"Error: Name {name} matches multiple input or output MIDI ports.");
                return(null, null);
            }
            var identities = await MidiDevices.ListDeviceIdentities(matchedInputs[0], matchedOutputs[0], TimeSpan.FromSeconds(1));

            if (identities.Count != 1)
            {
                console.WriteLine($"Error: {(identities.Count == 0 ? "No" : "Multiple")} devices detected for MIDI port {name}.");
                return(null, null);
            }

            var schemaKeys = SchemaRegistry.KnownSchemas.Keys;
            var identity   = identities[0];

            var matchingKeys = schemaKeys.Where(sk => sk.FamilyCode == identity.FamilyCode && sk.FamilyNumberCode == identity.FamilyNumberCode).ToList();

            if (matchingKeys.Count != 1)
            {
                console.WriteLine($"Error: {(matchingKeys.Count == 0 ? "No" : "Multiple")} schemas detected for MIDI device.");
                return(null, null);
            }
            var schema           = SchemaRegistry.KnownSchemas[matchingKeys[0]];
            var moduleIdentifier = schema.Value.Identifier;

            var client = await MidiDevices.CreateRolandMidiClientAsync(matchedInputs[0], matchedOutputs[0], identity, moduleIdentifier.ModelId);

            return(client, schema.Value);
        }
コード例 #5
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, RolandMidiClient client)
        {
            var schema              = ModuleSchema.KnownSchemas[ModuleIdentifier.AE10].Value;
            var instrumentField     = schema.PhysicalRoot.ResolveField("TemporaryStudioSet/Part[1]/Instrument");
            var instrumentDataField = new EnumDataField((EnumField)instrumentField);
            var deviceController    = new DeviceController(client, new ConsoleLogger(console));

            var temporaryKitRoot = schema.LogicalRoot.ResolveNode("TemporaryStudioSet");

            // First 100 presets
            client.SendData(0x01_00_00_05, new byte[] { 64 });
            for (int i = 1; i <= 100; i++)
            {
                console.WriteLine($"Copying studio set {i}");
                client.SendData(0x01_00_00_06, new byte[] { (byte)((i - 1) & 0x7f) });
                await Task.Delay(40);

                var data = ModuleData.FromLogicalRootNode(temporaryKitRoot);
                await deviceController.LoadDescendants(data.LogicalRoot, null, progressHandler : null, cancellationToken : default);

                await deviceController.SaveDescendants(data.LogicalRoot, schema.GetKitRoot(i).Container.Address, progressHandler : null, cancellationToken : default);
            }

            return(0);
        }
コード例 #6
0
        public void SetErrorInstruction(string message, NotificationErrorType notificationErrorType)
        {
            IStandardStreamWriter errorWriter = _console.Error;

            switch (notificationErrorType)
            {
            case NotificationErrorType.Error:

                Environment.ExitCode = CLIConsts.ExistErrorCode;

                _console.ForegroundColor = ConsoleColor.DarkRed;
                break;

            case NotificationErrorType.Attention:

                _console.ForegroundColor = ConsoleColor.DarkBlue;
                break;

            case NotificationErrorType.None:
            default:
                throw new Exception($"Invalid NotificationErrorType '{notificationErrorType}'");
            }

            errorWriter.WriteLine(message);

            _console.ForegroundColor = ConsoleColor.White;
        }
コード例 #7
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, RolandMidiClient client)
        {
            using (var device = new DeviceController(client))
            {
                var schema    = device.Schema;
                var channel   = context.ParseResult.ValueForOption <int>("channel");
                var keys      = context.ParseResult.ValueForOption <string>("keys");
                var targetKit = context.ParseResult.ValueForOption <int>("kit");
                if (targetKit < 1 || targetKit + 1 > schema.Kits)
                {
                    console.WriteLine($"Kit {targetKit} is out of range for {schema.Identifier.Name} for this command.");
                    console.WriteLine("Note that one extra kit is required after the specified one.");
                    return(1);
                }

                // Detect the current kit
                var currentKit = await device.GetCurrentKitAsync(CancellationToken.None);

                // Copy current kit to target kit and target kit + 1
                var kit = await device.LoadKitAsync(currentKit, progressHandler : null, CreateCancellationToken());

                var dataNode = new DataTreeNode(kit.Data, kit.KitRoot);
                await device.SaveDescendants(dataNode, schema.GetKitRoot(targetKit).Container.Address, progressHandler : null, CreateCancellationToken());

                await device.SaveDescendants(dataNode, schema.GetKitRoot(targetKit + 1).Container.Address, progressHandler : null, CreateCancellationToken());

                await device.SetCurrentKitAsync(targetKit, CancellationToken.None);

                var programChangeCommand = (byte)(0xc0 | (channel - 1));

                // Now listen for the foot switch...
                client.MessageReceived += async(sender, message) =>
                {
                    if (message.Data.Length == 2 && message.Data[0] == programChangeCommand)
                    {
                        console.WriteLine("Turning the page...");
                        SendKeysUtilities.SendWait(keys);
                        await device.SetCurrentKitAsync(targetKit, CancellationToken.None);
                    }
                };
                console.WriteLine("Listening for foot switch");
                await Task.Delay(TimeSpan.FromHours(1));
            }
            return(0);

            CancellationToken CreateCancellationToken() => new CancellationTokenSource(10000).Token;
        }
コード例 #8
0
 public void Write(string value)
 {
     foreach (var console in _consoles)
     {
         IStandardStreamWriter writer = _error ? console.Error : console.Out;
         writer.WriteLine(value.TrimEnd('\r', '\n'));
     }
 }
コード例 #9
0
        public static void VerboseWriteLine(this IStandardStreamWriter writer, string value = "")
        {
            if (!Globals.Verbose)
            {
                return;
            }

            writer.WriteLine(value);
        }
コード例 #10
0
        public void SetErrorMessage(string message)
        {
            Environment.ExitCode = CLIConsts.ExistErrorCode;

            IStandardStreamWriter errorWriter = _console.Error;

            _console.ForegroundColor = ConsoleColor.Red;
            errorWriter.WriteLine(message);

            _console.ForegroundColor = ConsoleColor.White;
        }
コード例 #11
0
    private static void LogException(IStandardStreamWriter output, Logger logger, Exception error)
    {
        logger.Error(error.Message);
        output.WriteLine(error.Message);
        if (error.StackTrace != null)
        {
            logger.Error(error.StackTrace);
            output.WriteLine(error.StackTrace);
        }

        if (error.InnerException == null)
        {
            return;
        }

        logger.Error("==Inner Exception==");
        output.WriteLine("==Inner Exception==");
        LogException(output, logger, error.InnerException);
        logger.Error("==End Inner Exception==");
        output.WriteLine("==End Inner Exception==");
    }
コード例 #12
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, DeviceController device)
        {
            var schema = device.Schema;

            for (int i = 1; i <= schema.Kits; i++)
            {
                var name = await device.LoadKitNameAsync(i, CancellationToken.None);

                console.WriteLine($"Kit {i}: {name}");
            }
            return(0);
        }
コード例 #13
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, RolandMidiClient client)
        {
            var channel  = context.ParseResult.ValueForOption <int>("channel");
            var keys     = context.ParseResult.ValueForOption <string>("keys");
            var midiNote = context.ParseResult.ValueForOption <int>("note");

            var noteOn = (byte)(0x90 | (channel - 1));

            // Now listen for the foot switch...
            client.MessageReceived += (sender, message) =>
            {
                if (message.Data.Length == 3 && message.Data[0] == noteOn && message.Data[1] == midiNote)
                {
                    console.WriteLine("Turning the page...");
                    SendKeysUtilities.SendWait(keys);
                }
            };
            console.WriteLine("Listening for MIDI note");
            await Task.Delay(TimeSpan.FromHours(1));

            return(0);
        }
コード例 #14
0
        /// <summary>
        /// Writes a kit to a device.
        /// </summary>
        internal static async Task WriteKit(RolandMidiClient client, Kit kit, int kitNumber, IStandardStreamWriter console)
        {
            var targetKitRoot = kit.Schema.KitRoots[kitNumber];
            var clonedData    = kit.KitRoot.Context.CloneData(kit.Data, targetKitRoot.Context.Address);
            var segments      = clonedData.GetSegments();

            console.WriteLine($"Writing {segments.Count} containers to device {kit.Schema.Identifier.Name} kit {kitNumber}");

            foreach (var segment in segments)
            {
                client.SendData(segment.Start.Value, segment.CopyData());
                await Task.Delay(40);
            }
        }
コード例 #15
0
        private static void OnException(Exception ex, InvocationContext context)
        {
            IStandardStreamWriter err = context.Console.Error;

            switch (ex)
            {
            case AggregateException aex:
                aex.Handle(x =>
                {
                    err.WriteLine($"fatal: {x.Message}");
                    return(true);
                });
                break;

            case Win32Exception wex:
                err.WriteLine($"fatal: {wex.Message} [0x{wex.NativeErrorCode:x}]");
                break;

            default:
                err.WriteLine($"fatal: {ex.Message}");
                break;
            }
        }
コード例 #16
0
        private static async Task PopulateSegment(RolandMidiClient client, ModuleData data, AnnotatedContainer annotatedContainer, CancellationToken token, IStandardStreamWriter console)
        {
            var timerToken     = new CancellationTokenSource(TimeSpan.FromSeconds(1)).Token;
            var effectiveToken = CancellationTokenSource.CreateLinkedTokenSource(token, timerToken).Token;

            try
            {
                var segment = await client.RequestDataAsync(annotatedContainer.Context.Address.Value, annotatedContainer.Container.Size, effectiveToken);

                data.Populate(annotatedContainer.Context.Address, segment);
            }
            catch (OperationCanceledException) when(timerToken.IsCancellationRequested)
            {
                console.WriteLine($"Device didn't respond for container {annotatedContainer.Path}; skipping.");
            }
        }
コード例 #17
0
        private static void WriteLine(this IConsole console, IStandardStreamWriter writer, string message, ConsoleColor?color)
        {
            lock (_writeLock)
            {
                if (color.HasValue)
                {
                    console.SetTerminalForeground(color.Value);
                }

                writer.WriteLine(message);

                if (color.HasValue)
                {
                    console.ResetColor();
                }
            }
        }
コード例 #18
0
        private static void WriteMessage(this IConsole console, IStandardStreamWriter writer, ConsoleColor color, string messsage)
        {
            var terminal = console.GetTerminal(preferVirtualTerminal: false);
            var originalForegroundColor = terminal?.ForegroundColor ?? Console.ForegroundColor;

            if (terminal is not null)
            {
                terminal.ForegroundColor = color;
            }

            writer.WriteLine(messsage);

            if (terminal is not null)
            {
                terminal.ForegroundColor = originalForegroundColor;
            }
        }
コード例 #19
0
        /// <summary>
        /// Reads a kit from a device.
        /// </summary>
        internal static async Task <Kit> ReadKit(ModuleSchema schema, RolandMidiClient client, int kitNumber, IStandardStreamWriter console)
        {
            // Allow up to 30 seconds in total, and 1 second per container.
            var overallToken = new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token;

            var moduleData = new ModuleData();
            var kitRoot    = schema.KitRoots[kitNumber];
            var containers = kitRoot.Context.AnnotateDescendantsAndSelf().Where(c => c.Container.Loadable).ToList();

            console.WriteLine($"Reading {containers.Count} containers from device {schema.Identifier.Name} kit {kitNumber}");
            foreach (var container in containers)
            {
                await PopulateSegment(client, moduleData, container, overallToken, console);
            }
            var clonedData = kitRoot.Context.CloneData(moduleData, schema.KitRoots[1].Context.Address);

            return(new Kit(schema, clonedData, kitNumber));
        }
コード例 #20
0
 public void WriteLine(string text) => _writer.WriteLine($"{_indent}{text}");
コード例 #21
0
 internal static void WriteLine(this IStandardStreamWriter streamWriter, string message)
 {
     streamWriter.Write(message);
     streamWriter.WriteLine();
 }
コード例 #22
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, DeviceController device)
        {
            var path                = context.ParseResult.ValueForOption <string>("path");
            var switchFieldName     = context.ParseResult.ValueForOption <string>("switch");
            var parametersFieldName = context.ParseResult.ValueForOption <string>("parameters");

            var mfxNode         = device.Schema.LogicalRoot.ResolveNode(path);
            var mfxContainer    = mfxNode.Container;
            var switchField     = (EnumField)mfxContainer.ResolveField(switchFieldName);
            var parametersField = (OverlayField)mfxContainer.ResolveField(parametersFieldName);

            var deviceData            = ModuleData.FromLogicalRootNode(mfxNode);
            var deviceParametersField = (OverlayDataField)deviceData.GetDataField(parametersField);

            console.WriteLine($"Loading original MFX data");
            await device.LoadDescendants(deviceData.LogicalRoot, null, null, default);

            var originalSnapshot = deviceData.CreateSnapshot();

            var modelData            = ModuleData.FromLogicalRootNode(mfxNode);
            var modelTypeField       = (EnumDataField)modelData.GetDataField(switchField);
            var modelParametersField = (OverlayDataField)modelData.GetDataField(parametersField);

            try
            {
                // Set it to the max value so that we'll be resetting it.
                await SetDeviceMfx(switchField.Max);

                for (int mfxType = switchField.Min; mfxType <= switchField.Max; mfxType++)
                {
                    // Make the change on the device...
                    await SetDeviceMfx(mfxType);

                    await device.LoadDescendants(deviceData.LogicalRoot, null, null, default);

                    // Make the change in the model...
                    modelTypeField.RawValue = mfxType;

                    var modelFields  = modelParametersField.CurrentFieldList;
                    var deviceFields = deviceParametersField.CurrentFieldList;

                    if (modelFields.Description != deviceFields.Description)
                    {
                        console.WriteLine($"Mismatch in description: '{modelFields.Description}' != '{deviceFields.Description}'. Skipping.");
                        continue;
                    }

                    console.WriteLine($"Comparing fields for {modelFields.Description}");
                    foreach (var(modelField, deviceField) in AsNumericFields(modelFields.Fields).Zip(AsNumericFields(deviceFields.Fields)))
                    {
                        if (modelField.RawValue != deviceField.RawValue)
                        {
                            console.WriteLine($"{modelField.SchemaField.Name}: Device={deviceField.RawValue}; Model={modelField.RawValue}");
                        }
                    }
                    console.WriteLine();
                }
            }
            finally
            {
                // Restore the original data
                console.WriteLine($"Restoring original kit data");
                deviceData.LoadSnapshot(originalSnapshot, NullLogger.Instance);
                await device.SaveDescendants(deviceData.LogicalRoot, targetAddress : null, progressHandler : null, CancellationToken.None);
            }
            return(0);

            async Task SetDeviceMfx(int type)
            {
                var segment = new DataSegment(mfxContainer.Address + switchField.Offset, new[] { (byte)type });
                await device.SaveSegment(segment, CancellationToken.None);
            }

            IEnumerable <NumericDataFieldBase> AsNumericFields(IEnumerable <IDataField> fields)
            {
                foreach (var field in fields)
                {
                    switch (field)
                    {
                    case NumericDataFieldBase numeric:
                        yield return(numeric);

                        break;

                    case TempoDataField tempo:
                        yield return(tempo.SwitchDataField);

                        yield return(tempo.NumericDataField);

                        yield return(tempo.MusicalNoteDataField);

                        break;

                    default:
                        throw new InvalidOperationException($"Can't convert {field.GetType()} into a numeric field");
                    }
                }
            }
        }
コード例 #23
0
 public static void WriteLine(this IStandardStreamWriter writer, string?value)
 {
     writer.WriteLine(value, avoidExtraNewLine: false);
 }
コード例 #24
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, RolandMidiClient client)
        {
            using var writer = File.CreateText(context.ParseResult.ValueForOption <string>("file"));
            int      chunkSize = context.ParseResult.ValueForOption <int>("chunkSize");
            TimeSpan timeout   = TimeSpan.FromMilliseconds(context.ParseResult.ValueForOption <int>("timeout"));

            var address          = ModuleAddress.FromDisplayValue(0);
            int messagesReceived = 0;

            client.DataSetMessageReceived += DumpMessage;

            // TODO: Make this tighter. But we're unlikely to really get here.
            while (address.DisplayValue < 0x74_00_00_00)
            {
                int receivedAtStartOfChunk = messagesReceived;
                client.SendDataRequestMessage(address.DisplayValue, chunkSize);

                int receivedAtStartOfDelay;
                do
                {
                    receivedAtStartOfDelay = messagesReceived;
                    await Task.Delay(timeout);
                } while (messagesReceived > receivedAtStartOfDelay);

                int receivedForChunk = messagesReceived - receivedAtStartOfChunk;

                console.WriteLine($"Received {receivedForChunk} messages in chunk at {address}");
                writer.Flush();
                address = address.PlusLogicalOffset(chunkSize);
            }

            return(0);

            void DumpMessage(object sender, DataSetMessage message)
            {
                ModuleAddress address = ModuleAddress.FromDisplayValue(message.Address);

                writer.WriteLine($"Address: {address} Length: {message.Length:x4}");
                int index = 0;

                while (index < message.Length)
                {
                    var builder     = new StringBuilder();
                    var textBuilder = new StringBuilder();
                    builder.Append(address);
                    builder.Append(" ");
                    for (int i = 0; i < 16 && index < message.Length; i++)
                    {
                        byte b = message.Data[index];
                        textBuilder.Append(b >= 32 && b < 127 ? (char)b : ' ');
                        builder.Append(b.ToString("x2"));
                        builder.Append(" ");
                        index++;
                    }
                    string text = builder.ToString().PadRight(9 + 16 * 3) + textBuilder;
                    writer.WriteLine(text);
                    address = address.PlusLogicalOffset(16);
                }
                writer.WriteLine();
                Interlocked.Increment(ref messagesReceived);
            }
        }
コード例 #25
0
 public static void WriteLineIndented(
     this IStandardStreamWriter writer,
     string text,
     int level  = 1,
     int indent = DefaultIndent) =>
 WriteIndented(s => writer.WriteLine(s), text, level, indent);
コード例 #26
0
        protected override async Task <int> InvokeAsync(InvocationContext context, IStandardStreamWriter console, DeviceController device)
        {
            var kit                   = context.ParseResult.ValueForOption <int>("kit");
            var triggerRoot           = device.Schema.GetTriggerRoot(kit, trigger: 1);
            var deviceData            = ModuleData.FromLogicalRootNode(triggerRoot);
            var modelData             = ModuleData.FromLogicalRootNode(triggerRoot);
            var defaultValuesSnapshot = modelData.CreateSnapshot();

            var deviceDataRoot = new DataTreeNode(deviceData, triggerRoot);
            await device.LoadDescendants(deviceDataRoot, targetAddress : null, progressHandler : null, CancellationToken.None);

            var originalSnapshot     = deviceData.CreateSnapshot();
            var instrumentField      = device.Schema.GetMainInstrumentField(kit, trigger: 1);
            var modelInstrumentField = (InstrumentDataField)modelData.GetDataField(instrumentField);

            var instrumentContainers = triggerRoot.DescendantFieldContainers();

            var differences = new List <Difference>();
            var logger      = new ConsoleLogger(console);

            try
            {
                // Reset the device to an empty snapshot
                deviceData.LoadSnapshot(defaultValuesSnapshot, logger);
                await device.SaveDescendants(deviceDataRoot, targetAddress : null, progressHandler : null, CancellationToken.None);

                foreach (var instrument in device.Schema.PresetInstruments)
                {
                    // Make the change on the real module and load the data.
                    // Assumption: the segment containing the instrument itself (e.g. KitPadInst) doesn't
                    // have any implicit model changes to worry about.
                    await device.SetInstrumentAsync(kit, trigger : 1, instrument, CancellationToken.None);

                    await device.LoadDescendants(deviceDataRoot, targetAddress : null, progressHandler : null, CancellationToken.None);

                    // Make the change in the model.
                    modelData.LoadSnapshot(defaultValuesSnapshot, logger);
                    modelInstrumentField.Instrument = instrument;

                    // Compare the two.
                    bool anyDifferences = false;
                    foreach (var container in instrumentContainers)
                    {
                        // We won't compare InstrumentDataField, TempoDataField or StringDataField this way, but that's okay.
                        var realFields  = deviceData.GetDataFields(container).SelectMany(ExpandOverlays).OfType <NumericDataFieldBase>().ToList();
                        var modelFields = modelData.GetDataFields(container).SelectMany(ExpandOverlays).OfType <NumericDataFieldBase>().ToList();
                        if (realFields.Count != modelFields.Count)
                        {
                            console.WriteLine($"Major failure: for instrument {instrument.Id} ({instrument.Group} / {instrument.Name}), found {realFields.Count} real fields and {modelFields.Count} model fields in container {container.Path}");
                            return(1);
                        }

                        foreach (var pair in realFields.Zip(modelFields))
                        {
                            var real  = pair.First;
                            var model = pair.Second;
                            if (real.SchemaField != model.SchemaField)
                            {
                                console.WriteLine($"Major failure: for instrument {instrument.Id} ({instrument.Group} / {instrument.Name}), mismatched schema field for {container.Path}: {real.SchemaField.Name} != {model.SchemaField.Name}");
                                return(1);
                            }
                            var realValue      = real.RawValue;
                            var predictedValue = model.RawValue;
                            if (realValue != predictedValue)
                            {
                                anyDifferences = true;
                                differences.Add(new Difference(instrument, container, real.SchemaField, realValue, predictedValue));
                            }
                        }
                    }
                    console.Write(anyDifferences ? "!" : ".");
                }
            }
            finally
            {
                // Restore the original data
                deviceData.LoadSnapshot(originalSnapshot, logger);
                await device.SaveDescendants(deviceDataRoot, targetAddress : null, progressHandler : null, CancellationToken.None);
            }
            console.WriteLine();
            foreach (var difference in differences)
            {
                console.WriteLine(difference.ToString());
            }
            console.WriteLine($"Total differences: {differences.Count}");
            return(0);

            IEnumerable <IDataField> ExpandOverlays(IDataField field) =>
            field is OverlayDataField odf ? odf.CurrentFieldList.Fields : Enumerable.Repeat(field, 1);
        }