Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seq66's MIDI timing completely falls apart at JACK buffer sizer larger than 128 #100

Open
unfa opened this issue Sep 3, 2022 · 24 comments

Comments

@unfa
Copy link

unfa commented Sep 3, 2022

Here's a 16th note blastbeat at 200 BPM I used to test this. I am using Geonkick to generate audio so the waveform should be identical every time (it plays internally-generated samples).

At buffer size 2048 the timing is completely broken:
image

At buffersize 128 - it seems good, but when you look closely at the waveform just before each transient you'll see that they differ and this is audible.
image

Here's 32 - audibly it's best so far, but we can see that it's still not perfect:
image

256 is the lowest usable imho but it's jittery:
image

For my work I usually need buffer of 1024 or 2048, as I do livestreaming and my DSP load is really high.

Here's 1024, which is the lowest feasible for me, but already will cause severe xruns:
image

It seems like the events are not properly scheduled withing each buffer and they get piled up at the end or start of each buffer cycle? I don't know how this works technically, but no other MIDI sequencer I ever used did this.

Here's the RaySession I used to test this. I'll require you to have RaySession, Carla and Geonkick installed to test:
unfa live 2022-09-03.tar.gz

RaySession allows to very easily change the buffer size:
image

Hopefully this can help :)

PS: I've tried changing JACK PPQN value from 192 to 2400 but it had absolutely no effect.

@falkTX
Copy link

falkTX commented Sep 3, 2022

looking at the code for jack-midi, there is a single call to write data at https://github.com/ahlstromcj/seq66/blob/master/seq_rtmidi/src/midi_jack.cpp#L375
the s_offset value being used there is always 0, causing events to lose their timing and thus introducing jitter.

reducing buffer size reduces the jitter because the 0 value will be closer to the target, by virtue of the buffer being smaller.
with a 8192 size buffer, a value of 0 means there are 8191 possibly incorrect values. with 128 there are only 127, with max deviation being 127 samples.

this is simply lack of event timing being present on the midi messages.

@ahlstromcj
Copy link
Owner

Thanks for the report and the input. I will investigate and get the fix into version 0.99.1.

I did try to duplicate this, and was able to get very choppy audio in qsynth by setting its audio buffer to 4096, in ALSA, JACK, and with other sequencers, but I think that's a different issue, and I was on the wrong track.

I will do a proper investigation soon.

Question: how would I impress event timing on the MIDI messages in JACK? They already have timestamps.

Thanks again!

@unfa
Copy link
Author

unfa commented Sep 3, 2022

Thanks! I was hoping maybe I'll be able to do a Livestream tomorrow with Seq66 to show it off, but I'll have to wait until this is fixed.

@ahlstromcj
Copy link
Owner

It will be awhile. I got geonkick and raysession installed, and extracted your tar.gz file to ~/Ray Sessions. But when I open the session, geonkick appears in Carla but qseq66 does not. (I also had to run "qseq66 --home ~/Ray Session/unfa..../config" separately to get rid of your MIDI ports and use what I had. So I still have some tinkering to figure out, and maybe a session issue to fix.) The saga continues....

@unfa
Copy link
Author

unfa commented Sep 3, 2022

Thanks! Yeah, I had some trouble making qseq66 run inside RaySession, though I think it was a bug in the latter.

@ahlstromcj
Copy link
Owner

So I gave up on your setup, and am making a manual setup, using either Carla or QjackCtl (trying both). I am stymied at trying to get any sound out of geonkick. I can try a kit and see the VU meter moving, but no sound. I have PulseAudo in play, but it doesn't matter if I direct the output to either of the PA Sources or to System playback.

I put all of the drums on ch 10 (9 re 0) just to simplify things.

Once that's working, how do you display the generated blastbeats?

Thanks!

@unfa
Copy link
Author

unfa commented Sep 4, 2022 via email

ahlstromcj added a commit that referenced this issue Sep 7, 2022
…the ringbuffer data, seems to work, need further verification.
@ahlstromcj
Copy link
Owner

ahlstromcj commented Sep 7, 2022 via email

@ahlstromcj
Copy link
Owner

ahlstromcj commented Sep 12, 2022 via email

@falkTX
Copy link

falkTX commented Sep 13, 2022

I am not sure what you are confused about..
the process call is an audio block of X number of samples, where X = buffer size.
it is called at regular intervals, that follow the real passage of time.

so under 48kHz with 512 buffer size, we have processing with blocks of 512 samples, where roughly after 94 blocks 1 second will have passed (512 * 94 = 48128, which is bigger than the 48kHz sampling rate).
the 48kHz in my example here means there are 48kHz samples in a second.

