Figured out a source of clicks when recording buffer to buffer

Probably. This is the minimum audio selection / trim.
And IIRC it correspond to the 1.5ms lag…
64/44100=0,0014512472

yep same gut feeling, 64samples shuffled around. In that case we could use int#64 a sample modulo and see if that makes something visible in math. Which is like Math.round(samplesNeeded) % 64 of the former js code above. Here the results.

bpm OctaTempo Samples Rounded Samples % 64 sec
56 1344 189000 8 4.285714sec
60 1440 176400 16 4.000000sec
63 1512 168000 0 3.809524sec
64 1536 165375 63 3.750000sec
72 1728 147000 56 3.333333sec
75 1800 141120 0 3.200000sec
84 2016 126000 48 2.857143sec
96 2304 110250 42 2.500000sec
100 2400 105840 48 2.400000sec
105 2520 100800 0 2.285714sec
108 2592 98000 16 2.222222sec
112 2688 94500 36 2.142857sec
120 2880 88200 8 2.000000sec
125 3000 84672 0 1.920000sec
126 3024 84000 32 1.904762sec
135 3240 78400 0 1.777778sec
140 3360 75600 16 1.714286sec
143 3432 74014 30 1.678322sec
144 3456 73500 28 1.666667sec
150 3600 70560 32 1.600000sec
157 3768 67414 22 1.528662sec
160 3840 66150 38 1.500000sec
168 4032 63000 24 1.428571sec
175 4200 60480 0 1.371429sec
180 4320 58800 48 1.333333sec
55 1320 192436 52 4.363636sec
61 1464 173508 4 3.934426sec
62 1488 170710 22 3.870968sec
65 1560 162831 15 3.692308sec
66 1584 160364 44 3.636364sec
67 1608 157970 18 3.582090sec
68 1632 155647 63 3.529412sec
82 1968 129073 49 2.926829sec
85 2040 124518 38 2.823529sec
86 2064 123070 62 2.790698sec
87 2088 121655 55 2.758621sec
88 2112 120273 17 2.727273sec
89 2136 118921 9 2.696629sec
95 2280 111411 51 2.526316sec
97 2328 109113 57 2.474227sec
104 2496 101769 9 2.307692sec
110 2640 96218 26 2.181818sec
122 2928 86754 34 1.967213sec
123 2952 86049 33 1.951220sec
124 2976 85355 43 1.935484sec
127 3048 83339 11 1.889764sec
128 3072 82688 0 1.875000sec
129 3096 82047 63 1.860465sec
130 3120 81415 7 1.846154sec
131 3144 80794 26 1.832061sec
132 3168 80182 54 1.818182sec
134 3216 78985 9 1.791045sec
136 3264 77824 0 1.764706sec
137 3288 77255 7 1.751825sec
138 3312 76696 24 1.739130sec
139 3336 76144 48 1.726619sec
141 3384 75064 56 1.702128sec
142 3408 74535 39 1.690141sec
145 3480 72993 33 1.655172sec
152 3648 69632 0 1.578947sec
153 3672 69176 56 1.568627sec
154 3696 68727 55 1.558442sec
155 3720 68284 60 1.548387sec
156 3744 67846 6 1.538462sec
165 3960 64145 17 1.454545sec
172 4128 61535 31 1.395349sec
173 4152 61179 59 1.387283sec
174 4176 60828 28 1.379310sec

for those curious how that table came to be, here the js code that applies the code above and generates the markup for Elektronauts forum posts…

