diff --git a/src/Chip8.Core/Audio/AudioRecorder.cs b/src/Chip8.Core/Audio/AudioRecorder.cs new file mode 100644 index 0000000..11a7747 --- /dev/null +++ b/src/Chip8.Core/Audio/AudioRecorder.cs @@ -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); + } + } +} diff --git a/src/Chip8.Core/Audio/AudioSynth.cs b/src/Chip8.Core/Audio/AudioSynth.cs new file mode 100644 index 0000000..59aa3a6 --- /dev/null +++ b/src/Chip8.Core/Audio/AudioSynth.cs @@ -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; + } +}