Skip to content
Closed
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
34 changes: 34 additions & 0 deletions Editors/Audio/ContextMenu/ExportCAVp8AsIvfCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.IO;
using Editors.Audio.Shared.Utilities;
using Shared.Core.Misc;
using Shared.Core.Services;
using Shared.Ui.BaseDialogs.PackFileTree;
using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands;

namespace Editors.Audio.ContextMenu
{
public class ExportCAVp8AsIvfCommand(IStandardDialogs standardDialogs, IFileSystemAccess fileSystemAccess) : IContextMenuCommand
{
public string GetDisplayName(TreeNode node) => "Export as IVF (without audio)";
public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null;
public bool IsEnabled(TreeNode node) => node.Item != null && node.Item.Name.EndsWith(".ca_vp8", StringComparison.OrdinalIgnoreCase);

public void Execute(TreeNode selectedNode)
{
var packFile = selectedNode.Item;
if (packFile == null)
return;

var dialogResult = standardDialogs.ShowSystemFolderBrowserDialog();
if (!dialogResult.Result || string.IsNullOrWhiteSpace(dialogResult.FolderPath))
return;

DirectoryHelper.EnsureCreated(dialogResult.FolderPath);

var ivfPath = Path.Combine(dialogResult.FolderPath, Path.ChangeExtension(packFile.Name, ".ivf"));
var ivfBytes = CAVp8Exporter.ExportToIvf(packFile);
fileSystemAccess.FileWriteAllBytes(ivfPath, ivfBytes);
}
}
}
40 changes: 40 additions & 0 deletions Editors/Audio/ContextMenu/ExportCAVp8AsWebMCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.IO;
using Editors.Audio.Shared.Storage;
using Editors.Audio.Shared.Utilities;
using Shared.Core.Misc;
using Shared.Core.PackFiles;
using Shared.Core.Services;
using Shared.Ui.BaseDialogs.PackFileTree;
using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu.Commands;

namespace Editors.Audio.ContextMenu
{
public class ExportCAVp8AsWebMCommand(
IPackFileService packFileService,
IAudioRepository audioRepository,
IStandardDialogs standardDialogs,
IFileSystemAccess fileSystemAccess) : IContextMenuCommand
{
public string GetDisplayName(TreeNode node) => "Export as WebM";
public bool ShouldAdd(TreeNode node) => node.NodeType == NodeType.File && node.Item != null;
public bool IsEnabled(TreeNode node) => node.Item != null && node.Item.Name.EndsWith(".ca_vp8", StringComparison.OrdinalIgnoreCase);

public void Execute(TreeNode selectedNode)
{
var packFile = selectedNode.Item;
if (packFile == null)
return;

var dialogResult = standardDialogs.ShowSystemFolderBrowserDialog();
if (!dialogResult.Result || string.IsNullOrWhiteSpace(dialogResult.FolderPath))
return;

DirectoryHelper.EnsureCreated(dialogResult.FolderPath);

var webMPath = Path.Combine(dialogResult.FolderPath, Path.ChangeExtension(packFile.Name, ".webm"));
var webMBytes = CAVp8Exporter.ExportToWebM(packFile, packFileService, audioRepository);
fileSystemAccess.FileWriteAllBytes(webMPath, webMBytes);
}
}
}
16 changes: 16 additions & 0 deletions Editors/Audio/DependencyInjectionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Editors.Audio.AudioExplorer;
using Editors.Audio.AudioProjectConverter;
using Editors.Audio.AudioProjectMerger;
using Editors.Audio.ContextMenu;
using Editors.Audio.DialogueEventMerger;
using Editors.Audio.Shared.AudioProject;
using Editors.Audio.Shared.AudioProject.Compiler;
Expand All @@ -31,6 +32,7 @@
using Shared.Core.DependencyInjection;
using Shared.Core.DevConfig;
using Shared.Core.ToolCreation;
using Shared.Ui.BaseDialogs.PackFileTree.ContextMenu;