var check = [
  56, 60, 63, 64, 72, 75, 84, 96, 100, 105, 108, 112, 120, 125, 126, 135, 140, 143, 144, 150, 157, 160, 168, 175, 180,
  55, 61, 62, 65, 66, 67, 68, 82, 85, 86, 87, 88, 89, 95, 97, 104, 110, 122, 123, 124, 127, 128, 129, 130, 131, 132, 134, 136, 137, 138, 139, 141, 142, 145, 152, 153, 154, 155, 156, 165, 172, 173, 174
];
var res = [];
for (var i=0; i<check.length; i++) {
   var test = check[i];
   var row = (rows.find(r=>r.bpm===test));
   res[i] = {bpm: test, OctaTempo: row.OctaTempo, samples_rounded: row.samples_rounded, sec: row.seconds_exact };
}
var tbl="| bpm | OctaTempo | Samples Rounded | Samples % 64 | sec |\n";
tbl+="| --- | --- | --- | --- | --- |\n";
for (var i=0; i<res.length; i++) { var t=res[i]; tbl+=("|"+t.bpm+"|"+t.OctaTempo+"|"+t.samples_rounded+"|"+(t.samples_rounded % 64)+"|"+t.sec+"|\n"); }
console.log(tbl);

beware a modulo % 64reveals the samples reminder left as last chunk to feed to make that loop complete in a 64sample bufferd audio stream.

1 Like

Not sure to understand. Always rounded to 64 multiples ?

44100 can’t be divided by 64, and at 120 bpm it records 88 200 perfectly.

64 samples is the minimum playback length, that is sure.

64 samples is very useful info. It could potentially dictate the resolution of the sequencer and how it interacts with the BPM.

Keep in mind that this is not a typical circular buffer delay line. It adds a restarting of the playback head, likely done at a 64 sample resolution. At some BPMs that lines up well (no or infrequent clicks) and at others poorly (frequent clicks).

The practical side of all this is that if you’re recording a loop that crosses the bar line you might need to be mindful of the tempo you choose. With the Pickup machine their solution was to encourage cross fading at the loop point. Which is fine for some types of audio, less so for others.

Or just always use a hi hat on the downbeat. :slight_smile:

As I said I don’t think so because 44100 can’t be divided by 64, and OT can record 44100 exactly at 120 bpm.

99% sure OT recordings are sample unit accurate, I already tested that before.
Helped me to define real tempo with decimals.

You stole my brilliant idea. :content:

I have a hypothesis that it is related to the decimal remainder when doing the mod 64 math, but there’s a missing piece.

Edit: I’ll write a little program that summarizes. I was trying to use ChatGPT to do the simple math and it kept screwing up some particular cases. So annoying.

I’ve stolen many of your brilliant ideas over the last few weeks!

1 Like

I think my list of fantastic BPMs for long looping is:

35, 45, 49, 63, 75, 105, 125, 135, 147, 175

All of those satisfy the following equation:

mod(240*44100/BPM,64) = 0

Other BPMs can perform very well if mod(240*44100/BPM,64) is a smaller integer. If it isn’t an integer, you’ll get clicks very quickly. Or maybe an integer that is either close to 0 or close to 64.

If you’re curious about the 240 it is just 4 beats (one measure) and converting from 1 minute to 60 seconds.

1 Like

if a minimal buffer is 64samples(frames) long, then it gets feed just 64 samples and then the next and the next. Until there must be one where the loop back must happen. That last 64 sample buffer chunk shuffled over is logically not entirely filled to complete full loop cycle unless the modulo falls exactly on its edge. The modulo tells how large such last chunk must/would be - aka the technical edge of the loop back event.

To provide continuous buffer that last chunk becomes part of the first on loopback, and more than likely filled with the first samples until full, and the next and next. This process repeats as long the machine runs of course and the imaginary chunk ‘phase’ would constantly shuffle in round robin fashion in relation to the wave data.

That is interesting only when thinking about where the click actually exactly happens. Does it only happen on 64’er chunk border to the next or also right in the middle of a 64’er chunk on that modulo edge…? - which would tell us something about the inner workings of the OT.

example:
When we’d look at 1 bar loop length at 112.0 bpm = 2688 Octatempo = 94500 samples/bar
considering shuffled in 64’er chunks, = (94500 % 64) = 36 samples to finish timespan to the bar end/edge in the very ‘last’ chunk. Which would mean = 64-36 = 28 samples left in that same ‘last’ chunk to fill up with the next triggered data from the beginning again.

