From b3857db929fad0a0d0c331175a5d13a15eb682d2 Mon Sep 17 00:00:00 2001 From: dexy Date: Sat, 7 Mar 2020 07:00:35 +1100 Subject: [PATCH] Added stereo playback option in AwcForm --- .../GameFiles/FileTypes/AwcFile.cs | 25 +- CodeWalker/Forms/AwcForm.cs | 30 +- CodeWalker/Utils/AudioUtils.cs | 281 ++++++++++++++---- 3 files changed, 275 insertions(+), 61 deletions(-) diff --git a/CodeWalker.Core/GameFiles/FileTypes/AwcFile.cs b/CodeWalker.Core/GameFiles/FileTypes/AwcFile.cs index 04b6483..7f0cc16 100644 --- a/CodeWalker.Core/GameFiles/FileTypes/AwcFile.cs +++ b/CodeWalker.Core/GameFiles/FileTypes/AwcFile.cs @@ -37,6 +37,9 @@ namespace CodeWalker.GameFiles public AwcStream[] Streams { get; set; } public AwcStream MultiChannelSource { get; set; } + public Dictionary StreamDict { get; set; } + + static public void Decrypt_RSXXTEA(byte[] data, uint[] key) { // Rockstar's modified version of XXTEA @@ -212,6 +215,8 @@ namespace CodeWalker.GameFiles Streams = streams.ToArray(); MultiChannelSource?.AssignMultiChannelSources(Streams); + + BuildStreamDict(); } private void Write(DataWriter w) @@ -343,6 +348,7 @@ namespace CodeWalker.GameFiles BuildStreamInfos(); + BuildStreamDict(); } public static void WriteXmlNode(AwcFile f, StringBuilder sb, int indent, string wavfolder, string name = "AudioWaveContainer") { @@ -461,6 +467,15 @@ namespace CodeWalker.GameFiles } + public void BuildStreamDict() + { + StreamDict = new Dictionary(); + if (Streams == null) return; + foreach (var stream in Streams) + { + StreamDict[stream.Hash] = stream; + } + } } public enum AwcCodecType @@ -538,6 +553,7 @@ namespace CodeWalker.GameFiles public AwcGranularLoopsChunk GranularLoopsChunk { get; set; } public AwcStreamFormatChunk StreamFormatChunk { get; set; } public AwcSeekTableChunk SeekTableChunk { get; set; } + public AwcStream[] ChannelStreams { get; set; } public AwcStream StreamSource { get; set; } public AwcStreamFormat StreamFormat { get; set; } public AwcStreamDataBlock[] StreamBlocks { get; set; } @@ -646,6 +662,11 @@ namespace CodeWalker.GameFiles { if (FormatChunk != null) return (float)FormatChunk.Samples / FormatChunk.SamplesPerSecond; if (StreamFormat != null) return (float)StreamFormat.Samples / StreamFormat.SamplesPerSecond; + if ((StreamFormatChunk != null) && (StreamFormatChunk.Channels?.Length > 0)) + { + var chan = StreamFormatChunk.Channels[0]; + return (float)chan.Samples / chan.SamplesPerSecond; + } return 0; } } @@ -965,6 +986,7 @@ namespace CodeWalker.GameFiles public void AssignMultiChannelSources(AwcStream[] streams) { + var cstreams = new List(); for (int i = 0; i < (streams?.Length ?? 0); i++) { var stream = streams[i]; @@ -990,9 +1012,10 @@ namespace CodeWalker.GameFiles stream.StreamSource = this; stream.StreamFormat = (srcind < chancnt) ? StreamFormatChunk.Channels[srcind] : null; stream.StreamChannelIndex = srcind; + cstreams.Add(stream); } - } + ChannelStreams = cstreams.ToArray(); } public void CompactMultiChannelSources(AwcStream[] streams) diff --git a/CodeWalker/Forms/AwcForm.cs b/CodeWalker/Forms/AwcForm.cs index 498b3cb..ccb5327 100644 --- a/CodeWalker/Forms/AwcForm.cs +++ b/CodeWalker/Forms/AwcForm.cs @@ -106,8 +106,11 @@ namespace CodeWalker.Forms strlist.Sort((a, b) => a.Name.CompareTo(b.Name)); foreach (var audio in strlist) { - if (audio.StreamBlocks != null) continue;//don't display multichannel source audios - var item = PlayListView.Items.Add(audio.Name); + var stereo = (audio.ChannelStreams?.Length == 2); + if ((audio.StreamBlocks != null) && (!stereo)) continue;//don't display multichannel source audios + var name = audio.Name; + if (stereo) name = "(Stereo Playback)"; + var item = PlayListView.Items.Add(name); item.SubItems.Add(audio.Type); item.SubItems.Add(audio.LengthStr); item.SubItems.Add(TextUtil.GetBytesReadable(audio.ByteLength)); @@ -170,11 +173,23 @@ namespace CodeWalker.Forms { var item = PlayListView.SelectedItems[0]; var audio = item.Tag as AwcStream; - - if ((audio?.FormatChunk != null) || (audio?.StreamFormat != null)) + var stereo = (audio?.ChannelStreams?.Length == 2); + if (stereo) { + var name0 = audio.ChannelStreams[0].Name; + var left0 = name0.EndsWith("left") || name0.EndsWith(".l"); + var f0 = left0 ? 1.0f : 0.0f; + var f1 = left0 ? 0.0f : 1.0f; + Player.LoadAudio(audio.ChannelStreams); Player.SetVolume(VolumeTrackBar.Value / 100.0f); + Player.SetOutputMatrix(0, f0, f1); + Player.SetOutputMatrix(1, f1, f0); + Player.Play(); + } + else if ((audio?.FormatChunk != null) || (audio?.StreamFormat != null)) + { Player.LoadAudio(audio); + Player.SetVolume(VolumeTrackBar.Value / 100.0f); Player.Play(); } else if (audio.MidiChunk != null) @@ -360,6 +375,13 @@ namespace CodeWalker.Forms { var item = PlayListView.SelectedItems[0]; var audio = item.Tag as AwcStream; + var stereo = (audio?.ChannelStreams?.Length == 2); + + if (stereo) + { + MessageBox.Show("Sorry, export for stereo output is not currently available."); + return; + } var ext = ".wav"; if (audio?.MidiChunk != null) diff --git a/CodeWalker/Utils/AudioUtils.cs b/CodeWalker/Utils/AudioUtils.cs index a572e2f..a121c45 100644 --- a/CodeWalker/Utils/AudioUtils.cs +++ b/CodeWalker/Utils/AudioUtils.cs @@ -16,14 +16,19 @@ namespace CodeWalker.Utils public class AudioPlayer { - - private AwcStream currentAudio; - private Stream wavStream; - private SoundStream soundStream; private XAudio2 xAudio2; private MasteringVoice masteringVoice; - private AudioBuffer audioBuffer; - private SourceVoice sourceVoice; + + public class AudioVoice + { + public AwcStream audio; + public SoundStream soundStream; + public AudioBuffer audioBuffer; + public SourceVoice sourceVoice; + public float[] outputMatrix = new[] { 1.0f, 1.0f }; //left/right channel output levels + public float trackLength; + } + private AudioVoice[] voices = new AudioVoice[0]; public enum PlayerState { Stopped, Playing, Paused }; public PlayerState State { get; private set; } = PlayerState.Stopped; @@ -35,7 +40,6 @@ namespace CodeWalker.Utils private float volume = 1.0f; - public int PlayTimeMS { get @@ -52,32 +56,7 @@ namespace CodeWalker.Utils } - public void LoadAudio(AwcStream audio) - { - if (audio != currentAudio) - { - CloseStreams(); - - currentAudio = audio; - trackLength = audio.Length; - wavStream = audio.GetWavStream(); - soundStream = new SoundStream(wavStream); - } - } - - private void CloseStreams() - { - if (soundStream != null) - { - soundStream.Close(); - } - if (wavStream != null) - { - wavStream.Close();//is this necessary? - } - } - - private void InitializeAudio(float playBegin = 0) + public void LoadAudio(params AwcStream[] audios) { if (xAudio2 == null) { @@ -85,17 +64,49 @@ namespace CodeWalker.Utils masteringVoice = new MasteringVoice(xAudio2); } - wavStream.Position = 0; - soundStream.Position = 0; - audioBuffer = new AudioBuffer + if ((voices == null) || (voices.Length != audios.Length)) { - Stream = soundStream.ToDataStream(), - AudioBytes = (int)soundStream.Length, - Flags = BufferFlags.EndOfStream - }; + voices = new AudioVoice[audios.Length]; + for (int i = 0; i < audios.Length; i++) + { + voices[i] = new AudioVoice(); + } + } + + trackLength = 0; + for (int i = 0; i < audios.Length; i++) + { + var voice = voices[i]; + var audio = audios[i]; + if (audio != voice.audio) + { + voice.audio = audio; + voice.trackLength = audio.Length; + trackLength = Math.Max(trackLength, voice.trackLength); + var wavStream = audio.GetWavStream(); + var soundStream = new SoundStream(wavStream); + voice.soundStream = soundStream; + voice.audioBuffer = new AudioBuffer + { + Stream = soundStream.ToDataStream(), + AudioBytes = (int)soundStream.Length, + Flags = BufferFlags.EndOfStream + }; + soundStream.Close(); + wavStream.Close(); + } + } + + } + + private void CreateSourceVoices(float playBegin = 0) + { if (playBegin > 0) { - audioBuffer.PlayBegin = (int)(soundStream.Format.SampleRate * playBegin) / 128 * 128; + foreach (var voice in voices) + { + voice.audioBuffer.PlayBegin = (int)(voice.soundStream.Format.SampleRate * playBegin) / 128 * 128; + } if (playtimer.IsRunning) { playtimer.Restart(); @@ -112,10 +123,15 @@ namespace CodeWalker.Utils } trackFinished = false; - sourceVoice = new SourceVoice(xAudio2, soundStream.Format, true); - sourceVoice.SubmitSourceBuffer(audioBuffer, soundStream.DecodedPacketsInfo); - sourceVoice.BufferEnd += (context) => trackFinished = true; - sourceVoice.SetVolume(volume); + foreach (var voice in voices) + { + var sourceVoice = new SourceVoice(xAudio2, voice.soundStream.Format, true); + sourceVoice.SubmitSourceBuffer(voice.audioBuffer, voice.soundStream.DecodedPacketsInfo); + sourceVoice.BufferEnd += (context) => trackFinished = true; + sourceVoice.SetVolume(volume); + sourceVoice.SetOutputMatrix(1, 2, voice.outputMatrix); + voice.sourceVoice = sourceVoice; + } } private void SetPlayerState(PlayerState newState) @@ -148,18 +164,35 @@ namespace CodeWalker.Utils volume = v; if (State == PlayerState.Playing) { - sourceVoice.SetVolume(v); + foreach (var voice in voices) + { + voice.sourceVoice.SetVolume(v); + } + } + } + + public void SetOutputMatrix(int v, float l, float r) + { + var voice = voices[v]; + voice.outputMatrix[0] = l; + voice.outputMatrix[1] = r; + if (State == PlayerState.Playing) + { + voice.sourceVoice.SetOutputMatrix(1, 2, voice.outputMatrix); } } public void DisposeAudio() { - CloseStreams(); if (xAudio2 != null) { masteringVoice.Dispose(); xAudio2.Dispose(); } + foreach (var voice in voices) + { + voice?.audioBuffer?.Stream?.Dispose(); + } } @@ -167,9 +200,11 @@ namespace CodeWalker.Utils { if (State != PlayerState.Stopped) { - sourceVoice.DestroyVoice(); - sourceVoice.Dispose(); - audioBuffer.Stream.Dispose(); + foreach (var voice in voices) + { + voice.sourceVoice.DestroyVoice(); + voice.sourceVoice.Dispose(); + } SetPlayerState(PlayerState.Stopped); } } @@ -177,8 +212,11 @@ namespace CodeWalker.Utils public void Play(float playBegin = 0) { Stop(); - InitializeAudio(playBegin); - sourceVoice.Start(); + CreateSourceVoices(playBegin); + foreach (var voice in voices) + { + voice.sourceVoice.Start(); + } SetPlayerState(PlayerState.Playing); } @@ -192,7 +230,7 @@ namespace CodeWalker.Utils { var state = State; Stop(); - InitializeAudio(playBegin); + CreateSourceVoices(playBegin); State = state; } } @@ -201,7 +239,10 @@ namespace CodeWalker.Utils { if (State == PlayerState.Playing) { - sourceVoice.Stop(); + foreach (var voice in voices) + { + voice.sourceVoice.Stop(); + } SetPlayerState(PlayerState.Paused); } } @@ -210,14 +251,142 @@ namespace CodeWalker.Utils { if (State == PlayerState.Paused) { - sourceVoice.Start(); + foreach (var voice in voices) + { + voice.sourceVoice.Start(); + } SetPlayerState(PlayerState.Playing); } } + } + + + + public class AudioDatabase + { + + public bool IsInited { get; set; } + + public Dictionary SoundsDB { get; set; } + public Dictionary GameDB { get; set; } + public Dictionary ContainerDB { get; set; } + + public void Init(GameFileCache gameFileCache, bool sounds = true, bool game = true) + { + + + var rpfman = gameFileCache.RpfMan; + + var datrelentries = new Dictionary(); + var awcentries = new Dictionary(); + void addRpfDatRels(RpfFile rpffile) + { + if (rpffile.AllEntries == null) return; + foreach (var entry in rpffile.AllEntries) + { + if (entry is RpfFileEntry) + { + var fentry = entry as RpfFileEntry; + //if (entry.NameLower.EndsWith(".rel")) + //{ + // datrels[entry.NameHash] = fentry; + //} + if (sounds && entry.NameLower.EndsWith(".dat54.rel")) + { + datrelentries[entry.NameHash] = fentry; + } + if (game && entry.NameLower.EndsWith(".dat151.rel")) + { + datrelentries[entry.NameHash] = fentry; + } + } + } + } + void addRpfAwcs(RpfFile rpffile) + { + if (rpffile.AllEntries == null) return; + foreach (var entry in rpffile.AllEntries) + { + if (entry is RpfFileEntry) + { + var fentry = entry as RpfFileEntry; + if (entry.NameLower.EndsWith(".awc")) + { + var shortname = entry.GetShortNameLower(); + var parentname = entry.Parent?.GetShortNameLower() ?? ""; + if (string.IsNullOrEmpty(parentname) && (entry.Parent?.File != null)) + { + parentname = entry.Parent.File.NameLower; + int ind = parentname.LastIndexOf('.'); + if (ind > 0) + { + parentname = parentname.Substring(0, ind); + } + } + var contname = parentname + "/" + shortname; + var hash = JenkHash.GenHash(contname); + awcentries[hash] = fentry; + } + } + } + } + + var audrpf = rpfman.FindRpfFile("x64\\audio\\audio_rel.rpf"); + if (audrpf != null) + { + addRpfDatRels(audrpf); + } + foreach (var baserpf in gameFileCache.BaseRpfs) + { + addRpfAwcs(baserpf); + } + if (gameFileCache.EnableDlc) + { + var updrpf = rpfman.FindRpfFile("update\\update.rpf"); + if (updrpf != null) + { + addRpfDatRels(updrpf); + } + foreach (var dlcrpf in gameFileCache.DlcActiveRpfs) //load from current dlc rpfs + { + addRpfDatRels(dlcrpf); + addRpfAwcs(dlcrpf); + } + } + + + + var soundsdb = new Dictionary(); + var gamedb = new Dictionary(); + foreach (var datentry in datrelentries.Values) + { + var relfile = rpfman.GetFile(datentry); + if (relfile?.RelDatas != null) + { + foreach (var rd in relfile.RelDatas) + { + if (rd is Dat54Sound sd) + { + soundsdb[sd.NameHash] = sd; + } + else if (rd is Dat151RelData gd) + { + gamedb[gd.NameHash] = gd; + } + } + } + } + + ContainerDB = awcentries; + if (sounds) SoundsDB = soundsdb; + if (game) GameDB = gamedb; + + IsInited = true; + } + } - }