Skip to content

Instantly share code, notes, and snippets.

@LennardF1989
Last active January 18, 2022 15:29
Show Gist options
  • Save LennardF1989/a5d7d54c89cb6cd0e6bc9551b6fa6a48 to your computer and use it in GitHub Desktop.
Save LennardF1989/a5d7d54c89cb6cd0e6bc9551b6fa6a48 to your computer and use it in GitHub Desktop.
OggSongProcessor and FxcEffectProcessor for FNA and MGCB

Requirements

  • MonoGame.Framework.Content.Pipeline.dll from MonoGame
  • fxc.exe from the DirectX Software Development Kit

General usage

  1. Add a reference to MonoGame.Framework.Content.Pipeline.dll to your Class Library project
  2. Compile the 2 classes to a DLL
  3. Make sure fxc.exe is at the same location as the resulting DLL
  4. Add a reference to your .mgcb file, eg. /reference:ContentPipeline.dll, this can also be a relative path to the .mgcb file.
  5. Make sure your .mgcb file is set to the Windows platform: /platform:Windows
  6. Change all your songs to "Ogg Song - MonoGame"
  7. Change all your shaders to "Effect - Fxc"
  8. Compile your assets! The resulting files for the songs and effects will be compatible with FNA on any platform.

OggSongProcessor

This class will generate a .hash and a .ogg at the location of the asset included in the .mgbc file. This is because the output of .ogg files are not determinstic. Meaning given the same input, you get a different output every time you run the compiler. This is annoying since songs can be rather large in filesize, needlessly inflating patch sizes. In case of source control, be sure to commit these files as part of your assets! They will automatically update if you change the original song file.

FxcEffectProcessor

This class will generate a .fxc file at the location of the asset included in the .mgcb file. This is to ensure you can also run the compiler on other platforms than Windows, given you have compiled your assets on Windows once first. In case of source control, be sure to commit this file as part of your assets! It will automatically update if you change the original .fx file.

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
namespace ContentPipeline
{
[ContentProcessor(DisplayName = "Effect - Fxc")]
public class FxcEffectProcessor : EffectProcessor
{
public override CompiledEffectContent Process(EffectContent input, ContentProcessorContext context)
{
string compiledFile = Path.Combine(
Path.GetDirectoryName(input.Identity.SourceFilename),
string.Format("{0}.fxc", Path.GetFileNameWithoutExtension(input.Identity.SourceFilename))
);
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
{
if (File.Exists(compiledFile))
{
return new CompiledEffectContent(File.ReadAllBytes(compiledFile));
}
throw new InvalidContentException("Compiling on a non-Windows platform requires a precompiled effect!", input.Identity);
}
string compiledTempFile = string.Format("{0}.fxc", Path.GetTempFileName());
string toolPath = string.Format("{0}\\fxc.exe", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
Process process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = toolPath,
Arguments = string.Format("/T fx_2_0 \"{0}\" /Fo \"{1}\"", input.Identity.SourceFilename, compiledTempFile),
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
UseShellExecute = false
}
};
process.Start();
process.WaitForExit();
if (!File.Exists(compiledTempFile))
{
string output = process.StandardError.ReadToEnd();
throw new InvalidContentException(output, input.Identity);
}
byte[] buffer = File.ReadAllBytes(compiledTempFile);
File.WriteAllBytes(compiledFile, buffer);
return new CompiledEffectContent(buffer);
}
}
}
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Audio;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using MonoGame.Framework.Content.Pipeline.Builder;
namespace ContentPipeline
{
[ContentProcessor(DisplayName = "Ogg Song - MonoGame")]
public class OggSongProcessor : ContentProcessor<AudioContent, SongContent>
{
public ConversionQuality Quality { get; set; } = ConversionQuality.Best;
private static readonly FieldInfo _managerField = typeof(PipelineProcessorContext).GetField("_manager", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly ConstructorInfo _constructor = typeof(SongContent).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new [] { typeof(string), typeof(TimeSpan) }, null);
private static readonly SHA1Managed _sha1Managed = new SHA1Managed();
public override SongContent Process(AudioContent input, ContentProcessorContext context)
{
string outputFilename = Path.ChangeExtension(context.OutputFilename, "ogg");
string songContentFilename = PathHelper.GetRelativePath(Path.GetDirectoryName(context.OutputFilename) + Path.DirectorySeparatorChar, outputFilename);
context.AddOutputFile(outputFilename);
//Use the ogg-file as-is
if (Path.GetExtension(input.FileName) == "ogg")
{
File.Copy(input.FileName, outputFilename);
return (SongContent) _constructor.Invoke(new object[]
{
songContentFilename,
input.Duration
});
}
//Prepare some useful paths and checks
string hashFile = Path.ChangeExtension(input.FileName, "hash");
string oggFile = Path.ChangeExtension(input.FileName, "ogg");
bool oggFileExists = File.Exists(oggFile);
//Compare a previous hash, if there is one.
string currentHash = CalculateSHA1(input.FileName);
string previousHash = null;
if (File.Exists(hashFile) && oggFileExists)
{
previousHash = File.ReadAllText(hashFile);
}
else
{
File.WriteAllText(hashFile, currentHash);
}
//Determine if we can re-use a previously generated ogg-file
if (oggFileExists && previousHash == currentHash)
{
File.Copy(oggFile, outputFilename);
}
else
{
ConversionQuality conversionQuality = AudioProfile.ForPlatform(TargetPlatform.DesktopGL).ConvertStreamingAudio(TargetPlatform.DesktopGL, Quality, input, ref outputFilename);
if (Quality != conversionQuality)
{
context.Logger.LogMessage("Failed to convert using \"{0}\" quality, used \"{1}\" quality", Quality, conversionQuality);
}
if (oggFileExists)
{
File.Delete(oggFile);
}
File.Copy(outputFilename, oggFile);
}
return (SongContent)_constructor.Invoke(new object[]
{
songContentFilename,
input.Duration
});
}
private static string CalculateSHA1(string fileName)
{
using (var stream = new FileStream(fileName, FileMode.Open))
{
var hash = _sha1Managed.ComputeHash(stream);
return string.Join(string.Empty, hash.Select(b => b.ToString("X2")).ToArray()).ToLower();
}
}
}
}
@tomcashman
Copy link