for JACK MIDI, the "frame" just refers to the offset within the process function it is called within.
because the process is an audio block, you have 0 up to block-size-frames (512 in my example) of possible values the event is present on.

This is basically how all audio and plugin APIs work, when we deal with audio in blocks.

Sometimes events cross the process call boundary, where a note starts near the end of the block and goes on for a while until it stops in another block (and thus its duration is higher than 512 samples, which means 10.6ms at 48kHz).

@ahlstromcj
Copy link
Owner

ahlstromcj commented Sep 13, 2022 via email

@falkTX
Copy link

falkTX commented Sep 13, 2022

what do you mean by "nperiods"? the alsa value? if so, it is completely irrelevant here.

JACK audio and MIDI it is all sample-accurate and in the same thread, same way as plugins APIs do it.
the timing of the events is meant to be in sync with the audio, the "offset" means exactly the same regardless if using audio or midi.
How you reach to a precise audio-relative accurate offset is up to you. and it is not something exactly unique to JACK, so there is bound to be some info online about it. (it is the same situation as when one tries to add an audio/midi offset to a gui-generated event, typically a calculation based on current-absolute-time vs when next audio callback is expected)

jack_transport_query always works, if you get valid BBT information or not depends if there is a transport master supplying that information at any given moment or not.

@ahlstromcj
Copy link
Owner

ahlstromcj commented Sep 13, 2022 via email

ahlstromcj added a commit that referenced this issue Sep 27, 2022
ahlstromcj added a commit that referenced this issue Sep 30, 2022
@ahlstromcj
Copy link
Owner

So I have been thrashing around this issue for a long time now, and still cannot get rid of lurching notes at a cycle or periods count of 4096. What I have done is (1) replace JACK's ringbuffer with a midi_message ringbuffer, much easier to manage (and tweak); (2) In the process callback, calculate the offset from the timestamp and use that in the event-write JACK function; (3) If the offset is less than the last offset, belay the output of that event until the next process callback. The issues is that there is a lag (typically ~40 ms in my setup, but it can vary a lot) between putting an event in the ringbuffer and then retrieving it in the callback. Now, Seq66's JACK MIDI is based on the (flawed) RtMidi project, and I am afraid I may ultimately have to refactor radically... which I am very tempted to put off until Seq66v2. Non-sequencer uses a buffer as well, but it calculates note offs; but I can't test that because I can't build the app thanks to the GUI fltk extensions project being yanked by the author.

So, unfa, what sequencers have you used that work well? I've look at many projects, but obviously need some more research. A most daunting issue!

