// Let's add something new here: Either save data we can use to do it, or do it // Detect the best Flute instrument // The best flute instrument: Has many notes, has long notes, covers a large octave area // So while we already track the octaves covered, we should track numNotes and avgNoteLength of each track or channel // That also might mean we need to start tracking data for tracks, instead of just channels, and allow a toggle to show those instead private void HandleLoadCompleted(object sender, AsyncCompletedEventArgs e) { tickMetaNotes.Clear(); var startTime = DateTime.Now; lock (loadLock) { if (e.Error == null) { //channelNames = new Dictionary<int, string>(); //trackNames = new Dictionary<int, string>(); channels = new Dictionary <int, MidiChannelItem>(); tracks = new Dictionary <int, TrackItem>(); // Set default channel names foreach (var channelNum in sequence.Channels) { var midiChannel = new MidiChannelItem(); midiChannel.Id = channelNum; midiChannel.Name = MidiChannelTypes.Names[channelNum]; midiChannel.Active = true; channels[channelNum] = midiChannel; } foreach (var midiTrack in sequence.Tracks) { var track = new TrackItem(); track.Id = midiTrack.Id; if (!string.IsNullOrWhiteSpace(midiTrack.Name)) { track.Name = midiTrack.Name; } else { track.Name = "Untitled Track"; } track.lowestNote = midiTrack.MinNoteId; track.highestNote = midiTrack.MaxNoteId; track.Active = true; tracks[midiTrack.Id] = track; } //Parallel.ForEach(sequence, track => //{ foreach (var track in sequence) { foreach (var midiEvent in track.Iterator()) { if (midiEvent.MidiMessage is ChannelMessage c) { if (!channels.ContainsKey(c.MidiChannel)) { var midiChannel = new MidiChannelItem(); midiChannel.Id = c.MidiChannel; midiChannel.Name = MidiChannelTypes.Names[c.MidiChannel]; midiChannel.Active = true; channels[c.MidiChannel] = midiChannel; } if (c.Data2 > 0 && c.Command == ChannelCommand.NoteOn) { if (c.MidiChannel == 9) // Glockenspiel - always channel 9 { drumNoteCounts[c.Data1]++; // They're all 0 by default channels[c.MidiChannel].Active = false; // Disabled by default } if (c.Data1 > channels[c.MidiChannel].highestNote) { channels[c.MidiChannel].highestNote = c.Data1; } if (c.Data1 < channels[c.MidiChannel].lowestNote) { channels[c.MidiChannel].lowestNote = c.Data1; } // Track noteOn events and when we get a noteOff or velocity 0, add up note lengths channels[c.MidiChannel].noteTicks[c.Data1] = midiEvent.AbsoluteTicks; channels[c.MidiChannel].numNotes++; if (!channels[c.MidiChannel].tickNotes.ContainsKey(midiEvent.AbsoluteTicks)) { channels[c.MidiChannel].tickNotes[midiEvent.AbsoluteTicks] = new List <MidiNote>(); } channels[c.MidiChannel].tickNotes[midiEvent.AbsoluteTicks].Add(new MidiNote() { note = c.Data1, tickNumber = midiEvent.AbsoluteTicks, track = track.Id, channel = c.MidiChannel }); // Ideally I'd like to parse out chords of size 3 or more, using some setting like RemoveLowest, RemoveHighest, RemoveMiddle // Though I'm unsure how I'd really do that to anything except lutemod; lutebot is reading straight from the midi data isn't it? } else if (c.Command == ChannelCommand.ProgramChange) { string channelName = MidiChannelTypes.Names[c.Data1]; channels[c.MidiChannel].Name = channelName; } else if (c.Command == ChannelCommand.NoteOff || (c.Command == ChannelCommand.NoteOn && c.Data2 == 0)) { if (channels[c.MidiChannel].noteTicks.ContainsKey(c.Data1)) { // Find the original note... var startTick = channels[c.MidiChannel].noteTicks[c.Data1]; var length = midiEvent.AbsoluteTicks - startTick; var note = channels[c.MidiChannel].tickNotes[startTick].Where(n => n.note == c.Data1).FirstOrDefault(); note.length = length; // If we already have a note with a length on this same tick, we don't need to add to totalNoteLength...? // I want to avoid a chord of 3 having 3x the detected total length... // I'll just have to deal with that later, and we won't bother tracking totalLength here // Just add all the lengths, we'll divide after we read everything channels[c.MidiChannel].avgNoteLength += length; channels[c.MidiChannel].noteTicks.Remove(c.Data1); } } } else if (midiEvent.MidiMessage is MetaMessage meta) { if (meta.MetaType == MetaType.TrackName) { var bytes = meta.GetBytes(); // Byte 3 is the length, bytes 4+ are the text in ascii // But apparently the library trims out that for me, and the entire bytes array is what we want if (bytes.Length > 0) { string trackName = ASCIIEncoding.ASCII.GetString(bytes); tracks[track.Id].Name = trackName; } } else if (meta.MetaType == MetaType.Tempo) { var newTempo = BytesToInt(meta.GetBytes()); // We divide by division when converting for lutemod only, apparently if (!tickMetaNotes.ContainsKey(midiEvent.AbsoluteTicks)) { tickMetaNotes[midiEvent.AbsoluteTicks] = new List <MidiNote>(); } tickMetaNotes[midiEvent.AbsoluteTicks].Add(new MidiNote() { note = newTempo, tickNumber = midiEvent.AbsoluteTicks, track = track.Id }); } } } }//); // So, now we have a totalNoteLength... except, no. That's not quite useful // We do have the list of all notes with lengths on them, though. // So we can get a total length of parts by, for each tick/entry, take the longest length for that tick // And then only count new notes if their start+length > thatTick+thatLength // Compile noteLengths of channels foreach (var kvp in channels) { var channel = kvp.Value; if (channel.numNotes > 0) { channel.avgNoteLength = channel.avgNoteLength / channel.numNotes; } int nextValidTick = 0; foreach (var noteKvp in channel.tickNotes.OrderBy(n => n.Key)) { var noteList = noteKvp.Value; var note = noteList.OrderByDescending(n => n.length).First(); if (note.tickNumber > nextValidTick) { channel.totalNoteLength += note.length; nextValidTick = note.tickNumber + note.length; } else if (note.tickNumber + note.length > nextValidTick) { var diff = (note.tickNumber + note.length) - nextValidTick; channel.totalNoteLength += diff; nextValidTick = nextValidTick + diff; } } if (channel.tickNotes.Count > 0) { channel.maxChordSize = channel.tickNotes.Max(t => t.Value.Count); } } // Add a note on the drum channel if (channels.ContainsKey(9)) { channels[9].Name += " (DRUMS)"; } // Remove any channels or tracks that don't contain any notes channels = channels.Values.Where(c => c.numNotes > 0).ToDictionary(c => c.Id); Console.WriteLine("Read track data in " + (DateTime.Now - startTime).TotalMilliseconds); // Now let's get a sorted list of drum note counts // I can't figure out how to do this nicely. //for (int i = 0; i < drumNoteCounts.Length; i++) //{ // DrumNoteCounts.Add(new KeyValuePair<int, int>(i, drumNoteCounts[i])); //} //DrumNoteCounts = DrumNoteCounts.OrderBy((kvp) => kvp.Value).ToList(); base.LoadCompleted(this, e); mordhauOutDevice.HighMidiNoteId = sequence.MaxNoteId; mordhauOutDevice.LowMidiNoteId = sequence.MinNoteId; rustOutDevice.HighMidiNoteId = sequence.MaxNoteId; rustOutDevice.LowMidiNoteId = sequence.MinNoteId; } } }