开发者

Programming with midi, and tuning notes to specific frequencies

I am working on a project in which I need to be able to generate midi notes of varying frequencies with as much accuracy as possible. I originally tried to write my program in Java, but it turns out that the sound.midi package does not support changing the tunings of notes unless the frequencies are Equal Tempered frequencies (or at least it didn't in 1.4, and I haven't been able to find evidence that this has been fixed in recent versions). I have been trying to find a more appropriate language/library to accomplish this task, but since this is my first time programming with MIDI and my need for specific tuning functionality is essentia开发者_StackOverflow社区l, I have been having considerable trouble finding exactly what I need.

I am looking for advice from people who have experience writing MIDI programs as to what languages are useful, especially for tuning notes to specific frequencies. Any links to websites with API docs and example code would also be extremely helpful.


You can't universally change the tuning. This is a feature of the synthesizer and has nothing to do with MIDI.

Now, there are some SysEx messages that are commonly understood for this task. See this reference for more information: http://www.midi.org/techspecs/midituning.php

Another reference: http://www.microtonal-synthesis.com/MIDItuning.html

Again, MIDI is just a control protocol. Producing the sounds is up to the synthesizer. The synth doesn't have to support changing the tuning, and often doesn't. This has nothing to do with MIDI, and nothing to do with the language you are sending MIDI data in.


I had the same problem for my music application. As supposed by @Brad, here is a solution with the MIDI Tuning Standard:

The steps are:

  1. Request for tuning change
  2. Map all the 127 MIDI keys to the new computed frequencies

The source code of Gervills TuningApllet3.java helped me a lot to get this work.

Furtunatly, on my test enviroment with Windows 7 and JDK 1.8 the standard MIDI synthesizer supports the MIDI Tuning Standard. I don't know if there is a possibility to check, if a synthesizer supports this standard or not.

How to Compute the new Frequencies?

private static float getFrequency(final int keyNumber,
        final double concertAFreq) {
    // Concert A Pitch is A4 and has the key number 69
    final int KEY_A4 = 69;
    // Returns the frequency of the given key (equal temperament)
    return (float) (concertAFreq * Math.pow(2, (keyNumber - KEY_A4) / 12d));
}

For other tunings like the pythagorean tuning, you can use other computing methods. Here we are using the equal temperament as MIDI use it without retuning.

How to get the Frequencies into the Frequency Data Format?

As described in Frequency Data Format, every frequency f have to represented by 3 bytes:

Byte 1: Base Key. The key number wich has in standard MIDI tuning (equal temperament, A4 = 440 Hz) a lower or equal frequency f' than f

private static int computeBaseKey(final double freq) {
    // Concert A Pitch is A4 and has the key number 69
    final int A4_KEY = 69;
    final double A4_FREQ = 440d;

    // Returns the highest key number with a lower or equal frequency than
    // freq in standard MIDI frequency mapping (equal temparement, concert
    // pitch A4 = 440 Hz).
    int baseKey = (int) Math.round((12 * log2(freq / A4_FREQ) + A4_KEY));
    double baseFreq = getFrequency(baseKey, A4_FREQ);
    if (baseFreq > freq) {
        baseKey--;
    }
    return baseKey;
}

Byte 2 and Byte 3: Interval in Cent from f' to f

private static double getCentInterval(final double f1, final double f2) {
    // Returns the interval between f1 and f2 in cent
    // (100 Cent complies to one semitone)
    return 1200d * log2(f2 / f1);
}

The integer representation of this cent interval is

tuning = (int) (centInterval * 16384d / 100d);

and can split into Byte 2 and Byte 3 with this code:

byte2 = (tuning >> 7) & 0x7f; // Higher 7 Bit
byte3 = tuning & 0x7f; // Lower 7 Bit

Please notice that not every frequency can be represented by this format. The base key have to be in the range 0..127 and the tuning in the range of 0..2^14 - 1 = 0..16383. Also (byte1, byte2, byte3) = (0x7f, 0x7f, 0x7f) is reserved.

Full Working example

This example retunes to A4 = 500 Hz and play the chromatic scale from C4 to B4:

public static void retune(final Track track, final double concertAFreq) {
    if (track == null) {
        throw new NullPointerException();
    } else if (concertAFreq <= 0) {
        throw new IllegalArgumentException("concertAFreq " + concertAFreq
                + " <= 0");
    }

    final int bank = 0;
    final int preset = 0;
    final int channel = 0;
    addTuningChange(track, channel, preset);

    // New frequencies in Hz for the 128 MIDI keys
    final double[] frequencies = new double[128];
    for (int key = 0; key < 128; key++) {
        frequencies[key] = getFrequency(key, concertAFreq);
    }

    final MidiMessage message = createSingleNoteTuningChange(bank, preset,
            frequencies);
    track.add(new MidiEvent(message, 0));
}

private static void addTuningChange(final Track track, final int channel,
        final int preset) {
    try {
        // Data Entry
        final ShortMessage dataEntry = new ShortMessage(
                ShortMessage.CONTROL_CHANGE, channel, 0x64, 03);
        final ShortMessage dataEntry2 = new ShortMessage(
                ShortMessage.CONTROL_CHANGE, channel, 0x65, 00);
        track.add(new MidiEvent(dataEntry, 0));
        track.add(new MidiEvent(dataEntry2, 0));
        // Tuning program
        final ShortMessage tuningProgram = new ShortMessage(
                ShortMessage.CONTROL_CHANGE, channel, 0x06, preset);
        track.add(new MidiEvent(tuningProgram, 0));
        // Data Increment
        final ShortMessage dataIncrement = new ShortMessage(
                ShortMessage.CONTROL_CHANGE, channel, 0x60, 0x7F);
        track.add(new MidiEvent(dataIncrement, 0));
        // Data Decrement
        final ShortMessage dataDecrement = new ShortMessage(
                ShortMessage.CONTROL_CHANGE, channel, 0x61, 0x7F);
        track.add(new MidiEvent(dataDecrement, 0));
    } catch (final InvalidMidiDataException e) {
        throw new AssertionError("Unexpected InvalidMidiDataException", e);
    }
}

private static MidiMessage createSingleNoteTuningChange(final int bank,
        final int preset, final double[] frequencies) {
    // Compute the integer representation of the frequencies
    final int[] baseKeys = new int[128];
    final int[] tunings = new int[128];
    // MIDI Standard tuning frequency
    final double STANDARD_A4_FREQ = 440d;
    for (int key = 0; key < 128; key++) {
        final int baseKey = computeBaseKey(frequencies[key]);
        if (baseKey >= 0 && baseKey <= 127) {
            final double baseFreq = getFrequency(baseKey, STANDARD_A4_FREQ);
            assert baseFreq <= frequencies[key];
            final double centInterval = getCentInterval(baseFreq,
                    frequencies[key]);
            baseKeys[key] = baseKey;
            tunings[key] = (int) (centInterval * 16384d / 100d);
        } else {
            // Frequency is out of range. Using default MIDI tuning for it
            // TODO: Use LOGGER.warn to warn about
            baseKeys[key] = key;
            tunings[key] = 0;
        }
    }

    // Data to send
    final ByteArrayOutputStream stream = new ByteArrayOutputStream();
    stream.write((byte) 0xf0); // SysEx Header
    stream.write((byte) 0x7e); // Non-Realtime. For Realtime use 0x7f
    stream.write((byte) 0x7f); // Target Device: All Devices
    stream.write((byte) 0x08); // MIDI Tuning Standard
    stream.write((byte) 0x07); // Single Note Tuning Change Bank
    stream.write((byte) bank);
    stream.write((byte) preset);
    stream.write(128); // Number of keys to retune
    for (int key = 0; key < 128; key++) {
        stream.write(key); // Key to retune
        stream.write(baseKeys[key]);
        stream.write((tunings[key] >> 7) & 0x7f); // Higher 7 Bit
        stream.write(tunings[key] & 0x7f); // Lower 7 Bit
    }
    stream.write((byte) 0xf7); // EOX
    final byte[] data = stream.toByteArray();

    final MidiMessage message;
    try {
        message = new SysexMessage(data, data.length);
    } catch (final InvalidMidiDataException e) {
        throw new AssertionError("Unexpected InvalidMidiDataException", e);
    }
    return message;
}

private static int computeBaseKey(final double freq) {
    // Concert A Pitch is A4 and has the key number 69
    final int A4_KEY = 69;
    final double A4_FREQ = 440d;

    // Returns the highest key number with a lower or equal frequency than
    // freq in standard MIDI frequency mapping (equal temparement, concert
    // pitch A4 = 440 Hz).
    int baseKey = (int) Math.round((12 * log2(freq / A4_FREQ) + A4_KEY));
    double baseFreq = getFrequency(baseKey, A4_FREQ);
    if (baseFreq > freq) {
        baseKey--;
    }
    return baseKey;
}

private static double getCentInterval(final double f1, final double f2) {
    // Returns the interval between f1 and f2 in cent
    // (100 Cent complies to one semitone)
    return 1200d * log2(f2 / f1);
}

private static double log2(final double x) {
    // Returns the logarithm dualis (log with base 2)
    return Math.log(x) / Math.log(2);
}

private static float getFrequency(final int keyNumber,
        final double concertAFreq) {
    // Concert A Pitch is A4 and has the key number 69
    final int KEY_A4 = 69;
    // Returns the frequency of the given key (equal temperament)
    return (float) (concertAFreq * Math.pow(2, (keyNumber - KEY_A4) / 12d));
}

public static void main(String[] args) throws Exception {
    final int PPQN = 16; // Pulses/Ticks per quarter note
    Sequence sequence = new Sequence(Sequence.PPQ, PPQN);
    final Track track = sequence.createTrack();

    final double a4Freq = 500; // Hz
    retune(track, a4Freq);

    // Play chromatic Scale from C4 to B4
    final int C4_KEY = 60;
    final int B4_KEY = 71;
    final long quarterTicks = PPQN;
    long tick = 0;
    for (int key = C4_KEY; key <= B4_KEY; key++) {
        final int channel = 0;
        final int velocity = 96;
        final ShortMessage noteOn = new ShortMessage(ShortMessage.NOTE_ON,
                channel, key, velocity);
        track.add(new MidiEvent(noteOn, tick));
        tick += quarterTicks;
        final ShortMessage noteOff = new ShortMessage(
                ShortMessage.NOTE_OFF, channel, key, 0);
        track.add(new MidiEvent(noteOff, tick));
    }

    final Sequencer sequencer = MidiSystem.getSequencer();
    sequencer.setSequence(sequence);
    final CountDownLatch waitForEnd = new CountDownLatch(1);
    sequencer.addMetaEventListener(e -> {
        if (e.getType() == 47) {
            waitForEnd.countDown();
        }
    });
    sequencer.open();
    sequencer.start();
    System.out.println("started");
    waitForEnd.await();
    sequencer.stop();
    sequencer.close();
    System.out.println("ready");
}

I've used the non-realtime message in hope, that more synthesizers supports this than the realtime version. The difference between non-realtime and realtime should be, that realtime allows retuning while playing. Non-realtime version effects only the notes played after the retuning.

Does it work? Yes, I've recorded the output and analyzed it with the Sonic Visualiser:

Programming with midi, and tuning notes to specific frequencies

As you can see, the peak frequency for A4 in the spectrogram is nearly 500 Hz.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