17 Commits

Author SHA1 Message Date
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 601 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)
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
+335
View File
@@ -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
+9 -9
View File
@@ -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.
+4 -1
View File
@@ -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