记录生成的midi事件:从时间戳到midi文件增量时间的内部转换

时间:2012-06-24 18:47:22

标签: java timestamp midi javasound

我正在开发一个小型桌面应用程序,它从声音输入端口获取单声道旋律,并使用音高跟踪技术将其转换为midi消息,以转储到midi端口或存储到midi文件。

当我使用Receiver中的RealTimeSequencer将此midi消息录制到midi文件时,会出现实际问题。

程序在另一个线程中生成一个便利类(TSMidiMessage),它包含一个MidiMessage(主要是ShortMessages)和一个以System.currentMilliSecs()值创建的时间戳,并且它被放入与此主题共享的BlockingQueue。因此,当Receiver得到一个MidiMessage和一个以毫秒表示的时间戳时,现在很清楚或很好地记录了如何在Sequencer内完成此时间戳到midi delta时间的转换

另外,使用十六进制编辑器查看midi文件时,midi事件的所有时间戳都是0x00,因此它解释了为什么在导入到乐谱编辑器时它没有显示任何音符。

下面是相关代码的redux版本:

public class MidiListener implements Runnable {
private BlockingQueue<TSMidiMessage[]> messageQueue = new ArrayBlockingQueue<TSMidiMessage[]>(10);

private volatile boolean run=false;
public volatile boolean start=false;

public static long startTime;

public void run() {
    run = true;

    TSMidiMessage[] messageBlock = null;

    Sequencer sequencer = null;
    Sequence sequence = null;
    Track pista1 = null;
    Receiver receiver = null;

    try{
        log.debug("Starting sequencer");
        sequencer = MidiSystem.getSequencer();
        receiver = sequencer.getReceiver();
        // almacenar mensaje midi
        log.debug("Starting sequence");
        sequence = new Sequence(Sequence.PPQ, cuantizeStringToPPQ(cuantizeMode));
        pista1 = sequence.createTrack();

        //****  General MIDI sysex -- turn on General MIDI sound set  ****
        // http://www.automatic-pilot.com/midifile.html
        log.trace("Loading General MIDI");
        byte[] b = {(byte)0xF0, 0x7E, 0x7F, 0x09, 0x01, (byte)0xF7};
        SysexMessage sm = new SysexMessage();
        sm.setMessage(b, 6);
        MidiEvent me = new MidiEvent(sm,(long)0);
        pista1.add(me);

        log.trace("Add tempo message");
        me =  new MidiEvent(midiTools.forgeTempoMessage(dataHolder.bpm), 0L);
        pista1.add(me);
        log.trace("Add signature message");
        me =  new MidiEvent(midiTools.forgeSignatureMessage(dataHolder.highMeasureNibble, dataHolder.lowMeasureNibble), 0L);
        pista1.add(me);


        //****  set omni on  ****
        ShortMessage mm = new ShortMessage();
        mm.setMessage(0xB0, 0x7D,0x00);
        me = new MidiEvent(mm,(long)0);
        pista1.add(me);

        //****  set poly on  ****
        mm = new ShortMessage();
        mm.setMessage(0xB0, 0x7F,0x00);
        me = new MidiEvent(mm,(long)0);
        pista1.add(me);

        mm = new ShortMessage();
        mm.setMessage(0xC0, 0x00, 0x00);
        me = new MidiEvent(mm,(long)0);
        pista1.add(me);

        sequencer.setSequence(sequence);
        sequencer.recordEnable(pista1, 0);
        sequencer.setTempoInBPM(dataHolder.bpm);

        sequencer.open();
        while(!this.start){
            // hold for a moment
        }
        log.debug("Start recording");
        startTime = System.currentTimeMillis();
        sequencer.startRecording();
    }catch (MidiUnavailableException e) {
        log.error("Midi device error", e);
    } catch (InvalidMidiDataException e) {
        log.error("Invalid midi data", e);
    }
    while(run){
        try {
            messageBlock = messageQueue.take();
            // Message load
            log.debug("Message load");

            if(messageBlock[0] == null)
                throw new InterruptedException("Thread interrupted");

            for(int i=0; i < messageBlock.length; i++){
                receiver.send(messageBlock[i].getMessage(), messageBlock[i].getTimeStamp());
            }

        } catch (InterruptedException e1) {
            log.debug("Thread interrupted");
            this.run = false;
        }
    }
    log.debug("Saving midi file");
    log.trace("Sequence "+sequence);
    log.trace("Length  "+sequence.getMicrosecondLength()+"ms");
    MetaMessage endOfTrack = new MetaMessage();
    try {
        endOfTrack.setMessage(0x2F, new byte[]{}, 0);
        receiver.send(endOfTrack, System.currentTimeMillis());
    } catch (InvalidMidiDataException e1) {
        log.error(e1);
    }
    sequencer.stopRecording();
    sequencer.recordDisable(pista1);
    sequencer.close();
    try {
        midiTools.renderMidiFile(sequence, mainControl.selectedFile);
    } catch (IOException e) {
        log.error("Error while saving midi file", e);
    }
}

}

