namespace TravelEar.Core; /// /// Sink format helper (REQ-SINK-FORMAT): the Sink carries the Tap's channel count unless /// config Downmix is set, in which case every frame is folded to mono before framing. The /// fold is an equal-weight average of the interleaved channels, so a signal identical in every /// channel comes out unchanged and full-scale input cannot clip. /// public static class Downmixer { // [impl->REQ-SINK-FORMAT] /// /// Folds ( samples per frame) into /// , one sample per frame. Returns the number of frames written. The /// output may alias the front of the input: frame i is read before mono sample /// i is written and i <= i * channels, so an in-place fold is safe. /// public static int ToMono(ReadOnlySpan interleaved, int channels, Span mono) { if (channels < 1) throw new ArgumentOutOfRangeException(nameof(channels), channels, "Channel count must be at least 1."); if (interleaved.Length % channels != 0) throw new ArgumentException("Interleaved length must be a whole number of frames.", nameof(interleaved)); var frames = interleaved.Length / channels; if (mono.Length < frames) throw new ArgumentException("Mono destination too small.", nameof(mono)); if (channels == 1) { if (interleaved != (ReadOnlySpan)mono.Slice(0, frames)) interleaved.CopyTo(mono); return frames; } var scale = 1f / channels; for (var frame = 0; frame < frames; frame++) { var sum = 0f; var offset = frame * channels; for (var c = 0; c < channels; c++) sum += interleaved[offset + c]; mono[frame] = sum * scale; } return frames; } }