BPM & Sampling Rate question

This forum is for developers of Rack Extensions to discuss the RE SDK, share code, and offer tips to other developers.
Post Reply
User avatar
pongasoft
RE Developer
Posts: 478
Joined: 21 Apr 2016
Location: Las Vegas
Contact:

24 Oct 2020

Let's assume the sampling rate is 44100.

If the BPM is 120, then 1 bar (4 beats) is exactly 2s which means this represents exactly 88200 samples

But now if the BPM is 121, then 1 bar is no longer exact (60*4/121 =) 1.9834....s and so how many samples does it represent (87471.07438)? How does Reason handle this? How do other DAWs handle this? Is there a standard?

What happens is you start at beat 5 for example? Which sample are you starting at?

Thanks
Yan

User avatar
selig
RE Developer
Posts: 11685
Joined: 15 Jan 2015
Location: The NorthWoods, CT, USA

25 Oct 2020

pongasoft wrote:
24 Oct 2020
Let's assume the sampling rate is 44100.

If the BPM is 120, then 1 bar (4 beats) is exactly 2s which means this represents exactly 88200 samples

But now if the BPM is 121, then 1 bar is no longer exact (60*4/121 =) 1.9834....s and so how many samples does it represent (87471.07438)? How does Reason handle this? How do other DAWs handle this? Is there a standard?

What happens is you start at beat 5 for example? Which sample are you starting at?

Thanks
Yan
I would guess it would have to be like starting a video on bar 5 when you have an odd number of frames per second and the play head falls between the frames. It's the next full frame we actually see as the first, but it's only a few milliseconds later than if we fell right on a frame boundary so we don't notice any actual delay as such.
A more extreme example may be when you start a REX loop ahead of the beat and it won't start playing until the next down beat. Whatever the underlying grid, the next logical "event' in each case waits for the proper start time from where ever playback begins.
For audio samples/video frames, they simply wait for the next logical sample/frame as the playhead advances, but you don't see this as a delay or other issue because it happens in a fraction of a second.

The point is that in all examples there is an underlying grid [the sample rate, the frame rate, the bar/beat tempo] that determines where all "like" parameters occur so they are all running in perfect sync (and in sync with the master clock). Additionally, the CPU clock runs at many times faster rate than even the highest sample rate so it can be extremely precise about where each of the undying grids falls in relation to each other.

Specifically for your example, the sample # 87472 (the next sample after (87471.07438) is the first one we hear, which is only a fraction of a millisecond late and would never be noticed. Granted, this is just my logical guess, but we CAN assume the samples don't move in relation to the underlying grid just because playback begins at a point between the grid lines. So that leaves one alternative IMO - is there another way this could be handled that I'm overlooking?
Selig Audio, LLC

User avatar
Enlightenspeed
RE Developer
Posts: 1103
Joined: 03 Jan 2019

13 Nov 2020

There's actually two different timelines running, one for MIDI and one for Audio.

MIDI runs by a standard called PPQ (pulses per quarter-note), where a pulse is the atomic value of MIDI. This sounds fine until you find out that different platforms might all use different onboard timing standards. Reason's constant is 15360 PPQ, which is pretty high.

The first thing to note here is that at 125 Bpm, the PPQ is 15360. At 149.047, the PPQ is 15360. The only difference is the time it takes to reach the end of that quarter note, which is of course less in the 2nd example.

The audio sample rate, and therefore the batch frame rate, couldn't care less what the PPQ is. Each frame is "packed" as a set of explicit instructions to be output. The content of each frame is of course dynamically controllable by code which is subscribed to details from the PPQ counters, in Reason's case from the Transport object.

So the flow is thus:

PPQ (The Input) --> RE C++ Native Object (The Process) --> Audio/MIDI Frame (The Output)

