18 Commits

Author SHA1 Message Date
Relationship e1e5b3344a Update README.md 2026-07-05 10:10:07 +01:00
Relationship 32af9fe29b Update RelationshipsJukeBox.toc 2026-07-05 10:01:17 +01:00
Relationship 282517f2d4 Add files via upload 2026-07-05 10:00:43 +01:00
Relationship 2062be4c5b Delete RelationshipsJukeBox.toc 2026-07-05 10:00:10 +01:00
Relationship 74e91c6da1 Delete GeneratePlaylist.ps1 2026-07-05 10:00:06 +01:00
Relationship acc770233f Delete GeneratePlaylist.bat 2026-07-05 10:00:02 +01:00
Relationship 80cfdff8f6 Delete Core.lua 2026-07-05 09:59:58 +01:00
Relationship 634ebd62c4 Update README.md 2026-07-05 08:39:45 +01:00
Relationship 00e05690ec Delete AutoPlaylist.lua 2026-07-05 08:28:18 +01:00
Relationship 2031057559 Add files via upload 2026-07-05 08:26:06 +01:00
Relationship c3c4488cdc Delete Core.lua 2026-07-05 08:25:48 +01:00
Relationship a1e45d9ed5 Delete GeneratePlaylist.bat 2026-07-05 08:25:44 +01:00
Relationship 343f892029 Delete GeneratePlaylist.ps1 2026-07-05 08:25:40 +01:00
Relationship f0996e4fce Delete RelationshipsJukeBox.toc 2026-07-05 08:25:34 +01:00
Relationship 5cd1a221bb Update README.md 2026-07-05 07:25:54 +01:00
Relationship e92321ada1 Update RelationshipsJukeBox.toc 2026-07-05 07:16:17 +01:00
Relationship 99d3881e88 Update RelationshipsJukeBox.toc 2026-07-05 07:13:48 +01:00
Relationship 504bdd7473 Update README.md 2026-07-05 07:02:13 +01:00
4 changed files with 603 additions and 49 deletions
+253 -39
View File
@@ -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
+335
View File
@@ -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
+11 -9
View File
@@ -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.
+4 -1
View File
@@ -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