Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions Utility/HAST3_XRUN_Monitor.jsfx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
desc: Time-based XRUN Monitor
author: Håvard Endresen HAST3
version: 1.0
provides:
[script] HAST3_XRUN_Monitor/HAST3_XRUN_WriteCSV.lua
[script] HAST3_XRUN_Monitor/HAST3_XRUN_AddRegions.lua
[script] HAST3_XRUN_Monitor/HAST3_XRUN_InsertTime_EXPERIMENTAL.lua
[script] HAST3_XRUN_Monitor/HAST3_XRUN_ClearRegions.lua
about:
This package detects XRUNs based on drift between the software audio clock and OS clock.

This is useful if you're recording audio and would like to be aware of any XRUNs that occur to that file. The XRUNs are detected by a JSFX script which writes them to REAPER's GMEM so they can be read with actions. These actions can put detected XRUNs in the project via regions or log them to a CSV file.

An experimental action, XRUN_InsertTime, inserts space in the project where XRUNs were detected, which effectively aligns the recorded audio with the OS clock, and might help sync the audio to externally recorded media. However, it also inserts space for falsely detected XRUNs. The offset should be compensated in the long run, but will add excessive gaps in the project.

JSFX Features:
- Underrun/overrun sensitivity: This is how many samples need to drift during a window for it to count as an XRUN.
- Window: adjusts how frequently the drift is measured. A shorter window measures the XRUN positions more accurately, but might get over-sensitive due to jitter with the OS clock. A longer window is more stable but logs less accurate time positions.
- Warmup: Ignores XRUNs from startup. The first couple of seconds with playback usually get over-sensitive, so it's recommended to ignore them.
- Display toggles: Toggles what to show in the GFX window. Might improve performance if disabled. The log display window is maximized to 50 lines for performance reasons. Max lines does not limit how many XRUNs can be read by the actions.

slider1:16<0,256,1>underrun sens[spl] (0=off)
slider2:16<0,256,1>overrun sens[spl] (0=off)
slider3:4<1,64,1>window[multiple of buffer size]
slider4:3<0,5,0.5>warmup[s] (to avoid false xruns from startup)
slider5:0<0,1,1>info[on/off]
slider6:1<0,1,1>main display[on/off]
slider7:0<0,1,1>log display[on/off] (off clears display)

@init
/*CONFIG*/
max_lines = 50;

time_os_start = time_precise();

#dropout_log = "time_os, time_audio, length\n";
i=4;
warmup = 0;
warmup_counter = 0;
dropouts_total = 0;
log_entries = 0;
time_os_prev = 0;
counter = 0;
total_count = 0;
offsync = 0;
prev_display_log = 0;

gfx_open = 1;
gfx_r = 1; gfx_g = 1; gfx_b = 1; // txt color white
gfx_a = 1; // alpha color opaque

@slider
sens = slider1;
sens_neg = slider2;
window = slider3;
warmupsec = slider4;
display_info = slider5;
display_main = slider6;
display_log = slider7;

