2

我正在做一个项目,我需要能够尽可能准确地生成不同频率的 MIDI 音符。我最初尝试用 Java 编写我的程序,但结果表明 sound.midi 包不支持更改音符的调音,除非频率是 Equal Tempered 频率(或者至少在 1.4 中没有,而且我没有t 能够找到证据表明这已在最近的版本中得到修复)。我一直在尝试寻找更合适的语言/库来完成这项任务,但由于这是我第一次使用 MIDI 进行编程,并且我对特定调音功能的需求是必不可少的,因此我很难找到我真正需要的东西。

我正在向有编写 MIDI 程序经验的人寻求关于哪些语言有用的建议,特别是对于将音符调整到特定频率。任何带有 API 文档和示例代码的网站链接也将非常有帮助。

4

2 回答 2

3

你不能普遍改变调音。这是合成器的一个特性,与 MIDI 无关。

现在,有一些 SysEx 消息通常被理解为这个任务。有关更多信息,请参阅此参考:http: //www.midi.org/techspecs/midituning.php

另一个参考: http: //www.microtonal-synthesis.com/MIDItuning.html

同样,MIDI 只是一个控制协议。产生声音取决于合成器。合成器不必支持更改调音,而且通常不支持。这与 MIDI 无关,也与您发送 MIDI 数据的语言无关。

于 2010-12-29T06:59:57.860 回答
3

我的音乐应用程序也遇到了同样的问题。正如@Brad 所假设的,这是一个使用 MIDI 调音标准的解决方案:

步骤是:

  1. 调整更改请求
  2. 将所有 127 个 MIDI 键映射到新的计算频率

Gervills TuningApllet3.java的源代码帮助我完成了这项工作。

Furtunatly,在我使用 Windows 7 和 JDK 1.8 的测试环境中,标准 MIDI 合成器支持 MIDI 调音标准。我不知道是否有可能检查合成器是否支持此标准。

如何计算新频率?

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));
}

对于毕达哥拉斯调优等其他调优,您可以使用其他计算方法。在这里,我们使用与 MIDI 相同的律动,无需重新调音。

如何将频率转换为频率数据格式?

频率数据格式中所述,每个频率f必须由 3 个字节表示:

字节 1:基本密钥。在标准 MIDI 调音(等律,A4 = 440 赫兹)中具有低于或等于 f 的频率 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;
}

字节 2 和字节 3 :从f'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);
}

这个分区间的整数表示是

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

并且可以使用以下代码拆分为字节 2 和字节 3:

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

请注意,并非每个频率都可以用这种格式表示。基本键必须在 0..127 范围内,调整在 0..2^14 - 1 = 0..16383 范围内。另外 (byte1, byte2, byte3) = (0x7f, 0x7f, 0x7f) 被保留。

完整的工作示例

此示例重新调整到 A4 = 500 Hz 并播放从 C4 到 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");
}

我希望使用非实时消息,与实时版本相比,更多的合成器支持这一点。非实时和实时之间的区别应该是,实时允许在播放时重新调整。非实时版本仅影响重新调音后播放的音符。

它有效吗?是的,我已经记录了输出并使用Sonic Visualiser对其进行了分析:

重新调谐到 A4 = 500 Hz。 A4 在频谱图中突出显示

如您所见,频谱图中 A4 的峰值频率接近 500 Hz。

于 2014-08-28T12:32:07.883 回答