Microtonality

Hi,

It looks like there is no built-in way to get microtones in the Tracktion Engine – the MidiNote class has noteNumber as an uint8 and there is no additional field for intonation.

What would be a sensible way to go? Would it be possible to subclass MidiNote to add an intonation field, for instance?

Any ideas are very welcome!

Erik

I’d probably apply a pitch-bend NoteExpression to each note?
You can’t really subclass engine internals.

Thanks for your quick answer!

You can’t really subclass engine internals.

I see.

I’d probably apply a pitch-bend NoteExpression to each note?

That implies using MPE, no? While that is possible it is not really optimal in my setup.

There are two parts of the issue:

  1. How could non-12-EDO pitches be represented in MIDI clips?
  2. How should these pitches be communicated to audio processors/synthesisers?

The second question seems easier to handle: many synths support the MIDI Tuning standard, so Single Note Tuning MIDI messages could just be inserted into the MIDI sequence.

Regarding 1), imagining a clip representation that had float note numbers, notes could be converted to “normal” notes (with integer note numbers) + MIDI tuning messages at some point. But currently, there seems to be no way of storing the extra pitch information in the first place.

With MIDI 2.0, pitches are indeed no longer restricted to integer values and although a full implementation of MIDI 2.0 is probably far away, IMHO having non-integer pitch values would probably be a step in the right direction anyway!

In the meantime, is there any way to add custom data to the notes in a MIDI sequence/MIDI clip? I’m really a beginner with the Tracktion Engine so please forgive me if I’m not making sense here!

I really don’t know much about how micro tuning is handled between DAWs and plugins. I though they were just tuning modes in synths that adjusted the pitch of incoming MIDI notes?

I also don’t fully understand what side (host or synth) you are implementing. If you want micro tunings to work with any existing plugin, I presume you’ll have to adhere to an existing standard?

One of those standard that might be supported by plugins is MPE. So if you wanted to us that, you could apply PITCHBEND tracktion_engine::MidiExpression to your notes (which has a float semitones number IIRC) then then that will be converted to MPE when sent to synths.

But like I said, I’m not really sure what the best method here is.

I also don’t fully understand what side (host or synth) you are implementing.

I’m implementing a host.

If you want micro tunings to work with any existing plugin, I presume you’ll have to adhere to an existing standard?

Yes, that’s what I meant with 2) above: communicating the pitch information to plugins can be done quite easily – albeit ugly – using e.g. pitch shift for MPE synths or MIDI Tuning messages.

But it would be very nice to not have to put these workarounds in the MidiClips directly! Imagine for instance a piano-roll editor which allows non-discrete placement of notes. As I understand it, I currently would have to implement my own class/format to store such a clip (and then under the hood probably convert it to a te::MidiClip before playback).

However, if MidiNote allowed for float pitches, I could let the GUI use the MidiClip as-is. Obviously, the same pitch bend messages would still have to be produced and sent to plugins at some point, but it would be abstracted away more nicely. Isn’t that very much like how MidiNote appears to have a ”length”, abstracting away the concept of individual note-on/note-off messages?

Even without direct support for microtonal pitches, just any way of attaching auxilary data to a MidiNote would be very useful for these kind of extensions! Because then I could explicitly pre-process my MidiList and convert this data to whatever MIDI messages would be appropriate.

I realize that this may not currently be possible, but perhaps it could be considered a feature request? :slight_smile:

Hmm, making a change like that would need a large amount of thought I’m afraid as we have thousands of lines of code expecting it to be an int.

You can add arbitrary data to a MidiNote though by simply changing a non-reserved property of the MidiNote::state tree.

Hi

Two things to look at

The folks at ODDSound.com have done a lovely job making a plug-in based tuning mechanism where a single plug-in can send tuning information to virtual instruments through a variety of mechanisms. Plus they are nice to work with.

If you want to handle tuning at the host level you may also want to check out the surge team tuning library GitHub - surge-synthesizer/tuning-library: Micro-tuning format parsing and frequency finding as a header-only C+ library which given an scl and kbm file gives you complete keyboard retuning. We licensed that lib mit only and made it header on so folks could use it in lots of projects. You could then allow your host to chose how to react to plain midi to non tuning aware hosts. Or do a variety of other things

Having now written a few goes at microtonal plug-ins it seems the daw sends standard midi and the plug-in (or plug-in set in ODDSound case) figures out the frequency based on tuning in play. The math from scl + kbm to tuning is irritating but we think we got all the edges in our lib.

The folks on the surge discord micro tuning channel have loads of experience and are happy to help if any of this is of interest. And if it’s not - still have fun with a tuning aware project! It’s a neat field.

3 Likes

I want to point out that OddSound also released a library meant to replace MTS or MPE, called MTS-ESP. Although there aren’t a ton of synths that support it yet, @baconpaul already added this functionality to Surge. You can use the library linked above to add this support to your microtonal plugins (it took us just an hour or so to set up).

2 Likes

You can add arbitrary data to a MidiNote though by simply changing a non-reserved property of the MidiNote::state tree.

Thanks! That is just what I need!

@baconpaul & @zac

Thanks for your suggestions! I’ll definetely look into it!

2 Likes

I just wanted to follow up this thread. As a proof of concept, I patched addToSequence in tracktion_MidiList.cpp to insert MIDI Tuning SysEx messages. I also added a member to MidiNote called pitchDeviation (being a float value describing the deviation from 12 EDO in semitones).

In tracktion_MidiList.cpp:

