psst
This commit is contained in:
318
Editor/Core/PSXAudioConverter.cs
Normal file
318
Editor/Core/PSXAudioConverter.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using SplashEdit.RuntimeCode;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads psxavenc and converts WAV audio to PS1 SPU ADPCM format.
|
||||
/// psxavenc is the standard tool for PS1 audio encoding from the
|
||||
/// WonderfulToolchain project.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class PSXAudioConverter
|
||||
{
|
||||
static PSXAudioConverter()
|
||||
{
|
||||
// Register the converter delegate so Runtime code can call it
|
||||
// without directly referencing this Editor assembly.
|
||||
PSXSceneExporter.AudioConvertDelegate = ConvertToADPCM;
|
||||
}
|
||||
|
||||
private const string PSXAVENC_VERSION = "v0.3.1";
|
||||
private const string PSXAVENC_RELEASE_BASE =
|
||||
"https://github.com/WonderfulToolchain/psxavenc/releases/download/";
|
||||
|
||||
private static readonly HttpClient _http = new HttpClient();
|
||||
|
||||
/// <summary>
|
||||
/// Path to the psxavenc binary inside .tools/
|
||||
/// </summary>
|
||||
public static string PsxavencBinary
|
||||
{
|
||||
get
|
||||
{
|
||||
string dir = Path.Combine(SplashBuildPaths.ToolsDir, "psxavenc");
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||
return Path.Combine(dir, "psxavenc.exe");
|
||||
return Path.Combine(dir, "psxavenc");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsInstalled() => File.Exists(PsxavencBinary);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads and installs psxavenc from the official GitHub releases.
|
||||
/// </summary>
|
||||
public static async Task<bool> DownloadAndInstall(Action<string> log = null)
|
||||
{
|
||||
string platformSuffix;
|
||||
string archiveName;
|
||||
switch (Application.platform)
|
||||
{
|
||||
case RuntimePlatform.WindowsEditor:
|
||||
platformSuffix = "x86_64-pc-windows-msvc";
|
||||
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.zip";
|
||||
break;
|
||||
case RuntimePlatform.LinuxEditor:
|
||||
platformSuffix = "x86_64-unknown-linux-gnu";
|
||||
archiveName = $"psxavenc-{PSXAVENC_VERSION}-{platformSuffix}.tar.gz";
|
||||
break;
|
||||
default:
|
||||
log?.Invoke("Only Windows and Linux are supported.");
|
||||
return false;
|
||||
}
|
||||
|
||||
string downloadUrl = $"{PSXAVENC_RELEASE_BASE}{PSXAVENC_VERSION}/{archiveName}";
|
||||
log?.Invoke($"Downloading psxavenc: {downloadUrl}");
|
||||
|
||||
try
|
||||
{
|
||||
string tempFile = Path.Combine(Path.GetTempPath(), archiveName);
|
||||
EditorUtility.DisplayProgressBar("Downloading psxavenc", "Downloading...", 0.1f);
|
||||
|
||||
using (var response = await _http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
long? totalBytes = response.Content.Headers.ContentLength;
|
||||
long downloaded = 0;
|
||||
|
||||
using (var fs = File.Create(tempFile))
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
byte[] buffer = new byte[81920];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
await fs.WriteAsync(buffer, 0, bytesRead);
|
||||
downloaded += bytesRead;
|
||||
if (totalBytes.HasValue)
|
||||
{
|
||||
float progress = (float)downloaded / totalBytes.Value;
|
||||
EditorUtility.DisplayProgressBar("Downloading psxavenc",
|
||||
$"{downloaded / 1024}/{totalBytes.Value / 1024} KB", progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log?.Invoke("Extracting...");
|
||||
EditorUtility.DisplayProgressBar("Installing psxavenc", "Extracting...", 0.9f);
|
||||
|
||||
string installDir = Path.Combine(SplashBuildPaths.ToolsDir, "psxavenc");
|
||||
if (Directory.Exists(installDir))
|
||||
Directory.Delete(installDir, true);
|
||||
Directory.CreateDirectory(installDir);
|
||||
|
||||
if (tempFile.EndsWith(".zip"))
|
||||
{
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(tempFile, installDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
// tar.gz extraction — use system tar
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "tar",
|
||||
Arguments = $"xzf \"{tempFile}\" -C \"{installDir}\" --strip-components=1",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var proc = Process.Start(psi);
|
||||
proc.WaitForExit();
|
||||
}
|
||||
|
||||
// Fix nested directory (sometimes archives have one extra level)
|
||||
FixNestedDirectory(installDir);
|
||||
|
||||
try { File.Delete(tempFile); } catch { }
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
if (IsInstalled())
|
||||
{
|
||||
// Make executable on Linux
|
||||
if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
var chmod = Process.Start("chmod", $"+x \"{PsxavencBinary}\"");
|
||||
chmod?.WaitForExit();
|
||||
}
|
||||
log?.Invoke("psxavenc installed successfully!");
|
||||
return true;
|
||||
}
|
||||
|
||||
log?.Invoke($"psxavenc binary not found at: {PsxavencBinary}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Invoke($"psxavenc download failed: {ex.Message}");
|
||||
EditorUtility.ClearProgressBar();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void FixNestedDirectory(string dir)
|
||||
{
|
||||
// If extraction created exactly one subdirectory, flatten it
|
||||
var subdirs = Directory.GetDirectories(dir);
|
||||
if (subdirs.Length == 1)
|
||||
{
|
||||
string nested = subdirs[0];
|
||||
foreach (string file in Directory.GetFiles(nested))
|
||||
{
|
||||
string dest = Path.Combine(dir, Path.GetFileName(file));
|
||||
if (!File.Exists(dest)) File.Move(file, dest);
|
||||
}
|
||||
foreach (string sub in Directory.GetDirectories(nested))
|
||||
{
|
||||
string dest = Path.Combine(dir, Path.GetFileName(sub));
|
||||
if (!Directory.Exists(dest)) Directory.Move(sub, dest);
|
||||
}
|
||||
try { Directory.Delete(nested, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Unity AudioClip to PS1 SPU ADPCM format using psxavenc.
|
||||
/// Returns the ADPCM byte array, or null on failure.
|
||||
/// </summary>
|
||||
public static byte[] ConvertToADPCM(AudioClip clip, int targetSampleRate, bool loop)
|
||||
{
|
||||
if (!IsInstalled())
|
||||
{
|
||||
Debug.LogError("[SplashEdit] psxavenc not installed. Install it from the Setup tab.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (clip == null)
|
||||
{
|
||||
Debug.LogError("[SplashEdit] AudioClip is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Export Unity AudioClip to a temporary WAV file
|
||||
string tempWav = Path.Combine(Path.GetTempPath(), $"psx_audio_{clip.name}.wav");
|
||||
string tempVag = Path.Combine(Path.GetTempPath(), $"psx_audio_{clip.name}.vag");
|
||||
|
||||
try
|
||||
{
|
||||
ExportWav(clip, tempWav);
|
||||
|
||||
// Run psxavenc: convert WAV to SPU ADPCM
|
||||
// -t spu: raw SPU ADPCM output (no header, ready for DMA upload)
|
||||
// -f <rate>: target sample rate
|
||||
// -L: enable looping flag in the last ADPCM block
|
||||
string loopFlag = loop ? "-L" : "";
|
||||
string args = $"-t spu -f {targetSampleRate} {loopFlag} \"{tempWav}\" \"{tempVag}\"";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = PsxavencBinary,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
var process = Process.Start(psi);
|
||||
string stderr = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
Debug.LogError($"[SplashEdit] psxavenc failed: {stderr}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(tempVag))
|
||||
{
|
||||
Debug.LogError("[SplashEdit] psxavenc produced no output file.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// -t spu outputs raw SPU ADPCM blocks (no header) — use directly.
|
||||
byte[] adpcm = File.ReadAllBytes(tempVag);
|
||||
if (adpcm.Length == 0)
|
||||
{
|
||||
Debug.LogError("[SplashEdit] psxavenc produced empty output.");
|
||||
return null;
|
||||
}
|
||||
return adpcm;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (File.Exists(tempWav)) File.Delete(tempWav); } catch { }
|
||||
try { if (File.Exists(tempVag)) File.Delete(tempVag); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports a Unity AudioClip to a 16-bit mono WAV file.
|
||||
/// </summary>
|
||||
private static void ExportWav(AudioClip clip, string path)
|
||||
{
|
||||
float[] samples = new float[clip.samples * clip.channels];
|
||||
clip.GetData(samples, 0);
|
||||
|
||||
// Downmix to mono if stereo
|
||||
float[] mono;
|
||||
if (clip.channels > 1)
|
||||
{
|
||||
mono = new float[clip.samples];
|
||||
for (int i = 0; i < clip.samples; i++)
|
||||
{
|
||||
float sum = 0;
|
||||
for (int ch = 0; ch < clip.channels; ch++)
|
||||
sum += samples[i * clip.channels + ch];
|
||||
mono[i] = sum / clip.channels;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mono = samples;
|
||||
}
|
||||
|
||||
// Write WAV
|
||||
using (var fs = new FileStream(path, FileMode.Create))
|
||||
using (var writer = new BinaryWriter(fs))
|
||||
{
|
||||
int sampleCount = mono.Length;
|
||||
int dataSize = sampleCount * 2; // 16-bit
|
||||
int fileSize = 44 + dataSize;
|
||||
|
||||
// RIFF header
|
||||
writer.Write(new char[] { 'R', 'I', 'F', 'F' });
|
||||
writer.Write(fileSize - 8);
|
||||
writer.Write(new char[] { 'W', 'A', 'V', 'E' });
|
||||
|
||||
// fmt chunk
|
||||
writer.Write(new char[] { 'f', 'm', 't', ' ' });
|
||||
writer.Write(16); // chunk size
|
||||
writer.Write((short)1); // PCM
|
||||
writer.Write((short)1); // mono
|
||||
writer.Write(clip.frequency);
|
||||
writer.Write(clip.frequency * 2); // byte rate
|
||||
writer.Write((short)2); // block align
|
||||
writer.Write((short)16); // bits per sample
|
||||
|
||||
// data chunk
|
||||
writer.Write(new char[] { 'd', 'a', 't', 'a' });
|
||||
writer.Write(dataSize);
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
short sample = (short)(Mathf.Clamp(mono[i], -1f, 1f) * 32767f);
|
||||
writer.Write(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user