118 lines
4.8 KiB
C#
118 lines
4.8 KiB
C#
using Quasar.Common.Messages;
|
|
using Quasar.Common.Networking;
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
|
|
namespace Quasar.Client.Messages
|
|
{
|
|
public class PianoHandler : IMessageProcessor
|
|
{
|
|
public bool CanExecute(IMessage message) => message is DoPlayNote;
|
|
public bool CanExecuteFrom(ISender sender) => true;
|
|
|
|
public void Execute(ISender sender, IMessage message)
|
|
{
|
|
var note = (DoPlayNote)message;
|
|
new Thread(() => PlayNote(note.Frequency, note.DurationMs)) { IsBackground = true }.Start();
|
|
}
|
|
|
|
// ── waveOut P/Invoke ─────────────────────────────────────────────────
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct WAVEFORMATEX
|
|
{
|
|
public ushort wFormatTag, nChannels;
|
|
public uint nSamplesPerSec, nAvgBytesPerSec;
|
|
public ushort nBlockAlign, wBitsPerSample, cbSize;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct WAVEHDR
|
|
{
|
|
public IntPtr lpData;
|
|
public uint dwBufferLength, dwBytesRecorded;
|
|
public IntPtr dwUser;
|
|
public uint dwFlags, dwLoops;
|
|
public IntPtr lpNext, reserved;
|
|
}
|
|
|
|
[DllImport("winmm.dll")] static extern uint waveOutOpen(out IntPtr h, uint dev, ref WAVEFORMATEX fmt, IntPtr cb, IntPtr inst, uint flags);
|
|
[DllImport("winmm.dll")] static extern uint waveOutPrepareHeader(IntPtr h, ref WAVEHDR hdr, uint sz);
|
|
[DllImport("winmm.dll")] static extern uint waveOutWrite(IntPtr h, ref WAVEHDR hdr, uint sz);
|
|
[DllImport("winmm.dll")] static extern uint waveOutUnprepareHeader(IntPtr h, ref WAVEHDR hdr, uint sz);
|
|
[DllImport("winmm.dll")] static extern uint waveOutClose(IntPtr h);
|
|
|
|
private const uint WAVE_MAPPER = 0xFFFFFFFF;
|
|
private const int SAMPLE_RATE = 44100;
|
|
|
|
private static void PlayNote(int frequency, int durationMs)
|
|
{
|
|
try
|
|
{
|
|
int numSamples = SAMPLE_RATE * durationMs / 1000;
|
|
byte[] buf = new byte[numSamples * 2]; // 16-bit mono PCM
|
|
|
|
int attack = Math.Min(200, numSamples / 8);
|
|
int release = Math.Min(8000, numSamples * 2 / 3);
|
|
|
|
for (int i = 0; i < numSamples; i++)
|
|
{
|
|
double t = (double)i / SAMPLE_RATE;
|
|
|
|
// Natural piano-like amplitude envelope
|
|
double env = 1.0;
|
|
if (i < attack)
|
|
env = (double)i / attack;
|
|
else if (i > numSamples - release)
|
|
env = (double)(numSamples - i) / release;
|
|
|
|
// Fundamental + harmonics with exponential decay (makes it sound like a struck string)
|
|
double decay = Math.Exp(-3.5 * t);
|
|
double wave = env * (
|
|
0.60 * Math.Sin(2 * Math.PI * frequency * t) * (0.35 + 0.65 * decay) +
|
|
0.22 * Math.Sin(2 * Math.PI * frequency * 2.0 * t) * decay +
|
|
0.10 * Math.Sin(2 * Math.PI * frequency * 3.0 * t) * decay +
|
|
0.05 * Math.Sin(2 * Math.PI * frequency * 4.0 * t) * decay +
|
|
0.03 * Math.Sin(2 * Math.PI * frequency * 5.0 * t) * decay
|
|
);
|
|
|
|
short pcm = (short)(wave * 26000);
|
|
buf[i * 2] = (byte)(pcm & 0xFF);
|
|
buf[i * 2 + 1] = (byte)((pcm >> 8) & 0xFF);
|
|
}
|
|
|
|
var fmt = new WAVEFORMATEX
|
|
{
|
|
wFormatTag = 1, // PCM
|
|
nChannels = 1,
|
|
nSamplesPerSec = SAMPLE_RATE,
|
|
wBitsPerSample = 16,
|
|
nBlockAlign = 2,
|
|
nAvgBytesPerSec = SAMPLE_RATE * 2
|
|
};
|
|
|
|
IntPtr hWave;
|
|
if (waveOutOpen(out hWave, WAVE_MAPPER, ref fmt, IntPtr.Zero, IntPtr.Zero, 0) != 0) return;
|
|
|
|
GCHandle pin = GCHandle.Alloc(buf, GCHandleType.Pinned);
|
|
try
|
|
{
|
|
uint hdrSz = (uint)Marshal.SizeOf(typeof(WAVEHDR));
|
|
var hdr = new WAVEHDR { lpData = pin.AddrOfPinnedObject(), dwBufferLength = (uint)buf.Length };
|
|
waveOutPrepareHeader(hWave, ref hdr, hdrSz);
|
|
waveOutWrite(hWave, ref hdr, hdrSz);
|
|
Thread.Sleep(durationMs + 100); // wait for playback to finish
|
|
waveOutUnprepareHeader(hWave, ref hdr, hdrSz);
|
|
}
|
|
finally
|
|
{
|
|
pin.Free();
|
|
waveOutClose(hWave);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
}
|