some thought: as we hear no wobbling frequencies when playback changes tempo even a tiny bit that likely means the buffer shuffle can run free and does not slow down, neigher speed up, it is just constantly filled… Isn’t it?

1 Like

It is a circular buffer. If record/playback heads were not changed it would be constantly filled, relative location of “heads” impacts length of loop. Read/write rate is constant.

The difference between OT and a normal circular delay line is that it restarts the buffer with the playback trig. That restarting is limited by the 64 sample long blocks.

1 Like

I’ll mention that you aren’t constrained to integer BPM for potentially well-performing BPMs. 83.3 (actually 83 and 8/24) results in an integer with the mod formula. Should be relatively clickfree.

I’ll explore some more.

Integer values that should be totally clickless:
35, 45, 49, 63, 75, 105, 125, 135, 147, 175, 189

Decimal values that should be totally clickless:
30.6
33.7
36.7
40.8
43.7
55.1
58.3
65.6
78.7
93.7
118.1
140.6
145.8
153.1
168.7
183.7

Success!

1 Like

Attempt to predict click free loops of 1 bar length nicely with…

Bpm OctaTempo Samples ChunkModulo 1 bar in sec nice? octanice?
31.5 756 336000 0 7.619048s true false
35 840 302400 0 6.857143s true false
37.1 891 285091 35 6.464646s false true
37.5 900 282240 0 6.400000s true false
37.8 908 279753 9 6.343612s true false
41.5 996 255036 60 5.783133s false true
44.1 1059 239864 56 5.439093s true false
45 1080 235200 0 5.333333s true false
47.9 1150 220883 19 5.008696s false true
49 1176 216000 0 4.897959s true false
52.5 1260 201600 0 4.571429s true false
58.6 1407 180537 57 4.093817s false true
58.7 1409 180281 57 4.088006s false true
62.5 1500 169344 0 3.840000s true false
63 1512 168000 0 3.809524s true false
67.5 1620 156800 0 3.555556s true false
73.5 1764 144000 0 3.265306s true false
75 1800 141120 0 3.200000s true false
82.7 1985 127968 32 2.901763s false true
82.8 1988 127775 31 2.897384s false true
82.9 1990 127646 30 2.894472s false true
83 1992 127518 30 2.891566s false true
83.1 1995 127326 30 2.887218s false true
83.2 1997 127199 31 2.884326s false true
83.3 2000 127008 32 2.880000s false true
87.5 2100 120960 0 2.742857s true false
88.2 2117 119989 53 2.720831s true false
94.5 2268 112000 0 2.539683s true false
105 2520 100800 0 2.285714s true false
112.5 2700 94080 0 2.133333s true false
122.5 2940 86400 0 1.959184s true false
125 3000 84672 0 1.920000s true false
132.3 3176 79980 44 1.813602s true false
135 3240 78400 0 1.777778s true false
147 3528 72000 0 1.632653s true false
157.5 3780 67200 0 1.523810s true false
175 4200 60480 0 1.371429s true false
187.5 4500 56448 0 1.280000s true false
189 4536 56000 0 1.269841s true false
220.5 5292 48000 0 1.088435s true false
225 5400 47040 0 1.066667s true false
245 5880 43200 0 0.979592s true false
262.5 6300 40320 0 0.914286s true false
264.6 6351 39996 60 0.906944s true false

slightly different result, but your integer values appear just same same.
here the code…

