Skip to content

Commit

Permalink
Add loop, volume, and title editing to BGM editor (#52)
Browse files Browse the repository at this point in the history
Co-authored-by: William <[email protected]>
  • Loading branch information
jonko0493 and WiIIiam278 authored Mar 28, 2023
1 parent 5668e55 commit 83538c1
Show file tree
Hide file tree
Showing 40 changed files with 979 additions and 91 deletions.
1 change: 1 addition & 0 deletions src/SerialLoops.Gtk/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Eto.Forms;
using SerialLoops.Utility;
using System;

namespace SerialLoops.Gtk
Expand Down
1 change: 1 addition & 0 deletions src/SerialLoops.Gtk/SoundPlayerHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Eto.GtkSharp.Forms;
using Gtk;
using NAudio.Wave;
using SerialLoops.Utility;

namespace SerialLoops.Gtk
{
Expand Down
2 changes: 2 additions & 0 deletions src/SerialLoops.Lib/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class Config
public string ProjectsDirectory => Path.Combine(UserDirectory, "Projects");
[JsonIgnore]
public string LogsDirectory => Path.Combine(UserDirectory, "Logs");
[JsonIgnore]
public string CachesDirectory => Path.Combine(UserDirectory, "Caches");
public string DevkitArmPath { get; set; }
public string EmulatorPath { get; set; }
public bool AutoReopenLastProject { get; set; }
Expand Down
6 changes: 3 additions & 3 deletions src/SerialLoops.Lib/Items/BackgroundItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ public class BackgroundItem : Item, IPreviewableGraphic
public BackgroundItem(string name) : base(name, ItemType.Background)
{
}
public BackgroundItem(string name, int id, BgTableEntry entry, Project project, ExtraFile extras) : base(name, ItemType.Background)
public BackgroundItem(string name, int id, BgTableEntry entry, Project project) : base(name, ItemType.Background)
{
Id = id;
BackgroundType = entry.Type;
Graphic1 = project.Grp.Files.First(g => g.Index == entry.BgIndex1);
Graphic2 = project.Grp.Files.FirstOrDefault(g => g.Index == entry.BgIndex2); // can be null if type is SINGLE_TEX
CgStruct? cgEntry = extras.Cgs.FirstOrDefault(c => c.BgId == Id);
if (cgEntry.HasValue)
CgStruct cgEntry = project.Extra.Cgs.FirstOrDefault(c => c.BgId == Id);
if (cgEntry is not null)
{
CgName = cgEntry?.Name?.GetSubstitutedString(project);
ExtrasShort = cgEntry?.Unknown02 ?? 0;
Expand Down
77 changes: 68 additions & 9 deletions src/SerialLoops.Lib/Items/BackgroundMusicItem.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using HaruhiChokuretsuLib.Archive.Data;
using HaruhiChokuretsuLib.Archive.Event;
using HaruhiChokuretsuLib.Archive.Event;
using HaruhiChokuretsuLib.Audio;
using HaruhiChokuretsuLib.Util;
using NAudio.Flac;
using NAudio.Vorbis;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using NLayer.NAudioSupport;
using SerialLoops.Lib.Util;
using System;
using System.IO;
Expand All @@ -18,14 +21,15 @@ public class BackgroundMusicItem : Item, ISoundItem
public int Index { get; set; }
public string BgmName { get; set; }
public string ExtrasShort { get; set; }
public string CachedWaveFile { get; set; }
public (string ScriptName, ScriptCommandInvocation command)[] ScriptUses { get; set; }

public BackgroundMusicItem(string bgmFile, int index, ExtraFile extras, Project project) : base(Path.GetFileNameWithoutExtension(bgmFile), ItemType.BGM)
public BackgroundMusicItem(string bgmFile, int index, Project project) : base(Path.GetFileNameWithoutExtension(bgmFile), ItemType.BGM)
{
BgmFile = Path.GetRelativePath(project.IterativeDirectory, bgmFile);
_bgmFile = bgmFile;
Index = index;
BgmName = extras.Bgms.FirstOrDefault(b => b.Index == Index).Name?.GetSubstitutedString(project) ?? "";
BgmName = project.Extra.Bgms.FirstOrDefault(b => b.Index == Index)?.Name?.GetSubstitutedString(project) ?? "";
DisplayName = string.IsNullOrEmpty(BgmName) ? Name : BgmName;
PopulateScriptUses(project);
}
Expand All @@ -43,13 +47,67 @@ public void PopulateScriptUses(Project project)
.Where(t => t.c.Parameters[0] == Index).ToArray();
}

public void Replace(string wavFile, string baseDirectory, string iterativeDirectory)
public void Replace(string audioFile, string baseDirectory, string iterativeDirectory, string bgmCachedFile, bool loopEnabled, uint loopStartSample, uint loopEndSample, ILogger log)
{
AdxUtil.EncodeWav(wavFile, Path.Combine(baseDirectory, BgmFile), false);
// The MP3 reader is able to create wave files but for whatever reason messes with the ADX encoder
// So we just convert to WAV AOT
if (Path.GetExtension(audioFile).Equals(".mp3", StringComparison.OrdinalIgnoreCase))
{
log.Log($"Converting {audioFile} to WAV...");
using Mp3FileReaderBase mp3Reader = new(audioFile, new Mp3FileReaderBase.FrameDecompressorBuilder(wf => new Mp3FrameDecompressor(wf)));
WaveFileWriter.CreateWaveFile(bgmCachedFile, mp3Reader.ToSampleProvider().ToWaveProvider16());
audioFile = bgmCachedFile;
}
// Ditto the Vorbis decoder
else if (Path.GetExtension(audioFile).Equals(".ogg", StringComparison.OrdinalIgnoreCase))
{
log.Log($"Converting {audioFile} to WAV...");
using VorbisWaveReader vorbisReader = new(audioFile);
WaveFileWriter.CreateWaveFile(bgmCachedFile, vorbisReader.ToSampleProvider().ToWaveProvider16());
audioFile = bgmCachedFile;
}
using WaveStream audio = Path.GetExtension(audioFile).ToLower() switch
{
".wav" => new WaveFileReader(audioFile),
".flac" => new FlacReader(audioFile),
_ => null,
};
if (audio is null)
{
log.LogError($"Invalid audio file '{audioFile}' selected.");
return;
}
if (audio.WaveFormat.SampleRate > SoundItem.MAX_SAMPLERATE)
{
log.Log($"Downsampling audio from {audio.WaveFormat.SampleRate} to NDS max sample rate {SoundItem.MAX_SAMPLERATE}...");
string newAudioFile = Path.Combine(Path.GetDirectoryName(bgmCachedFile), $"{Path.GetFileNameWithoutExtension(bgmCachedFile)}-downsampled.wav");
WaveFileWriter.CreateWaveFile(newAudioFile, new WdlResamplingSampleProvider(audio.ToSampleProvider(), SoundItem.MAX_SAMPLERATE).ToWaveProvider16());
log.Log($"Encoding audio to ADX...");
AdxUtil.EncodeWav(newAudioFile, Path.Combine(baseDirectory, BgmFile), loopEnabled, loopStartSample, loopEndSample);
audioFile = newAudioFile;
}
else
{
log.Log($"Encoding audio to ADX...");
AdxUtil.EncodeAudio(audio, Path.Combine(baseDirectory, BgmFile), loopEnabled, loopStartSample, loopEndSample);
}
File.Copy(Path.Combine(baseDirectory, BgmFile), Path.Combine(iterativeDirectory, BgmFile), true);
if (!string.Equals(audioFile, bgmCachedFile))
{
log.Log($"Attempting to cache audio file from {audioFile} to {bgmCachedFile}...");
if (Path.GetExtension(audioFile).Equals(".wav", StringComparison.OrdinalIgnoreCase))
{
File.Copy(audioFile, bgmCachedFile, true);
}
else
{
audio.Seek(0, SeekOrigin.Begin);
WaveFileWriter.CreateWaveFile(bgmCachedFile, audio.ToSampleProvider().ToWaveProvider16());
}
}
}
public IWaveProvider GetWaveProvider(ILogger log)

public IWaveProvider GetWaveProvider(ILogger log, bool loop)
{
byte[] adxBytes = Array.Empty<byte>();
try
Expand All @@ -68,7 +126,8 @@ public IWaveProvider GetWaveProvider(ILogger log)
}
}

return new AdxWaveProvider(new AdxDecoder(adxBytes, log));
AdxDecoder decoder = new(adxBytes, log) { DoLoop = loop };
return new AdxWaveProvider(decoder, decoder.Header.LoopInfo.EnabledInt == 1, decoder.LoopInfo.StartSample, decoder.LoopInfo.EndSample);
}
}
}
16 changes: 11 additions & 5 deletions src/SerialLoops.Lib/Items/ISoundItem.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using HaruhiChokuretsuLib.Util;
using NAudio.Wave;