Audio going through the device, GUI property changes from the motherboard custom properties set et al., and other subscribable Reason Objects such as Transport and Note_States are all forms of input too, but again they can only inform the Audio/MIDI Frame at output. You can, with a bit of work, output changes to the GUI, and where the design allows you can get output to the motherboard custom properties, including the doc_owner variables - this MUST be done with a Custom Display though.

So basically, your C++ object code must deal with the "conversion" from PPQ to audio rate, and of course, be able to handle it for different sample rates. This sounds simple enough at first, but each device I've done so far which has needed to use clocking services has done so in a different manner, with the nature of the device informing the implementation. So you need to consider exactly what it is you want to do first before going head long into it.

For more details, and some code that will at least point you in the right direction, you should look at the SimplePatternPlayer example RE in the SDK - this will give you some clues as to how PPQ works, and has a useful "PPQ to Index Calculator" on board, which you can modify or just build new functions using the details.

Hope this helps,
B ;)

User avatar
Enlightenspeed
RE Developer
Posts: 1103
Joined: 03 Jan 2019

13 Nov 2020

Something I forgot to mention above, is that the output positions within the frame are always an 8-bit integer between 0 and 63. So regardless of however the fractions work in the C++ object they must always cast back to an 8-bit integer.

I can't remember for sure, but there might be a bug which allows higher numbers to get through without blowing the fuse, and they just get truncated to 63? It was due to a Recon bug IIRC, but it might actually be something else, I'm not sure.

User avatar
pongasoft
RE Developer
Posts: 478
Joined: 21 Apr 2016
Location: Las Vegas
Contact:

14 Nov 2020

Thank you for taking the time to respond. I guess I am more confused :). You focus on PPQ which seems to be tied to MIDI (which I had never heard about before). And I am not sure what is the link with audio samples. I have no doubt that the question I am asking can be asked as well for MIDI events and it seems that your answer is about that. Or is there a tie between PPQ and audio samples?

Another way to frame my confusion: how does PPQ interact with my question " if the BPM is 121, then 1 bar is no longer exact (60*4/121 =) 1.9834....s and so how many samples does it represent (87471.07438)?" Are they connected at all? Or completely separate?

I will take a look at the code example you are talking about and see if it can help.

Thanks!

User avatar
Enlightenspeed
RE Developer
Posts: 1103
Joined: 03 Jan 2019

14 Nov 2020

pongasoft wrote:
14 Nov 2020
Another way to frame my confusion: how does PPQ interact with my question " if the BPM is 121, then 1 bar is no longer exact (60*4/121 =) 1.9834....s and so how many samples does it represent (87471.07438)?" Are they connected at all? Or completely separate?
Ok,

I'll try and give some useful examples:

First up PPQ.
If the BPM is either 120 or 121, or 136, then the PPQ will not change. 1 Bar at either speed will equal 61440, or 15360 per quarter note (1/4 of a bar). 1/16th note is 15360/4 = 3840, 1/32nd note is 15360/8 = 1920 = the same as 61440/64.

So this measurement is actually constant and is the resolution of the MIDI platform we are operating on - Reason. In a lot of hardware it is significantly lower, especially older hardware, and the effect is that the lower the PPQ the more robotic it will sound. The measurement could be as low as 4 PPQ in older drum machines, and thus the maximum resolution that the device was capable of was the 16th note and everything would have to be quantized to this regardless of the incoming performance.

From everything that I've been able to find on this, Reason's PPQ res seems to be exceptionally high, probably for the purpose of future proofing. Cubase defaults to 480, although you can change it up to 4000 max, but this is still a good deal less than Reason offers.

Next post will be about how this relates to audio sample rates, after dinner and a movie, I'm afraid ;)

B

User avatar
Boombastix
Competition Winner
Posts: 1929
Joined: 18 May 2018
Location: Bay Area, CA

14 Nov 2020

Hmm, a bit unrelated maybe, but since the brains are here.
Audio inside the DAW goes into a buffer, so even if a bit late, the buffer will sort it out so long as it doesn't run dry, and everything sounds normal.

But what if the midi stream is late? Is it buffered and corrected or can notes lag? I've heard producers say the feel the need to print to audio as it is more stable, and midi jitter is a well know phenomenon, but I always thought the jitter/lag issue only existed outside of the DAW.

Back in the days Emagic developed its midi interface so it got midi data out of the computer at high speed in chunks and then the interface sorted out the timing for much better timing performance.

Comments?
10% off at Waves with link: https://www.waves.com/r/6gh2b0
Disclaimer - I get 10% as well.

User avatar
Enlightenspeed
RE Developer
Posts: 1103
Joined: 03 Jan 2019

15 Nov 2020

So, after a good nights rest....

Audio sample rates and batches. @Yan/Pongasoft - I realise you'll probably know most of this, but it's easier to go from the ground up.

Ok, each sample rate is declared as a measurement in Hz, where the measurements represent the number of samples per second that is being processed - 96000Hz is 96000 samples processed and output per second. Simple.

So the math that is needed is to establish where our PPQ number is relative to our sample number. Again simple enough but there are caveats when doing this in DSP. First of all the samples are always in batches, so that processing can buffer and be a bit more stable. In Rack Extensions we are always coding for batrches of 64 samples, regardless of the sample rate and buffer size on the audio hardware.

Now, we get a batch of property changes via our "HandleDiffs" methods - I just use the RS house style for most method names, as it helps with familiarity. This is important because each property can only change once per batch unless something dynamic is going on to change it on a per frame basis. Also note that Custom Displays don't refresh themselves as often as this.

HandleDiffs is in turn called once per batch by RenderBatch(). This tells us something important. Because RenderBatch() is called by JBox_Export_RenderRealtime() (JB_Ex_RR() from here on) once every 64 samples, then per second, JB_Ex_RR() will be called 'sample_rate/64' times per second. So if we're at 96000, then JB_Ex_RR() will call RenderBatch, which in turn calls HandleDiffs() 1500 times per second. So now we know the relation of batches to the second. It's worth noting at this stage before looking at code, that C++ native object is completely recreated every time the sample rate is changed, so our rate of batches is also effectively a constant, but it will need to be changeable when the sample rate is changed - I just handle this with a switch-case in the constructor that informs a variable declared in the constructor.



From here I'm going to go through a code example from SimplePatternPlayer - Note that the comments will probably be misaligned if you open this is Visual Studio yourself, so you might need to adjust it a bit first.

//// Code example - PPQToFrameIndex ////

// First up, this will return the FrameIndex variable. This is always an 8-bit Integer ranged 0-63.
// This method is only ever called from the PlayRange() method. It's purpose is to calculate where in a batch frame something is.
// The method arguments are as follows-
// iPPQ - This is 'offsetPPQ', which is declared in the PlayRange() when that is called. Note that in the header declaration it's meant to be an Int64, not a 64-bit floating point - the compiler is fine with this, but it's probably best avoided.
// iTempo - This is the tempo argument given by PlayRange. It's actually initialised as 0 in the constructor, then updated by HandleDiffs().
// iSampleRate - This is the sample rate taken from the constructor at the point of initialization, you need to declare in the JBox_Export.cpp file that you want the sample rate to be passed to the main .cpp class file.

