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 % 64
reveals the samples reminder left as last chunk to feed to make that loop complete in a 64sample bufferd audio stream.
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.
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.
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!
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.
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?
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.
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!
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
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?