using System;
using System.Collections.Generic;
using System.IO;
using OpenTK.Audio;
using OpenTK.Audio.OpenAL;
public class AudioEngine : IDisposable
{
public class Voice
{
public string Path;
public int Buffer;
public int Source;
public float Gain;
}
private AudioContext _context;
private readonly Dictionary<string, Voice> _voices =
new Dictionary<string, Voice>(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new object();
public event Action<string> Log;
public bool Init()
{
try
{
_context = new AudioContext(); // opens default device + context
_context.MakeCurrent();
Emit("OpenAL ready: " + AudioContext.CurrentContext.CurrentDevice);
return true;
}
catch (Exception ex)
{
Emit("OpenAL init failed: " + ex.Message);
return false;
}
}
/// Load + start looping at gain 0. Idempotent.
public Voice Warm(string wavPath)
{
if (string.IsNullOrEmpty(wavPath) || !File.Exists(wavPath))
{
Emit("Sound file not found: " + wavPath);
return null;
}
lock (_lock)
{
Voice v;
if (_voices.TryGetValue(wavPath, out v)) return v;
try
{
WavFile w = WavFile.Load(wavPath);
int buf = AL.GenBuffer();
AL.BufferData(buf, w.Format, w.Data, w.Data.Length, w.SampleRate);
int src = AL.GenSource();
AL.Source(src, ALSourcei.Buffer, buf);
AL.Source(src, ALSourceb.Looping, true);
AL.Source(src, ALSourcef.Gain, 0.0f);
AL.SourcePlay(src); // runs continuously, silent
CheckError("Warm " + Path.GetFileName(wavPath));
v = new Voice { Path = wavPath, Buffer = buf, Source = src, Gain = 0f };
_voices[wavPath] = v;
Emit("Warmed " + Path.GetFileName(wavPath));
return v;
}
catch (Exception ex)
{
Emit("Load failed " + wavPath + ": " + ex.Message);
return null;
}
}
}
/// Replaces SourceVoice.SetVolume. Linear 0..1.
public void SetGain(Voice v, float gain)
{
if (v == null) return;
if (gain < 0f) gain = 0f;
if (gain > 1f) gain = 1f;
if (Math.Abs(gain - v.Gain) < 0.001f) return; // skip redundant AL calls
lock (_lock)
{
v.Gain = gain;
AL.Source(v.Source, ALSourcef.Gain, gain);
if (gain > 0f && AL.GetSourceState(v.Source) != ALSourceState.Playing)
AL.SourcePlay(v.Source);
}
}
/// Form2's PlayColorFromStart — restart with no leading gap.
public void PlayFromStart(Voice v, float gain)
{
if (v == null) return;
lock (_lock)
{
AL.SourceRewind(v.Source);
v.Gain = gain;
AL.Source(v.Source, ALSourcef.Gain, gain);
AL.SourcePlay(v.Source);
}
}
public void MuteAll()
{
lock (_lock)
foreach (var v in _voices.Values)
{
v.Gain = 0f;
AL.Source(v.Source, ALSourcef.Gain, 0f);
}
}
public void StopAll()
{
lock (_lock)
foreach (var v in _voices.Values)
{
AL.SourceStop(v.Source);
v.Gain = 0f;
}
}
/// Form2's Volume_Control_XAudio2, unchanged — keeps Linux loudness
/// matched to the Windows build.
public static float VolumeCurve(int range)
{
const double CURVE = 9.5;
double y = Math.Pow(range / 255.0, 1.0 / CURVE);
int db = (int)(10000.0 * y - 10000);
if (db > 0) db = 0;
if (db < -10000) db = -10000;
float lin = (float)Math.Pow(10.0, db / 2000.0);
return Math.Max(0f, Math.Min(1f, lin));
}
// ── shims so MapWindow's existing calls still compile ──
private Voice _main;
public void Start(string wavPath)
{
_main = Warm(wavPath);
SetGain(_main, 0f);
}
public void Stop() { MuteAll(); }
public Voice MainVoice { get { return _main; } }
public void Dispose()
{
lock (_lock)
{
foreach (var v in _voices.Values)
{
AL.SourceStop(v.Source);
AL.DeleteSource(v.Source);
AL.DeleteBuffer(v.Buffer);
}
_voices.Clear();
}
if (_context != null) { _context.Dispose(); _context = null; }
}
private void CheckError(string where)
{
ALError e = AL.GetError();
if (e != ALError.NoError) Emit(where + ": " + AL.GetErrorString(e));
}
private void Emit(string m)
{
var h = Log;
if (h != null) h(m); else Console.WriteLine("[audio] " + m);
}
}