namespace SerialLoops.Lib.Items;

public interface ISoundItem
namespace SerialLoops.Lib.Items
{
IWaveProvider GetWaveProvider(ILogger log);
}
public interface ISoundItem
{
IWaveProvider GetWaveProvider(ILogger log, bool loop);
}

public static class SoundItem
{
public const int MAX_SAMPLERATE = 32768;
}
}
69 changes: 66 additions & 3 deletions src/SerialLoops.Lib/Items/VoicedLineItem.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using HaruhiChokuretsuLib.Archive.Event;
using HaruhiChokuretsuLib.Audio;
using HaruhiChokuretsuLib.Util;
using NAudio.Flac;
using NAudio.Vorbis;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using NLayer.NAudioSupport;
using System;
using System.IO;
using System.Linq;
Expand All @@ -25,7 +29,7 @@ public VoicedLineItem(string voiceFile, int index, Project project) : base(Path.
PopulateScriptUses(project);
}

public IWaveProvider GetWaveProvider(ILogger log)
public IWaveProvider GetWaveProvider(ILogger log, bool loop = false)
{
byte[] adxBytes = Array.Empty<byte>();
try
Expand Down Expand Up @@ -59,9 +63,68 @@ public IWaveProvider GetWaveProvider(ILogger log)
return new AdxWaveProvider(decoder);
}