@block
(counter >= window) ? (
time_real = time_precise();
time_os = (time_real - time_os_start) * srate;
time_audio = total_count * samplesblock;
interval_os = time_os - time_os_prev;
interval_audio = counter * samplesblock;

(warmup_counter <= warmup) ? (
(warmup_counter == 0) ? (
/*INIT AFTER WARMUP*/
warmup = floor(warmupsec * srate / (samplesblock * window));
gmem[0] = srate;
);
(warmup_counter == warmup) ? (
//total_count = floor(time_os / samplesblock); /*alternative warmup*/
time_os_start = time_real - time_audio / srate;
time_os_prev = time_audio;
);
warmup_counter += 1;
) : (
offsync = time_os - time_audio;
len = (interval_os - interval_audio);
(((len >= sens) && sens) || ((-len >= sens_neg) && sens_neg)) ? (
dropout_time_os = time_os;
dropout_time_audio = time_audio;
dropout_len = len;
dropouts_total += 1;
(display_log) ? (
(log_entries <= max_lines) ? (
sprintf(#line, "%.4f\t%.4f\t%d\n", dropout_time_os/srate, dropout_time_audio/srate, dropout_len);
#dropout_log += #line;
(log_entries == max_lines) ? (#dropout_log += "DISPLAY FULL (GMEM is still logging!)\n");
log_entries += 1;
);
);
gmem[i] = 42;
gmem[i+1] = dropout_time_os;
gmem[i+2] = dropout_time_audio;
gmem[i+3] = dropout_len;
gmem[i+4] = 0; //makes sure gmem reading stops
i += 4;
);
time_os_prev = time_os;
);
counter = 0;
);
counter += 1;
total_count += 1;

@gfx
gfx_x = 8;
gfx_y = 8;
(display_info) ? (
gfx_printf("Detects xruns based on drift between the audio clock and os clock.
This is useful if you're recording audio and would like to
be aware of any xruns that occur to that file.
The xruns are logged to REAPER gmem so they can be read with actions.
These actions put detected xruns in the project via regions
or log them to a CSV file.
A shorter window measures the xrun position more accurately,
but might get over-sensitive due to jitter with the OS clock.
A longer window is more stable but logs less acurate time positions.\n"
);
);
(display_main) ? (
gfx_printf("Sample Rate: %d\n", srate);
gfx_printf("Buffer Size: %d\n", samplesblock);
gfx_printf("Window: %.3fs\n", samplesblock * window / srate);
gfx_printf("Play State: %d\n", play_state);
gfx_printf("%d dropouts total\n", dropouts_total);
gfx_printf("%d spl offsync\n", offsync);
gfx_printf("Time: %.1f s\n", time_os / srate);
gfx_printf("Block no.: %.0f\n\n", total_count);
);
(display_log) ? (
gfx_printf("%s", #dropout_log);
) : (prev_display_log) ? ( /*only resets string once, for less redundant CPU*/
#dropout_log = "time_os, time_audio, length\n";log_entries=0
);
prev_display_log = display_log;
(warmup_counter <= warmup) ? (gfx_printf("Warming up...\n"));
53 changes: 53 additions & 0 deletions Utility/HAST3_XRUN_Monitor/HAST3_XRUN_AddRegions.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- @noindex

--CONFIG
local color_gain = 24000

reaper.Undo_BeginBlock2(0)

local NAMESPACE = "xrun_monitor"
reaper.gmem_attach(NAMESPACE)

local srate = reaper.gmem_read(0)

local no=1
local i=4

while reaper.gmem_read(i) == 42 do
local time_os = reaper.gmem_read(i+1)
local time_audio = reaper.gmem_read(i+2)
local len = reaper.gmem_read(i+3)

local intensity = math.floor(math.abs(len*color_gain))
if intensity > 255 then intensity = 255 end
local xrunstring = string.format("XRUN%d %.0f %.0f", no, len, (time_os-time_audio))

if len > 0 then
local color = reaper.ColorToNative(intensity,0,0) | 0x01000000
reaper.AddProjectMarker2(
0,
true,
time_audio / srate,
(time_audio + len) / srate,
xrunstring,
-1,
color
)
else
local color = reaper.ColorToNative(0,0,128) | 0x01000000
reaper.AddProjectMarker2(
0,
true,
time_audio / srate,
(time_audio + len) / srate,
xrunstring,
-1,
color
)
end

no = no + 1
i = i + 4
end

reaper.Undo_EndBlock2(0, "Update XRUN Regions", -1)
16 changes: 16 additions & 0 deletions Utility/HAST3_XRUN_Monitor/HAST3_XRUN_ClearRegions.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- @noindex

reaper.Undo_BeginBlock2(0)

local num_markers_regions = reaper.CountProjectMarkers(0)

for i = num_markers_regions-1, 0, -1 do
local retval, isrgn, pos, rgnend, name, markrgnindex =
reaper.EnumProjectMarkers(i)

if retval and name and name:sub(1,4) == "XRUN" then
reaper.DeleteProjectMarkerByIndex(0, i)
end
end

reaper.Undo_EndBlock2(0, "Delete XRUN Regions", -1)
37 changes: 37 additions & 0 deletions Utility/HAST3_XRUN_Monitor/HAST3_XRUN_InsertTime_EXPERIMENTAL.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- @noindex

--CONFIG
local offset = 0

local NAMESPACE = "xrun_monitor"
reaper.gmem_attach(NAMESPACE)

local srate = reaper.gmem_read(0)

local no=1
local i=4
local prev_offsync = 0

reaper.Undo_BeginBlock2(0)

while reaper.gmem_read(i) == 42 do
local time_os = reaper.gmem_read(i+1)
local time_audio = reaper.gmem_read(i+2)
local len = reaper.gmem_read(i+3)


local offsync = time_os - time_audio

local sync_len = offsync - prev_offsync

if sync_len > 0 then
reaper.GetSet_LoopTimeRange2(0, true, false, (time_audio + offset)/srate, (time_audio + sync_len + offset)/srate, false)
reaper.Main_OnCommand(40200, 0)
prev_offsync = prev_offsync + sync_len
end

no = no + 1
i = i + 4
end

reaper.Undo_EndBlock2(0, "Insert XRUN compensation in project", -1)
41 changes: 41 additions & 0 deletions Utility/HAST3_XRUN_Monitor/HAST3_XRUN_WriteCSV.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- @noindex

--FILE PATH:
local fname = "/Users/havard/Desktop/xruns.csv"

local NAMESPACE = "xrun_monitor"
reaper.gmem_attach(NAMESPACE)

reaper.ClearConsole()

local srate = reaper.gmem_read(0)

local no=1
local i=4

if fname == "/path/to/file.csv" then
reaper.ShowConsoleMsg('File path not set! Go to "Edit action..." and set it.\n')
return
end

local f = io.open(fname, "w")
if not f then
reaper.ShowConsoleMsg("Could not open file for writing\n")
return
end

f:write(string.format("Sample rate: %d,unit:spl\nno,TimeOS,TimeAudio,Length\n", srate))

while reaper.gmem_read(i) == 42 do
local time_os = reaper.gmem_read(i+1)
local time_audio = reaper.gmem_read(i+2)
local len = reaper.gmem_read(i+3)

f:write(string.format("%.0f,%.1f,%.0f,%.1f\n", no, time_os, time_audio, len))
no = no + 1
i = i + 4
end

f:close()

reaper.ShowConsoleMsg(no-1 .. " XRUNs written to " .. fname)