namespace Editors.Audio
{
Expand Down Expand Up @@ -132,6 +134,11 @@ public override void Register(IServiceCollection serviceCollection)
serviceCollection.AddSingleton<BnkLoader>();
serviceCollection.AddSingleton<DatLoader>();

// Context menu
serviceCollection.AddScoped<ExportCAVp8AsWebMCommand>();
serviceCollection.AddScoped<ExportCAVp8AsIvfCommand>();
serviceCollection.AddSingleton<IPackFileContextMenuRegistration, AudioPackFileContextMenuRegistration>();

RegisterAllAsInterface<IDeveloperConfiguration>(serviceCollection, ServiceLifetime.Transient);
}

Expand All @@ -148,4 +155,13 @@ public override void RegisterTools(IEditorDatabase factory)
.Build(factory);
}
}

public class AudioPackFileContextMenuRegistration : IPackFileContextMenuRegistration
{
public void Register(PackFileContextMenuRegistry registry)
{
registry.RegisterPackFileContextMenuItem<ExportCAVp8AsWebMCommand>(ContextMenuType.MainApplication, path: "Export", priority: 20, ContextMenuCluster.Export);
registry.RegisterPackFileContextMenuItem<ExportCAVp8AsIvfCommand>(ContextMenuType.MainApplication, path: "Export", priority: 30, ContextMenuCluster.Export);
}
}
}
100 changes: 100 additions & 0 deletions Editors/Audio/Shared/Utilities/CAVp8Exporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Linq;
using Editors.Audio.Shared.GameInformation.Warhammer3;
using Editors.Audio.Shared.Storage;
using Editors.Audio.Shared.Wwise.HircExploration;
using Shared.Core.PackFiles;
using Shared.Core.PackFiles.Models;
using Shared.GameFormats.Audio.Codecs;
using Shared.GameFormats.Audio.Wav;
using Shared.GameFormats.Video;
using Shared.GameFormats.Wwise;
using Shared.GameFormats.Wwise.Hirc;
using Shared.GameFormats.Wwise.Wem.V132;
using Shared.GameFormats.Wwise.Wem.V132.Decoding;
using Shared.GameFormats.Wwise.Wem.V132.Encoding;

namespace Editors.Audio.Shared.Utilities
{
public static class CAVp8Exporter
{
public static byte[] ExportToWebM(PackFile packFile, IPackFileService packFileService, IAudioRepository audioRepository)
{
var caVp8File = new CAVp8File(packFile.DataSource.ReadData());
var vorbisAudio = GetVorbisAudio(packFile, packFileService, audioRepository);

var webMFile = new WebMFile
{
Width = caVp8File.Width,
Height = caVp8File.Height,
Framerate = caVp8File.Framerate,
FrameTable = caVp8File.FrameTable,
FrameData = caVp8File.FrameData,
VorbisCodecPrivate = vorbisAudio.VorbisCodecPrivateData,
VorbisAudioPackets = vorbisAudio.Packets,
VorbisSampleRate = checked((int)vorbisAudio.SampleRate),
VorbisChannels = vorbisAudio.Channels,
};

return webMFile.WriteData();
}

public static byte[] ExportToWav(PackFile packFile, IPackFileService packFileService, IAudioRepository audioRepository)
{
var vorbisAudio = GetVorbisAudio(packFile, packFileService, audioRepository);
var pcmAudio = vorbisAudio.ToPcm();
var wavFile = new WavFile { Audio = pcmAudio };
return wavFile.WriteData();
}

public static byte[] ExportToWem(PackFile packFile, IPackFileService packFileService, IAudioRepository audioRepository)
{
var wavBytes = ExportToWav(packFile, packFileService, audioRepository);
var wemFile = WemFile.CreateFromWavBytes(wavBytes);
return wemFile.WriteData();
}

public static byte[] ExportToIvf(PackFile packFile)
{
var caVp8File = new CAVp8File(packFile.DataSource.ReadData());
var ivfFile = new IvfFile(caVp8File);
return ivfFile.WriteData();
}

private static VorbisAudio GetVorbisAudio(PackFile packFile, IPackFileService packFileService, IAudioRepository audioRepository)
{
var wemBytes = GetWem(packFile, packFileService, audioRepository);
var wemFile = WemFile.CreateFromBytes(wemBytes);
var codebookLibrary = new WwiseCodebookLibrary();
var decoder = new WemVorbisDecoder(codebookLibrary);
return decoder.Decode(wemFile);
}

private static byte[] GetWem(PackFile packFile, IPackFileService packFileService, IAudioRepository audioRepository)
{
audioRepository.Load(Wh3LanguageInformation.GetAllLanguages());

var movieFilePath = packFileService.GetFullPath(packFile);
var actionEventName = Wh3ActionEventInformation.GetMovieActionEventName(movieFilePath);
var actionEventId = WwiseHash.Compute(actionEventName);

var actionEventHircs = audioRepository.GetHircs(actionEventId);
if (actionEventHircs.Count == 0)
throw new Exception($"Cannot find Action Event: {actionEventName}.");

var hircTreeChildrenParser = new HircTreeChildrenParser(audioRepository);
var nodes = hircTreeChildrenParser.BuildHierarchyAsFlatList(actionEventHircs.First());

var soundNode = nodes.FirstOrDefault(node => node.Hirc is ICAkSound);
if (soundNode == null)
throw new Exception($"Cannot find a Sound node for Action Event: {actionEventName}.");

var sourceId = ((ICAkSound)soundNode.Hirc).GetSourceId();
var wemFile = audioRepository.FindWem(sourceId.ToString());
if (wemFile == null)
throw new Exception($"Cannot find {sourceId}.wem");

return wemFile.DataSource.ReadData();
}
}
}
167 changes: 167 additions & 0 deletions Shared/GameFiles/Video/CAVp8File.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System.Text;
using Shared.ByteParsing;