public void Replace(string wavFile, string baseDirectory, string iterativeDirectory)
public void Replace(string audioFile, string baseDirectory, string iterativeDirectory, string vceCachedFile, ILogger log)
{
AdxUtil.EncodeWav(wavFile, Path.Combine(baseDirectory, VoiceFile), true);
// The MP3 decoder is able to create wave files but for whatever reason messes with the ADX encoder
// So we just convert to WAV AOT
if (Path.GetExtension(audioFile).Equals(".mp3", StringComparison.OrdinalIgnoreCase))
{
using Mp3FileReaderBase mp3Reader = new(audioFile, new Mp3FileReaderBase.FrameDecompressorBuilder(wf => new Mp3FrameDecompressor(wf)));
WaveFileWriter.CreateWaveFile(vceCachedFile, mp3Reader.ToSampleProvider().ToWaveProvider16());
audioFile = vceCachedFile;
}
// Ditto the Vorbis decoder
else if (Path.GetExtension(audioFile).Equals(".ogg", StringComparison.OrdinalIgnoreCase))
{
using VorbisWaveReader vorbisReader = new(audioFile);
WaveFileWriter.CreateWaveFile(vceCachedFile, vorbisReader.ToSampleProvider().ToWaveProvider16());
audioFile = vceCachedFile;
}
using WaveStream audio = Path.GetExtension(audioFile).ToLower() switch
{
".wav" => new WaveFileReader(audioFile),
".flac" => new FlacReader(audioFile),
_ => null,
};
if (audio is null)
{
log.LogError($"Invalid audio file '{audioFile}' selected.");
return;
}
if (audio.WaveFormat.Channels > 1 || audio.WaveFormat.SampleRate > SoundItem.MAX_SAMPLERATE)
{
string newAudioFile = "";
if (audio.WaveFormat.Channels > 1)
{
log.Log($"Downmixing audio from stereo to mono for AHX conversion...");
newAudioFile = Path.Combine(Path.GetDirectoryName(vceCachedFile), $"{Path.GetFileNameWithoutExtension(vceCachedFile)}-downmixed.wav");
WaveFileWriter.CreateWaveFile(newAudioFile, audio.ToSampleProvider().ToMono().ToWaveProvider16());
}
if (audio.WaveFormat.SampleRate > SoundItem.MAX_SAMPLERATE)
{
log.Log($"Downsampling audio from {audio.WaveFormat.SampleRate} to NDS max sample rate {SoundItem.MAX_SAMPLERATE}...");
string prevAudioFile = $"{newAudioFile}";
newAudioFile = Path.Combine(Path.GetDirectoryName(vceCachedFile), $"{Path.GetFileNameWithoutExtension(vceCachedFile)}-downsampled.wav");
if (!string.IsNullOrEmpty(prevAudioFile))
{
using WaveFileReader newAudio = new(prevAudioFile);
WaveFileWriter.CreateWaveFile(newAudioFile, new WdlResamplingSampleProvider(newAudio.ToSampleProvider(), SoundItem.MAX_SAMPLERATE).ToWaveProvider16());
}
else
{
WaveFileWriter.CreateWaveFile(newAudioFile, new WdlResamplingSampleProvider(audio.ToSampleProvider(), SoundItem.MAX_SAMPLERATE).ToWaveProvider16());
}
}

log.Log($"Encoding audio to AHX...");
audioFile = newAudioFile;
AdxUtil.EncodeWav(newAudioFile, Path.Combine(baseDirectory, VoiceFile), true);
}
else
{
log.Log($"Encoding audio to AHX...");
AdxUtil.EncodeAudio(audio, Path.Combine(baseDirectory, VoiceFile), true);
}
File.Copy(Path.Combine(baseDirectory, VoiceFile), Path.Combine(iterativeDirectory, VoiceFile), true);
}

Expand Down
37 changes: 32 additions & 5 deletions src/SerialLoops.Lib/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class Project
[JsonIgnore]
public string ProjectFile => Path.Combine(MainDirectory, $"{Name}.{PROJECT_FORMAT}");
[JsonIgnore]
public Config Config { get; private set; }
[JsonIgnore]
public ProjectSettings Settings { get; set; }
[JsonIgnore]
public List<ItemDescription> Items { get; set; } = new();
Expand All @@ -53,6 +55,9 @@ public class Project
[JsonIgnore]
public SKBitmap FontBitmap { get; set; }

[JsonIgnore]
public ExtraFile Extra { get; set; }

public Project()
{
}
Expand All @@ -62,6 +67,7 @@ public Project(string name, string langCode, Config config, ILogger log)
Name = name;
LangCode = langCode;
MainDirectory = Path.Combine(config.ProjectsDirectory, name);
Config = config;
log.Log("Creating project directories...");
try
{
Expand All @@ -78,9 +84,11 @@ public Project(string name, string langCode, Config config, ILogger log)
}
}

public void Load(ILogger log, IProgressTracker tracker)
public void Load(Config config, ILogger log, IProgressTracker tracker)
{
Config = config;
LoadProjectSettings(log, tracker);
ClearOrCreateCaches(config.CachesDirectory, log);
LoadArchives(log, tracker);
}

Expand Down Expand Up @@ -128,7 +136,7 @@ public void LoadArchives(ILogger log, IProgressTracker tracker)
tracker.Finished++;

tracker.Focus("Extras", 1);
ExtraFile extras = Dat.Files.First(f => f.Name == "EXTRAS").CastTo<ExtraFile>();
Extra = Dat.Files.First(f => f.Name == "EXTRAS").CastTo<ExtraFile>();
tracker.Finished++;

BgTableFile bgTable = Dat.Files.First(f => f.Name == "BGTBLS").CastTo<BgTableFile>();
Expand All @@ -145,7 +153,7 @@ public void LoadArchives(ILogger log, IProgressTracker tracker)
{
name = $"{bgNameBackup}{j:D2}";
}
Items.Add(new BackgroundItem(name, i, entry, this, extras));
Items.Add(new BackgroundItem(name, i, entry, this));
}
tracker.Finished++;
}
Expand All @@ -154,7 +162,7 @@ public void LoadArchives(ILogger log, IProgressTracker tracker)
tracker.Focus("BGM Tracks", bgmFiles.Length);
for (int i = 0; i < bgmFiles.Length; i++)
{
Items.Add(new BackgroundMusicItem(bgmFiles[i], i, extras, this));
Items.Add(new BackgroundMusicItem(bgmFiles[i], i, this));
tracker.Finished++;
}

Expand Down Expand Up @@ -221,6 +229,25 @@ public void LoadArchives(ILogger log, IProgressTracker tracker)
}
}

public static void ClearOrCreateCaches(string cachesDirectory, ILogger log)
{
if (Directory.Exists(cachesDirectory))
{
Directory.Delete(cachesDirectory, true);
}

log.Log("Creating cache directory...");
Directory.CreateDirectory(cachesDirectory);

string bgmCache = Path.Combine(cachesDirectory, "bgm");
log.Log("Creating BGM cache...");
Directory.CreateDirectory(bgmCache);

string vceCache = Path.Combine(cachesDirectory, "vce");
log.Log("Creating voice file cache...");
Directory.CreateDirectory(vceCache);
}

public ItemDescription FindItem(string name)
{
return Items.FirstOrDefault(i => i.Name == name.Split(" - ")[0]);
Expand All @@ -239,7 +266,7 @@ public static Project OpenProject(string projFile, Config config, ILogger log, I
tracker.Focus($"{Path.GetFileNameWithoutExtension(projFile)} Project Data", 1);
Project project = JsonSerializer.Deserialize<Project>(File.ReadAllText(projFile));
tracker.Finished++;
project.Load(log, tracker);
project.Load(config, log, tracker);
return project;
}
catch (Exception exc)
Expand Down
6 changes: 5 additions & 1 deletion src/SerialLoops.Lib/SerialLoops.Lib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="HaruhiChokuretsuLib" Version="0.23.6" />
<PackageReference Include="BunLabs.NAudio.Flac" Version="2.0.1" />
<PackageReference Include="HaruhiChokuretsuLib" Version="0.24.1" />
<PackageReference Include="NAudio.Vorbis" Version="1.5.0" />
<PackageReference Include="NitroPacker.Core" Version="2.0.1" />
<PackageReference Include="NLayer" Version="1.14.0" />
<PackageReference Include="NLayer.NAudioSupport" Version="1.3.0" />
<PackageReference Include="QuikGraph" Version="2.5.0" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 83538c1

Please sign in to comment.