The FxcEffectProcessor implementation doesn't support fxh imports correctly. If anyone else encounters this, update the Process method to the following to fix:

            string compiledFile = Path.Combine(
                Path.GetDirectoryName(input.Identity.SourceFilename),
                string.Format("{0}.fxc", Path.GetFileNameWithoutExtension(input.Identity.SourceFilename))
                );

            if (Environment.OSVersion.Platform != PlatformID.Win32NT)
            {
                if (File.Exists(compiledFile))
                {
                    return new CompiledEffectContent(File.ReadAllBytes(compiledFile));
                }

                throw new InvalidContentException("Compiling on a non-Windows platform requires a precompiled effect!", input.Identity);
            }

            string compiledTempFile = Path.GetTempFileName() + ".fxc";

            string toolPath = string.Format("{0}\\fxc.exe", Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
            if(!File.Exists(toolPath))
            {
                toolPath = "C:\\Program Files (x86)\\Microsoft DirectX SDK (June 2010)\\Utilities\\bin\\x64\\fxc.exe";
            }

            Process process = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    FileName = toolPath,
                    Arguments = string.Format("/T fx_2_0 \"{0}\" /Fo \"{1}\"", input.Identity.SourceFilename, compiledTempFile),
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    CreateNoWindow = true,
                    UseShellExecute = false
                }
            };

            process.Start();

            process.WaitForExit();

            if (!File.Exists(compiledTempFile))
            {
                string output = process.StandardError.ReadToEnd();

                throw new InvalidContentException(output, input.Identity);
            }

            byte[] buffer = File.ReadAllBytes(compiledTempFile);

            File.WriteAllBytes(compiledFile, buffer);

            return new CompiledEffectContent(buffer);

@LennardF1989
Copy link
Author

LennardF1989 commented Jan 18, 2022

@tomcashman Thanks for sharing!

EDIT: My bad, I forgot this was a bigger gist :P

EDIT: Ah, I see what you did, you got rid of the temp file and just use the original file location as a source,

EDIT: Updated, but without the hardcoded DX path. I would rather use the %DXSDK_DIR% environment variable for that, but then I need to decide on the proper architecture (x32 or x64)... so meh :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment