Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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)
|
||||
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
|
||||
-- to detect when it finishes, so autoplay/shuffle can advance.
|
||||
RJ.pollingForEnd = false
|
||||
@@ -364,7 +375,7 @@ local function ResumePlayback()
|
||||
RJ.isPaused = false
|
||||
|
||||
if RJ.currentPath then
|
||||
if IsMp3Path and IsMp3Path(RJ.currentPath) and PlayMusic then
|
||||
if IsMusicPath(RJ.currentPath) and PlayMusic then
|
||||
PlayMusic(RJ.currentPath)
|
||||
RJ.currentCanStop = true
|
||||
elseif PlaySoundFile then
|
||||
@@ -564,6 +575,22 @@ end
|
||||
-----------------------------------------------------------------------
|
||||
-- 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)
|
||||
return string.find(string.lower(path or ""), "%.mp3$") ~= nil
|
||||
end
|
||||
@@ -631,7 +658,10 @@ PlayTrack = function(indexOrEntry, fromQueue)
|
||||
RJ.consecutiveNotPlaying = 0
|
||||
|
||||
-- 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)
|
||||
ok = 1
|
||||
RJ.currentCanStop = true
|
||||
@@ -662,7 +692,7 @@ PlayTrack = function(indexOrEntry, fromQueue)
|
||||
RJ.trackEndTime = GetTime() + duration
|
||||
SetStatus("Playing " .. posLabel .. ": " .. name .. " (" .. duration .. "s)", 0.2, 1, 0.2)
|
||||
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.pollingForEnd = true
|
||||
RJ.pollTimer = GetTime() + RJ.pollStartDelay
|
||||
@@ -876,21 +906,51 @@ end
|
||||
local function CreateRow(parent, i)
|
||||
local row = CreateFrame("Frame", nil, parent)
|
||||
row:SetWidth(680)
|
||||
row:SetHeight(24)
|
||||
row:SetHeight(27)
|
||||
|
||||
if row.SetBackdrop then
|
||||
row:SetBackdrop({
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 8, edgeSize = 8,
|
||||
insets = { left = 2, right = 2, top = 2, bottom = 2 }
|
||||
})
|
||||
row:SetBackdropColor(0, 0, 0, 0.35)
|
||||
-- Give every track its own visible container so each song reads like a
|
||||
-- separate boxed row instead of one flat list.
|
||||
--
|
||||
-- Some 1.12-based clients are inconsistent about drawing SetBackdrop()
|
||||
-- borders on anonymous child frames. Use explicit textures instead so the
|
||||
-- row box always appears on Turtle WoW / OctoWoW.
|
||||
row.bg = row:CreateTexture(nil, "BACKGROUND")
|
||||
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
|
||||
|
||||
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:SetPoint("LEFT", row, "LEFT", 8, 0)
|
||||
row.label:SetWidth(500)
|
||||
row.label:SetPoint("LEFT", row, "LEFT", 10, 0)
|
||||
row.label:SetWidth(496)
|
||||
row.label:SetJustifyH("LEFT")
|
||||
row.label:SetText("")
|
||||
|
||||
@@ -932,6 +992,7 @@ local function CreateUI()
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
||||
})
|
||||
f:SetBackdropColor(0, 0, 0, 0.85)
|
||||
end
|
||||
|
||||
-- Title bar
|
||||
@@ -985,9 +1046,9 @@ local function CreateUI()
|
||||
for i = 1, RJ.pageSize do
|
||||
local row = CreateRow(listAnchor, i)
|
||||
if i == 1 then
|
||||
row:SetPoint("TOPLEFT", listAnchor, "TOPLEFT", 0, 0)
|
||||
row:SetPoint("TOPLEFT", listAnchor, "TOPLEFT", 0, 1)
|
||||
else
|
||||
row:SetPoint("TOPLEFT", RJ.rows[i-1], "BOTTOMLEFT", 0, -4)
|
||||
row:SetPoint("TOPLEFT", RJ.rows[i-1], "BOTTOMLEFT", 0, -1)
|
||||
end
|
||||
RJ.rows[i] = row
|
||||
end
|
||||
@@ -1068,15 +1129,29 @@ function RefreshMiniPlayer()
|
||||
local isActive = RJ.currentPath ~= nil
|
||||
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.isPaused then
|
||||
mf.trackLabel:SetText("|| " .. RJ.currentName)
|
||||
newText = "|| " .. RJ.currentName
|
||||
else
|
||||
mf.trackLabel:SetText("> " .. RJ.currentName)
|
||||
newText = RJ.currentName
|
||||
end
|
||||
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
|
||||
|
||||
-- Enable/disable buttons with 1-second cooldown checks
|
||||
@@ -1109,7 +1184,7 @@ local function CreateMiniPlayer()
|
||||
|
||||
local mf = CreateFrame("Frame", ADDON_NAME .. "MiniFrame", UIParent)
|
||||
RJ.miniFrame = mf
|
||||
mf:SetWidth(176)
|
||||
mf:SetWidth(216)
|
||||
mf:SetHeight(56)
|
||||
mf:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -60, -200)
|
||||
mf:SetMovable(true)
|
||||
@@ -1118,6 +1193,9 @@ local function CreateMiniPlayer()
|
||||
mf:SetScript("OnDragStart", function() mf:StartMoving() end)
|
||||
mf:SetScript("OnDragStop", function() mf:StopMovingOrSizing() end)
|
||||
mf:SetClampedToScreen(true)
|
||||
mf:SetScript("OnShow", function()
|
||||
RJ.marqueeLastUpdate = 0
|
||||
end)
|
||||
|
||||
if mf.SetBackdrop then
|
||||
mf:SetBackdrop({
|
||||
@@ -1129,25 +1207,150 @@ local function CreateMiniPlayer()
|
||||
mf:SetBackdropColor(0, 0, 0, 0.7)
|
||||
end
|
||||
|
||||
-- Track name (top row)
|
||||
mf.trackLabel = mf:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
||||
mf.trackLabel:SetPoint("TOPLEFT", mf, "TOPLEFT", 8, -6)
|
||||
mf.trackLabel:SetPoint("RIGHT", mf, "RIGHT", -24, 0)
|
||||
mf.trackLabel:SetJustifyH("LEFT")
|
||||
mf.trackLabel:SetTextColor(0.4, 1, 0.4)
|
||||
mf.trackLabel:SetText("Relationships Jukebox")
|
||||
-- Track name (top row) - marquee scrolling text
|
||||
-- Use a ScrollFrame viewport plus repeated labels so the text scrolls
|
||||
-- smoothly at the pixel level without re-slicing strings every frame.
|
||||
mf.trackClip = CreateFrame("ScrollFrame", nil, mf)
|
||||
mf.trackClip:SetPoint("TOPLEFT", mf, "TOPLEFT", 8, -9)
|
||||
mf.trackClip:SetPoint("BOTTOMRIGHT", mf, "TOPRIGHT", -8, -27)
|
||||
mf.trackClip:SetHorizontalScroll(0)
|
||||
|
||||
-- Minimize button (top-right corner)
|
||||
mf.minBtn = MakeButton(nil, mf, 20, 18, "_")
|
||||
mf.minBtn:SetPoint("TOPRIGHT", mf, "TOPRIGHT", -4, -2)
|
||||
mf.minBtn:SetScript("OnClick", function()
|
||||
mf:Hide()
|
||||
if RJ.minimapButton then
|
||||
RJ.minimapButton:Show()
|
||||
mf.trackCanvas = CreateFrame("Frame", nil, mf.trackClip)
|
||||
mf.trackCanvas:SetWidth(1)
|
||||
mf.trackCanvas:SetHeight(14)
|
||||
mf.trackClip:SetScrollChild(mf.trackCanvas)
|
||||
|
||||
mf.trackLabels = {}
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
-- Control buttons (bottom row)
|
||||
-- Control buttons (bottom row): <<, Stop, >>, +, -
|
||||
mf.prevBtn = MakeButton(nil, mf, 40, 20, "<<")
|
||||
mf.prevBtn:SetPoint("BOTTOMLEFT", mf, "BOTTOMLEFT", 6, 6)
|
||||
mf.prevBtn:SetScript("OnClick", PreviousTrack)
|
||||
@@ -1163,7 +1366,7 @@ local function CreateMiniPlayer()
|
||||
mf.skipBtn:SetScript("OnClick", SkipTrack)
|
||||
mf.skipBtn:Disable()
|
||||
|
||||
-- Open full GUI button (far right)
|
||||
-- Open full GUI button
|
||||
mf.openBtn = MakeButton(nil, mf, 28, 20, "+")
|
||||
mf.openBtn:SetPoint("LEFT", mf.skipBtn, "RIGHT", 4, 0)
|
||||
mf.openBtn:SetScript("OnClick", function()
|
||||
@@ -1173,11 +1376,22 @@ local function CreateMiniPlayer()
|
||||
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()
|
||||
-- Reset marquee timer when mini player first appears
|
||||
RJ.marqueeLastUpdate = 0
|
||||
RefreshMiniPlayer()
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Minimap button
|
||||
-----------------------------------------------------------------------
|
||||
@@ -1266,7 +1480,7 @@ ticker:SetScript("OnUpdate", function()
|
||||
return
|
||||
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 GetTime() < RJ.pollTimer then
|
||||
return
|
||||
|
||||
@@ -1259,6 +1259,341 @@ if ($sanitized -eq 0 -and $skipped -eq 0 -and $mp3Files.Count -gt 0) {
|
||||
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 ----------
|
||||
# WoW 1.12's FMOD engine may fail to play files whose names contain characters
|
||||
# like apostrophes ('), parentheses, or other special symbols. This step
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<img src="https://cdn.discordapp.com/attachments/1510333697277300856/1523207096915460226/wwww.png?ex=6a4b44a7&is=6a49f327&hm=392e90557fee2d45b436fd72c1fe1ef975d318acdbb0fd5df799e8d06a4690d0&"/>
|
||||
|
||||
|
||||
# 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.
|
||||
@@ -16,9 +19,8 @@ The addon scans a local `Media` folder, builds a playlist automatically, and let
|
||||
- **Right-click:** Toggle Full GUI
|
||||
- Automatically imports tracks from `AutoPlaylist.lua`
|
||||
- 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
|
||||
- Works even without metadata by falling back to estimated durations
|
||||
|
||||
- Attempts to improve MP3, WAV, and OGG compatibility by sanitizing problematic files during playlist generation
|
||||
- Works without metadata by falling back to estimated durations
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -69,8 +71,8 @@ AutoPlaylist.lua
|
||||
- creates or updates `AutoPlaylist.lua`
|
||||
- captures track names
|
||||
- tries to determine durations
|
||||
- checks MP3 files for WoW 1.12 compatibility issues
|
||||
- optionally uses `ffmpeg` / `ffprobe` for better metadata and MP3 sanitization
|
||||
- checks MP3, WAV, and OGG files for WoW 1.12 compatibility issues
|
||||
- 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.
|
||||
|
||||
@@ -171,9 +173,9 @@ Important: if the audio file still exists in the `Media` folder and is still inc
|
||||
- Make sure `AutoPlaylist.lua` was created
|
||||
- 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
|
||||
|
||||
@@ -195,5 +197,3 @@ Install one of the following:
|
||||
## Credits
|
||||
|
||||
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
|
||||
## Title: Relationships Jukebox
|
||||
## Notes: Simple in-game MP3/WAV player with GUI.
|
||||
## Notes: Simple in-game MP3/WAV/OGG player with GUI.
|
||||
## Author: Relationship
|
||||
## Version: 1.2
|
||||
## SavedVariables: RelationshipsJukeboxDB
|
||||
## X-Website: https://github.com/Relationship-OctoWoW/RelationshipsJukeBox
|
||||
## X-GitHub: https://github.com/Relationship-OctoWoW/RelationshipsJukeBox
|
||||
AutoPlaylist.lua
|
||||
Core.lua
|
||||
|
||||
Reference in New Issue
Block a user