var samplerate = 44100;
var minBpm = 30.0, maxBpm = 300.0;
var stepBpm = 0.1;
var bars = 1; // number of bars the 16-step loop represents (default 1)
var ticksPerBar = 24 * 4 * bars; // 96 for bars=1
var numerator = samplerate * 60 * ticksPerBar; // samples = numerator / P_int
var chunksize = 64;
function perfect(bpm) {
    return (((240 * samplerate) / bpm ) % chunksize) === 0;
}
function octaperfect(samples,octabpm) {
    return ( Math.round( (Math.round(samples)) / octabpm ) % chunksize) === 0;
}
function octatempo_precise(bpm) {
    //return Math.round(bpm * 24); //classic assumption, written in project files.
    var flat = Math.floor(bpm) * 24;
    var intfract = Math.ceil((bpm * 24.0) - flat);
    //console.log(bpm,flat,intfract,flat+intfract);
    return Math.min(Math.max(flat + intfract,720),7200);
}
var rows = [];
var range = Math.round((maxBpm - minBpm) / stepBpm);
for (var i = 0; i <= range; i++) {
  var bpm = +(minBpm + i * stepBpm).toFixed(1); //make sure only sharp 1commata values are used
  var octatempo = octatempo_precise(bpm);
  var samplesNeeded = numerator / octatempo; // exact float
  rows.push({
    bpm: bpm,
    OctaTempo: octatempo,
    samples_exact: samplesNeeded,
    samples_rounded: Math.round(samplesNeeded),
    seconds_exact: (samplesNeeded / samplerate).toFixed(6) + "s",
    nice: perfect(bpm),
    octanice: octaperfect(samplesNeeded,octatempo)
  });
}

//gimme all that fit according to prediction of fn perfect and fn octaperfect
var res = (rows.filter(r=> ((r.nice===true) || (r.octanice===true)) ));

// generate markup for Elektronauts post
var tbl="| Bpm | OctaTempo | Samples | ChunkModulo | 1 bar in sec | nice? | octanice? |\n";
tbl+="| --- | --- | --- | --- | --- | --- | --- |\n";
for (var i=0; i<res.length; i++) {
   var t =res[i];
   tbl +=( "|"+ t.bpm + "|"+ t.OctaTempo +
           "|"+ t.samples_rounded + "|"+ (t.samples_rounded % 64) + "|"+ t.seconds_exact +
           "|"+ t.nice + "|"+ t.octanice + "|\n"
   );
}
console.log(tbl);

I don’t think your list is right. 122.5 (122 13/24 on Octatrack) definitely produces clicks.

The fractional parts of bpm on octatrack are:
.1 is 3/24
.2 is 5/24
.3 is 8/24
.4 is 10/24
.5 is 13/24
.6 is 15/24
.7 is 18/24
.8 is 20/24
.9 is 23/24

i try to refine the above source according to your suggestion…
Which would mean to improve the OctaTempo as a function that takes those non-linear spread of tempi with … think think a lookuptable into account to calculate the correct Octatempo… but knowing the machine does really write those simple numbers into files its a tiny bit more tricky… Just to avoid having multiple references of Octatempo as defined by bpm * 24.

I mean thats an opportunity to check if the octatrack really does work fractions of tempi behind the integer values.

haha, what would I give for a XOUT parameter instead of a FOUT.

hmm, would this work properly to output Octatempi in integer? Sadly no access to my octa today (out of studio), so if someone would set a project tempo of 120.1 and then 120.2 and so on, the following function should predict the proper integers that end up in project.work/strd files

function octatempo_precise(bpm) {
    //return Math.round(bpm * 24); //classic assumption
    var flat = Math.floor(bpm) * 24;
    var intfract = Math.ceil((bpm * 24.0) - flat);
    var test = ((flat+intfract) / 24).toFixed(1);
    console.log(bpm, flat, intfract, flat+intfract, "test:", test);
    return flat + intfract;
}

If those are not ending up the same, then my function is wrong :slight_smile:

Perhaps stating the obvious, but these BPM values will also work with Pickup machines. If you’re annoyed with setting FIN to do the cross fade, you should be able to use any of these BPMs and not worry about clicks at the loop point.

Could you elaborate?

DSPs operate in a fixed clock domain. Their internal tick speed is locked to an oscillator (hardware), just like a CPU. Its clock rate does not change with playback tempo or buffer fill level. Audible tempo changes are realized in example, by resampling, stretching, or changing how data is scheduled. The buffer itself remains continuously filled only the consumption rate of data changes to adapt to slow down or speed up. The read pointer through the buffer moves faster or slower, which is - not really guessing but concluding - a software operation.

QUANTIZE LIVE REC checked or not, does that matter? :innocent: