feat(core/audio): add square-wave synth and audio recorder

This commit is contained in:
dualfroz committed 2026-08-24 11:09:20 +02:00
1 parent d14fcd126d
commit 8c516cccbf
2 files changed
+128

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
using System;
namespace Chip8.Core.Audio;
public sealed class AudioRecorder
{
public const int defaultSampleRate = 44100;
public const double defaultBuzzerFrequency = 440.0;
public const double frameSeconds = 1.0 / 60.0;
readonly int sampleRate;
readonly double buzzerFrequency;
public AudioRecorder(int sampleRate = defaultSampleRate, double buzzerFrequency = defaultBuzzerFrequency)
{
if (sampleRate <= 0)
{
throw new ArgumentOutOfRangeException(nameof(sampleRate), "sample rate must be positive");
}
this.sampleRate = sampleRate;
this.buzzerFrequency = buzzerFrequency;
}
public AudioTrack record(Machine machine, int frames, int instructionsPerFrame)
{
if (frames < 0)
{
throw new ArgumentOutOfRangeException(nameof(frames), "frame count cannot be negative");
}
var track = new AudioTrack(sampleRate);
int samplesPerFrame = track.samplesForSeconds(frameSeconds);
for (int frame = 0; frame < frames && !machine.exited; frame++)
{
for (int cycle = 0; cycle < instructionsPerFrame; cycle++)
{
machine.step();
}
appendFrame(machine, track, samplesPerFrame);
machine.tickTimers();
}
return track;
}
void appendFrame(Machine machine, AudioTrack track, int samplesPerFrame)
{
if (!machine.soundActive)
{
track.appendSilence(samplesPerFrame);
return;
}
if (machine.platform == Platform.XoChip)
{
track.appendPattern(machine.audioPattern, machine.audioFrequency, samplesPerFrame);
}
else
{
track.appendSquare(buzzerFrequency, samplesPerFrame);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
using System;
namespace Chip8.Core.Audio;
public static class AudioSynth
{
public const int patternBits = 128;
public static short amplitudeToPcm(double amplitude)
{
double clamped = amplitude < 0 ? 0 : amplitude > 1 ? 1 : amplitude;
return (short)(clamped * short.MaxValue);
}
public static bool[] decodePattern(byte[] pattern)
{
var bits = new bool[patternBits];
for (int index = 0; index < patternBits; index++)
{
int sourceByte = index / 8;
int bit = 7 - (index % 8);
byte value = sourceByte < pattern.Length ? pattern[sourceByte] : (byte)0;
bits[index] = (value & (1 << bit)) != 0;
}
return bits;
}
public static short[] square(double frequency, int sampleRate, int sampleCount, double amplitude = 0.4, double duty = 0.5)
{
var samples = new short[sampleCount];
short high = amplitudeToPcm(amplitude);
short low = (short)-high;
double step = frequency / sampleRate;
double phase = 0.0;
for (int index = 0; index < sampleCount; index++)
{
samples[index] = (phase % 1.0) < duty ? high : low;
phase += step;
}
return samples;
}
public static short[] renderPattern(byte[] pattern, double playbackRate, int sampleRate, int sampleCount, double amplitude = 0.4)
{
bool[] bits = decodePattern(pattern);
var samples = new short[sampleCount];
short high = amplitudeToPcm(amplitude);
short low = (short)-high;
double step = playbackRate / sampleRate;
double position = 0.0;
for (int index = 0; index < sampleCount; index++)
{
int bit = ((int)position) % patternBits;
samples[index] = bits[bit] ? high : low;
position += step;
}
return samples;
}
}