From 6f9a1facbba441dc4fe8a99c0c1abeb4188d4111 Mon Sep 17 00:00:00 2001 From: arx-ein Date: Thu, 21 May 2026 15:30:12 +0900 Subject: [PATCH 1/2] Support m4a audio import --- OpenUtau.Core/Format/FFmpegWaveReader.cs | 58 ++++++++++++++++++++++++ OpenUtau.Core/Format/Wave.cs | 9 ++++ OpenUtau/FilePicker.cs | 2 +- OpenUtau/Views/MainWindow.axaml.cs | 2 +- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 OpenUtau.Core/Format/FFmpegWaveReader.cs diff --git a/OpenUtau.Core/Format/FFmpegWaveReader.cs b/OpenUtau.Core/Format/FFmpegWaveReader.cs new file mode 100644 index 000000000..8e7c7483e --- /dev/null +++ b/OpenUtau.Core/Format/FFmpegWaveReader.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using NAudio.Wave; + +namespace OpenUtau.Core.Format { + public class FFmpegWaveReader(string filepath) : WaveStream { + private readonly WaveFileReader inner = new(DecodeToMemory(filepath)); + private static MemoryStream DecodeToMemory(string filepath) { + using var proc = new Process { + StartInfo = new ProcessStartInfo { + FileName = "ffmpeg", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + } + }; + proc.StartInfo.ArgumentList.Add("-i"); + proc.StartInfo.ArgumentList.Add(filepath); + proc.StartInfo.ArgumentList.Add("-f"); + proc.StartInfo.ArgumentList.Add("wav"); + proc.StartInfo.ArgumentList.Add("pipe:1"); + var stderrLog = new StringBuilder(); + proc.ErrorDataReceived += (_, e) => { if (e.Data != null) stderrLog.AppendLine(e.Data); }; + try { + proc.Start(); + } catch (Exception) { + throw new Exception("ffmpeg not found. Install ffmpeg and ensure it is on PATH."); + } + proc.BeginErrorReadLine(); + var memStream = new MemoryStream(); + proc.StandardOutput.BaseStream.CopyTo(memStream); + proc.WaitForExit(); + if (proc.ExitCode != 0) { + throw new Exception($"ffmpeg failed to decode '{Path.GetFileName(filepath)}'.\n{stderrLog}"); + } + memStream.Seek(0, SeekOrigin.Begin); + return memStream; + } + + public override WaveFormat WaveFormat => inner.WaveFormat; + public override long Length => inner.Length; + public override long Position { + get => inner.Position; + set => inner.Position = value; + } + public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); + + protected override void Dispose(bool disposing) { + if (disposing) { + inner.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/OpenUtau.Core/Format/Wave.cs b/OpenUtau.Core/Format/Wave.cs index db9e294d5..8f4e5b736 100644 --- a/OpenUtau.Core/Format/Wave.cs +++ b/OpenUtau.Core/Format/Wave.cs @@ -47,6 +47,15 @@ public static WaveStream OpenFile(string filepath) { if (ext == ".aiff" || ext == ".aif" || ext == ".aifc") { return new AiffFileReader(filepath); } + // M4A/AAC: MP4 container has a "ftyp" box at bytes 4–7 + string ftyp = System.Text.Encoding.ASCII.GetString(buffer.AsSpan(4, 4)); + if (ftyp == "ftyp" || ext == ".m4a" || ext == ".mp4") { +#if WINDOWS + return new MediaFoundationReader(filepath); +#else + return new FFmpegWaveReader(filepath); +#endif + } throw new Exception("Unsupported audio file format."); } diff --git a/OpenUtau/FilePicker.cs b/OpenUtau/FilePicker.cs index 4ffdceaa4..0b9f78ad3 100644 --- a/OpenUtau/FilePicker.cs +++ b/OpenUtau/FilePicker.cs @@ -30,7 +30,7 @@ internal class FilePicker { Patterns = new[] { "*.musicxml" }, }; public static FilePickerFileType AudioFiles { get; } = new("Audio Files") { - Patterns = new[] { "*.wav", "*.mp3", "*.ogg", "*.opus", "*.flac" }, + Patterns = new[] { "*.wav", "*.mp3", "*.ogg", "*.opus", "*.flac", "*.m4a" }, }; public static FilePickerFileType WAV { get; } = new("WAV") { Patterns = new[] { "*.wav" }, diff --git a/OpenUtau/Views/MainWindow.axaml.cs b/OpenUtau/Views/MainWindow.axaml.cs index 342ab19e3..7f51f3024 100644 --- a/OpenUtau/Views/MainWindow.axaml.cs +++ b/OpenUtau/Views/MainWindow.axaml.cs @@ -879,7 +879,7 @@ void OnPointerPressed(object? sender, PointerPressedEventArgs args) { async void OnDrop(object? sender, DragEventArgs args) { string[] ProjectExts = { ".ustx", ".ust", ".vsqx", ".ufdata", ".musicxml", ".mid", ".midi" }; string[] ArchiveExts = { ".zip", ".rar", ".uar" }; - string[] AudioExts = { ".mp3", ".wav", ".ogg", ".flac" }; + string[] AudioExts = { ".mp3", ".wav", ".ogg", ".flac", ".m4a" }; string[] SupportedExts = ProjectExts .Concat(ArchiveExts) .Concat(AudioExts) From 4e1d23af49bb6bd30d77cb9064fa0fd9f093da54 Mon Sep 17 00:00:00 2001 From: arx-ein Date: Thu, 21 May 2026 17:14:42 +0900 Subject: [PATCH 2/2] Fix m4a import for non-Win platforms --- OpenUtau.Core/Format/FFmpegWaveReader.cs | 28 +++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/OpenUtau.Core/Format/FFmpegWaveReader.cs b/OpenUtau.Core/Format/FFmpegWaveReader.cs index 8e7c7483e..879254d32 100644 --- a/OpenUtau.Core/Format/FFmpegWaveReader.cs +++ b/OpenUtau.Core/Format/FFmpegWaveReader.cs @@ -36,10 +36,36 @@ private static MemoryStream DecodeToMemory(string filepath) { if (proc.ExitCode != 0) { throw new Exception($"ffmpeg failed to decode '{Path.GetFileName(filepath)}'.\n{stderrLog}"); } - memStream.Seek(0, SeekOrigin.Begin); + // ffmpeg cannot seek back to fix WAV chunk sizes when writing to a non-seekable + // pipe, leaving them as 0 or 0xFFFFFFFF. Patch them now that the full size is known. + PatchWavHeader(memStream); return memStream; } + private static void PatchWavHeader(MemoryStream stream) { + int total = (int)stream.Length; + // Fix RIFF chunk size at offset 4 + stream.Position = 4; + stream.Write(BitConverter.GetBytes(total - 8), 0, 4); + // Scan sub-chunks starting after "RIFF" + size + "WAVE" + stream.Position = 12; + var id = new byte[4]; + var sz = new byte[4]; + while (stream.Position <= stream.Length - 8) { + stream.Read(id, 0, 4); + stream.Read(sz, 0, 4); + if (id[0] == 'd' && id[1] == 'a' && id[2] == 't' && id[3] == 'a') { + stream.Position -= 4; + stream.Write(BitConverter.GetBytes(total - (int)stream.Position - 4), 0, 4); + break; + } + int chunkSize = BitConverter.ToInt32(sz, 0); + if (chunkSize < 0) break; + stream.Position += chunkSize; + } + stream.Position = 0; + } + public override WaveFormat WaveFormat => inner.WaveFormat; public override long Length => inner.Length; public override long Position {