static void addToSequence (juce::MidiMessageSequence& seq, const MidiClip& clip,
                           const MidiNote& note, int channelNumber, bool addNoteUp,
                           const GrooveTemplate* grooveTemplate)

    // ...

    float pitchDev = note.getPitchDeviation();
    
    if (addNoteUp)
    {
        // nudge the note-up backwards just a bit to make sure the ordering is correct
        double upTime = note.getPlaybackTime (MidiNote::endEdge, clip, grooveTemplate);

        if (upTime > downTime && upTime > 0.0)
        {
            // Tune the note if necessary
            if (pitchDev != 0) {
                seq.addEvent(createSingleNoteTuningMessage(noteNumber, pitchDev, channelNumber), std::max (0.0, downTime));
            }
            seq.addEvent (juce::MidiMessage::noteOn (channelNumber, noteNumber, velocity), std::max (0.0, downTime));
            // Tune back
            if (pitchDev != 0) {
                seq.addEvent(createSingleNoteTuningMessage(noteNumber, 0.0, channelNumber), std::max (0.0, downTime));
            }

            seq.addEvent (juce::MidiMessage::noteOff (channelNumber, noteNumber), upTime);
        }
    }
    // ... etc
}

static juce::MidiMessage createSingleNoteTuningMessage(int noteNumber, float pitchDev, int ch)
{
    float tuned = noteNumber + pitchDev;
    juce::uint8 data1 = tuned;
    juce::uint16 micro = (tuned - data1) * 0x3fff;
    juce::uint8 data2 = micro / 0x80;
    juce::uint8 data3 = micro % 0x80;
    juce::uint8 data[11] = {
        0x7e, // non-real time
        0x7f, // target device ID (7F = all devices)
        0x08, // sub-id #1 "Midi Tuning Standard"
        0x07, // sub-id #2 "Single note tuning change (non real-time) (bank)"
        0x01, // bank (always use bank 1 for now)
        (juce::uint8) ch-1, // tuning preset ("program"), use channel number for that
        0x01, // number of changes (1 in this case)
        (juce::uint8) noteNumber, // MIDI key
        data1,                    // change 1 - freq byte 1
        data2,                    // change 1 - freq byte 2
        data3                     // change 1 - freq byte 3
    };
    return juce::MidiMessage::createSysExMessage (data, sizeof(data));
}

So far, this seems to work very well: the noteNumber is still an integer so existing code would not have to be changed and the pitchDeviation value can be safely ignored by code not interested in microtonality.

So the question now would be if and how this could be achieved without the need of monkey-patching the tracktion engine source code. From my perspective of course it would be most convenient if the tracktion engine added support for this out of the box, but another possible way would be to add a more general hook into the conversion from clip to MidiMessageSequence (or indeed the possibility to override the appropriate functions such as addToSequence).

What is createSingleNoteTuningMessage? Is this a standard sysex message? How big is it?
One of the problems with sending sysex in the audio graph is that they usually require an allocation because they don’t fit in to the standard 3 MIDI bytes.

How many bits do you need to represent the pitch deviation?

What is createSingleNoteTuningMessage ? Is this a standard sysex message? How big is it?

As you can see in my code example above it constructs a SysEx message of 11 bytes (+ head/tail). This is a standard MIDI message specified in the MIDI 1.0 spec and supported by many (albeit far from all) synthesizers.

One of the problems with sending sysex in the audio graph is that they usually require an allocation because they don’t fit in to the standard 3 MIDI bytes.

Ah! I didn’t think of that! Hmm, that obviously complicates things…

How many bits do you need to represent the pitch deviation?

The pitch value in the SysEx representation is 3 7-bit bytes but the first is sort of redundant as it allows any key to be tuned to any pitch. So in practice, 14 bits are used to represent the deviation.

For most practical purposes I guess 7 bits would suffice, that would still give a resolution of less than one cent. As a general solution though some might find it somewhat limiting.

Sorry, I missed that there was more code to scroll down to :man_facepalming:

I could also mention that in our old audio engine we solved this by having “extended midi messages” having two extra bytes allocated. These were then used to communicate extra pitch information to the synth but ignored in other contexts.

This is an even more simplistic solution in many ways and very easy to implement – the only problem is that this extra piece of information has to be “agreed on” between the host and the plugins. The SysEx model is much less elegant but it has the advantage that it is a standardized way to communicate microtonal pitch.

Yeah, standardising real-time sysex messages is problematic as discussed above…

I guess the other way to do it would be with a pair of NRPMs, the first with the note number as the data packet and the second with the pitch. You only get 7 bits then though.

This really needs MIDI 2.0 to be more elegant where you get extra data bits.

As far as I understand it, MIDI 2.0 already has native support for non-integer pitches so none of this would be an issue in the first place… but I haven’t properly read the full spec so I don’t know the details.

One of the problems with sending sysex in the audio graph is that they usually require an allocation because they don’t fit in to the standard 3 MIDI bytes.

I just have to double-check: is the conversion from MidiNotes to MidiMessages really performed in the audio thread? Otherwise it wouldn’t be much of a problem, would it?

No, that’s message thread. The problem is when you pass MIDI events through the audio graph they get copied a lot. With a pre-allocated array of juce::MidiMessages this isn’t a problem as their size is static but with sysex messages they internally have to do a heap allocation every time they’re copied.

This is I presume why a lot of hosts simply block sysex messages from entering the mix bus (i.e. plugins just wont’ receive them). I’ve considered doing the same for Tracktion Engine as it would make handling MIDI in the audio engine simpler and probably quicker.

So that’s something I have to think about when people ask me to make changes like this I’m afraid.

Ok, I see