Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1e5b3344a | |||
| 32af9fe29b | |||
| 282517f2d4 | |||
| 2062be4c5b | |||
| 74e91c6da1 | |||
| acc770233f | |||
| 80cfdff8f6 | |||
| 634ebd62c4 | |||
| 00e05690ec | |||
| 2031057559 | |||
| c3c4488cdc | |||
| a1e45d9ed5 | |||
| 343f892029 | |||
| f0996e4fce | |||
| 5cd1a221bb | |||
| e92321ada1 | |||
| 99d3881e88 | |||
| 504bdd7473 |
@@ -27,6 +27,17 @@ RJ.prevCooldownUntil = 0 -- timestamp until which Previous is blocked (1s cool
|
|||||||
RJ.stopCooldownUntil = 0 -- timestamp until which Stop is blocked (1s cooldown)
|
RJ.stopCooldownUntil = 0 -- timestamp until which Stop is blocked (1s cooldown)
|
||||||
local COOLDOWN_SECS = 1 -- universal cooldown for all playback buttons
|
local COOLDOWN_SECS = 1 -- universal cooldown for all playback buttons
|
||||||
|
|
||||||
|
-- Marquee (scrolling text) state for mini player
|
||||||
|
RJ.marqueeOffset = 0 -- current pixel offset for scrolling
|
||||||
|
RJ.marqueeSpeed = 25 -- pixels per second
|
||||||
|
RJ.marqueeGap = 0 -- legacy gap value; smooth looping now uses a text separator instead
|
||||||
|
RJ.marqueeSeparator = " • "
|
||||||
|
RJ.marqueeCycleWidth = 0 -- width of one full repeated cycle
|
||||||
|
RJ.marqueeLastUpdate = 0 -- last OnUpdate timestamp
|
||||||
|
RJ.marqueeFullText = "" -- the repeated display text used for looping
|
||||||
|
RJ.marqueeSingleWidth = 0 -- pixel width of the single-copy display text
|
||||||
|
RJ.marqueeSingleText = "" -- the single-copy display text (before duplication)
|
||||||
|
|
||||||
-- Polling state: when a track has no duration metadata, we poll IsPlaying
|
-- Polling state: when a track has no duration metadata, we poll IsPlaying
|
||||||
-- to detect when it finishes, so autoplay/shuffle can advance.
|
-- to detect when it finishes, so autoplay/shuffle can advance.
|
||||||
RJ.pollingForEnd = false
|
RJ.pollingForEnd = false
|
||||||
@@ -364,7 +375,7 @@ local function ResumePlayback()
|
|||||||
RJ.isPaused = false
|
RJ.isPaused = false
|
||||||
|
|
||||||
if RJ.currentPath then
|
if RJ.currentPath then
|
||||||
if IsMp3Path and IsMp3Path(RJ.currentPath) and PlayMusic then
|
if IsMusicPath(RJ.currentPath) and PlayMusic then
|
||||||
PlayMusic(RJ.currentPath)
|
PlayMusic(RJ.currentPath)
|
||||||
RJ.currentCanStop = true
|
RJ.currentCanStop = true
|
||||||
elseif PlaySoundFile then
|
elseif PlaySoundFile then
|
||||||
@@ -564,6 +575,22 @@ end
|
|||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
-- Track playback core
|
-- Track playback core
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
|
-- Check whether a file path should be played via PlayMusic (WoW 1.12 FMOD 3.x
|
||||||
|
-- requires PlayMusic for .mp3, .wav AND .ogg — PlaySoundFile cannot play any
|
||||||
|
-- of these formats). Only files NOT matching these extensions should use
|
||||||
|
-- PlaySoundFile (e.g. custom SoundKit IDs or Blizzard sound paths).
|
||||||
|
--
|
||||||
|
-- Uses separate string.find calls per extension instead of a regex-style
|
||||||
|
-- alternation (|) because Lua patterns are NOT regular expressions — the
|
||||||
|
-- pipe character has no special meaning in Lua patterns.
|
||||||
|
local function IsMusicPath(path)
|
||||||
|
local lower = string.lower(path or "")
|
||||||
|
return string.find(lower, "%.mp3$") ~= nil
|
||||||
|
or string.find(lower, "%.wav$") ~= nil
|
||||||
|
or string.find(lower, "%.ogg$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Legacy alias kept for clarity in comments / status messages
|
||||||
local function IsMp3Path(path)
|
local function IsMp3Path(path)
|
||||||
return string.find(string.lower(path or ""), "%.mp3$") ~= nil
|
return string.find(string.lower(path or ""), "%.mp3$") ~= nil
|
||||||
end
|
end
|
||||||
@@ -631,7 +658,10 @@ PlayTrack = function(indexOrEntry, fromQueue)
|
|||||||
RJ.consecutiveNotPlaying = 0
|
RJ.consecutiveNotPlaying = 0
|
||||||
|
|
||||||
-- Play the track
|
-- Play the track
|
||||||
if IsMp3Path(path) and PlayMusic then
|
-- WoW 1.12 FMOD 3.x: PlayMusic supports .mp3, .wav and .ogg.
|
||||||
|
-- PlaySoundFile does NOT play .wav or .ogg (it silently fails),
|
||||||
|
-- so all three formats must go through PlayMusic.
|
||||||
|
if IsMusicPath(path) and PlayMusic then
|
||||||
PlayMusic(path)
|
PlayMusic(path)
|
||||||
ok = 1
|
ok = 1
|
||||||
RJ.currentCanStop = true
|
RJ.currentCanStop = true
|
||||||
@@ -662,7 +692,7 @@ PlayTrack = function(indexOrEntry, fromQueue)
|
|||||||
RJ.trackEndTime = GetTime() + duration
|
RJ.trackEndTime = GetTime() + duration
|
||||||
SetStatus("Playing " .. posLabel .. ": " .. name .. " (" .. duration .. "s)", 0.2, 1, 0.2)
|
SetStatus("Playing " .. posLabel .. ": " .. name .. " (" .. duration .. "s)", 0.2, 1, 0.2)
|
||||||
elseif RJ.currentCanStop and IsPlaying then
|
elseif RJ.currentCanStop and IsPlaying then
|
||||||
-- MP3 with no duration: poll IsPlaying() to detect end
|
-- PlayMusic track with no duration: poll IsPlaying() to detect end
|
||||||
RJ.trackEndTime = nil
|
RJ.trackEndTime = nil
|
||||||
RJ.pollingForEnd = true
|
RJ.pollingForEnd = true
|
||||||
RJ.pollTimer = GetTime() + RJ.pollStartDelay
|
RJ.pollTimer = GetTime() + RJ.pollStartDelay
|
||||||
@@ -876,21 +906,51 @@ end
|
|||||||
local function CreateRow(parent, i)
|
local function CreateRow(parent, i)
|
||||||
local row = CreateFrame("Frame", nil, parent)
|
local row = CreateFrame("Frame", nil, parent)
|
||||||
row:SetWidth(680)
|
row:SetWidth(680)
|
||||||
row:SetHeight(24)
|
row:SetHeight(27)
|
||||||
|
|
||||||
if row.SetBackdrop then
|
-- Give every track its own visible container so each song reads like a
|
||||||
row:SetBackdrop({
|
-- separate boxed row instead of one flat list.
|
||||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
--
|
||||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
-- Some 1.12-based clients are inconsistent about drawing SetBackdrop()
|
||||||
tile = true, tileSize = 8, edgeSize = 8,
|
-- borders on anonymous child frames. Use explicit textures instead so the
|
||||||
insets = { left = 2, right = 2, top = 2, bottom = 2 }
|
-- row box always appears on Turtle WoW / OctoWoW.
|
||||||
})
|
row.bg = row:CreateTexture(nil, "BACKGROUND")
|
||||||
row:SetBackdropColor(0, 0, 0, 0.35)
|
row.bg:SetPoint("TOPLEFT", row, "TOPLEFT", 1, -1)
|
||||||
|
row.bg:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -1, 1)
|
||||||
|
|
||||||
|
if math.mod(i, 2) == 1 then
|
||||||
|
row.bg:SetTexture(0.10, 0.10, 0.10, 0.82)
|
||||||
|
else
|
||||||
|
row.bg:SetTexture(0.06, 0.06, 0.06, 0.78)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
row.borderTop = row:CreateTexture(nil, "BORDER")
|
||||||
|
row.borderTop:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||||
|
row.borderTop:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0)
|
||||||
|
row.borderTop:SetHeight(1)
|
||||||
|
row.borderTop:SetTexture(0.45, 0.45, 0.45, 0.95)
|
||||||
|
|
||||||
|
row.borderBottom = row:CreateTexture(nil, "BORDER")
|
||||||
|
row.borderBottom:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
|
||||||
|
row.borderBottom:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||||
|
row.borderBottom:SetHeight(1)
|
||||||
|
row.borderBottom:SetTexture(0.45, 0.45, 0.45, 0.95)
|
||||||
|
|
||||||
|
row.borderLeft = row:CreateTexture(nil, "BORDER")
|
||||||
|
row.borderLeft:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
|
||||||
|
row.borderLeft:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
|
||||||
|
row.borderLeft:SetWidth(1)
|
||||||
|
row.borderLeft:SetTexture(0.45, 0.45, 0.45, 0.95)
|
||||||
|
|
||||||
|
row.borderRight = row:CreateTexture(nil, "BORDER")
|
||||||
|
row.borderRight:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0)
|
||||||
|
row.borderRight:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
|
||||||
|
row.borderRight:SetWidth(1)
|
||||||
|
row.borderRight:SetTexture(0.45, 0.45, 0.45, 0.95)
|
||||||
|
|
||||||
row.label = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
row.label = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||||
row.label:SetPoint("LEFT", row, "LEFT", 8, 0)
|
row.label:SetPoint("LEFT", row, "LEFT", 10, 0)
|
||||||
row.label:SetWidth(500)
|
row.label:SetWidth(496)
|
||||||
row.label:SetJustifyH("LEFT")
|
row.label:SetJustifyH("LEFT")
|
||||||
row.label:SetText("")
|
row.label:SetText("")
|
||||||
|
|
||||||
@@ -932,6 +992,7 @@ local function CreateUI()
|
|||||||
tile = true, tileSize = 32, edgeSize = 32,
|
tile = true, tileSize = 32, edgeSize = 32,
|
||||||
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
||||||
})
|
})
|
||||||
|
f:SetBackdropColor(0, 0, 0, 0.85)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Title bar
|
-- Title bar
|
||||||
@@ -985,9 +1046,9 @@ local function CreateUI()
|
|||||||
for i = 1, RJ.pageSize do
|
for i = 1, RJ.pageSize do
|
||||||
local row = CreateRow(listAnchor, i)
|
local row = CreateRow(listAnchor, i)
|
||||||
if i == 1 then
|
if i == 1 then
|
||||||
row:SetPoint("TOPLEFT", listAnchor, "TOPLEFT", 0, 0)
|
row:SetPoint("TOPLEFT", listAnchor, "TOPLEFT", 0, 1)
|
||||||
else
|
else
|
||||||
row:SetPoint("TOPLEFT", RJ.rows[i-1], "BOTTOMLEFT", 0, -4)
|
row:SetPoint("TOPLEFT", RJ.rows[i-1], "BOTTOMLEFT", 0, -1)
|
||||||
end
|
end
|
||||||
RJ.rows[i] = row
|
RJ.rows[i] = row
|
||||||
end
|
end
|
||||||
@@ -1068,15 +1129,29 @@ function RefreshMiniPlayer()
|
|||||||
local isActive = RJ.currentPath ~= nil
|
local isActive = RJ.currentPath ~= nil
|
||||||
local now = GetTime()
|
local now = GetTime()
|
||||||
|
|
||||||
-- Track name label (shows pause indicator in text since no pause button)
|
-- Build the display text for the mini player. The ScrollFrame marquee
|
||||||
|
-- will lay it out on the next frame; here we only reset state when the
|
||||||
|
-- visible text actually changes.
|
||||||
|
local newText
|
||||||
if RJ.currentName and RJ.currentName ~= "" then
|
if RJ.currentName and RJ.currentName ~= "" then
|
||||||
if RJ.isPaused then
|
if RJ.isPaused then
|
||||||
mf.trackLabel:SetText("|| " .. RJ.currentName)
|
newText = "|| " .. RJ.currentName
|
||||||
else
|
else
|
||||||
mf.trackLabel:SetText("> " .. RJ.currentName)
|
newText = RJ.currentName
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
mf.trackLabel:SetText("Relationships Jukebox")
|
newText = "Relationships Jukebox"
|
||||||
|
end
|
||||||
|
|
||||||
|
if RJ.marqueeSingleText ~= newText then
|
||||||
|
if mf.trackLabel then
|
||||||
|
mf.trackLabel:SetText(newText)
|
||||||
|
end
|
||||||
|
RJ.marqueeOffset = 0
|
||||||
|
RJ.marqueeLastUpdate = 0
|
||||||
|
RJ.marqueeCycleWidth = 0
|
||||||
|
RJ.marqueeSingleText = ""
|
||||||
|
RJ.marqueeSingleWidth = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Enable/disable buttons with 1-second cooldown checks
|
-- Enable/disable buttons with 1-second cooldown checks
|
||||||
@@ -1109,7 +1184,7 @@ local function CreateMiniPlayer()
|
|||||||
|
|
||||||
local mf = CreateFrame("Frame", ADDON_NAME .. "MiniFrame", UIParent)
|
local mf = CreateFrame("Frame", ADDON_NAME .. "MiniFrame", UIParent)
|
||||||
RJ.miniFrame = mf
|
RJ.miniFrame = mf
|
||||||
mf:SetWidth(176)
|
mf:SetWidth(216)
|
||||||
mf:SetHeight(56)
|
mf:SetHeight(56)
|
||||||
mf:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -60, -200)
|
mf:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -60, -200)
|
||||||
mf:SetMovable(true)
|
mf:SetMovable(true)
|
||||||
@@ -1118,6 +1193,9 @@ local function CreateMiniPlayer()
|
|||||||
mf:SetScript("OnDragStart", function() mf:StartMoving() end)
|
mf:SetScript("OnDragStart", function() mf:StartMoving() end)
|
||||||
mf:SetScript("OnDragStop", function() mf:StopMovingOrSizing() end)
|
mf:SetScript("OnDragStop", function() mf:StopMovingOrSizing() end)
|
||||||
mf:SetClampedToScreen(true)
|
mf:SetClampedToScreen(true)
|
||||||
|
mf:SetScript("OnShow", function()
|
||||||
|
RJ.marqueeLastUpdate = 0
|
||||||
|
end)
|
||||||
|
|
||||||
if mf.SetBackdrop then
|
if mf.SetBackdrop then
|
||||||
mf:SetBackdrop({
|
mf:SetBackdrop({
|
||||||
@@ -1129,25 +1207,150 @@ local function CreateMiniPlayer()
|
|||||||
mf:SetBackdropColor(0, 0, 0, 0.7)
|
mf:SetBackdropColor(0, 0, 0, 0.7)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Track name (top row)
|
-- Track name (top row) - marquee scrolling text
|
||||||
mf.trackLabel = mf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
-- Use a ScrollFrame viewport plus repeated labels so the text scrolls
|
||||||
mf.trackLabel:SetPoint("TOPLEFT", mf, "TOPLEFT", 8, -6)
|
-- smoothly at the pixel level without re-slicing strings every frame.
|
||||||
mf.trackLabel:SetPoint("RIGHT", mf, "RIGHT", -24, 0)
|
mf.trackClip = CreateFrame("ScrollFrame", nil, mf)
|
||||||
mf.trackLabel:SetJustifyH("LEFT")
|
mf.trackClip:SetPoint("TOPLEFT", mf, "TOPLEFT", 8, -9)
|
||||||
mf.trackLabel:SetTextColor(0.4, 1, 0.4)
|
mf.trackClip:SetPoint("BOTTOMRIGHT", mf, "TOPRIGHT", -8, -27)
|
||||||
mf.trackLabel:SetText("Relationships Jukebox")
|
mf.trackClip:SetHorizontalScroll(0)
|
||||||
|
|
||||||
-- Minimize button (top-right corner)
|
mf.trackCanvas = CreateFrame("Frame", nil, mf.trackClip)
|
||||||
mf.minBtn = MakeButton(nil, mf, 20, 18, "_")
|
mf.trackCanvas:SetWidth(1)
|
||||||
mf.minBtn:SetPoint("TOPRIGHT", mf, "TOPRIGHT", -4, -2)
|
mf.trackCanvas:SetHeight(14)
|
||||||
mf.minBtn:SetScript("OnClick", function()
|
mf.trackClip:SetScrollChild(mf.trackCanvas)
|
||||||
mf:Hide()
|
|
||||||
if RJ.minimapButton then
|
mf.trackLabels = {}
|
||||||
RJ.minimapButton:Show()
|
for labelIndex = 1, 3 do
|
||||||
|
local label = mf.trackCanvas:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||||
|
label:SetJustifyH("LEFT")
|
||||||
|
label:SetTextColor(0.4, 1, 0.4)
|
||||||
|
label:SetText("Relationships Jukebox")
|
||||||
|
mf.trackLabels[labelIndex] = label
|
||||||
|
end
|
||||||
|
|
||||||
|
mf.trackLabel = mf.trackLabels[1]
|
||||||
|
|
||||||
|
local function PositionMiniMarquee(offset)
|
||||||
|
mf.trackCanvas:ClearAllPoints()
|
||||||
|
|
||||||
|
if RJ.marqueeCycleWidth > 0 then
|
||||||
|
-- Keep the middle copy inside the viewport and slide the whole strip to
|
||||||
|
-- the left. Wrapping happens by reusing the previous/next copies, so the
|
||||||
|
-- text never disappears into a blank gap between loops.
|
||||||
|
mf.trackCanvas:SetPoint("TOPLEFT", mf.trackClip, "TOPLEFT", -RJ.marqueeCycleWidth - offset, 0)
|
||||||
|
else
|
||||||
|
mf.trackCanvas:SetPoint("TOPLEFT", mf.trackClip, "TOPLEFT", 0, 0)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function LayoutMiniMarquee(singleText)
|
||||||
|
if not singleText or singleText == "" then
|
||||||
|
singleText = "Relationships Jukebox"
|
||||||
|
end
|
||||||
|
|
||||||
|
local viewWidth = mf.trackClip:GetWidth()
|
||||||
|
if viewWidth <= 0 then return end
|
||||||
|
|
||||||
|
mf.trackLabels[1]:SetText(singleText)
|
||||||
|
|
||||||
|
local singleWidth
|
||||||
|
if mf.trackLabels[1].GetStringWidth then
|
||||||
|
singleWidth = mf.trackLabels[1]:GetStringWidth()
|
||||||
|
else
|
||||||
|
singleWidth = string.len(singleText) * 7
|
||||||
|
end
|
||||||
|
|
||||||
|
RJ.marqueeSingleText = singleText
|
||||||
|
RJ.marqueeSingleWidth = singleWidth
|
||||||
|
|
||||||
|
if singleWidth <= viewWidth or singleWidth == 0 then
|
||||||
|
RJ.marqueeCycleWidth = 0
|
||||||
|
RJ.marqueeOffset = 0
|
||||||
|
RJ.marqueeFullText = singleText
|
||||||
|
|
||||||
|
mf.trackLabels[1]:ClearAllPoints()
|
||||||
|
mf.trackLabels[1]:SetPoint("TOPLEFT", mf.trackCanvas, "TOPLEFT", 0, 0)
|
||||||
|
mf.trackLabels[1]:SetText(singleText)
|
||||||
|
mf.trackLabels[2]:SetText("")
|
||||||
|
mf.trackLabels[3]:SetText("")
|
||||||
|
|
||||||
|
mf.trackCanvas:SetWidth(math.max(viewWidth, singleWidth))
|
||||||
|
PositionMiniMarquee(0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
RJ.marqueeFullText = singleText .. (RJ.marqueeSeparator or " ")
|
||||||
|
for labelIndex = 1, 3 do
|
||||||
|
mf.trackLabels[labelIndex]:SetText(RJ.marqueeFullText)
|
||||||
|
end
|
||||||
|
|
||||||
|
if mf.trackLabels[1].GetStringWidth then
|
||||||
|
RJ.marqueeCycleWidth = mf.trackLabels[1]:GetStringWidth()
|
||||||
|
else
|
||||||
|
RJ.marqueeCycleWidth = string.len(RJ.marqueeFullText) * 7
|
||||||
|
end
|
||||||
|
|
||||||
|
for labelIndex = 1, 3 do
|
||||||
|
mf.trackLabels[labelIndex]:ClearAllPoints()
|
||||||
|
mf.trackLabels[labelIndex]:SetPoint("TOPLEFT", mf.trackCanvas, "TOPLEFT", (labelIndex - 1) * RJ.marqueeCycleWidth, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
mf.trackCanvas:SetWidth(RJ.marqueeCycleWidth * 3)
|
||||||
|
PositionMiniMarquee(RJ.marqueeOffset)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Smooth marquee update fully clipped to the mini player bounds.
|
||||||
|
mf:SetScript("OnUpdate", function()
|
||||||
|
local singleText
|
||||||
|
if RJ.currentName and RJ.currentName ~= "" then
|
||||||
|
if RJ.isPaused then
|
||||||
|
singleText = "|| " .. RJ.currentName
|
||||||
|
else
|
||||||
|
singleText = RJ.currentName
|
||||||
|
end
|
||||||
|
else
|
||||||
|
singleText = "Relationships Jukebox"
|
||||||
|
end
|
||||||
|
|
||||||
|
if RJ.marqueeSingleText ~= singleText then
|
||||||
|
RJ.marqueeOffset = 0
|
||||||
|
RJ.marqueeLastUpdate = 0
|
||||||
|
LayoutMiniMarquee(singleText)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not RJ.currentName or RJ.currentName == "" then
|
||||||
|
RJ.marqueeOffset = 0
|
||||||
|
RJ.marqueeLastUpdate = 0
|
||||||
|
PositionMiniMarquee(0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if RJ.marqueeCycleWidth <= 0 then
|
||||||
|
PositionMiniMarquee(0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local now = GetTime()
|
||||||
|
if RJ.marqueeLastUpdate == 0 then
|
||||||
|
RJ.marqueeLastUpdate = now
|
||||||
|
PositionMiniMarquee(RJ.marqueeOffset)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = now - RJ.marqueeLastUpdate
|
||||||
|
RJ.marqueeLastUpdate = now
|
||||||
|
if elapsed > 0.5 then elapsed = 0.5 end
|
||||||
|
|
||||||
|
RJ.marqueeOffset = RJ.marqueeOffset + (RJ.marqueeSpeed * elapsed)
|
||||||
|
while RJ.marqueeOffset >= RJ.marqueeCycleWidth do
|
||||||
|
RJ.marqueeOffset = RJ.marqueeOffset - RJ.marqueeCycleWidth
|
||||||
|
end
|
||||||
|
|
||||||
|
PositionMiniMarquee(RJ.marqueeOffset)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Control buttons (bottom row)
|
-- Control buttons (bottom row): <<, Stop, >>, +, -
|
||||||
mf.prevBtn = MakeButton(nil, mf, 40, 20, "<<")
|
mf.prevBtn = MakeButton(nil, mf, 40, 20, "<<")
|
||||||
mf.prevBtn:SetPoint("BOTTOMLEFT", mf, "BOTTOMLEFT", 6, 6)
|
mf.prevBtn:SetPoint("BOTTOMLEFT", mf, "BOTTOMLEFT", 6, 6)
|
||||||
mf.prevBtn:SetScript("OnClick", PreviousTrack)
|
mf.prevBtn:SetScript("OnClick", PreviousTrack)
|
||||||
@@ -1163,7 +1366,7 @@ local function CreateMiniPlayer()
|
|||||||
mf.skipBtn:SetScript("OnClick", SkipTrack)
|
mf.skipBtn:SetScript("OnClick", SkipTrack)
|
||||||
mf.skipBtn:Disable()
|
mf.skipBtn:Disable()
|
||||||
|
|
||||||
-- Open full GUI button (far right)
|
-- Open full GUI button
|
||||||
mf.openBtn = MakeButton(nil, mf, 28, 20, "+")
|
mf.openBtn = MakeButton(nil, mf, 28, 20, "+")
|
||||||
mf.openBtn:SetPoint("LEFT", mf.skipBtn, "RIGHT", 4, 0)
|
mf.openBtn:SetPoint("LEFT", mf.skipBtn, "RIGHT", 4, 0)
|
||||||
mf.openBtn:SetScript("OnClick", function()
|
mf.openBtn:SetScript("OnClick", function()
|
||||||
@@ -1173,11 +1376,22 @@ local function CreateMiniPlayer()
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- Minimize button (bottom row, after + button)
|
||||||
|
mf.minBtn = MakeButton(nil, mf, 28, 20, "-")
|
||||||
|
mf.minBtn:SetPoint("LEFT", mf.openBtn, "RIGHT", 4, 0)
|
||||||
|
mf.minBtn:SetScript("OnClick", function()
|
||||||
|
mf:Hide()
|
||||||
|
if RJ.minimapButton then
|
||||||
|
RJ.minimapButton:Show()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
mf:Show()
|
mf:Show()
|
||||||
|
-- Reset marquee timer when mini player first appears
|
||||||
|
RJ.marqueeLastUpdate = 0
|
||||||
RefreshMiniPlayer()
|
RefreshMiniPlayer()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
-- Minimap button
|
-- Minimap button
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
@@ -1266,7 +1480,7 @@ ticker:SetScript("OnUpdate", function()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 2) IsPlaying polling: detect when MP3 finishes for tracks with no duration
|
-- 2) IsPlaying polling: detect when PlayMusic track finishes for tracks with no duration
|
||||||
if RJ.autoPlayEnabled and not RJ.isPaused and RJ.pollingForEnd then
|
if RJ.autoPlayEnabled and not RJ.isPaused and RJ.pollingForEnd then
|
||||||
if GetTime() < RJ.pollTimer then
|
if GetTime() < RJ.pollTimer then
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1259,6 +1259,341 @@ if ($sanitized -eq 0 -and $skipped -eq 0 -and $mp3Files.Count -gt 0) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# -- Sanitize WAV/OGG files for WoW 1.12 compatibility -----------------------
|
||||||
|
# WoW 1.12's FMOD 3.x engine is very picky about WAV and OGG files too.
|
||||||
|
# Many WAV/OGG files that play fine in modern players will silently fail in-game.
|
||||||
|
#
|
||||||
|
# Known WAV issues:
|
||||||
|
# 1. 24-bit or 32-bit float samples (FMOD 3.x only supports 8-bit and 16-bit PCM)
|
||||||
|
# 2. Sample rates other than 44100 Hz or 22050 Hz
|
||||||
|
# 3. More than 2 channels (surround sound WAVs)
|
||||||
|
# 4. ADPCM or other compressed WAV formats
|
||||||
|
# 5. RIFF LIST/INFO chunks that can confuse FMOD
|
||||||
|
# 6. Very large WAV files that exceed FMOD's buffer
|
||||||
|
# 7. IEEE float format (format code 3) instead of PCM (format code 1)
|
||||||
|
#
|
||||||
|
# Known OGG issues:
|
||||||
|
# 1. Vorbis encoding at unusual bitrates or quality levels
|
||||||
|
# 2. OGG files with video streams or metadata that FMOD cannot parse
|
||||||
|
# 3. OGG files encoded with very high quality settings (-q10) that FMOD 3.x rejects
|
||||||
|
# 4. Non-standard channel configurations
|
||||||
|
# 5. Opus codec inside an OGG container (FMOD 3.x only supports Vorbis in OGG)
|
||||||
|
#
|
||||||
|
# The ONLY reliable fix is to re-encode with ffmpeg using settings known to
|
||||||
|
# produce FMOD 3.x-compatible output. WAV is re-encoded to 16-bit PCM at
|
||||||
|
# 44100 Hz stereo; OGG is re-encoded to Vorbis at quality 6 (~192kbps).
|
||||||
|
|
||||||
|
function Test-WavNeedsReencode {
|
||||||
|
param([string]$FilePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ffmpegExe = Find-Ffmpeg -Name 'ffprobe'
|
||||||
|
if ($ffmpegExe) {
|
||||||
|
$ffOutFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
$ffErrFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
$ffProc = Start-Process -FilePath $ffmpegExe -ArgumentList @('-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', $FilePath) -NoNewWindow -Wait -PassThru -RedirectStandardOutput $ffOutFile -RedirectStandardError $ffErrFile
|
||||||
|
Remove-Item -LiteralPath $ffErrFile -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ffProc.ExitCode -eq 0 -and (Test-Path -LiteralPath $ffOutFile -PathType Leaf)) {
|
||||||
|
$ffOut = Get-Content -LiteralPath $ffOutFile -Raw
|
||||||
|
Remove-Item -LiteralPath $ffOutFile -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ffOut) {
|
||||||
|
$ffJson = $ffOut | ConvertFrom-Json
|
||||||
|
foreach ($stream in $ffJson.streams) {
|
||||||
|
if ($stream.codec_type -eq 'audio') {
|
||||||
|
# Check sample format - must be s16 (signed 16-bit PCM)
|
||||||
|
if ($stream.sample_rate -and [int]$stream.sample_rate -ne 44100 -and [int]$stream.sample_rate -ne 22050) {
|
||||||
|
return $true, "Sample rate $($stream.sample_rate) Hz (FMOD 3.x requires 44100 or 22050 Hz)"
|
||||||
|
}
|
||||||
|
if ($stream.channels -and [int]$stream.channels -gt 2) {
|
||||||
|
return $true, "$($stream.channels) channels (FMOD 3.x supports max 2 channels)"
|
||||||
|
}
|
||||||
|
# Check codec - must be PCM (not ADPCM, float, etc.)
|
||||||
|
if ($stream.codec_name -and $stream.codec_name -ne 'pcm_s16le' -and $stream.codec_name -ne 'pcm_s16be' -and $stream.codec_name -ne 'pcm_u8' -and $stream.codec_name -ne 'pcm_s24le' -and $stream.codec_name -ne 'pcm_f32le' -and $stream.codec_name -ne 'adpcm_ms' -and $stream.codec_name -ne 'adpcm_ima_wav') {
|
||||||
|
# Not a standard PCM codec
|
||||||
|
} elseif ($stream.codec_name -eq 'pcm_s24le') {
|
||||||
|
return $true, '24-bit PCM (FMOD 3.x only supports 8-bit and 16-bit)'
|
||||||
|
} elseif ($stream.codec_name -eq 'pcm_f32le' -or $stream.codec_name -eq 'pcm_f64le') {
|
||||||
|
return $true, 'Float PCM format (FMOD 3.x only supports integer PCM)'
|
||||||
|
} elseif ($stream.codec_name -match 'adpcm') {
|
||||||
|
return $true, 'ADPCM compressed WAV (FMOD 3.x may not support this codec)'
|
||||||
|
}
|
||||||
|
# Check bit depth via bits_per_sample if available
|
||||||
|
if ($stream.bits_per_sample -and [int]$stream.bits_per_sample -gt 16 -and [int]$stream.bits_per_sample -ne 0) {
|
||||||
|
return $true, "$($stream.bits_per_sample)-bit audio (FMOD 3.x only supports 8-bit and 16-bit)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Remove-Item -LiteralPath $ffOutFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback: check WAV header bytes manually
|
||||||
|
$bytes = [System.IO.File]::ReadAllBytes($FilePath)
|
||||||
|
if ($bytes.Length -lt 44) { return $false, 'File too small' }
|
||||||
|
|
||||||
|
# Check RIFF header
|
||||||
|
if ($bytes[0] -ne 0x52 -or $bytes[1] -ne 0x49 -or $bytes[2] -ne 0x46 -or $bytes[3] -ne 0x46) {
|
||||||
|
return $false, 'Not a valid RIFF/WAV file'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find the 'fmt ' chunk
|
||||||
|
$fmtOffset = -1
|
||||||
|
for ($i = 12; $i -lt [Math]::Min($bytes.Length - 24, 4096); $i++) {
|
||||||
|
if ($bytes[$i] -eq 0x66 -and $bytes[$i+1] -eq 0x6D -and $bytes[$i+2] -eq 0x74 -and $bytes[$i+3] -eq 0x20) {
|
||||||
|
$fmtOffset = $i + 8 # Skip past 'fmt ' and chunk size
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fmtOffset -ge 0 -and $fmtOffset + 16 -le $bytes.Length) {
|
||||||
|
# Parse format fields (little-endian)
|
||||||
|
$audioFormat = $bytes[$fmtOffset] -bor ($bytes[$fmtOffset+1] -shl 8)
|
||||||
|
$numChannels = $bytes[$fmtOffset+2] -bor ($bytes[$fmtOffset+3] -shl 8)
|
||||||
|
$sampleRate = $bytes[$fmtOffset+4] -bor ($bytes[$fmtOffset+5] -shl 8) -bor ($bytes[$fmtOffset+6] -shl 16) -bor ($bytes[$fmtOffset+7] -shl 24)
|
||||||
|
$bitsPerSample = $bytes[$fmtOffset+14] -bor ($bytes[$fmtOffset+15] -shl 8)
|
||||||
|
|
||||||
|
if ($audioFormat -ne 1) {
|
||||||
|
return $true, "Non-PCM format code $audioFormat (FMOD 3.x only supports PCM format code 1)"
|
||||||
|
}
|
||||||
|
if ($bitsPerSample -gt 16) {
|
||||||
|
return $true, "$bitsPerSample-bit audio (FMOD 3.x only supports 8-bit and 16-bit)"
|
||||||
|
}
|
||||||
|
if ($numChannels -gt 2) {
|
||||||
|
return $true, "$numChannels channels (FMOD 3.x supports max 2 channels)"
|
||||||
|
}
|
||||||
|
if ($sampleRate -ne 44100 -and $sampleRate -ne 22050 -and $sampleRate -ne 11025) {
|
||||||
|
return $true, "Sample rate $sampleRate Hz (FMOD 3.x requires 11025/22050/44100 Hz)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false, 'Compatible'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $true, "Error analyzing file: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-OggNeedsReencode {
|
||||||
|
param([string]$FilePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ffmpegExe = Find-Ffmpeg -Name 'ffprobe'
|
||||||
|
if ($ffmpegExe) {
|
||||||
|
$ffOutFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
$ffErrFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
$ffProc = Start-Process -FilePath $ffmpegExe -ArgumentList @('-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', $FilePath) -NoNewWindow -Wait -PassThru -RedirectStandardOutput $ffOutFile -RedirectStandardError $ffErrFile
|
||||||
|
Remove-Item -LiteralPath $ffErrFile -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ffProc.ExitCode -eq 0 -and (Test-Path -LiteralPath $ffOutFile -PathType Leaf)) {
|
||||||
|
$ffOut = Get-Content -LiteralPath $ffOutFile -Raw
|
||||||
|
Remove-Item -LiteralPath $ffOutFile -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ffOut) {
|
||||||
|
$ffJson = $ffOut | ConvertFrom-Json
|
||||||
|
foreach ($stream in $ffJson.streams) {
|
||||||
|
if ($stream.codec_type -eq 'audio') {
|
||||||
|
# OGG must use Vorbis codec (not Opus, FLAC, etc.)
|
||||||
|
if ($stream.codec_name -and $stream.codec_name -ne 'vorbis') {
|
||||||
|
return $true, "Non-Vorbis codec '$($stream.codec_name)' in OGG container (FMOD 3.x only supports Vorbis)"
|
||||||
|
}
|
||||||
|
if ($stream.channels -and [int]$stream.channels -gt 2) {
|
||||||
|
return $true, "$($stream.channels) channels (FMOD 3.x supports max 2 channels)"
|
||||||
|
}
|
||||||
|
if ($stream.sample_rate -and [int]$stream.sample_rate -ne 44100 -and [int]$stream.sample_rate -ne 22050) {
|
||||||
|
return $true, "Sample rate $($stream.sample_rate) Hz (FMOD 3.x prefers 44100 or 22050 Hz)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($stream.codec_type -eq 'video') {
|
||||||
|
return $true, "Embedded video stream in OGG (FMOD 3.x cannot handle)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Remove-Item -LiteralPath $ffOutFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false, 'Compatible'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $true, "Error analyzing file: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sanitize-WavFile {
|
||||||
|
param([System.IO.FileInfo]$File)
|
||||||
|
|
||||||
|
$tempFile = $File.FullName + '.sanitize.tmp'
|
||||||
|
|
||||||
|
# Re-encode with ffmpeg: 16-bit PCM, 44100 Hz, stereo, strip metadata
|
||||||
|
$ffmpegExe = Find-Ffmpeg -Name 'ffmpeg'
|
||||||
|
try {
|
||||||
|
if ($ffmpegExe) {
|
||||||
|
$stderrFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
$proc = Start-Process -FilePath $ffmpegExe -ArgumentList @('-y', '-i', $File.FullName, '-vn', '-c:a', 'pcm_s16le', '-ac', '2', '-ar', '44100', '-map_metadata', '-1', $tempFile) -NoNewWindow -Wait -PassThru -RedirectStandardError $stderrFile
|
||||||
|
Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($proc.ExitCode -ne 0) {
|
||||||
|
Write-Host " ffmpeg exited with code $($proc.ExitCode) for $($File.Name), trying alternate settings..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " ffmpeg re-encode failed for $($File.Name): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-Path -LiteralPath $tempFile -PathType Leaf) -and ((Get-Item -LiteralPath $tempFile).Length -gt 0)) {
|
||||||
|
Move-Item -LiteralPath $tempFile -Destination $File.FullName -Force
|
||||||
|
Write-Host " Sanitized (re-encoded to 16-bit PCM 44100Hz stereo for WoW 1.12): $($File.Name)"
|
||||||
|
return $true
|
||||||
|
} elseif (Test-Path -LiteralPath $tempFile) {
|
||||||
|
Remove-Item -LiteralPath $tempFile -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sanitize-OggFile {
|
||||||
|
param([System.IO.FileInfo]$File)
|
||||||
|
|
||||||
|
$tempFile = $File.FullName + '.sanitize.tmp'
|
||||||
|
|
||||||
|
# Re-encode with ffmpeg: Vorbis codec, quality 6 (~192kbps), 44100 Hz, stereo
|
||||||
|
$ffmpegExe = Find-Ffmpeg -Name 'ffmpeg'
|
||||||
|
try {
|
||||||
|
if ($ffmpegExe) {
|
||||||
|
$stderrFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
$proc = Start-Process -FilePath $ffmpegExe -ArgumentList @('-y', '-i', $File.FullName, '-vn', '-c:a', 'libvorbis', '-q:a', '6', '-ac', '2', '-ar', '44100', '-map_metadata', '-1', $tempFile) -NoNewWindow -Wait -PassThru -RedirectStandardError $stderrFile
|
||||||
|
Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($proc.ExitCode -ne 0) {
|
||||||
|
Write-Host " ffmpeg exited with code $($proc.ExitCode) for $($File.Name), trying alternate settings..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " ffmpeg re-encode failed for $($File.Name): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-Path -LiteralPath $tempFile -PathType Leaf) -and ((Get-Item -LiteralPath $tempFile).Length -gt 0)) {
|
||||||
|
Move-Item -LiteralPath $tempFile -Destination $File.FullName -Force
|
||||||
|
Write-Host " Sanitized (re-encoded to Vorbis q6 44100Hz stereo for WoW 1.12): $($File.Name)"
|
||||||
|
return $true
|
||||||
|
} elseif (Test-Path -LiteralPath $tempFile) {
|
||||||
|
Remove-Item -LiteralPath $tempFile -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and sanitize WAV files for WoW 1.12 compatibility
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Scanning WAV files for WoW 1.12 compatibility..."
|
||||||
|
$wavFiles = @(Get-ChildItem -LiteralPath $Media -Recurse -File |
|
||||||
|
Where-Object { $_.Extension -match '^(?i)\.wav$' })
|
||||||
|
|
||||||
|
$wavSanitized = 0
|
||||||
|
$wavSkipped = 0
|
||||||
|
$wavCompatOk = 0
|
||||||
|
|
||||||
|
if ($wavFiles.Count -eq 0) {
|
||||||
|
Write-Host "No WAV files found in Media folder."
|
||||||
|
} else {
|
||||||
|
Write-Host "Found $($wavFiles.Count) WAV file(s) to check..."
|
||||||
|
foreach ($wav in $wavFiles) {
|
||||||
|
try {
|
||||||
|
$needsReencode, $reason = Test-WavNeedsReencode -FilePath $wav.FullName
|
||||||
|
if ($needsReencode) {
|
||||||
|
Write-Host " Issue found: $($wav.Name) - $reason"
|
||||||
|
try {
|
||||||
|
$fixed = Sanitize-WavFile -File $wav
|
||||||
|
if ($fixed) { $wavSanitized++ } else { $wavSkipped++ }
|
||||||
|
} catch {
|
||||||
|
Write-Host " WARNING: Could not sanitize $($wav.Name): $($_.Exception.Message)"
|
||||||
|
$wavSkipped++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$wavCompatOk++
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " WARNING: Could not scan $($wav.Name): $($_.Exception.Message)"
|
||||||
|
$wavSkipped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " WAV Compatible: $wavCompatOk Sanitized: $wavSanitized Skipped: $wavSkipped"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($wavSanitized -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Sanitized $wavSanitized WAV file(s) for WoW 1.12 compatibility."
|
||||||
|
Write-Host "These files were re-encoded to 16-bit PCM 44100Hz stereo."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
if ($wavSkipped -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WARNING: $wavSkipped WAV file(s) could not be sanitized (ffmpeg not available?)."
|
||||||
|
Write-Host "These tracks may not play correctly in-game. Install ffmpeg and re-run."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
if ($wavSanitized -eq 0 -and $wavSkipped -eq 0 -and $wavFiles.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "All $($wavFiles.Count) WAV file(s) passed compatibility check."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and sanitize OGG files for WoW 1.12 compatibility
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Scanning OGG files for WoW 1.12 compatibility..."
|
||||||
|
$oggFiles = @(Get-ChildItem -LiteralPath $Media -Recurse -File |
|
||||||
|
Where-Object { $_.Extension -match '^(?i)\.ogg$' })
|
||||||
|
|
||||||
|
$oggSanitized = 0
|
||||||
|
$oggSkipped = 0
|
||||||
|
$oggCompatOk = 0
|
||||||
|
|
||||||
|
if ($oggFiles.Count -eq 0) {
|
||||||
|
Write-Host "No OGG files found in Media folder."
|
||||||
|
} else {
|
||||||
|
Write-Host "Found $($oggFiles.Count) OGG file(s) to check..."
|
||||||
|
foreach ($ogg in $oggFiles) {
|
||||||
|
try {
|
||||||
|
$needsReencode, $reason = Test-OggNeedsReencode -FilePath $ogg.FullName
|
||||||
|
if ($needsReencode) {
|
||||||
|
Write-Host " Issue found: $($ogg.Name) - $reason"
|
||||||
|
try {
|
||||||
|
$fixed = Sanitize-OggFile -File $ogg
|
||||||
|
if ($fixed) { $oggSanitized++ } else { $oggSkipped++ }
|
||||||
|
} catch {
|
||||||
|
Write-Host " WARNING: Could not sanitize $($ogg.Name): $($_.Exception.Message)"
|
||||||
|
$oggSkipped++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$oggCompatOk++
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " WARNING: Could not scan $($ogg.Name): $($_.Exception.Message)"
|
||||||
|
$oggSkipped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " OGG Compatible: $oggCompatOk Sanitized: $oggSanitized Skipped: $oggSkipped"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($oggSanitized -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Sanitized $oggSanitized OGG file(s) for WoW 1.12 compatibility."
|
||||||
|
Write-Host "These files were re-encoded to Vorbis q6 44100Hz stereo."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
if ($oggSkipped -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WARNING: $oggSkipped OGG file(s) could not be sanitized (ffmpeg not available?)."
|
||||||
|
Write-Host "These tracks may not play correctly in-game. Install ffmpeg and re-run."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
if ($oggSanitized -eq 0 -and $oggSkipped -eq 0 -and $oggFiles.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "All $($oggFiles.Count) OGG file(s) passed compatibility check."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
# -- Rename files with special characters for WoW 1.12 compatibility ----------
|
# -- Rename files with special characters for WoW 1.12 compatibility ----------
|
||||||
# WoW 1.12's FMOD engine may fail to play files whose names contain characters
|
# WoW 1.12's FMOD engine may fail to play files whose names contain characters
|
||||||
# like apostrophes ('), parentheses, or other special symbols. This step
|
# like apostrophes ('), parentheses, or other special symbols. This step
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
<img src="https://cdn.discordapp.com/attachments/1510333697277300856/1523253951594627112/I.png?ex=6a4b704a&is=6a4a1eca&hm=4b4a2c04ae8702c5ff92cc50296ceb65d4c5c13f1174fc767fd7f3c4351337fc&"/>
|
||||||
|
|
||||||
|
<img src="https://cdn.discordapp.com/attachments/1510333697277300856/1523253988269756616/ezgif-1f660e370df27e35.gif?ex=6a4b7053&is=6a4a1ed3&hm=0835c0247c885d0fb6d25b86064a792989ab1d6a8f1c91570b30e7936fc7bd16&"/>
|
||||||
|
|
||||||
|
|
||||||
# Relationships Jukebox
|
# Relationships Jukebox
|
||||||
|
|
||||||
**Relationships Jukebox** is a lightweight World of Warcraft music addon that lets you play your own local audio files in-game through a full playlist window, a compact mini player, and simple slash commands.
|
**Relationships Jukebox** is a lightweight World of Warcraft music addon that lets you play your own local audio files in-game through a full playlist window, a compact mini player, and simple slash commands.
|
||||||
@@ -16,9 +21,8 @@ The addon scans a local `Media` folder, builds a playlist automatically, and let
|
|||||||
- **Right-click:** Toggle Full GUI
|
- **Right-click:** Toggle Full GUI
|
||||||
- Automatically imports tracks from `AutoPlaylist.lua`
|
- Automatically imports tracks from `AutoPlaylist.lua`
|
||||||
- Stores track names and durations for smoother autoplay and shuffle playback
|
- Stores track names and durations for smoother autoplay and shuffle playback
|
||||||
- Attempts to improve **WoW 1.12 MP3 compatibility** by sanitizing problematic MP3 files during playlist generation
|
- Attempts to improve MP3, WAV, and OGG compatibility by sanitizing problematic files during playlist generation
|
||||||
- Works even without metadata by falling back to estimated durations
|
- Works without metadata by falling back to estimated durations
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -69,8 +73,8 @@ AutoPlaylist.lua
|
|||||||
- creates or updates `AutoPlaylist.lua`
|
- creates or updates `AutoPlaylist.lua`
|
||||||
- captures track names
|
- captures track names
|
||||||
- tries to determine durations
|
- tries to determine durations
|
||||||
- checks MP3 files for WoW 1.12 compatibility issues
|
- checks MP3, WAV, and OGG files for WoW 1.12 compatibility issues
|
||||||
- optionally uses `ffmpeg` / `ffprobe` for better metadata and MP3 sanitization
|
- optionally uses `ffmpeg` / `ffprobe` for better metadata and MP3, WAV, and OGG sanitization
|
||||||
|
|
||||||
After generating the playlist, use `/reload` so the addon can import the updated track list.
|
After generating the playlist, use `/reload` so the addon can import the updated track list.
|
||||||
|
|
||||||
@@ -171,9 +175,9 @@ Important: if the audio file still exists in the `Media` folder and is still inc
|
|||||||
- Make sure `AutoPlaylist.lua` was created
|
- Make sure `AutoPlaylist.lua` was created
|
||||||
- Use `/reload`
|
- Use `/reload`
|
||||||
|
|
||||||
### Some MP3s do not play
|
### Some MP3, WAV, and OGG files do not play
|
||||||
|
|
||||||
WoW 1.12 can be picky about MP3 encoding. Re-run the generator and let it sanitize files if `ffmpeg` is available.
|
WoW 1.12 can be picky about MP3, WAV, and OGG encoding. Re-run the generator and let it sanitize files if `ffmpeg` is available.
|
||||||
|
|
||||||
### Durations seem wrong
|
### Durations seem wrong
|
||||||
|
|
||||||
@@ -195,5 +199,3 @@ Install one of the following:
|
|||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Author: **Relationship**
|
Author: **Relationship**
|
||||||
|
|
||||||
If you package releases, consider including this README alongside the addon so users know to place tracks in `Media`, run the generator, and reload the UI.
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
## Interface: 11200
|
## Interface: 11200
|
||||||
## Title: Relationships Jukebox
|
## Title: Relationships Jukebox
|
||||||
## Notes: Simple in-game MP3/WAV player with GUI.
|
## Notes: Simple in-game MP3/WAV/OGG player with GUI.
|
||||||
## Author: Relationship
|
## Author: Relationship
|
||||||
|
## Version: 1.2
|
||||||
## SavedVariables: RelationshipsJukeboxDB
|
## SavedVariables: RelationshipsJukeboxDB
|
||||||
|
## X-Website: https://github.com/Relationship-OctoWoW/RelationshipsJukeBox
|
||||||
|
## X-GitHub: https://github.com/Relationship-OctoWoW/RelationshipsJukeBox
|
||||||
AutoPlaylist.lua
|
AutoPlaylist.lua
|
||||||
Core.lua
|
Core.lua
|
||||||
|
|||||||
Reference in New Issue
Block a user