The latest can be found in the portfix branch, which also includes work on song-recording (issue #44 revisited) and adds a prettier way to display notes and triggers. If you want, you can build it and see if there's any improvement in "The Seq66 Rag" :-D.

@ahlstromcj
Copy link
Owner

Another possibility of error just occurred to me. Stay tuned.

@unfa
Copy link
Author

unfa commented Oct 14, 2022

I think Giada doesn't have this problem, but I haven't tested it in a long time.
Ardour 7 has added a live-looping mode but the codebase may be quite large.

Maybe you'd like to join my community chat? There's tons of users and also developers (including Paul Davis of Ardour and falktx) - you can ask them directly and hopefully get some help!

You can use Rocket.Chat (open-source, self-hosted): https://chat.unfa.xyz
Or Discord: https://discord.gg/d8x9aby (proprietary)

Both services are bridged together so you can talk to everyone, but not everyone has matching accounts on both sides.

@falkTX
Copy link

falkTX commented Oct 14, 2022

I have this working in ttymidi for quite a while.
Messages from the serial get a timestamp when entering jack, and vice-versa. Code at https://github.com/moddevices/mod-ttymidi/blob/master/src/ttymidi.c

But realistically, since this is all in software and you do not have to deal with hardware and hardware timing communication, you dont need to care about this at all...
When drawing a note, you already know what exact position that note has. You simply have to map between that position to sample-frames, to be happening on the right audio cycle.

Say you have a note at exactly the start of the song. That obviously matches to sample-frame 0 on the very first audio cycle (assuming audio rolls together with transport).
Now you have a note somewhere later, say 4 beats later. Just a matter of calculating the frame position based on BPM/sample-rate and beats-per-bar etc.
on 120 BPM with 48000 sample rate: we have 120 beats every minute (60*48000 samples), which is roughly 0.000041666.. beats every frame. this value is quite small, so typically it is easier to do calculations based on beat divisions.
This is the typical stuff for sequencers. How that works out in the end heavily depends on the engine implementation, if you store time in beats or seconds or something else.

@ahlstromcj
Copy link
Owner

Currently, the events are timestamped with a pulse value which gets converted to a frame, then offset using the PPQN (or JACK ticks_per_beat) and the BPM. This conversion is done in the callback, which figures out the offset for that frame and uses it, unless the offset is out of order, when the event is the ringbuffer is left alone for the next cycle. Obviously the lag between insertion into the ringbuffer and extraction from the ringbuffer is an issue.

As for giada, I see it is based on the same (flawed) RtMidi JACK implementation. But I haven't yet figured out how to import an existing MIDI file into giada to test it.

@ahlstromcj
Copy link
Owner

I have had persistent problems at 4096 frames per period (cycle) with the fill-in drum pattern from the Peter_Gunn reconstructed MIDI file. The blastbeats at the end are syncopated! I ran some detailed tests, the results are in the new file contrib/tests/test_numbers.ods in the portfix branch. I also ran it with ttymidi.c's microsecond time method and the iffy "compensation" factor, with the same results. (Search for SEQ66_ENCODE_JACK_FRAME_TIME).

I managed to run the same pattern in ardour... and it was also syncopated.

Any other sequencers you've tried?

At this point, I am putting this one on the way back burner unless some inspiration bubbles up. I am working on a new library for MIDI starting from rtmidi, which will first cover basic MIDI and JACK, and I will use it to dig deeper at some point. I will make a new release with the current mitigation efforts plus some fixes and features. Thanks!

@mxmilkiib
Copy link

Possibly https://github.com/jcelerier/libremidi?

@ahlstromcj
Copy link
Owner

Dang, that library has the same defect, inherited from RtMidi, of always reserving a 0 offset.

I might rebuild for falkTx's microsecond frame time again soon and play some more with it. The big issue is that sometimes an event, which is by calculation meant for cycle C, is not actually processed until cycle C + 1. ("cycle" is one call of the process callback).

@mxmilkiib
Copy link

@jcelerier might you have a thought?

@jcelerier
Copy link

jcelerier commented Nov 22, 2022

hmm it should be pretty easy to add a writeMessage(message, timestamp_in_samples) to the API in libremidi that will do the right thing with jack, doing that, thanks for the heads up :)

@ahlstromcj
Copy link
Owner

I actually had added a timestamp to the send_message() in my perverse implmentation of the rtmidi library. The issue is that there is a variable delay between putting the event in the ringbuffer and getting it out again.

However, your post made me go back to the ttymidi.c module and refurbish my implementation. It's a little different than the original, but works fairly well for the 4096 bufsize case. If more testing proves out, I'll feel a little better about making that 0.99.1 release. Not quite sure where I went wrong before when trying the ttymidi way.

I also looked at your libremidi update. Thanks!

ahlstromcj added a commit that referenced this issue Apr 19, 2023
This file lists __major__ changes from version 0.99.1 to 0.99.4.
These notes are not complete, just trying to get it to work.

*   Version 0.99.4:
    *   Issue #xyz.  Expand-pattern functionality.

*   Previous changes:

    *   Issue #40.  Enhanced NSM handling and debugging.
    *   Issue #44. Revisited to fix related additional issues.
    *   Issue #93. Revisited to fix related open pattern-editor issues.
    *   Issue #100. Partly mitigated. Added a custom JACK ringbuffer.
    *   Issue #103.  Some improvements to pattern loop-count.
    *   Pull request #106. User phuel added checkmarks for active buss.
    *   Issue #107.  Expand-pattern functionality.
    *   A raft of MIDI automation/display fixes.
    *   Added reading/writing/displaying Meta textual events.
    *   Improvements to playlist handling.
    *   Fixes to mute-group handling.
    *   Fixed the daemonization and log-file functionality.
    *   Fixed broken "recent-files" feature.
    *   Improved error reporting.
    *   Fixed background sequence not displaying with linear-gradient brush.
    *   Fixes to brushes; made the linear gradient the default.
    *   Other minor fixes and documentation updates, including the manual.
    *   Fixed partial breakage of pattern-merge function.
    *   Fixed odd breakage of ALSA playback in release mode.
    *   Fixed Stop button when another Master has started playback.
    *   Shift-click on Stop button rewinds JACK transport when running
        as JACK Slave.
    *   Display of some JACK server settings in Edit / Preferences.
    *   Fixed handling of Ctrl vs non-Ctrl zoom keys in perfroll.
    *   Event-dump now prompts for a text-file name.
    *   Added linear-gradient compile-time option for displaying notes
        and triggers.

Read the NEWS, README.md, and TODO files.  Never-ending!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants