Skip to content

Commit 87503cc

Browse files
committed
feat: 将导入导出谱面的WAV<->MP3互转,改为基于FFMPEG的实现
详见#40中的讨论,NAudio对LAME MP3 Gapless并不支持,造成了严重的音频延迟行为不确定等问题。而经过测试FFMPEG对Gapless的支持是十分完善的,所以全部改用FFMPEG做音频编解码。
1 parent c6ca3fc commit 87503cc

3 files changed

Lines changed: 92 additions & 9 deletions

File tree

MaiChartManager/Controllers/Music/MusicTransferController.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.VisualBasic.FileIO;
1010
using NAudio.Lame;
11-
using SimaiSharp;
1211
using Vanara.Windows.Forms;
13-
using Xabe.FFmpeg;
1412
using FolderBrowserDialog = System.Windows.Forms.FolderBrowserDialog;
1513

1614
namespace MaiChartManager.Controllers.Music;
@@ -555,7 +553,7 @@ public async Task ExportAsMaidata(int id, string assetDir, bool ignoreVideo = fa
555553
simaiFile.AppendLine($"&title={music.Name}");
556554
simaiFile.AppendLine($"&artist={music.Artist}");
557555
simaiFile.AppendLine($"&wholebpm={music.Bpm}");
558-
simaiFile.AppendLine("&first=0.0333");
556+
simaiFile.AppendLine("&first=0");
559557
simaiFile.AppendLine($"&shortid={music.Id}");
560558
simaiFile.AppendLine($"&genreid={music.GenreId}");
561559
var genre = StaticSettings.GenreList.FirstOrDefault(it => it.Id == music.GenreId);

MaiChartManager/Utils/Audio.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using NAudio.Lame;
22
using NAudio.Wave;
3+
using Xabe.FFmpeg;
34
using VGAudio;
45
using VGAudio.Cli;
56
using Xv2CoreLib.ACB;
@@ -59,7 +60,12 @@ public static byte[] LoadAndConvertFile(string path, FileType convertToType, boo
5960

6061
public static Stream ConvertToWav(Stream src, bool isOgg, float padding = 0)
6162
{
62-
using WaveStream reader = isOgg ? new NAudio.Vorbis.VorbisWaveReader(src, true) : new StreamMediaFoundationReader(src);
63+
using WaveStream reader = isOgg
64+
? new NAudio.Vorbis.VorbisWaveReader(src, true)
65+
// 如果直接用StreamMediaFoundationReader的话,mp3转wav的过程会有问题,详见https://github.com/MuNET-OSS/MaiChartManager/issues/40#issuecomment-4029798667。
66+
// 因此,将使用ffmpeg完成从mp3到wav的转换;但由于需要进行padding,所以还是得再以sample的形式读出来然后重新保存。
67+
// PS:使用NAudio的WaveFileReader来直接处理wav文件是没有问题的,因为wav本身是一种基于sample的无损采样格式,没有encdelay之类的问题。只要确保不要使用NAudio进行MP3编解码即可。
68+
: new WaveFileReader(ConvertMp3ToWavViaFfmpeg(src));
6369
var sample = reader.ToSampleProvider();
6470

6571
switch (padding)
@@ -82,6 +88,39 @@ public static Stream ConvertToWav(Stream src, bool isOgg, float padding = 0)
8288
return stream;
8389
}
8490

91+
private static MemoryStream ConvertMp3ToWavViaFfmpeg(Stream src)
92+
{
93+
var tempFileGuid = Guid.NewGuid();
94+
var inputPath = Path.Combine(StaticSettings.tempPath, $"ConvertToWav_{tempFileGuid:N}.mp3");
95+
var outputPath = Path.Combine(StaticSettings.tempPath, $"ConvertToWav_{tempFileGuid:N}.wav");
96+
try
97+
{
98+
Directory.CreateDirectory(StaticSettings.tempPath);
99+
100+
using (var inputFile = new FileStream(inputPath, FileMode.Create, FileAccess.Write))
101+
{
102+
src.CopyTo(inputFile);
103+
}
104+
105+
var conversion = FFmpeg.Conversions.New()
106+
.AddParameter("-i " + inputPath.Escape())
107+
.AddParameter("-c:a pcm_s16le") // 转为16-bit little-endian PCM
108+
.SetOutput(outputPath)
109+
.SetOverwriteOutput(true);
110+
conversion.Start().GetAwaiter().GetResult();
111+
112+
if (!File.Exists(outputPath) || new FileInfo(outputPath).Length == 0)
113+
throw new InvalidOperationException("ffmpeg produced empty wav file from mp3 input.");
114+
115+
return new MemoryStream(File.ReadAllBytes(outputPath));
116+
}
117+
finally
118+
{
119+
File.Delete(inputPath);
120+
File.Delete(outputPath);
121+
}
122+
}
123+
85124
public static byte[] ConvertFile(
86125
Stream s,
87126
FileType encodeType,

MaiChartManager/Utils/AudioConvert.cs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using NAudio.Lame;
2-
using NAudio.Wave;
3-
using NAudio.Wave.SampleProviders;
42
using Standart.Hash.xxHash;
3+
using Xabe.FFmpeg;
54

65
namespace MaiChartManager.Utils;
76

@@ -76,8 +75,55 @@ public static async Task<string> GetCachedWavPath(string acbPath, string awbPath
7675

7776
public static void ConvertWavPathToMp3Stream(string wavPath, Stream mp3Stream, ID3TagData? tagData = null)
7877
{
79-
using var reader = new WaveFileReader(wavPath);
80-
using var writer = new LameMP3FileWriter(mp3Stream, reader.WaveFormat, 256, tagData);
81-
reader.CopyTo(writer);
78+
var outputPath = Path.Combine(StaticSettings.tempPath, $"ConvertToMp3_{Guid.NewGuid():N}.mp3");
79+
string? albumArtPath = null;
80+
try
81+
{
82+
Directory.CreateDirectory(StaticSettings.tempPath);
83+
84+
var conversion = FFmpeg.Conversions.New()
85+
.AddParameter($"-i " + wavPath.Escape());
86+
87+
if (tagData != null)
88+
{
89+
if (tagData.AlbumArt != null && tagData.AlbumArt.Length > 0)
90+
{
91+
// 把专辑封面写到临时文件,然后让ffmpeg把它嵌入mp3
92+
albumArtPath = Path.Combine(StaticSettings.tempPath, $"ConvertToMp3_{Guid.NewGuid():N}.png");
93+
File.WriteAllBytes(albumArtPath, tagData.AlbumArt);
94+
conversion.AddParameter($"-i {albumArtPath.Escape()}");
95+
} // 顺序不能换!这个必须在第一个,因为-i必须在任何其他参数之前。
96+
if (!string.IsNullOrEmpty(tagData.Title)) conversion.AddParameter($"-metadata title=" + tagData.Title.Escape());
97+
if (!string.IsNullOrEmpty(tagData.Artist)) conversion.AddParameter($"-metadata artist=" + tagData.Artist.Escape());
98+
if (!string.IsNullOrEmpty(tagData.Album)) conversion.AddParameter($"-metadata album=" + tagData.Album.Escape());
99+
if (!string.IsNullOrEmpty(tagData.Year)) conversion.AddParameter($"-metadata date=" + tagData.Year.Escape());
100+
if (!string.IsNullOrEmpty(tagData.Comment)) conversion.AddParameter($"-metadata comment=" + tagData.Comment.Escape());
101+
if (!string.IsNullOrEmpty(tagData.Genre)) conversion.AddParameter($"-metadata genre=" + tagData.Genre.Escape());
102+
if (!string.IsNullOrEmpty(tagData.Track)) conversion.AddParameter($"-metadata track=" + tagData.Track.Escape());
103+
}
104+
105+
conversion.AddParameter("-c:a libmp3lame -b:a 256k"); // 把wav编码为256kbps的LAME mp3
106+
107+
if (albumArtPath != null)
108+
{ // 如果有专辑封面,还需要加一堆参数以写入专辑封面
109+
conversion.AddParameter("-map 0:a -map 1:v -c:v copy -disposition:v attached_pic");
110+
}
111+
112+
conversion.SetOutput(outputPath).SetOverwriteOutput(true);
113+
conversion.Start().GetAwaiter().GetResult();
114+
115+
if (!File.Exists(outputPath) || new FileInfo(outputPath).Length == 0)
116+
{
117+
throw new InvalidOperationException("ffmpeg produced empty mp3 file from wav input.");
118+
}
119+
120+
using var outputFile = new FileStream(outputPath, FileMode.Open, FileAccess.Read);
121+
outputFile.CopyTo(mp3Stream);
122+
}
123+
finally
124+
{
125+
File.Delete(outputPath);
126+
if (albumArtPath != null) File.Delete(albumArtPath);
127+
}
82128
}
83129
}

0 commit comments

Comments
 (0)