namespace Shared.GameFormats.Video
{
public class CAVp8ExtraHeaderData
{
public byte UnknownByte { get; set; }
public uint UnknownUInt32First { get; set; }
public uint UnknownUInt32Second { get; set; }
}

public class CAVp8File
{
private const string Signature = "CAMV";
private const ushort HeaderLengthV0 = 40;
private const ushort HeaderLengthV1 = 40;

public ushort Version { get; set; }
public string CodecFourCC { get; set; } = "VP80";
public ushort Width { get; set; }
public ushort Height { get; set; }
public uint NumberOfFrames { get; set; }
public float Framerate { get; set; }
public CAVp8ExtraHeaderData? ExtraData { get; set; }
public List<FrameTableRecord> FrameTable { get; set; } = [];
public byte[] FrameData { get; set; } = [];

public CAVp8File(byte[] data)
{
ArgumentNullException.ThrowIfNull(data);
ReadData(new ByteChunk(data));
}

private void ReadData(ByteChunk chunk)
{
var bytes = chunk.ReadBytes(chunk.BytesLeft);
using var reader = new BinaryReader(new MemoryStream(bytes));

var signature = Encoding.ASCII.GetString(reader.ReadBytes(4));
if (signature != Signature)
throw new Exception($"CA_VP8 signature mismatch: expected '{Signature}' but got '{signature}'.");

Version = reader.ReadUInt16();
var rawHeaderLength = reader.ReadUInt16();
CodecFourCC = Encoding.ASCII.GetString(reader.ReadBytes(4));
Width = reader.ReadUInt16();
Height = reader.ReadUInt16();
var msPerFrame = reader.ReadSingle();
reader.ReadUInt32(); // No idea, always 1.
var numFramesMinusOne = reader.ReadUInt32(); // Same as num_frames, but sometimes is num_frames - 1. When it's the same, there are 9 extra bytes in the header.
var offsetFrameTable = reader.ReadUInt32();
NumberOfFrames = reader.ReadUInt32();
reader.ReadUInt32(); // Largest frame's size, in bytes. Recalculated on save.

if (numFramesMinusOne == NumberOfFrames)
{
ExtraData = new CAVp8ExtraHeaderData
{
UnknownByte = reader.ReadByte(),
UnknownUInt32First = reader.ReadUInt32(),
UnknownUInt32Second = reader.ReadUInt32(),
};
}

var expectedHeaderEnd = (long)(rawHeaderLength + 8);
if (reader.BaseStream.Position != expectedHeaderEnd)
throw new Exception($"CA_VP8 header size mismatch: expected stream to be at position {expectedHeaderEnd} after reading the header, but it is at position {reader.BaseStream.Position}.");

var frameDataLength = (int)(offsetFrameTable - reader.BaseStream.Position);
FrameData = reader.ReadBytes(frameDataLength);

var totalFileLength = reader.BaseStream.Length;
var frameTableLength = totalFileLength - reader.BaseStream.Position;
var hasBells = frameTableLength / 13 == NumberOfFrames && frameTableLength % 13 == 0;

var runningOffset = 0u;
FrameTable = new List<FrameTableRecord>((int)NumberOfFrames);
using var frameTableDecoded = new MemoryStream();

for (var frameIndex = 0; frameIndex < NumberOfFrames; frameIndex++)
{
var frameOffsetReal = reader.ReadUInt32();
var frameSize = reader.ReadUInt32();
if (hasBells)
reader.ReadUInt32();
var isKeyFrame = reader.ReadBoolean();

var frame = new FrameTableRecord
{
Offset = runningOffset,
Size = frameSize,
IsKeyFrame = isKeyFrame,
};

runningOffset += frame.Size;
FrameTable.Add(frame);

var frameOffsetRealEnd = frameOffsetReal + frameSize;
if (frameOffsetRealEnd > totalFileLength)
throw new Exception($"CA_VP8 frame {frameIndex} has an incorrect or unknown frame size: it would end at file offset {frameOffsetRealEnd} which is beyond the end of the file at {totalFileLength}.");

var savedPosition = reader.BaseStream.Position;
reader.BaseStream.Seek(frameOffsetReal, SeekOrigin.Begin);
frameTableDecoded.Write(reader.ReadBytes((int)frameSize));
reader.BaseStream.Seek(savedPosition, SeekOrigin.Begin);
}

if (reader.BaseStream.Position != totalFileLength)
throw new Exception($"CA_VP8 file size mismatch: expected {totalFileLength} bytes but stream is at position {reader.BaseStream.Position}.");

Framerate = 1000f / msPerFrame;
}

public byte[] WriteData()
{
using var memStream = new MemoryStream();
using var writer = new BinaryWriter(memStream);

ushort headerLength;
if (Version == 0)
headerLength = ExtraData != null ? (ushort)(HeaderLengthV0 + 9) : HeaderLengthV0;
else
headerLength = ExtraData != null ? (ushort)(HeaderLengthV1 + 9) : HeaderLengthV1;

var rawHeaderLength = (ushort)(headerLength - 8);

writer.Write(Encoding.ASCII.GetBytes(Signature));
writer.Write(Version);
writer.Write(rawHeaderLength);
writer.Write(Encoding.ASCII.GetBytes(CodecFourCC));
writer.Write(Width);
writer.Write(Height);
writer.Write(1000f / Framerate);
writer.Write((uint)1);

if (ExtraData != null || NumberOfFrames == 0)
writer.Write(NumberOfFrames);
else
writer.Write(NumberOfFrames - 1);

writer.Write((uint)(headerLength + FrameData.Length));
writer.Write(NumberOfFrames);
writer.Write(FrameTable.Max(frame => frame.Size));

if (ExtraData != null)
{
writer.Write(ExtraData.UnknownByte);
writer.Write(ExtraData.UnknownUInt32First);
writer.Write(ExtraData.UnknownUInt32Second);
}

writer.Write(FrameData);

var runningOffset = ExtraData != null ? (uint)rawHeaderLength : (uint)headerLength;
foreach (var frame in FrameTable)
{
writer.Write(runningOffset);
writer.Write(frame.Size);
writer.Write(frame.IsKeyFrame);
runningOffset += frame.Size;
}

return memStream.ToArray();
}
}
}
4 changes: 4 additions & 0 deletions Shared/GameFiles/Video/ClusterBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace Shared.GameFormats.Video
{
public record ClusterBlock(long TimestampMs, byte[] Data);
}
Loading
Loading