Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions O2JamConverter/Encoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Diagnostics;
using TagLib;

namespace O2JamConverter
{
public static class Encoder
{
public static bool EncodeToMp3(string wavPath, string mp3Path)
{
// We assume 'lame' is in the system's PATH.
// Using -V 2 for high quality VBR, a good default.
string arguments = $"-V 2 \"{wavPath}\" \"{mp3Path}\"";
try
{
using (var process = new Process())
{
process.StartInfo = new ProcessStartInfo
{
FileName = "lame",
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};

process.Start();
string stdOut = process.StandardOutput.ReadToEnd();
string stdErr = process.StandardError.ReadToEnd();
process.WaitForExit();

if (process.ExitCode != 0)
{
Console.WriteLine("LAME encoder failed.");
Console.WriteLine("STDOUT: " + stdOut);
Console.WriteLine("STDERR: " + stdErr);
return false;
}
return true;
}
}
catch (Exception ex)
{
Console.WriteLine("Failed to run LAME encoder. Is 'lame' installed and in your PATH?");
Console.WriteLine(ex.Message);
return false;
}
}

public static void TagMp3(string mp3Path, MusicHeader header)
{
try
{
var file = TagLib.File.Create(mp3Path);
file.Tag.Title = header.Title;
file.Tag.Performers = new[] { header.Artist };
file.Tag.AlbumArtists = new[] { header.Charter }; // Using AlbumArtists for the charter.
file.Tag.Genres = new[] { MapGenre(header.NewGenreCode) };
// file.Tag.Track = (uint)header.NewSongID; // NewSongID can be 0, which is invalid for Track.

file.Tag.Comment = "Generated by O2JamConverter";

file.Save();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to write tags to {mp3Path}: {ex.Message}");
}
}

private static string MapGenre(uint genreCode)
{
// This mapping is from the C++ source code.
string[] genres = { "Ballad", "Rock", "Dance", "Techno", "Hip-hop", "Soul/R&B", "Jazz", "Funk", "Classical", "Traditional", "Etc" };
if (genreCode < genres.Length)
{
return genres[genreCode];
}
return "Etc";
}
}
}
65 changes: 65 additions & 0 deletions O2JamConverter/FileFormats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace O2JamConverter
{
// Corresponds to the MusicHeader struct in the C++ project.
// Note: The char arrays (strings) will be handled during binary reading.
public class MusicHeader
{
public uint NewSongID;
public uint FileSignature;
public float NewEncVersion;
public uint NewGenreCode;
public float Tempo;
public ushort[] Level { get; set; } = new ushort[3];
public uint[] NumEvents { get; set; } = new uint[3];
public uint[] NumNotes { get; set; } = new uint[3];
public uint[] NumMeasures { get; set; } = new uint[3];
public uint[] NumNoteSets { get; set; } = new uint[3];
public ushort OldEncVersion;
public ushort OldSongID;
public string OldGenre { get; set; } = "";
public uint OldCoverArtSize;
public float NoteChartVersion;
public string Title { get; set; } = "";
public string Artist { get; set; } = "";
public string Charter { get; set; } = "";
public string OJMFile { get; set; } = "";
public uint NewCoverArtSize;
public uint[] Duration { get; set; } = new uint[3];
public uint[] DataOffset { get; set; } = new uint[4];
}

// Base class for musical events.
public abstract class Event
{
public uint Measure;
public uint Grid;
public float Time;
public bool IsApplied;
}

// Represents a tempo change event.
public class TempoEvent : Event
{
public float Value;
}

// Represents a note/sound event.
public class SoundEvent : Event
{
public ushort RefID;
public sbyte Volume;
public sbyte Pan;
public byte NoteType;
}

// Represents a decoded audio sample.
public class Sample
{
public ushort RefID;
public uint Filesize;
public byte BankType;
public string Name { get; set; } = "";
// In the C# version, this will hold the raw WAV data.
public byte[]? AudioData { get; set; }
}
}
65 changes: 65 additions & 0 deletions O2JamConverter/MusicRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace O2JamConverter
{
public static class MusicRenderer
{
public static void RenderToFile(string outputPath, List<SoundEvent> soundEvents, List<Sample> samples)
{
if (!soundEvents.Any())
{
Console.WriteLine("No sound events to render.");
return;
}

// Define a standard output format. NAudio's mixer will resample inputs to match.
var outputFormat = new WaveFormat(44100, 16, 2);
var mixer = new MixingSampleProvider(outputFormat);

// Create a dictionary of sample data for quick lookup.
var sampleData = samples.Where(s => s.AudioData != null)
.ToDictionary(s => s.RefID, s => s.AudioData);

foreach (var soundEvent in soundEvents)
{
if (sampleData.TryGetValue(soundEvent.RefID, out var audioBytes))
{
// For each event, create a new reader from the sample's byte array.
var reader = new WaveFileReader(new MemoryStream(audioBytes!));

// Convert to a sample provider. The mixer will handle resampling if needed.
ISampleProvider sampleProvider = reader.ToSampleProvider();

// Apply volume and pan if the data is available in the event.
// Note: The original C++ code didn't seem to use volume/pan, but we support it here.
if (soundEvent.Volume != 0 || soundEvent.Pan != 0)
{
var panningProvider = new PanningSampleProvider(sampleProvider);
// Pan is -1 (left) to 1 (right). O2Jam is -100 to 100? Let's assume a simple mapping.
panningProvider.Pan = Math.Max(-1.0f, Math.Min(1.0f, soundEvent.Pan / 100.0f));
sampleProvider = panningProvider;

// Volume is not directly available in ISampleProvider, but we can wrap it.
// We'll skip volume for now to keep it simple, as it wasn't used in the C++ source.
}

// Schedule the sample to be played at the correct time.
var offsetProvider = new OffsetSampleProvider(sampleProvider)
{
DelayBy = TimeSpan.FromMilliseconds(soundEvent.Time)
};

mixer.AddMixerInput(offsetProvider);
}
}

// Render the mixed audio to a 16-bit WAV file.
WaveFileWriter.CreateWaveFile16(outputPath, mixer);
}
}
}
17 changes: 17 additions & 0 deletions O2JamConverter/O2JamConverter.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.9" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>

</Project>
Loading