这里是一个小生成的midi文件的十六进制转储:

0000000: 4d54 6864 0000 0006 0001 0001 0004 4d54  MThd..........MT
0000010: 726b 0000 0268 00f0 057e 7f09 01f7 00ff  rk...h...~......
0000020: 5103 0b71 b000 ff58 0406 0324 0c00 b07d  Q..q...X...$...}
0000030: 0000 7f00 00c0 0083 b4b0 5990 4c5d 0080  ..........Y.L]..
0000040: 4c00 0090 4e44 0080 4e00 0090 5064 0080  L...ND..N...Pd..
0000050: 5000 0090 4b68 0080 4b00 0090 506c 0080  P...Kh..K...Pl..
0000060: 5000 0090 4d4f 0080 4d00 0090 582d 0080  P...MO..M...X-..
0000070: 5800 0090 473b 0080 4700 0090 4c39 0080  X...G;..G...L9..
0000080: 4c00 0090 5734 0080 5700 0090 4d2c 0080  L...W4..W...M,..
0000090: 4d00 0090 5731 0080 5700 0090 4b2e 0080  M...W1..W...K...
00000a0: 4b00 0090 5832 0080 5800 0090 4d2d 0080  K...X2..X...M-..
00000b0: 4d00 0090 4e30 0080 4e00 0090 4c2f 0080  M...N0..N...L/..
00000c0: 4c00 0090 4c3c 0080 4c00 0090 4f5d 0080  L...L<..L...O]..
00000d0: 4f00 0090 4a76 0080 4a00 0090 5264 0080  O...Jv..J...Rd..
00000e0: 5200 0090 505b 0080 5000 0090 4c60 0080  R...P[..P...L`..
00000f0: 4c00 0090 5059 0080 5000 0090 4d57 0080  L...PY..P...MW..
0000100: 4d00 0090 505c 0080 5000 0090 4e5a 0080  M...P\..P...NZ..
0000110: 4e00 0090 4d6a 0080 4d00 0090 4b63 0080  N...Mj..M...Kc..
0000120: 4b00 0090 5059 0080 5000 0090 4e5d 0080  K...PY..P...N]..
0000130: 4e00 0090 4d53 0080 4d00 0090 4c52 0080  N...MS..M...LR..
0000140: 4c00 0090 4b41 0080 4b00 0090 4c44 0080  L...KA..K...LD..
0000150: 4c00 0090 4a3f 0080 4a00 0090 4c4b 0080  L...J?..J...LK..
0000160: 4c00 0090 4e4e 0080 4e00 0090 4b5a 0080  L...NN..N...KZ..
0000170: 4b00 0090 4c52 0080 4c00 0090 4e5a 0080  K...LR..L...NZ..
0000180: 4e00 0090 504b 0080 5000 0090 4f56 0080  N...PK..P...OV..
0000190: 4f00 0090 5953 0080 5900 0090 4c55 0080  O...YS..Y...LU..
00001a0: 4c00 0090 4a55 0080 4a00 0090 4e54 0080  L...JU..J...NT..
00001b0: 4e00 0090 4b4f 0080 4b00 0090 4a4c 0080  N...KO..K...JL..
00001c0: 4a00 0090 4c4b 0080 4c00 0090 4b49 0080  J...LK..L...KI..
00001d0: 4b00 0090 4e3d 0080 4e00 0090 4f4b 0080  K...N=..N...OK..
00001e0: 4f00 0090 4e52 0080 4e00 0090 4d4b 0080  O...NR..N...MK..
00001f0: 4d00 0090 4b49 0080 4b00 0090 4f3c 0080  M...KI..K...O<..
0000200: 4f00 0090 4d42 0080 4d00 0090 4b47 0080  O...MB..M...KG..
0000210: 4b00 0090 4f41 0080 4f00 0090 4b4c 0080  K...OA..O...KL..
0000220: 4b00 0090 4d44 0080 4d00 0090 4e3e 0080  K...MD..M...N>..
0000230: 4e00 0090 4c44 0080 4c00 0090 5042 0080  N...LD..L...PB..
0000240: 5000 0090 4b3b 0080 4b00 0090 4c3c 0080  P...K;..K...L<..
0000250: 4c00 0090 4b3e 0080 4b00 0090 4c3b 0080  L...K>..K...L;..
0000260: 4c00 0090 4b3a 0080 4b00 0090 4e35 0080  L...K:..K...N5..
0000270: 4e00 0090 4922 0080 4900 00ff 2f00       N...I"..I.../.

可能有必要提一下,我正在使用较小的(但我猜这些是正确的)滴答分辨率值(以PPQ表示),因为我预计它将有助于量化midi事件,蚂蚁这可能是其中之一解释这个问题。我的另一个想法是,发送给Receiver的时间戳数据与其内部工作不一致,因为它必须以其他方式表达,而不是以纪元时间戳表示。

谢谢=)


编辑1:阅读Java Sound Api Progrramer指南(第10章“发送给设备的消息的时间戳”)后,我发现了这一点:

  

可以选择伴随之间发送的消息的时间戳   Java Sound API中的设备与时序有很大不同   标准MIDI文件中的值。 MIDI文件中的计时值是   通常基于音乐概念,如节拍和节奏,以及每个   事件的时间测量自上次事件以来经过的时间。在   对比,发送到设备接收器的消息上的时间戳   object始终以微秒为单位测量绝对时间。的具体而言,   它测量自设备以来经过的微秒数   拥有接收器已经打开

所以我发现我发送的时间戳没有以预期的格式表示,但我做了一个快速测试,将这些时间戳替换为-1值,这意味着它应该忽略时间戳值,并且结果相同,事件中没有增量时间戳。


编辑2:当我还在使用它时,我将那些纪元时间戳转换为具有微秒分辨率的累积时间戳格式,期望来自Receiver。但这不会使它工作(但改进代码=))。此外,我试图将PP分辨率更改为更高的值,96,它应该足够好,如Java声音指南中所指定的那样。

1 个答案:

答案 0 :(得分:1)

好的,所以我会回答我自己的问题,因为我找到了解决问题的方法。

如果它无法正常工作,为什么要使用Receiver以正确的时间获取MidiMessages?我们可以绕过这个并手动伪造MidiEvents,使用累积时间戳(以微秒为单位)并将其转换为以滴答表示的累积时间戳,与PPQ参数成比例。

private long microsecondTickToPPQTick(long msTick, Sequencer seq){
    long ret = msTick / 1000;
    double rawValue = ret / this.tickSize;

    double valueA  = (rawValue - Math.floor(rawValue)); 
    double valueB = ((Math.floor(rawValue)+1) - rawValue);
    double min = Math.min(valueA, valueB);

    if(min == valueA){
        ret =(long) Math.floor(rawValue);
    }else{
        ret =(long) (Math.floor(rawValue)+1);
    }

    log.info("MidiEvent's timestamp: "+ret);

    return ret;
}

此方法还会将音符量化为所选PPQ,从而解决后续音符。您可以使用以下公式找到刻度尺寸(以毫秒为单位):

tickSize = (60.0/bpm)/(double)ppq;

然后你得到正确的滴答测量,并将MidiEvent添加到序列中。

// startTime holds the value for the epoch time when the sequencer started recording.
long microTS = (messageBlock[i].getTimeStamp() - startTime)*1000;
long tick = microsecondTickToPPQTick(microTS, sequencer);
MidiEvent me = new MidiEvent(messageBlock[i].getMessage(), tick);
track1.add(me);