std::uint8_t PPQToFrameIndex(TJBox_Float64 iPPQ, TJBox_Float64 iTempo, TJBox_Float64 iSampleRate)
{
// Set FrameIndex to 0. This can be done with a TJBox_UInt8 if not inside namespace.
std::uint8_t frameIndex = 0;

// Now we take the tempo, e.g.120, and divide it by 60 to get the beats per second.
const TJBox_Float64 beatsPerSecond = iTempo / 60;

// Now we calculate the PPQ's per second, by multiplying the beatsPerSecond by 15360, which is a constant value (called kPPQResolution for easier reading)
const TJBox_Float64 numPPQsPerSecond = beatsPerSecond * kPPQResolution;

// Next we divide the iPPQ argument by numPPQspersecond, to get seconds
const TJBox_Float64 seconds = iPPQ / numPPQsPerSecond;

// We then divide this by the sample rate.
const TJBox_Float64 frames = seconds * iSampleRate;

// Lastly there is a cast to get the value as a UInt8, after some min/max to ensure the value is between 0 and 63.
frameIndex = static_cast<std::uint8_t>(std::max<TJBox_Float64>(0, std::min<TJBox_Float64>(frames, 63)));

// Test code Assert to check it is between 0 and 63
JBOX_ASSERT(frameIndex >= 0 && frameIndex < 64);

// and voila a frameIndex value between 0 and 64.
return frameIndex;
}


The above example is all you really need to get an understanding of how it works in a method, but note that this method is part of the SPP device, so it needs that context to really be functional, your own design will need to use similar maths, but obviously will be called from elsewhere etc, with a different context. But, it gives you the basis for calculating the relationship.

DISCLAIMER - The above code snippet is the property of Reason Studios Ltd and is distributed as part of the Rack Extension SDK, with some notes added by Enlightenspeed purely for educational purposes.

Hope this helps,
B ;)

User avatar
Enlightenspeed
RE Developer
Posts: 1103
Joined: 03 Jan 2019

15 Nov 2020

Boombastix wrote:
14 Nov 2020
Hmm, a bit unrelated maybe, but since the brains are here.
*Brian ;)
Boombastix wrote:
14 Nov 2020
Audio inside the DAW goes into a buffer, so even if a bit late, the buffer will sort it out so long as it doesn't run dry, and everything sounds normal.

But what if the midi stream is late? Is it buffered and corrected or can notes lag? I've heard producers say the feel the need to print to audio as it is more stable, and midi jitter is a well know phenomenon, but I always thought the jitter/lag issue only existed outside of the DAW.
Righties, we need to be specific by what we are saying with "MIDI stream". Inside the PC there is no "lag" as such because the modern PC goes at such a ridiculously fast speed compared to the MIDI rate. The fastest MIDI tick rate available in Reason is 256,000 per second, when you put the tempo up to 1000 - ok, it's actually 999.999, but I'm sure 1000 will do :D 256,000 per second is 256MHz, and the clock speed on my PC that I type this on is 3.6GHz, which is still significantly more than a thousand times faster. So you can trust that a PC finds MIDI slow and easy to deal with, providing that you aren't being misadventurous with your code.

A MIDI stream coming out of an external hardware device such as an analog sequencer is a different thing entirely. They work on slower less reliable processors, and are far more susceptible to fluctuations in electrical current from the mains. So, if you record 100 passes of a loop that is coming in from such a device there will most likely be significant inconsistencies in the output.

Jitter is also a factor when you sync two different DAW's, say Reason and Live via the Ableton sync thing. I would guess that this is becuase different programs will handle their internal clocking in different ways, and the CPU thread will always do one before the other. These numbers need to be asynchronous in processing and then match to each other, but because of the differences in under the hood calculation they will constantly mismatch and whichever program is outputting the highest number etc will change constantly, and thus the fluctuation. This is guesswork, though.

Boombastix wrote:
14 Nov 2020
Back in the days Emagic developed its midi interface so it got midi data out of the computer at high speed in chunks and then the interface sorted out the timing for much better timing performance.

Comments?
No idea :D
This tech is probably long superseded though.

Cheers,
B

User avatar
pongasoft
RE Developer
Posts: 478
Joined: 21 Apr 2016
Location: Las Vegas
Contact:

15 Nov 2020

Thank you very much for the detailed explanation. I think I have a better understanding now. I need to put it in practice to see if I get it all right and I can do what I have been meaning to achieve.

Thanks!
Yan

Post Reply
  • Information
  • Who is online

    Users browsing this forum: No registered users and 1 guest