melanchall / drywetmidi

.NET library to read, write, process MIDI files and to work with MIDI devices
https://melanchall.github.io/drywetmidi
MIT License
523 stars 73 forks source link

please help a noob #254

Closed clave-sounds closed 1 year ago

clave-sounds commented 1 year ago

Hi. I'm a musician and sound engineer trying to learn unity and C#. My knowledge of Midi is pretty good, but mostly just channel mode messages, notes, CC's etc. as I don't normally have to deal with clocks.

I'm trying to make a simple game which requires functionality of either the creation of MIDI files with randomised parameters in game, OR realtime midi events in game to occur based on beats & 480 ticks.

I'd much rather do this the realtime way and just make a big sequence list of e.g noteOn(60 100) on beat 76 tick 240 etc. But I'm not convinced that realtime midi can sound stable. I've asked chat gpt to build me a few tick generators (and learned a lot in the process), and all of them are really bad when it gets to 16ths and even slower events in many cases. I've downloaded the Melanchall libraries to try and fix this.

So my first question is: Can realtime midi with 480 ticks sound stable to a musicians ear at say 180BPM swung 16ths? If it can't, that's fine and I'll investigate the MIDI file route instead. I've tested the Melanchall midi file playback and it's tight enough for my needs.

I'm in limbo at the moment, if possible, I'd much rather do realtime. but I can't seem to make anything that functions to test its stability. I basically just need 480 ticks and a beat count. Whether that's just with HighPrecisionTickGen or using MidiClock as well, I don't really mind, although with MidiClock would be preferable as I may want to adjust the tempo dynamically later and my understanding is that using MidiClock is better for this than just generating ticks.

Please could someone make me a simple C# script using Melanchall libraries that creates 16th notes based on 480 ticks (0, 120, 240, 360) with public variables for tempo and start/stop and the midi channel outport and note.

I'm happy to spend hours learning, I just don't know which direction to address my learning energy and I've really hit a wall.

Thanks

melanchall commented 1 year ago

Hi David,

Unfortunately it's not clear enough what you want, so I'm just going to write some information related with your message.

First of all, MidiClock is just a timer, so you can set its interval to some time span X and get Ticked event fired every X. X should be in milliseconds. We can't just say it to tick every X ticks, because a tick means nothing without tempo information and you definitely know this if your knowledge of Midi is pretty good. So to setup a clock to tick every 480 ticks:

var tempoMap = TempoMap.Default;
var interval480ticks = TimeConverter.ConvertTo<MetricTimeSpan>(480, tempoMap);
var midiClock = new MidiClock(
    true,
    new HighPrecisionTickGenerator(),
    interval480ticks);
midiClock.Ticked += OnTicked;

Well, about stability of ticking. HighPrecisionTickGenerator is implemented currently for Windows and macOS. None of those operating systems are real-time so obviously it's impossible to build a timer that would tick exactly every X ms. Actual interval can drift around the specified value. But the precision of the HighPrecisionTickGenerator is pretty good. Of course if you run many other programs in parallel in Windows, it can affect the tick generator and that can't be avoided (the situation is better on macOS but is not ideal anyway).

As for creating 16th notes, what about this code to create 100 A#2 notes stored to a MIDI file:

var tempoMap = TempoMap.Create(new TicksPerQuarterNoteTimeDivision(480));
var midiFile = new PatternBuilder()
    .SetNoteLength(MusicalTimeSpan.Sixteenth)
    .Note(Octave.Get(2).ASharp)
    .Repeat(99)
    .Build()
    .ToFile(tempoMap);

You can also do something like that:

var tempoMap = TempoMap.Create(new TicksPerQuarterNoteTimeDivision(480));
var notes = SevenBitNumber
    .Values
    .Select(noteNumber => new Note(noteNumber).SetLength(MusicalTimeSpan.Sixteenth, tempoMap))
    .ToArray();

This code will create 16th notes for all possible MIDI note numbers.

I hope I've provided you information to get started.

Thanks, Max

clave-sounds commented 1 year ago

thank you. The closest I can get to your suggestion working for me is below:

using UnityEngine; using Melanchall.DryWetMidi.Common; using Melanchall.DryWetMidi.Core; using Melanchall.DryWetMidi.Multimedia; using Melanchall.DryWetMidi.Interaction;

public class MidiClockExample : MonoBehaviour { private MidiClock midiClock;

void Start()
{
    var tempoMap = TempoMap.Default;
    var interval480ticks = TimeConverter.ConvertTo<MetricTimeSpan>(480, tempoMap);
    midiClock = new MidiClock(true, new HighPrecisionTickGenerator(), interval480ticks);
    midiClock.Ticked += OnTicked;
}

void OnTicked()
{
    Debug.Log("MIDI clock ticked!");
}

}

However I get an error: "No Overload for 'OnTicked' matches delegate 'EventHandler" If I comment out the line //midiClock.Ticked += OnTicked; Then I get no errors but can't subscribe to the ticks in any way. How do I subscribe to the ticks? I'm feeling my way in the dark...

melanchall commented 1 year ago

Please note that DryWetMIDI has complete API documentation. If you see docs for MidiClock.Ticked you'll notice that the event's type is EventHandler. Then you can click on EventHandler and you'll be sent to Microsoft docs. Where you can see the correct signature of the event handler:

public delegate void EventHandler(object? sender, EventArgs e);

So you need to define OnTicked in this way:

void OnTicked(object sender, EventArgs e)
{
    Debug.Log("MIDI clock ticked!");
}
clave-sounds commented 1 year ago

Thanks. I've been through the docs many times, but understanding them is another matter. I actually got this working, but it was counting beats very slowly. I finally figured out that TempoMap default is 96 ticks, which is why It seemed so slow to me counting 480. So I'm trying to not use the default (var tempoMap = TempoMap.Default;) and instead pass in TimeDivision, Tempo, TimeSignature. but It keeps telling me the constructor is not 3 arguments, but the docs say it is 3 arguments. I've tried a few variations. Any idea why the below isn't working?

using Melanchall.DryWetMidi.Core; using Melanchall.DryWetMidi.Interaction; using Melanchall.DryWetMidi.Multimedia; using System; using UnityEngine;

public class MidiClockScript : MonoBehaviour { private MidiClock midiClock; public static int tickCounter = 0; public static int beatCounter = 1;

void Start()
{
    var timeDivision = new TicksPerQuarterNoteTimeDivision(480);
    var tempo = new Tempo(120);
    var timeSignature = new TimeSignature(4,4);
    var tempoMap = new TempoMap(timeDivision, tempo, timeSignature);

    var interval480ticks = TimeConverter.ConvertTo<MetricTimeSpan>(1, tempoMap);
    midiClock = new MidiClock(false, new HighPrecisionTickGenerator(), interval480ticks);
    midiClock.Ticked += OnTicked;

    Debug.Log("MidiClock initialized");

    midiClock.Start();

    Debug.Log("MidiClock started");
}

void OnTicked(object sender, EventArgs e)
{
    tickCounter++;
    if (tickCounter >= 480)
    {
        tickCounter = 0;
    }

    if (tickCounter == 0)
    {
        beatCounter += 1;
    }
}

}

melanchall commented 1 year ago

First of all, you didn't read the docs carefully. Here the docs on TempoMap - https://melanchall.github.io/drywetmidi/api/Melanchall.DryWetMidi.Interaction.TempoMap.html. As you can see the class doesn't have a constructor. You need to use Create methods.

More than that I've already shown you how to create a tempo map with TPQN of 480:

var tempoMap = TempoMap.Create(new TicksPerQuarterNoteTimeDivision(480));
clave-sounds commented 1 year ago

Thanks for ur time.