using FFmpeg.AutoGen;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace OpenVIII
{
#pragma warning disable IDE1006 // Naming Styles
public static class init_debugger_Audio
#pragma warning restore IDE1006 // Naming Styles
{
private static AV.Midi.DirectMedia dm_Midi;
private static AV.Midi.Fluid fluid_Midi;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
private struct SoundEntry
{
public UInt32 Size;
public UInt32 Offset;
private UInt32 output_TotalSize => Size + 70; // Total bytes of file -8 because for some reason 8 bytes don't count
private const UInt32 output_HeaderSize = 50; //Total bytes of Header
private UInt32 output_DataSize => Size; //Total bytes of Data Section
//public byte[] UNK; //12
//public WAVEFORMATEX WAVFORMATEX; //18 header starts here
//public ushort SamplesPerBlock; //2
//public ushort ADPCM; //2
//public ADPCMCOEFSET[] ADPCMCoefSets; //array should be of [ADPCM] size //7*4 = 28
public byte[] HeaderData;
public void fillHeader(BinaryReader br)
{
if (HeaderData == null)
{
HeaderData = new byte[output_HeaderSize + 28];
using (BinaryWriter bw = new BinaryWriter(new MemoryStream(HeaderData)))
{
bw.Write(System.Text.Encoding.ASCII.GetBytes("RIFF"));
bw.Write(output_TotalSize);
bw.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt "));
bw.Write(output_HeaderSize);
bw.Write(br.ReadBytes((int)output_HeaderSize));
bw.Write(System.Text.Encoding.ASCII.GetBytes("data"));
bw.Write(output_DataSize);
}
}
}
}
#pragma warning disable CS0649
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct WAVEFORMATEX
{
public ushort wFormatTag;
public ushort nChannels;
public uint nSamplesPerSec;
public uint nAvgBytesPerSec;
public ushort nBlockAlign;
public ushort wBitsPerSample;
public ushort cbSize;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
private struct ADPCMCOEFSET
{
public short iCoef1;
public short iCoef2;
};
#pragma warning restore CS0649
private static SoundEntry[] soundEntries;
public static int soundEntriesCount;
public const int MaxSoundChannels = 20;
///
/// This is for short lived sound effects. The Larger the array is the more sounds can be
/// played at once. If you want sounds to loop of have volume you'll need to have a
/// SoundEffectInstance added to ffcc, and have those sounds be played like music where they
/// loop in the background till stop.
///
public static AV.Audio[] SoundChannels { get; } = new AV.Audio[MaxSoundChannels];
public static int CurrentSoundChannel
{
get => _currentSoundChannel;
set
{
if (value >= MaxSoundChannels)
{
value = 0;
}
else if (value < 0)
{
value = MaxSoundChannels - 1;
}
_currentSoundChannel = value;
}
}
public static bool MusicPlaying => musicplaying;
public static void Init()
{
// PC 2000 version has an CD audio track for eyes on me. I don't think we can play that.
const int unkPrefix = 999;
const int altLoserPrefix = 512;
const int loserPrefix = 0;
const int eyesOnMePrefix = 513;
const int altEyesOnMePrefix = 22;
string[] ext = { ".ogg", ".sgt", ".wav", ".mp3" };
//Roses and Wine V07 moves most of the sgt files to dmusic_backup
//it leaves a few files behind. I think because RaW doesn't replace everything.
//ogg files stored in:
string RaW_ogg_pt = Extended.GetUnixFullPath(Path.Combine(Memory.FF8DIR, "RaW", "GLOBAL", "Music"));
// From what I gather the OGG files and the sgt files have the same numerical prefix. I
// might try to add the functionality to the debug screen monday.
string dmusic_pt = Extended.GetUnixFullPath(Path.Combine(Memory.FF8DIRdata, "Music", "dmusic_backup"));
string music_pt = Extended.GetUnixFullPath(Path.Combine(Memory.FF8DIRdata, "Music", "dmusic"));
string music_wav_pt = Extended.GetUnixFullPath(Path.Combine(Memory.FF8DIRdata, "Music"));
// goal of dicmusic is to be able to select a track by prefix. it adds an list of files
// with the same prefix. so you can later on switch out which one you want.
AddMusicPath(RaW_ogg_pt);
AddMusicPath(music_wav_pt);
AddMusicPath(dmusic_pt);
AddMusicPath(music_pt);
if (!Memory.dicMusic.ContainsKey(eyesOnMePrefix) && Memory.dicMusic.ContainsKey(altEyesOnMePrefix))
{
Memory.dicMusic.Add(eyesOnMePrefix, Memory.dicMusic[altEyesOnMePrefix]);
}
void AddMusicPath(string p)
{
if (!string.IsNullOrWhiteSpace(p) && Directory.Exists(p))
{
foreach (string m in Directory.GetFiles(p).Where(x => ext.Any(y => x.EndsWith(y, StringComparison.OrdinalIgnoreCase))))
{
AddMusic(m);
}
}
}
void AddMusic(string m)
{
if (ushort.TryParse(Path.GetFileName(m).Substring(0, 3), out ushort key))
{
//mismatched prefix's go here
if (key == altLoserPrefix)
{
key = loserPrefix; //loser.ogg and sgt don't match.
}
}
else if (m.IndexOf("eyes_on_me", StringComparison.OrdinalIgnoreCase) >= 0)
key = eyesOnMePrefix;
else
key = unkPrefix;
if (!Memory.dicMusic.ContainsKey(key))
{
Memory.dicMusic.Add(key, new List { m });
}
else
{
Memory.dicMusic[key].Add(m);
}
}
}
//I messed around here as figuring out how things worked probably didn't need to mess with this.
public static void Init_SoundAudio()
{
string path = Path.Combine(Memory.FF8DIRdata, "Sound", "audio.fmt");
if (File.Exists(path))
{
FileStream fs = null;
// fs is disposed by binary reader
using (BinaryReader br = new BinaryReader(fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
soundEntries = new SoundEntry[br.ReadUInt32()];
fs.Seek(36, SeekOrigin.Current);
for (int i = 0; i < soundEntries.Length - 1; i++)
{
uint sz = br.ReadUInt32();
if (sz == 0)
{
fs.Seek(34, SeekOrigin.Current); continue;
}
soundEntries[i] = new SoundEntry
{
Size = sz,
Offset = br.ReadUInt32()
};
fs.Seek(12, SeekOrigin.Current);
soundEntries[i].fillHeader(br);
}
fs = null;
}
}
soundEntriesCount = soundEntries == null ? 0 : soundEntries.Length;
////export sounds
//int item = 0;
//using (BinaryReader br = new BinaryReader(File.OpenRead(Path.Combine(Memory.FF8DIRdata, "Sound", "audio.dat"))))
// foreach (SoundEntry s in soundEntries)
// {
// Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "FF8Sounds"));
// if (s.HeaderData == null || s.Size == 0)
// {
// using (FileStream fc = File.Create(Path.Combine(Path.GetTempPath(), "FF8Sounds", $"{item++}.txt")))
// using (BinaryWriter bw = new BinaryWriter(fc))
// {
// bw.Write($"There is no info maybe audio.fmt listed {s.Size} size or there was an issue.");
// }
// continue;
// }
// using (FileStream fc = File.Create(Path.Combine(Path.GetTempPath(), "FF8Sounds",
// $"{item++}.wav"))) using (BinaryWriter bw = new BinaryWriter(fc)) {
// bw.Write(s.HeaderData); br.BaseStream.Seek(s.Offset, SeekOrigin.Begin);
// bw.Write(br.ReadBytes((int)s.Size)); } }
}
///
/// Play sound effect
///
///
/// ID number of sound
/// The real game uses soundID + 1, so you may need to -1 from any scripts.
///
///
/// If set sound will not be saved to SoundChannels
///
/// It will be up to the calling object to keep track of the sound object that is returned.
///
///
/// If loop, sound will loop from the set sample number.
public static AV.Audio PlaySound(int soundID, float volume = 1.0f, float pitch = 0.0f, float pan = 0.0f, bool persist = false, bool loop = false)
{
if (soundEntries == null || soundEntries[soundID].Size == 0)
{
return null;
}
AV.Audio ffcc = AV.Audio.Play(
new AV.BufferData {
DataSeekLoc = soundEntries[soundID].Offset,
DataSize = soundEntries[soundID].Size,
HeaderSize = (uint)soundEntries[soundID].HeaderData.Length,},
soundEntries[soundID].HeaderData, loop ? 0 : -1);
if (!persist)
SoundChannels[CurrentSoundChannel++] = ffcc;
ffcc.Play(volume, pitch, pan);
return ffcc;
}
public static void StopSound()
{
//waveout.Stop();
}
public static void Update()
{
//checks to see if music buffer is running low and getframe triggers a refill.
//if (ffccMusic != null && !ffccMusic.Ahead)
//{
// ffccMusic.Next();
//}
//if played in task we don't need to do this.
}
public static byte[] ReadFullyByte(Stream stream)
{
// following formula goal is to calculate the number of bytes to make buffer. might be wrong.
long size = stream.Length; // stream.Length should be in bytes. will error later if short.
int start = 0;
byte[] buffer = new byte[size];
int read = 0;
//do
//{
read = stream.Read(buffer, start, buffer.Length);
start++;
//}
//while (read == 0 && start < size);
if (read == 0)
{
return null;
}
if (read < size)
{
Array.Resize(ref buffer, read);
}
return buffer;
}
public static byte[] GetSamplesWaveData(float[] samples, int samplesCount)
{ // converts 32 bit float samples to 16 bit pcm. I think :P
// https://stackoverflow.com/questions/31957211/how-to-convert-an-array-of-int16-sound-samples-to-a-byte-array-to-use-in-monogam/42151979#42151979
byte[] pcm = new byte[samplesCount * 2];
int sampleIndex = 0,
pcmIndex = 0;
while (sampleIndex < samplesCount)
{
short outsample = (short)(samples[sampleIndex] * short.MaxValue);
pcm[pcmIndex] = (byte)(outsample & 0xff);
pcm[pcmIndex + 1] = (byte)((outsample >> 8) & 0xff);
sampleIndex++;
pcmIndex += 2;
}
return pcm;
}
private static bool musicplaying = false;
private static int lastplayed = -1;
public static void PlayStopMusic(ushort? index = null, float volume = 0.5f, float pitch = 0.0f, float pan = 0.0f)
{
if (!musicplaying || lastplayed != Memory.MusicIndex)
{
PlayMusic(index: index, volume: volume, pitch: pitch, pan: pan);
}
else
{
StopMusic();
}
}
private static AV.Audio ffccMusic = null; // testing using class to play music instead of Naudio / Nvorbis
private static int _currentSoundChannel;
public static void PlayMusic(ushort? index = null, float volume = 0.5f, float pitch = 0.0f, float pan = 0.0f, bool loop = true)
{
Memory.MusicIndex = index ?? Memory.MusicIndex;
if (musicplaying && lastplayed == Memory.MusicIndex) return;
string ext = "";
if (Memory.dicMusic.Count > 0 && Memory.dicMusic[Memory.MusicIndex].Count > 0)
{
ext = Path.GetExtension(Memory.dicMusic[Memory.MusicIndex][0]).ToLower();
}
else
return;
string pt = Memory.dicMusic[Memory.MusicIndex][0];
StopMusic();
switch (ext)
{
case ".ogg":
case ".wav":
default:
//ffccMusic = new Ffcc(@"c:\eyes_on_me.wav", AVMediaType.AVMEDIA_TYPE_AUDIO, Ffcc.FfccMode.STATE_MACH);
if (ffccMusic != null)
ffccMusic.Dispose();
ffccMusic = AV.Audio.Load(pt, loop ? 0 : -1);
if (!loop)
ffccMusic.LOOPSTART = -1;
ffccMusic.PlayInTask(volume, pitch, pan);
break;
case ".sgt":
#if _X64
if (fluid_Midi == null)
fluid_Midi = new AV.Midi.Fluid();
fluid_Midi.ReadSegmentFileManually(pt);
fluid_Midi.Play();
#else
if (Extended.IsLinux)
{
fluid_Midi.ReadSegmentFileManually(pt);
fluid_Midi.Play();
break;
}
else
{
if (dm_Midi == null)
dm_Midi = new AV.Midi.DirectMedia();
dm_Midi.Play(pt,loop);
}
#endif
break;
}
musicplaying = true;
lastplayed = Memory.MusicIndex;
}
public static void KillAudio()
{
SoundChannels.Where(x => x != null).ForEach(action => action.Dispose());
if (dm_Midi != null)
dm_Midi.Dispose();
if (fluid_Midi != null)
fluid_Midi.Dispose();
}
public static void StopMusic()
{
musicplaying = false;
if (ffccMusic != null)
{
ffccMusic.Dispose();
ffccMusic = null;
}
#if !_X64
if (dm_Midi != null)
dm_Midi.Stop();
#else
if (fluid_Midi != null)
fluid_Midi.Stop();
#endif
}
}
}