From d14fcd126da8bd472c787eb403629a9e7c909613 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 09:53:41 +0200 Subject: [PATCH] feat(core/audio): add WAV writer and audio track buffer --- src/Chip8.Core/Audio/AudioTrack.cs | 68 ++++++++++++++++++++++++++++++ src/Chip8.Core/Audio/WavFile.cs | 52 +++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/Chip8.Core/Audio/AudioTrack.cs create mode 100644 src/Chip8.Core/Audio/WavFile.cs diff --git a/src/Chip8.Core/Audio/AudioTrack.cs b/src/Chip8.Core/Audio/AudioTrack.cs new file mode 100644 index 0000000..320aee3 --- /dev/null +++ b/src/Chip8.Core/Audio/AudioTrack.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace Chip8.Core.Audio; + +public sealed class AudioTrack +{ + readonly List samples = new(); + readonly int sampleRate; + + double squarePhase; + double patternPosition; + + public AudioTrack(int sampleRate) + { + if (sampleRate <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sampleRate), "sample rate must be positive"); + } + this.sampleRate = sampleRate; + } + + public int sampleCount => samples.Count; + + public double durationSeconds => (double)samples.Count / sampleRate; + + public int samplesForSeconds(double seconds) => (int)Math.Round(seconds * sampleRate); + + public void appendSilence(int count) + { + for (int index = 0; index < count; index++) + { + samples.Add(0); + } + } + + public void appendSquare(double frequency, int count, double amplitude = 0.4, double duty = 0.5) + { + short high = AudioSynth.amplitudeToPcm(amplitude); + short low = (short)-high; + double step = frequency / sampleRate; + + for (int index = 0; index < count; index++) + { + samples.Add((squarePhase % 1.0) < duty ? high : low); + squarePhase += step; + } + } + + public void appendPattern(byte[] pattern, double playbackRate, int count, double amplitude = 0.4) + { + bool[] bits = AudioSynth.decodePattern(pattern); + short high = AudioSynth.amplitudeToPcm(amplitude); + short low = (short)-high; + double step = playbackRate / sampleRate; + + for (int index = 0; index < count; index++) + { + int bit = ((int)patternPosition) % AudioSynth.patternBits; + samples.Add(bits[bit] ? high : low); + patternPosition += step; + } + } + + public short[] toSamples() => samples.ToArray(); + + public byte[] toWav() => WavFile.write(samples.ToArray(), sampleRate); +} diff --git a/src/Chip8.Core/Audio/WavFile.cs b/src/Chip8.Core/Audio/WavFile.cs new file mode 100644 index 0000000..adfe740 --- /dev/null +++ b/src/Chip8.Core/Audio/WavFile.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Text; + +namespace Chip8.Core.Audio; + +public static class WavFile +{ + public const int bitsPerSample = 16; + public const int channels = 1; + + public static byte[] write(short[] samples, int sampleRate) + { + if (sampleRate <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sampleRate), "sample rate must be positive"); + } + + int dataBytes = samples.Length * sizeof(short); + int blockAlign = channels * (bitsPerSample / 8); + int byteRate = sampleRate * blockAlign; + + using var stream = new MemoryStream(44 + dataBytes); + using var writer = new BinaryWriter(stream, Encoding.ASCII); + + writer.Write(Encoding.ASCII.GetBytes("RIFF")); + writer.Write(36 + dataBytes); + writer.Write(Encoding.ASCII.GetBytes("WAVE")); + + writer.Write(Encoding.ASCII.GetBytes("fmt ")); + writer.Write(16); + writer.Write((short)1); + writer.Write((short)channels); + writer.Write(sampleRate); + writer.Write(byteRate); + writer.Write((short)blockAlign); + writer.Write((short)bitsPerSample); + + writer.Write(Encoding.ASCII.GetBytes("data")); + writer.Write(dataBytes); + foreach (short sample in samples) + { + writer.Write(sample); + } + + writer.Flush(); + return stream.ToArray(); + } + + public static void writeFile(string path, short[] samples, int sampleRate) => + File.WriteAllBytes(path, write(samples, sampleRate)); +}