Skip to content

Instantly share code, notes, and snippets.

@GavinRay97
Created August 6, 2023 22:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GavinRay97/7d4eeef3e5c15397ddda8677b9dee93e to your computer and use it in GitHub Desktop.
Save GavinRay97/7d4eeef3e5c15397ddda8677b9dee93e to your computer and use it in GitHub Desktop.
MX Tuner infobox mod
diff --git a/MX Tuner original.lua b/MX Tuner.lua
index 828e5b9..962b549 100644
--- a/MX Tuner original.lua
+++ b/MX Tuner.lua
@@ -44,6 +44,8 @@ local prev_use_rate
local prev_file_pitch
local sel_note_name
local prev_file
+local underlying_note_name
+local detected_pitch_source -- "Metadata" | "Filename" | "Algorithm" | "MIDI"
local locked_key
local hovered_key
@@ -1003,6 +1005,42 @@ function DrawPiano()
gfx.update()
end
+function DrawDetectedPitchInfo()
+ -- Set box dimensions
+ local box_width = 110
+ local box_height = 50
+
+ -- Calculate right aligned x position
+ local box_x = gfx.w - box_width
+
+ -- Draw gray box aligned to right side of window
+ gfx.set(0.3, 0.3, 0.3)
+ gfx.rect(box_x, 0, box_width, box_height, 1)
+
+ -- Add padding
+ local padding = 8
+
+ -- Draw key name
+ gfx.set(1, 1, 1)
+ gfx.setfont(1, '', 14, string.byte('b'))
+ local key_text = underlying_note_name and 'Key: ' .. underlying_note_name or ''
+ local text_w, text_h = gfx.measurestr(key_text)
+
+ gfx.x = box_x + padding
+ gfx.y = padding
+ gfx.drawstr(key_text)
+
+ -- Draw key source
+ gfx.set(1, 1, 1)
+ local source_text = detected_pitch_source and 'Source: ' .. detected_pitch_source or ''
+ local text_w, text_h = gfx.measurestr(source_text)
+
+ gfx.x = box_x + padding
+ gfx.y = padding + text_h + 4
+ gfx.drawstr(source_text)
+end
+
+
function Main()
-- Ensure hwnd for MX is valid (changes when docked etc.)
if not reaper.ValidatePtr(mx, 'HWND') then
@@ -1051,11 +1089,13 @@ function Main()
if parse_meta_mode == 1 then
curr_parsing_mode = 2
file_pitch = GetPitchFromMetadata(new_file)
+ detected_pitch_source = 'Metadata'
end
-- Check file name for pitch
if not file_pitch and parse_name_mode == 1 then
curr_parsing_mode = 1
file_pitch = GetPitchFromFileName(new_file)
+ detected_pitch_source = 'Filename'
end
-- Use chosen pitch detection algorithm to find pitch
if not file_pitch or is_parsing_bypassed then
@@ -1066,6 +1106,7 @@ function Main()
-- Get pitch from MIDI file
local root_name = GetMIDIFileRootName(new_file)
file_pitch = NameToFrequency(root_name)
+ detected_pitch_source = 'MIDI'
else
-- Get pitch from audio file
if algo_mode == 1 then
@@ -1074,6 +1115,7 @@ function Main()
if algo_mode == 2 then
file_pitch = GetPitchFFT(new_file)
end
+ detected_pitch_source = 'Algorithm'
end
end
prev_file_pitch = file_pitch
@@ -1084,6 +1126,7 @@ function Main()
pitch_offs = pitch_offs + 12 * math.log(mx_rate, 2)
end
sel_note_name = FrequencyToName(file_pitch, pitch_offs)
+ underlying_note_name = FrequencyToName(file_pitch)
end
if file_pitch and locked_key then
@@ -1177,7 +1220,10 @@ function Main()
is_redraw = true
end
- if is_redraw then DrawPiano() end
+ if is_redraw then
+ DrawPiano()
+ DrawDetectedPitchInfo()
+ end
-- Open settings menu on right click
if gfx.mouse_cap & 2 == 2 then
--[[
@author Ilias-Timon Poulakis (FeedTheCat)
@license MIT
@version 1.8.0
@provides [main=main,mediaexplorer] .
@about Simple tuner utility for the reaper media explorer
@changelog
- Alt-click on a key now adds 'key' metadata (not automatic on MacOS)
- Alt-click on the metadata icon now removes 'key' metadata
- Improved FFT pitch detection algorithm (better harmonics detection)
- MX tuner now finds and analyzes the loudest section of a file
- Added configurable analysis window in menu > algorithm
- FTC algorithm is now deprecated
]]
-- Check if js_ReaScriptAPI extension is installed
if not reaper.JS_Window_Find then
reaper.MB('Please install js_ReaScriptAPI extension', 'Error', 0)
return
end
local version = tonumber(reaper.GetAppVersion():match('[%d.]+'))
if version < 6.52 then
reaper.MB('Please install REAPER v6.52 or later', 'MX Tuner', 0)
return
end
-- Open media explorer window
local mx_title = reaper.JS_Localize('Media Explorer', 'common')
local mx = reaper.OpenMediaExplorer('', false)
local _, _, sec, cmd = reaper.get_action_context()
local w_x, w_y, w_w, w_h
local prev_mouse_x, prev_mouse_y
local prev_mouse_cap
local prev_h, prev_w
local prev_dock
local prev_color_theme
local prev_mx_pitch
local prev_mx_rate
local prev_use_rate
local prev_file_pitch
local sel_note_name
local prev_file
local underlying_note_name
local detected_pitch_source -- "Metadata" | "Filename" | "Algorithm" | "MIDI"
local locked_key
local hovered_key
local is_pressed = false
local is_window_hover = false
local rate_mode
local pitch_mode
local algo_mode
local algo_window
local parse_meta_mode
local parse_name_mode
local frameless_mode
local focus_mode
local ontop_mode
local curr_parsing_mode = 0
local is_parsing_bypassed = false
local is_option_bypassed = false
local trigger_pitch_rescan = false
local flat = {'C', 'D', 'E', 'F', 'G', 'A', 'B'}
local sharp = {'C#', 'D#', 'F#', 'G#', 'A#'}
local theme = {}
local theme_id
function print(msg) reaper.ShowConsoleMsg(tostring(msg) .. '\n') end
function GetSemitonesToA(f) return 12 * math.log(f / 440) / math.log(2) % 12 end
function GetMinSemitonesTo(f1, f2, semitone_offs)
local dist = GetSemitonesToA(f2) - GetSemitonesToA(f1) + semitone_offs
dist = dist < 0 and dist % 12 - 12 or dist % 12
if dist > 6 or dist > 2 and f1 > 4400 then dist = dist - 12 end
if dist < -6 or dist < -2 and f1 < 60 then dist = dist + 12 end
return dist
end
function FrequencyToName(f, semitone_offs)
local n = {'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'}
local dist = GetSemitonesToA(f) + (semitone_offs or 0)
local dist_rnd = math.floor(dist + 0.5) % 12
return n[dist_rnd + 1]
end
function NameToFrequency(name)
local n = {'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'}
if name == 'Db' then name = 'C#' end
if name == 'Eb' then name = 'D#' end
if name == 'Gb' then name = 'F#' end
if name == 'Ab' then name = 'G#' end
if name == 'Bb' then name = 'A#' end
for i = 1, #n do
if name == n[i] then
return 440 * math.exp((i - 1) * math.log(2) / 12)
end
end
end
function GetMIDIFileRootName(file)
local has_added_track = false
local track = reaper.GetTrack(0, 0)
-- Add track if project has no tracks
if not track then
has_added_track = true
reaper.InsertTrackAtIndex(0, false)
track = reaper.GetTrack(0, 0)
end
-- Add MIDI source to new take
local src = reaper.PCM_Source_CreateFromFileEx(file, true)
local src_len = reaper.GetMediaSourceLength(src)
local item = reaper.AddMediaItemToTrack(track)
local take = reaper.AddTakeToMediaItem(item)
reaper.SetMediaItemTake_Source(take, src)
-- Get lowest pitch in MIDI file (root)
local min_pitch = math.maxinteger
local _, note_cnt = reaper.MIDI_CountEvts(take)
for n = 0, note_cnt - 1 do
local ret, _, _, _, _, _, pitch = reaper.MIDI_GetNote(take, n)
min_pitch = pitch < min_pitch and pitch or min_pitch
end
-- Convert pitch to note name
local n = {'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'}
local root_name = min_pitch ~= math.maxinteger and n[min_pitch % 12 + 1]
-- Clean up
reaper.DeleteTrackMediaItem(track, item)
if has_added_track then reaper.DeleteTrack(track) end
return root_name
end
function IsMediaFile(file)
local ext = file:match('%.([^.]+)$')
if ext and reaper.IsMediaExtension(ext, false) then
ext = ext:lower()
if ext ~= 'xml' and ext ~= 'rpp' then return true end
end
end
function MediaExplorer_GetSelectedMediaFiles()
local show_full_path = reaper.GetToggleCommandStateEx(32063, 42026) == 1
local show_leading_path = reaper.GetToggleCommandStateEx(32063, 42134) == 1
local forced_full_path = false
local path_hwnd = reaper.JS_Window_FindChildByID(mx, 1002)
local path = reaper.JS_Window_GetTitle(path_hwnd)
local mx_list_view = reaper.JS_Window_FindChildByID(mx, 1001)
local _, sel_indexes = reaper.JS_ListView_ListAllSelItems(mx_list_view)
local sep = package.config:sub(1, 1)
local sel_files = {}
for index in string.gmatch(sel_indexes, '[^,]+') do
index = tonumber(index)
local file_name = reaper.JS_ListView_GetItem(mx_list_view, index, 0)
-- File name might not include extension, due to MX option
local ext = reaper.JS_ListView_GetItem(mx_list_view, index, 3)
if ext ~= '' and not file_name:match('%.' .. ext .. '$') then
file_name = file_name .. '.' .. ext
end
if IsMediaFile(file_name) then
-- Check if file_name is valid path itself (for searches and DBs)
if not reaper.file_exists(file_name) then
file_name = path .. sep .. file_name
end
-- If file does not exist, try enabling option that shows full path
if not show_full_path and not reaper.file_exists(file_name) then
show_full_path = true
forced_full_path = true
-- Browser: Show full path in databases and searches
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42026, 0, 0, 0)
file_name = reaper.JS_ListView_GetItem(mx_list_view, index, 0)
if ext ~= '' and not file_name:match('%.' .. ext .. '$') then
file_name = file_name .. '.' .. ext
end
end
sel_files[#sel_files + 1] = file_name
end
end
-- Restore previous settings
if forced_full_path then
-- Browser: Show full path in databases and searches
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42026, 0, 0, 0)
if show_leading_path then
-- Browser: Show leading path in databases and searches
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42134, 0, 0, 0)
end
end
return sel_files
end
function MediaExplorer_GetSelectedFileInfo(sel_file, id)
if not sel_file or not id then return end
local mx_list_view = reaper.JS_Window_FindChildByID(mx, 1001)
local _, sel_indexes = reaper.JS_ListView_ListAllSelItems(mx_list_view)
local sel_file_name = sel_file:match('([^\\/]+)$')
for index in string.gmatch(sel_indexes, '[^,]+') do
index = tonumber(index)
local file_name = reaper.JS_ListView_GetItem(mx_list_view, index, 0)
-- File name might not include extension, due to MX option
local ext = reaper.JS_ListView_GetItem(mx_list_view, index, 3)
if not file_name:match('%.' .. ext .. '$') then
file_name = file_name .. '.' .. ext
end
if file_name == sel_file_name then
local info = reaper.JS_ListView_GetItemText(mx_list_view, index, id)
return info ~= '' and info
end
end
end
function MediaExplorer_SetMetaDataKey(key)
if version < 6.79 then
reaper.MB('This feature requires REAPER v6.79 and above.', 'Error', 0)
return
end
-- Edit metadata tag: Key
reaper.JS_Window_OnCommand(mx, 42064)
function HandleDialog()
local title = reaper.JS_Localize('Edit metadata tag', 'common')
local dialog_hwnd = reaper.JS_Window_Find(title, true)
local edit_hwnd = reaper.JS_Window_FindChildByID(dialog_hwnd, 1007)
-- Insert key name into text field
reaper.JS_Window_SetTitle(edit_hwnd, key)
-- Click OK button
-- Note: This only works on Windows
local ok_button_hwnd = reaper.JS_Window_FindChildByID(dialog_hwnd, 1)
reaper.JS_WindowMessage_Send(ok_button_hwnd, 'WM_LBUTTONDOWN', 0, 0, 0, 0)
reaper.JS_WindowMessage_Send(ok_button_hwnd, 'WM_LBUTTONUP', 0, 0, 0, 0)
-- Press key: Enter
-- Note: This only works on Linux
reaper.JS_WindowMessage_Send(edit_hwnd, 'WM_KEYDOWN', 0x0D, 0, 0, 0)
reaper.JS_WindowMessage_Send(edit_hwnd, 'WM_KEYUP', 0x0D, 0, 0, 0)
locked_key = nil
if rate_mode == 1 then
MediaExplorer_SetRate(1)
else
MediaExplorer_SetPitch(0)
end
local cnt = 0
function DelayRescan()
if cnt == 1 then
trigger_pitch_rescan = true
return
end
cnt = cnt + 1
reaper.defer(DelayRescan)
end
reaper.defer(DelayRescan)
end
reaper.defer(HandleDialog)
end
function MediaExplorer_GetPitch()
local pitch_hwnd = reaper.JS_Window_FindChildByID(mx, 1021)
local pitch = reaper.JS_Window_GetTitle(pitch_hwnd)
return tonumber(pitch)
end
function MediaExplorer_SetPitch(pitch)
local curr_pitch = MediaExplorer_GetPitch()
local diff = math.abs(pitch - curr_pitch)
local is_upwards = pitch > curr_pitch
local semitones = math.floor(diff)
local cents = math.floor(diff % 1 * 100 + 0.5)
if is_upwards then
for i = 1, cents do
-- Preview: adjust pitch by +1 cents
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 40074, 0, 0, 0)
end
for i = 1, semitones do
-- Preview: adjust pitch by +1 semitones
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42163, 0, 0, 0)
end
else
for i = 1, cents do
-- Preview: adjust pitch by -1 cents
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 40075, 0, 0, 0)
end
for i = 1, semitones do
-- Preview: adjust pitch by -1 semitones
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42162, 0, 0, 0)
end
end
end
function MediaExplorer_GetRate()
local rate_hwnd = reaper.JS_Window_FindChildByID(mx, 1454)
local rate = reaper.JS_Window_GetTitle(rate_hwnd)
return tonumber(rate)
end
function MediaExplorer_SetRate(rate)
-- Set precise rate to rate textfield
local rate_hwnd = reaper.JS_Window_FindChildByID(mx, 1454)
local pattern = rate == 1 and ('%.1f') or ('%.3f')
reaper.JS_Window_SetTitle(rate_hwnd, pattern:format(rate))
end
function IsPitchPreservedWhenChangingRate()
return reaper.GetToggleCommandStateEx(32063, 40068) == 1
end
function TurnOffPreservePitchOption()
-- Turn off option to preserve pitch when changing rate
if reaper.GetToggleCommandStateEx(32063, 40068) == 1 then
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 40068, 0, 0, 0)
end
end
function GetApproximatePeakTime(src, src_len)
local rate = 1000
local spl_cnt = math.ceil(rate * src_len)
local buf = reaper.new_array(spl_cnt * 2)
local ret = reaper.PCM_Source_GetPeaks(src, rate, 0, 1, spl_cnt, 0, buf)
if not ret then return 0 end
spl_cnt = (ret & 0xfffff)
if spl_cnt == 0 then return 0 end
buf = buf.table()
local max_i = 0
local max_val = 0
for i = 1, spl_cnt do
local val = buf[i]
local val_abs = val < 0 and -val or val
if val_abs > max_val then
max_i = i
max_val = val_abs
end
end
return max_i / spl_cnt * src_len
end
function GetPitchFTC(file)
-- Get media source peaks
local src = reaper.PCM_Source_CreateFromFileEx(file, true)
if not reaper.ValidatePtr(src, 'PCM_source*') then return end
local src_len = reaper.GetMediaSourceLength(src)
if src_len == 0 then
reaper.PCM_Source_Destroy(src)
return
end
local time_window = math.min(src_len, algo_window)
local soffs = GetApproximatePeakTime(src, src_len)
soffs = soffs - time_window / 5
if soffs + time_window > src_len then
soffs = src_len - soffs
end
soffs = math.max(soffs, 0)
-- Limit length of analyzed sample
src_len = math.min(src_len, time_window)
local rate = reaper.GetMediaSourceSampleRate(src)
local spl_cnt = math.ceil(src_len * rate)
spl_cnt = math.min(spl_cnt, 2 ^ 19)
local buf = reaper.new_array(spl_cnt * 2)
local ret = reaper.PCM_Source_GetPeaks(src, rate, 0, 1, spl_cnt, 0, buf)
reaper.PCM_Source_Destroy(src)
if not ret then return end
spl_cnt = ret & 0xfffff
if spl_cnt == 0 then return end
buf = buf.table()
-- Note: window size of 1 * rate = 1 sec
local window_size = rate
-- A higher peak threshold is better for detecting lower notes, vice versa
local peak_thres = 0.25 -- [0.1 - 0.5]
-- Determine sample start and end position
local epos = math.min(window_size, spl_cnt)
local spos = math.max(epos - window_size, 1)
local zcross = {}
local peak = 0
local peak_since_last_zcross = 0
-- Get zero crossings between sample start and end position
for i = spos, epos do
local val = buf[i]
local val_abs = val < 0 and -val or val
-- Keep track of the highest peak we have encountered
if val_abs > peak then peak = val_abs end
-- Keep track of the highest peak we have encountered since last crossing
if val_abs > peak_since_last_zcross then
peak_since_last_zcross = val_abs
end
-- Check if samples cross the zero line
local next_val = buf[i + 1]
local is_lo = val < 0 and next_val >= 0
local is_hi = val > 0 and next_val <= 0
if is_lo or is_hi then
-- Filter out weak crossings (compared to current max peak)
if peak_since_last_zcross > peak * peak_thres then
-- Optimization: Instead of using the sample index i, we approximate
-- the exact crossing point (inter-sample) using linear interpolation.
local x1, y1, x2, y2 = i, val, i + 1, next_val
local pos = x1 + y2 * (x2 - x1) / (y2 - y1)
zcross[#zcross + 1] = {
i = i,
pos = pos,
is_hi = is_hi,
peak = peak_since_last_zcross,
}
end
peak_since_last_zcross = 0
end
end
local candidates = {}
local overlaps = math.min(#zcross // 2, 18)
-- Count occurences of lengths (in samples) between zero crossings. Repeat this
-- process for a set window of overlaps (length between zero crossing A and B,
-- A and C, A and D, etc.)
for m = overlaps - 2, 0, -2 do
local bins = {}
local max_len, max_weight = 0, 0
for i = 1, #zcross - (m + 2) do
for n = m + 1, m + 2 do
if zcross[i].is_hi == zcross[i + n].is_hi then
local len = zcross[i + n].pos - zcross[i].pos
local len_rnd = math.floor(len + 0.5)
-- Optimization: Instead of a simple count (+1), we weight the
-- count of zero crossings by using their related peak volume.
local zcross_peak = zcross[i].peak + zcross[i + n].peak
local weight = (bins[len_rnd] or 0) + zcross_peak
bins[len_rnd] = weight
if weight > max_weight then
max_len = len_rnd
max_weight = weight
end
end
end
end
-- Save the best candidate (highest count/weight) for each overlap iteration
if max_weight > 0 then
-- Optimization: Use parabolic interpolation to improve length precision
local prev = bins[max_len - 1] or 0
local next = bins[max_len + 1] or 0
local diff = (next - prev) / (max_weight + prev + next)
candidates[#candidates + 1] = {
len = max_len + diff,
weight = max_weight,
}
end
end
local max_weight = 0
local best_c
-- Find the best candidate
for c = 1, #candidates do
local curr_c = candidates[c]
for e = 1, c - 1 do
local prev_c = candidates[e]
local factor = prev_c.len / curr_c.len
local factor_rnd = math.floor(factor + 0.5)
local diff = math.abs(factor - factor_rnd)
-- Check if this candidate is a multiple of previous candidates
if factor > 0.5 and diff < 0.1 then
-- Add a portion of previous weight to this candidate
local prev_weight = prev_c.new_weight or prev_c.weight
curr_c.new_weight = curr_c.new_weight or curr_c.weight
curr_c.new_weight = curr_c.new_weight + prev_weight / factor_rnd
-- Optimization: Improve length (and frequency) accuracy
if not curr_c.diff or diff < curr_c.diff then
local prev_len = prev_c.new_len or prev_c.len
curr_c.new_len = prev_len / factor_rnd
end
end
end
-- The best candidate is the one with the highest (modified) count/weight
local curr_weight = curr_c.new_weight or curr_c.weight
if curr_weight > max_weight then
max_weight = curr_weight
best_c = curr_c
end
end
return best_c and (rate / (best_c.new_len or best_c.len))
end
function GetPitchFFT(file)
-- Get media source peaks
local src = reaper.PCM_Source_CreateFromFileEx(file, true)
if not src then return end
local src_len = reaper.GetMediaSourceLength(src)
if src_len == 0 then
reaper.PCM_Source_Destroy(src)
return
end
-- We find the highest peak and set our time window there
local time_window = math.min(src_len, algo_window)
local soffs = GetApproximatePeakTime(src, src_len)
soffs = soffs - time_window / 5
if soffs + time_window > src_len then
soffs = src_len - soffs
end
soffs = math.max(soffs, 0)
-- Limit length of analyzed sample
src_len = math.min(src_len, time_window)
local rate = reaper.GetMediaSourceSampleRate(src)
local spl_cnt = math.ceil(src_len * rate)
spl_cnt = math.min(spl_cnt, 2 ^ 19)
-- FFT window size has to be a power of 2
local window_size = 2 ^ 15
local buf = reaper.new_array(math.max(window_size, spl_cnt) * 2)
local ret = reaper.PCM_Source_GetPeaks(src, rate, soffs, 1, spl_cnt, 0, buf)
reaper.PCM_Source_Destroy(src)
if not ret then return end
spl_cnt = ret & 0xfffff
if spl_cnt == 0 then return end
-- Zero padding
buf.clear(0, spl_cnt - 1)
buf.fft(window_size, true)
buf = buf.table()
for i = 1, window_size do
local re = buf[i]
local im = buf[window_size * 2 - i]
buf[i] = re * re + im * im
end
local max_val = 0
local max_i
for i = 1, window_size do
local val = buf[i]
local val_abs = val < 0 and -val or val
if val_abs > max_val then
max_i = i
max_val = val_abs
end
end
if not max_i then return end
-- Look for fundamental harmonics
local limit = window_size // 500
for h = 7, 2, -1 do
local i = max_i // h
if max_i - i > limit and buf[i] ^ 0.5 > max_val ^ 0.5 * (0.05 * h) then
max_i = i
end
end
-- Use parabolic interpolation to improve precision
local prev = buf[max_i - 1]
local next = buf[max_i + 1]
if prev and next then
local diff = (next - prev) / (2 * (2 * max_val - prev - next))
max_i = max_i + diff
end
return max_i * rate / window_size / 4
end
function GetPitchFromFileName(file)
-- Parse note name from file name
local pattern_pre = '([%s_[(-.])'
local pattern_note = '([CDEFGAB][#b]?)'
local pattern_add = '(%d?m?[Mmdas]?[aiduM]?[jnmds]?o?r?%d*)'
local pattern_post = '([%s_[(-.])'
local pattern = pattern_pre .. pattern_note .. pattern_add .. pattern_post
local file_name = file:match('([^\\/]+)$')
local file_note
for pre, note, add, post in file_name:gmatch(pattern) do
-- Note: Avoid patterns like 'Color B 12'
-- if pre == ' ' and post == ' ' and add == '' then note = nil end
if not file_note then file_note = note end
-- Keep matches later in the item name (or longer e.g. with #)
if note and #note >= #file_note then file_note = note end
end
-- Check if note is at beginning of file
if not file_note then
pattern = '^' .. pattern_note .. pattern_add .. pattern_post
file_note = file_name:match(pattern)
end
return NameToFrequency(file_note)
end
function GetPitchFromMetadata(file)
local key = MediaExplorer_GetSelectedFileInfo(file, 12)
if key then
-- Remove any digits from key
key = key:gsub('%d', '')
return NameToFrequency(key)
end
end
function OpenWindow(is_docked)
local pos = reaper.GetExtState('FTC.MXTuner', 'pos')
if pos == '' then
-- Show script window in center of screen
local w, h = 406, 138
local x, y = reaper.GetMousePosition()
local l, t, r, b = reaper.my_getViewport(0, 0, 0, 0, x, y, x, y, 1)
gfx.init('MX Tuner', w, h, 0, (r + l - w) / 2, (b + t - h) / 2 - 24)
else
w_x, w_y, w_w, w_h = pos:match('(%-?%d+) (%-?%d+) (%-?%d+) (%-?%d+)')
-- Note: Matched type is string because of matching '-' for negative values
w_w, w_h = tonumber(w_w), tonumber(w_h)
w_x, w_y = tonumber(w_x), tonumber(w_y)
local dock = 0
if is_docked then
dock = tonumber(reaper.GetExtState('FTC.MXTuner', 'dock'))
end
gfx.init('MX Tuner', w_w, w_h, dock, w_x, w_y)
end
if focus_mode == 1 then
local mx_list_view = reaper.JS_Window_FindChildByID(mx, 1001)
reaper.JS_Window_SetFocus(mx_list_view)
end
end
function SetWindowFrame(has_frame)
local is_linux = reaper.GetOS():match('Other')
local hwnd = reaper.JS_Window_Find('MX Tuner', true)
if has_frame then
if is_linux then
local bar_h = reaper.GetExtState('FTC.MXTuner', 'bar_h')
if tonumber(bar_h) then
reaper.JS_Window_SetPosition(hwnd, w_x, w_y, w_w, w_h - bar_h)
end
end
reaper.JS_Window_SetStyle(hwnd, 'CAPTION,SIZEBOX,SYSMENU')
else
if is_linux then
-- Match behavior of other platforms (extend window to titlebar)
local _, s_y = gfx.clienttoscreen(0, 0)
local bar_h = s_y - w_y
reaper.SetExtState('FTC.MXTuner', 'bar_h', bar_h, true)
reaper.JS_Window_SetPosition(hwnd, w_x, w_y, w_w, w_h + bar_h)
end
reaper.JS_Window_SetStyle(hwnd, 'POPUP')
end
end
function SetWindowOnTop(is_ontop)
local hwnd = reaper.JS_Window_Find('MX Tuner', true)
local zorder = is_ontop and 'TOPMOST' or 'NOTOPMOST'
reaper.JS_Window_SetZOrder(hwnd, zorder)
end
function HexToNormRGB(color)
local r, g, b = reaper.ColorFromNative(color)
return {r / 255, g / 255, b / 255}
end
function OffsetColor(color, offs)
return {color[1] + offs, color[2] + offs, color[2] + offs}
end
function IsLightColor(color) return (color[1] + color[2] + color[3]) / 3 > 0.5 end
function GetColorLuminance(color)
local r, g, b = color[1], color[2], color[3]
return 0.2126 * r + 0.7152 * g + 0.0722 * b
end
function MatchColorLuminance(color, target_color)
local lum = GetColorLuminance(color)
local target_lum = GetColorLuminance(target_color)
-- Note: Only adjust the luminance by 1/3
local diff = (target_lum - lum) / 3
return {color[1] + diff, color[2] + diff, color[3] + diff}
end
function LoadTheme(id)
local is_docked = reaper.GetExtState('FTC.MXTuner', 'is_docked') == '1'
local toolbar_color = HexToNormRGB(reaper.GetThemeColor('col_main_bg2'))
theme.div_color = {0.08, 0.08, 0.08}
if is_docked and GetColorLuminance(toolbar_color) > 0.2 then
theme.div_color = toolbar_color or {0.08, 0.08, 0.08}
end
theme.button_color = {0.38, 0.51, 0.76}
theme.button_bypass_color = {0.5, 0.5, 0.5}
theme.button_text_color = {0.8, 0.8, 0.8}
if id == 1 then
-- Light theme
theme.nat_color = {0.82, 0.82, 0.82}
theme.nat_text_color = {0.14, 0.14, 0.14}
theme.nat_hover_color = {0.73, 0.73, 0.73}
theme.nat_sel_color = {0.61, 0.75, 0.38}
theme.nat_lock_color = {0.72, 0.44, 0.41}
theme.flat_color = {0.14, 0.14, 0.14}
theme.flat_text_color = {0.82, 0.82, 0.82}
theme.flat_hover_color = {0.21, 0.21, 0.21}
theme.flat_sel_color = {0.53, 0.67, 0.31}
theme.flat_lock_color = {0.65, 0.4, 0.37}
return
end
if id == 2 then
-- Dark theme
theme.nat_color = {0.14, 0.14, 0.14}
theme.nat_text_color = {0.82, 0.82, 0.82}
theme.nat_hover_color = {0.21, 0.21, 0.21}
theme.nat_sel_color = {0.45, 0.59, 0.23}
theme.nat_lock_color = {0.61, 0.36, 0.33}
theme.flat_color = {0.72, 0.72, 0.72}
theme.flat_text_color = {0.14, 0.14, 0.14}
theme.flat_hover_color = {0.71, 0.71, 0.71}
theme.flat_sel_color = {0.59, 0.73, 0.36}
theme.flat_lock_color = {0.72, 0.44, 0.41}
return
end
if id == 3 then
-- Reaper theme 1 : Uses piano key colors
local nat_color = HexToNormRGB(reaper.GetThemeColor('midi_pkey1'))
local flat_color = HexToNormRGB(reaper.GetThemeColor('midi_pkey2'))
local nat_hover_offs = IsLightColor(nat_color) and -0.1 or 0.1
local nat_hover_color = OffsetColor(nat_color, nat_hover_offs)
local flat_hover_offs = IsLightColor(flat_color) and -0.1 or 0.1
local flat_hover_color = OffsetColor(flat_color, flat_hover_offs)
local sel_color = {0.61, 0.75, 0.38}
local lock_color = {0.72, 0.44, 0.41}
theme.nat_color = nat_color
theme.nat_text_color = flat_color
theme.nat_hover_color = nat_hover_color
theme.nat_sel_color = MatchColorLuminance(sel_color, nat_color)
theme.nat_lock_color = MatchColorLuminance(lock_color, nat_color)
theme.flat_color = flat_color
theme.flat_text_color = nat_color
theme.flat_hover_color = flat_hover_color
theme.flat_sel_color = MatchColorLuminance(sel_color, flat_color)
theme.flat_lock_color = MatchColorLuminance(lock_color, flat_color)
return
end
if id == 4 then
-- Reaper theme 2 : Uses list view colors
local nat_color = HexToNormRGB(reaper.GetThemeColor('genlist_bg'))
local flat_color = HexToNormRGB(reaper.GetThemeColor('genlist_fg'))
local flat_hover_offs = IsLightColor(flat_color) and -0.1 or 0.1
local flat_hover_color = OffsetColor(flat_color, flat_hover_offs)
local nat_hover_offs = IsLightColor(nat_color) and -0.1 or 0.1
local nat_hover_color = OffsetColor(nat_color, nat_hover_offs)
local sel_color = {0.61, 0.75, 0.38}
local lock_color = {0.72, 0.44, 0.41}
theme.nat_color = nat_color
theme.nat_text_color = flat_color
theme.nat_hover_color = nat_hover_color
theme.nat_sel_color = MatchColorLuminance(sel_color, nat_color)
theme.nat_lock_color = MatchColorLuminance(lock_color, nat_color)
theme.flat_color = flat_color
theme.flat_text_color = nat_color
theme.flat_hover_color = flat_hover_color
theme.flat_sel_color = MatchColorLuminance(sel_color, flat_color)
theme.flat_lock_color = MatchColorLuminance(lock_color, flat_color)
return
end
end
function DrawPiano()
local keys = {}
local f_w = gfx.w // 7
local f_h = gfx.h
local m = 1
for i = 1, 7 do
keys[i] = {
title = flat[i],
x = (i - 1) * f_w + m,
y = m,
w = f_w - 2 * m,
h = f_h - 2 * m,
bg_color = theme.nat_color,
text_color = theme.nat_text_color,
hover_color = theme.nat_hover_color,
sel_color = theme.nat_sel_color,
lock_color = theme.nat_lock_color,
}
if i == 7 then
local rest = gfx.w - f_w * 7
keys[7].w = keys[7].w + rest
end
end
local s_w = math.floor(0.67 * f_w)
local s_h = math.floor((gfx.h < 70 and 0.57 or 0.67) * f_h)
local s_x = {
math.floor(1 * f_w - 0.75 * s_w),
math.floor(2 * f_w - 0.25 * s_w),
math.floor(4 * f_w - 0.75 * s_w),
math.floor(5 * f_w - 0.50 * s_w),
math.floor(6 * f_w - 0.25 * s_w),
}
for i = 1, 5 do
keys[7 + i] = {
title = sharp[i],
x = s_x[i],
y = m,
w = s_w,
h = s_h - 2 * m,
bg_color = theme.flat_color,
text_color = theme.flat_text_color,
hover_color = theme.flat_hover_color,
sel_color = theme.flat_sel_color,
lock_color = theme.flat_lock_color,
is_flat = true,
}
end
local m_x, m_y = gfx.mouse_x, gfx.mouse_y
hovered_key = nil
if gfx.mouse_cap & 1 == 0 then is_pressed = false end
local button_w = math.max(18, math.min(f_w // 4, gfx.h // 4))
if curr_parsing_mode > 0 and m_x <= button_w and m_y <= button_w then
if not is_pressed then
if gfx.mouse_cap & 17 == 17 then
MediaExplorer_SetMetaDataKey('')
is_pressed = true
elseif gfx.mouse_cap & 1 == 1 then
is_pressed = true
is_parsing_bypassed = not is_parsing_bypassed
trigger_pitch_rescan = true
end
end
end
for i = #keys, 1, -1 do
local key = keys[i]
local x, y, w, h = key.x, key.y, key.w, key.h
local is_hover = m_x >= x and m_x <= x + w and m_y >= y and m_y <= y + h
if key.title == sel_note_name and not locked_key then
key.bg_color = key.sel_color
end
if key.title == locked_key then key.bg_color = key.lock_color end
if not hovered_key and is_hover then
if not is_pressed then
if gfx.mouse_cap & 17 == 17 then
MediaExplorer_SetMetaDataKey(key.title)
is_pressed = true
elseif gfx.mouse_cap & 1 == 1 then
locked_key = key.title ~= locked_key and key.title or nil
if not locked_key then
OnUnlock()
else
OnLock()
end
trigger_pitch_rescan = true
key.bg_color = key.lock_color
is_pressed = true
end
end
hovered_key = key
if key.title ~= locked_key and key.title ~= sel_note_name then
key.bg_color = key.hover_color
end
end
end
-- Draw dividers for natural notes (window background)
gfx.set(table.unpack(theme.div_color))
gfx.rect(0, 0, gfx.w, gfx.h, 1)
local margin_bot = math.min(8, gfx.h // 18)
for _, key in ipairs(keys) do
-- Draw dividers for flat notes
if key.is_flat and theme.has_flat_divider then
gfx.set(table.unpack(theme.div_color))
gfx.rect(key.x - 1, key.y, key.w + 2, key.h + 1, 1)
end
-- Draw key background
gfx.set(table.unpack(key.bg_color))
gfx.rect(key.x, key.y, key.w, key.h, 1)
gfx.set(table.unpack(key.text_color))
if key.title == locked_key then
-- Draw lock
local size = 10
local half_size = size // 2
local x_center = key.x + (key.w + 1) // 2
local y_center = key.h - size - margin_bot + 1
gfx.rect(x_center - half_size, 3 + y_center - half_size, size,
size - 2, 1)
gfx.roundrect(x_center - half_size + 1, y_center - size + 3,
size - 3, size, size // 3.5, 1)
else
-- Draw text
gfx.setfont(1, '', 14, string.byte('b'))
local text_w, text_h = gfx.measurestr(key.title)
gfx.x = key.x + (key.w - text_w + 1) // 2
gfx.y = key.h - text_h - margin_bot
gfx.drawstr(key.title)
end
end
if curr_parsing_mode == 0 then
gfx.update()
return
end
-- Draw parse button inner border
gfx.set(table.unpack(theme.nat_color))
gfx.rect(0, 0, button_w, button_w, 1)
-- Draw parse button background
local button_bg_color = theme.button_color
if is_parsing_bypassed then button_bg_color = theme.button_bypass_color end
gfx.set(table.unpack(button_bg_color))
gfx.rect(2, 2, button_w - 4, button_w - 4, 1)
-- Draw parse button border
gfx.set(table.unpack(theme.flat_color))
gfx.rect(0, 0, button_w, button_w, 0)
-- Draw parse button text
gfx.set(table.unpack(theme.button_text_color))
local button_text = curr_parsing_mode == 1 and 'F' or 'M'
local text_w, text_h = gfx.measurestr(button_text)
gfx.x, gfx.y = (button_w - text_w) // 2, (button_w - text_h) // 2
gfx.drawstr(button_text)
gfx.update()
end
function DrawDetectedPitchInfo()
-- Set box dimensions
local box_width = 110
local box_height = 50
-- Calculate right aligned x position
local box_x = gfx.w - box_width
-- Draw gray box aligned to right side of window
gfx.set(0.3, 0.3, 0.3)
gfx.rect(box_x, 0, box_width, box_height, 1)
-- Add padding
local padding = 8
-- Draw key name
gfx.set(1, 1, 1)
gfx.setfont(1, '', 14, string.byte('b'))
local key_text = underlying_note_name and 'Key: ' .. underlying_note_name or ''
local text_w, text_h = gfx.measurestr(key_text)
gfx.x = box_x + padding
gfx.y = padding
gfx.drawstr(key_text)
-- Draw key source
gfx.set(1, 1, 1)
local source_text = detected_pitch_source and 'Source: ' .. detected_pitch_source or ''
local text_w, text_h = gfx.measurestr(source_text)
gfx.x = box_x + padding
gfx.y = padding + text_h + 4
gfx.drawstr(source_text)
end
function Main()
-- Ensure hwnd for MX is valid (changes when docked etc.)
if not reaper.ValidatePtr(mx, 'HWND') then
mx = reaper.JS_Window_FindTop(mx_title, true)
-- Exit script when media explorer is closed
if not mx then return end
end
local is_redraw = false
-- Monitor media explorer pitch and rate changes
local mx_pitch = MediaExplorer_GetPitch()
local mx_rate = MediaExplorer_GetRate()
local use_rate = not IsPitchPreservedWhenChangingRate()
local has_pitch_changed = mx_pitch ~= prev_mx_pitch
local has_rate_changed = mx_rate ~= prev_mx_rate
local has_rate_setting_changed = use_rate ~= prev_use_rate
if has_pitch_changed or has_rate_changed or has_rate_setting_changed then
prev_mx_pitch = mx_pitch
prev_mx_rate = mx_rate
prev_use_rate = use_rate
if prev_file_pitch then
-- Adjust displayed key when pitch has beend altered via knobs
local pitch_offs = mx_pitch
if use_rate then
pitch_offs = pitch_offs + 12 * math.log(mx_rate, 2)
end
sel_note_name = FrequencyToName(prev_file_pitch, pitch_offs)
if locked_key then locked_key = sel_note_name end
end
-- Redraw UI when pitch changes
is_redraw = true
end
-- Monitor media explorer file selection
local files = MediaExplorer_GetSelectedMediaFiles()
local new_file = files[1]
local has_file_changed = prev_file ~= new_file
if new_file and (has_file_changed or trigger_pitch_rescan) then
if has_file_changed then is_parsing_bypassed = false end
local file_pitch
-- Check metadata for pitch
if parse_meta_mode == 1 then
curr_parsing_mode = 2
file_pitch = GetPitchFromMetadata(new_file)
detected_pitch_source = 'Metadata'
end
-- Check file name for pitch
if not file_pitch and parse_name_mode == 1 then
curr_parsing_mode = 1
file_pitch = GetPitchFromFileName(new_file)
detected_pitch_source = 'Filename'
end
-- Use chosen pitch detection algorithm to find pitch
if not file_pitch or is_parsing_bypassed then
if not is_parsing_bypassed then curr_parsing_mode = 0 end
local ext = new_file:match('%.([^.]+)$')
if ext and ext:lower() == 'mid' then
-- Get pitch from MIDI file
local root_name = GetMIDIFileRootName(new_file)
file_pitch = NameToFrequency(root_name)
detected_pitch_source = 'MIDI'
else
-- Get pitch from audio file
if algo_mode == 1 then
file_pitch = GetPitchFTC(new_file)
end
if algo_mode == 2 then
file_pitch = GetPitchFFT(new_file)
end
detected_pitch_source = 'Algorithm'
end
end
prev_file_pitch = file_pitch
if file_pitch then
local pitch_offs = mx_pitch
if use_rate then
pitch_offs = pitch_offs + 12 * math.log(mx_rate, 2)
end
sel_note_name = FrequencyToName(file_pitch, pitch_offs)
underlying_note_name = FrequencyToName(file_pitch)
end
if file_pitch and locked_key then
local locked_freq = NameToFrequency(locked_key)
if locked_freq then
-- Account for offset in pitch that can be caused by rate knob
local offs = 0
if rate_mode == 1 then offs = -mx_pitch end
if rate_mode == 0 and use_rate then
offs = -12 * math.log(mx_rate, 2)
end
local dist = GetMinSemitonesTo(file_pitch, locked_freq, offs)
-- Round distance to semitones depending on pitch mode
if pitch_mode == 2 then
dist = math.floor(2 * dist + 0.5) / 2
end
if pitch_mode == 3 then
dist = math.floor(dist + 0.5)
end
if rate_mode == 1 then
TurnOffPreservePitchOption()
MediaExplorer_SetRate(2 ^ (dist / 12))
else
MediaExplorer_SetPitch(dist)
end
end
end
-- Redraw UI when file changes
is_redraw = true
trigger_pitch_rescan = false
end
if not new_file then sel_note_name = nil end
prev_file = new_file
-- Monitor changes to window dock state
local dock, x, y, w, h = gfx.dock(-1, 0, 0, 0, 0)
if prev_dock ~= dock then
prev_dock = dock
reaper.SetExtState('FTC.MXTuner', 'is_docked', dock & 1, true)
if dock & 1 == 1 then
reaper.SetExtState('FTC.MXTuner', 'dock', dock, true)
else
SetWindowFrame(frameless_mode == 0)
SetWindowOnTop(ontop_mode == 1)
end
-- Note: Reload theme here to change divider color
LoadTheme(theme_id)
end
-- Monitor changes to window position
if not (x == w_x and y == w_y and w == w_w and h == w_h) then
w_x, w_y, w_w, w_h = x, y, w, h
local pos = ('%d %d %d %d'):format(x, y, w, h)
reaper.SetExtState('FTC.MXTuner', 'pos', pos, true)
end
-- Redraw UI when mouse_cap changes
if prev_mouse_cap ~= gfx.mouse_cap then
prev_mouse_cap = gfx.mouse_cap
is_redraw = true
end
-- Redraw UI when window size changes
if prev_w ~= gfx.w or prev_h ~= gfx.h then
prev_w = gfx.w
prev_h = gfx.h
is_redraw = true
end
-- Redraw UI when mouse moves inside script window
local m_x, m_y = gfx.mouse_x, gfx.mouse_y
if m_x ~= prev_mouse_x or m_y ~= prev_mouse_y then
prev_mouse_x = m_x
prev_mouse_y = m_y
if m_x >= 0 and m_x <= gfx.w and m_y >= 0 and m_y <= gfx.h then
is_redraw = true
is_window_hover = true
elseif is_window_hover then
is_redraw = true
is_window_hover = false
end
end
-- Redraw UI and reload theme when active theme changes
local color_theme = reaper.GetLastColorThemeFile()
if color_theme ~= prev_color_theme then
prev_color_theme = color_theme
LoadTheme(theme_id)
is_redraw = true
end
if is_redraw then
DrawPiano()
DrawDetectedPitchInfo()
end
-- Open settings menu on right click
if gfx.mouse_cap & 2 == 2 then
local menu =
'>Window|%sDock window|%sHide frame|%sAlways on top|<%sAvoid focus\z
|>Pitch snap|%sContinuous|%sQuarter tones|%sSemitones||<%sTune with \z
rate|>Algorithm|%sFFT|%sFTC (deprecated)||<Set analysis time window|>Parsing|%sUse metadata tag \'key\'|\z
<%sSearch filename for key|>Theme|%sLight|%sDark|%sReaper 1|<%sReaper 2'
local is_docked = dock & 1 == 1
local menu_dock_state = is_docked and '!' or ''
local menu_frameless = frameless_mode == 1 and '!' or ''
local menu_focus = focus_mode == 1 and '!' or ''
local menu_ontop = ontop_mode == 1 and '!' or ''
local menu_pitch_continuous = pitch_mode == 1 and '!' or ''
local menu_pitch_quarter = pitch_mode == 2 and '!' or ''
local menu_pitch_semitones = pitch_mode == 3 and '!' or ''
local menu_rate = rate_mode == 1 and '!' or ''
local menu_algo_fft = algo_mode == 2 and '!' or ''
local menu_algo_ftc = algo_mode == 1 and '!' or ''
local menu_parse_meta = parse_meta_mode == 1 and '!' or ''
local menu_parse_name = parse_name_mode == 1 and '!' or ''
local menu_theme1 = theme_id == 1 and '!' or ''
local menu_theme2 = theme_id == 2 and '!' or ''
local menu_theme3 = theme_id == 3 and '!' or ''
local menu_theme4 = theme_id == 4 and '!' or ''
menu = menu:format(menu_dock_state, menu_frameless, menu_ontop,
menu_focus, menu_pitch_continuous,
menu_pitch_quarter, menu_pitch_semitones, menu_rate,
menu_algo_fft, menu_algo_ftc, menu_parse_meta,
menu_parse_name, menu_theme1, menu_theme2,
menu_theme3, menu_theme4)
gfx.x, gfx.y = m_x, m_y
local ret = gfx.showmenu(menu)
if ret == 1 then
if is_docked then
-- Undock window
gfx.dock(0)
else
-- Dock window to last known position
local last_dock = reaper.GetExtState('FTC.MXTuner', 'dock')
last_dock = tonumber(last_dock) or 256
gfx.dock(last_dock | 1)
end
end
if ret == 2 then
frameless_mode = 1 - frameless_mode
if not is_docked then SetWindowFrame(frameless_mode == 0) end
reaper.SetExtState('FTC.MXTuner', 'has_frame', frameless_mode, true)
end
if ret == 3 then
ontop_mode = 1 - ontop_mode
if not is_docked then SetWindowOnTop(ontop_mode == 1) end
reaper.SetExtState('FTC.MXTuner', 'is_ontop', ontop_mode, true)
end
if ret == 4 then
focus_mode = 1 - focus_mode
reaper.SetExtState('FTC.MXTuner', 'avoid_focus', focus_mode, true)
end
if ret == 5 then pitch_mode = 1 end
if ret == 6 then pitch_mode = 2 end
if ret == 7 then pitch_mode = 3 end
if ret == 8 then
rate_mode = 1 - rate_mode
reaper.SetExtState('FTC.MXTuner', 'rate_mode', rate_mode, true)
end
if ret == 9 then algo_mode = 2 end
if ret == 10 then algo_mode = 1 end
if ret == 11 then
local title = 'Analysis time window '
local caption = 'Time window in seconds (def: 1)'
local input = tostring(algo_window)
local _, user_text = reaper.GetUserInputs(title, 1, caption, input)
if tonumber(user_text) then
algo_window = tonumber(user_text)
reaper.SetExtState('FTC.MXTuner', 'algo_window', algo_window, 1)
end
end
if ret == 12 then parse_meta_mode = 1 - parse_meta_mode end
if ret == 13 then parse_name_mode = 1 - parse_name_mode end
-- Retrigger pitch detection when detection type changes
if ret >= 9 and ret <= 13 then trigger_pitch_rescan = true end
if ret >= 14 and ret <= 18 then
theme_id = ret - 13
LoadTheme(theme_id)
reaper.SetExtState('FTC.MXTuner', 'theme_id', theme_id, true)
end
reaper.SetExtState('FTC.MXTuner', 'pitch_mode', pitch_mode, true)
reaper.SetExtState('FTC.MXTuner', 'algo_mode', algo_mode, true)
reaper.SetExtState('FTC.MXTuner', 'meta_mode', parse_meta_mode, true)
reaper.SetExtState('FTC.MXTuner', 'name_mode', parse_name_mode, true)
end
if gfx.getchar(65536) & 2 == 2 and focus_mode == 1 then
local mx_list_view = reaper.JS_Window_FindChildByID(mx, 1001)
reaper.JS_Window_SetFocus(mx_list_view)
end
reaper.defer(Main)
end
function OnLock()
-- Turn off option to reset pitch when changing media
if reaper.GetToggleCommandStateEx(32063, 42014) == 1 then
-- Options: Reset pitch and rate when changing media
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42014, 0, 0, 0)
is_option_bypassed = true
end
end
function OnUnlock()
if rate_mode == 1 then
MediaExplorer_SetRate(1)
else
MediaExplorer_SetPitch(0)
end
-- Turn option back on to reset pitch when changing media
if is_option_bypassed then
-- Options: Reset pitch and rate when changing media
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42014, 0, 0, 0)
is_option_bypassed = false
end
end
function RefreshMXToolbar()
-- Toggle any option to refresh MX toolbar
reaper.JS_Window_OnCommand(mx, 42171)
reaper.JS_Window_OnCommand(mx, 42171)
end
function Exit()
-- Turn toolbar icon off
reaper.SetToggleCommandState(sec, cmd, 0)
reaper.RefreshToolbar2(sec, cmd)
RefreshMXToolbar()
-- Turn option back on to reset pitch when changing media
if is_option_bypassed then
-- Options: Reset pitch and rate when changing media
reaper.JS_WindowMessage_Send(mx, 'WM_COMMAND', 42014, 0, 0, 0)
end
gfx.quit()
end
theme_id = tonumber(reaper.GetExtState('FTC.MXTuner', 'theme_id')) or 1
LoadTheme(theme_id)
focus_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'avoid_focus')) or 1
local is_docked = reaper.GetExtState('FTC.MXTuner', 'is_docked') == '1'
OpenWindow(is_docked)
frameless_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'has_frame')) or 0
if not is_docked and frameless_mode == 1 then
local hwnd = reaper.JS_Window_Find('MX Tuner', true)
reaper.JS_Window_SetStyle(hwnd, 'POPUP')
end
ontop_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'is_ontop')) or 0
if not is_docked and ontop_mode == 1 then
local hwnd = reaper.JS_Window_Find('MX Tuner', true)
reaper.JS_Window_SetZOrder(hwnd, 'TOPMOST')
end
rate_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'rate_mode')) or 0
pitch_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'pitch_mode')) or 1
algo_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'algo_mode')) or 2
algo_window = tonumber(reaper.GetExtState('FTC.MXTuner', 'algo_window')) or 1
parse_meta_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'meta_mode')) or 1
parse_name_mode = tonumber(reaper.GetExtState('FTC.MXTuner', 'name_mode')) or 1
-- Turn toolbar icon on
reaper.SetToggleCommandState(sec, cmd, 1)
reaper.RefreshToolbar2(sec, cmd)
RefreshMXToolbar()
Main()
reaper.atexit(Exit)
-- Show a one time notice to users if they are using deprecated FTC algorithm
if reaper.GetExtState('FTC.MXTuner', 'show_deprecated') == '' then
reaper.SetExtState('FTC.MXTuner', 'show_deprecated', '1', true)
if algo_mode == 1 then
local msg = 'You are using the FTC algorithm which is now \z
deprecated.\n\nSwitch to the new and improved FFT algorithm?'
local ret = reaper.MB(msg, 'Notice', 3)
if ret == 6 then
algo_mode = 2
reaper.SetExtState('FTC.MXTuner', 'algo_mode', algo_mode, 1)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment