Home Artists Posts Import Register

Content

There's a pretty bad bug in Blockhead at the moment where if you go far enough left or right in the workspace and start playing audio all the way over there then you will start to hear a sample reduction effect, which gets worse the further to the left or right you go. This is the most audibly noticeable bug in a family of related bugs that I am currently working on related to floating-point precision errors.

The engine processes audio in buffers of 64 frames. At the start of each buffer it generates 64 song positions representing the playback traversal. For example if you just open Blockhead and press the spacebar the first thing it will do is generate the numbers 0..63, then on the next buffer it generates 64..127, etc. If the playback hits the end of a loop region then the numbers might jump back mid-buffer, e.g. [567, 568, 569, 123, 124, 125, ...] etc.

This is only actually the case if the audio device is currently set to 44100 Hz, because then it matches Blockhead's internal "song rate" which is hard-coded to 44100. If the song rate and the sample rate don't match then the engine increments the song position differently. For example if the sample rate is 88200 Hz (exactly double the song rate) then the engine will generate [0.0, 0.5, 1.0, 1.5, 2.0, ...] etc. The sample rate can theoretically be anything so the numbers have to be passed around in a format that can represent fractional numbers.

This internal song rate thing is a problem unique to Blockhead's design because by default I want the workspace to act like a big canvas that you can freely drag things around on without having everything lock to a global grid. Constraining elements to a grid is an optional thing that you can add where you want it.

The song rate of 44100 is essentially the resolution of the workspace (there are 44100 valid positions that the edge of a block can be at, over a 1-second range of time). I just picked 44100 because it's a common sample rate and it's nice when then song rate and sample rate match but it could be anything. The engine still needs to process audio at the sample rate regardless of the song rate.

As far as I know this is a completely different approach to most other DAWs which just have a global project tempo and an internal MIDI clock which they can snap everything to, which simplifies everything. Sometimes you can opt to turn off the grid and drag things around freely but I think this is usually achieved by still having things still technically locked to the grid but then having an extra "offset" value which nudges the element backwards or forward in time.

The problem in Blockhead right now is when these fractional numbers get big enough they start to lose precision, which often manifests audibly as a kind of sample-rate reduction effect.

There's a few solutions to this I have thought of. Using double-precision floats to represent positions has various problems and would be a pretty hacky solution anyway.

Currently I'm reworking everything so that instead of using a single fractional number, each song position is represented by an integral part and a fractional part. So some massive song position like 266592848.736 would be split into two pieces: 266592848 and 0.736.

This way the size of the song should only be limited by what it's possible to represent using a 32-bit integer. At a song rate of 44100 this puts the limit at roughly 27 hours in either direction of the zero point (since Blockhead lets you put things to the left of the zero point too).

Comments

Anonymous

can you just use doubles? Does that still cause you to run into precision problems? I used to be scared of doubles for performance, but most CPU's seem to crunch them as fast as 32 bit floats (SIMD aside). The idea you're suggesting (split the whole and fractional parts) sounds a bit like Q-notation which is used more in embedded stuff that doesn't have an FPU, which has uniform precision rather than float's very wonky precision, and with some cleverness, you can actually almost do maths directly on them

colugomusic

I thought of doubles too but the library I use to handle SIMD things (madronalib) only supports float vectors so they would need to be vectorized to float anyway. One problem with splitting it in two is the song position can be manipulated by the UI thread directly (unless I start shoving that into a queue). I made a class that encodes the int32 and float32 into a single int64 which can be updated atomically

Anonymous

Makes sense - you probably know this but if you have a float32 playhead in any sample playback, it's likely to drift and make things go out of tune, that bit me on the butt a few years ago!

colugomusic

Blockhead calculates the current sample position from scratch each frame using the same algorithm the waveform renderer uses, rather than incrementing a value each frame, so there shouldn't be any drift there unless you mean something else