Loading TestV/Playgrounds/AudioImage.cs +62 −53 Original line number Diff line number Diff line Loading @@ -14,10 +14,9 @@ public class AudioImage : IPlayground { public string LaunchCmd { get; } = "ai"; public string? Name { get; } = "Audio Image"; public string? Description { get; } = "将音频采样反向变换为图片(默认PNG,可选JPG/BMP,可设置宽度或高度)"; public string? Description { get; } = "将音频采样反向变换为图片(可逆转换,默认PNG,可选JPG/BMP,可设置宽度或高度)"; private const int BitsPerPixel = 32; // 与 ImageAudio 对应:R/G/B/A 各 8 bit private const int FramesPerPixel = BitsPerPixel; private const int Channels = 2; // 立体声(与 ImageAudio 对应) public async Task Run(string[] args) { Loading @@ -31,6 +30,12 @@ public class AudioImage : IPlayground return; } if (File.Exists(opts.OutputPath)) { AnsiConsole.MarkupLine($"[red]输出文件已存在: {opts.OutputPath}[/]"); return; } await Task.Run(() => Decode(opts)); } Loading @@ -46,7 +51,6 @@ public class AudioImage : IPlayground private static Options? ParseOptions(string[] args) { var audioPath = args[0]; // 默认输出路径:与音频同目录,扩展名 png string outPath = Path.ChangeExtension(audioPath, ".png"); var opts = new Options { AudioPath = audioPath, OutputPath = outPath }; Loading Loading @@ -78,7 +82,6 @@ public class AudioImage : IPlayground } } // 若未显式指定 --format,按输出扩展名推断 if (!IsFormatArg(args)) { var ext = Path.GetExtension(opts.OutputPath).TrimStart('.').ToLowerInvariant(); Loading @@ -99,15 +102,17 @@ public class AudioImage : IPlayground { using var reader = new AudioFileReader(opts.AudioPath); int audioChannels = reader.WaveFormat.Channels; if (audioChannels < 1) if (audioChannels != 2) { AnsiConsole.MarkupLine("[red]无效的音频声道数。[/]"); AnsiConsole.MarkupLine($"[red]仅支持立体声音频(当前: {audioChannels} 声道)。ImageAudio 生成的音频为立体声。[/]"); return; } long totalSamples = reader.Length / sizeof(float); // float 采样数 long totalFrames = totalSamples / audioChannels; long totalPixels = totalFrames / FramesPerPixel; // 每2个float采样=1个立体声帧=2个像素 long totalSamples = reader.Length / sizeof(float); long totalFrames = totalSamples / Channels; long totalPixels = totalFrames * 2; if (totalPixels <= 0) { Loading @@ -124,28 +129,51 @@ public class AudioImage : IPlayground (usedPixels < totalPixels ? $" [yellow](丢弃 {totalPixels - usedPixels:N0} 像素采样)[/]" : "") + "[/]"); using var image = new Image<Rgba32>(width, height); var rowBuf = new float[width * FramesPerPixel * audioChannels]; long decodedPixels = 0; var sampleBuf = new float[8192]; int bufPos = 0; int bufLen = 0; long pixelIdx = 0; for (int y = 0; y < height; y++) image.ProcessPixelRows(accessor => { int read = reader.Read(rowBuf, 0, rowBuf.Length); if (read < rowBuf.Length) Array.Clear(rowBuf, read, rowBuf.Length - read); int curY = -1; Span<Rgba32> curRow = default; image.ProcessPixelRows(accessor => while (pixelIdx < usedPixels) { var row = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) // 确保缓冲区有至少2个采样(1帧) if (bufPos + 2 > bufLen) { long pixelIdx = (long)y * width + x; row[x] = DecodePixel(rowBuf, x * FramesPerPixel, audioChannels, pixelIdx); int remaining = bufLen - bufPos; if (remaining > 0) Array.Copy(sampleBuf, bufPos, sampleBuf, 0, remaining); bufPos = 0; bufLen = remaining + reader.Read(sampleBuf, remaining, sampleBuf.Length - remaining); if (bufLen < 2) break; } }); decodedPixels += width; if (read < rowBuf.Length) break; // EOF:剩余行保持默认(透明) // 立体声帧:[left, right] float left = sampleBuf[bufPos]; float right = sampleBuf[bufPos + 1]; bufPos += 2; // 偶数像素 → right int y0 = (int)(pixelIdx / width); int x0 = (int)(pixelIdx % width); if (y0 != curY) { curY = y0; curRow = accessor.GetRowSpan(y0); } curRow[x0] = FloatToPixel(right); pixelIdx++; if (pixelIdx >= usedPixels) break; // 奇数像素 → left int y1 = (int)(pixelIdx / width); int x1 = (int)(pixelIdx % width); if (y1 != curY) { curY = y1; curRow = accessor.GetRowSpan(y1); } curRow[x1] = FloatToPixel(left); pixelIdx++; } }); Save(image, opts.OutputPath, opts.Format); AnsiConsole.MarkupLine($"[green]已写入: {opts.OutputPath} ({opts.Format})[/]"); Loading @@ -153,19 +181,15 @@ public class AudioImage : IPlayground private static (int width, int height) ComputeDimensions(long totalPixels, int? reqWidth, int? reqHeight) { // 两者都设置:固定尺寸,多余采样丢弃 if (reqWidth is { } w && reqHeight is { } h) return (w, h); // 只设置宽度:高度自动 = totalPixels / width if (reqWidth is { } w2) return (w2, (int)Math.Max(1, totalPixels / w2)); // 只设置高度:宽度自动 = totalPixels / height if (reqHeight is { } h2) return ((int)Math.Max(1, totalPixels / h2), h2); // 都未设置:尽量正方形 int side = (int)Math.Ceiling(Math.Sqrt(totalPixels)); side = Math.Max(1, side); int autoHeight = (int)Math.Ceiling((double)totalPixels / side); Loading @@ -173,32 +197,17 @@ public class AudioImage : IPlayground } /// <summary> /// 从 32 个音频帧还原 1 个 Rgba32 像素。 /// 奇数像素取左声道,偶数像素取右声道(与 ImageAudio 正向映射对应); /// 采样值 > 0 → bit 1,否则 → bit 0。 /// float → RGBA 4字节(与 ImageAudio.PixelToFloat 互逆) /// </summary> private static Rgba32 DecodePixel(float[] buf, int frameBase, int audioChannels, long pixelIdx) { bool useLeft = (pixelIdx & 1) != 0; int channelOffset = useLeft ? 0 : (audioChannels >= 2 ? 1 : 0); byte r = 0, g = 0, b = 0, a = 0; for (int j = 0; j < FramesPerPixel; j++) { float v = buf[(frameBase + j) * audioChannels + channelOffset]; if (v > 0f) { byte mask = (byte)(1 << (j & 7)); switch (j >> 3) { case 0: r |= mask; break; case 1: g |= mask; break; case 2: b |= mask; break; case 3: a |= mask; break; } } } return new Rgba32(r, g, b, a); private static Rgba32 FloatToPixel(float v) { int bits = BitConverter.SingleToInt32Bits(v); return new Rgba32( (byte)(bits & 0xFF), (byte)((bits >> 8) & 0xFF), (byte)((bits >> 16) & 0xFF), (byte)((bits >> 24) & 0xFF) ); } private static void Save(Image image, string path, string format) Loading TestV/Playgrounds/ImageAudio.cs +54 −51 Original line number Diff line number Diff line Loading @@ -12,9 +12,8 @@ public class ImageAudio : IPlayground { public string LaunchCmd { get; } = "ia"; public string? Name { get; } = "Image Audio"; public string? Description { get; } = "将32位像素转换成音频采样(奇数像素→左声道,偶数像素→右声道,支持WAV/MP3输出、Ctrl+C停止)"; public string? Description { get; } = "将像素RGBA字节直接作为音频采样(奇数像素→左声道,偶数像素→右声道,可逆转换,支持WAV/MP3输出、Ctrl+C停止)"; private const int BitsPerPixel = 32; // R/G/B/A 各 8 bit private const int Channels = 2; // 立体声 private const int DefaultSampleRate = 44100; private const int DefaultMp3BitRate = 128; // kbps Loading @@ -31,9 +30,13 @@ public class ImageAudio : IPlayground return; } if (!EnsureOutputAvailable(opts.WavPath)) return; if (!EnsureOutputAvailable(opts.Mp3Path)) return; using var image = Image.Load<Rgba32>(opts.ImagePath); int pixelCount = image.Width * image.Height; long totalFrames = (long)pixelCount * BitsPerPixel; // 每2个像素=1个立体声帧(偶数像素→右声道,奇数像素→左声道) long totalFrames = (pixelCount + 1) / 2; long totalSamples = totalFrames * Channels; long totalBytes = totalSamples * sizeof(float); Loading Loading @@ -74,6 +77,16 @@ public class ImageAudio : IPlayground } } private static bool EnsureOutputAvailable(string? path) { if (path is not null && File.Exists(path)) { AnsiConsole.MarkupLine($"[red]输出文件已存在: {path}[/]"); return false; } return true; } private sealed class Options { public required string ImagePath { get; init; } Loading Loading @@ -171,8 +184,8 @@ public class ImageAudio : IPlayground } /// <summary> /// 按需生成采样的 ISampleProvider。 /// 奇数像素 → 左声道,偶数像素 → 右声道;每像素 32 个立体声帧。 /// 可逆映射:像素的 RGBA 4字节直接作为一个 float 采样的4字节。 /// 偶数像素 → 右声道,奇数像素 → 左声道;每2个像素产生1个立体声帧。 /// 仅缓存一行像素,避免一次性持有全部采样。 /// </summary> private sealed class PixelSampleProvider : ISampleProvider Loading @@ -182,11 +195,7 @@ public class ImageAudio : IPlayground private readonly Rgba32[] _rowBuf; private int _rowY; private int _x; private int _bitInPixel; private long _pixelIdx; private Rgba32 _current; private bool _toLeft; private bool _colorWritten; public WaveFormat WaveFormat { get; } Loading @@ -199,7 +208,6 @@ public class ImageAudio : IPlayground _rowY = 0; _x = 0; LoadRow(0); SetCurrent(); } private void LoadRow(int y) Loading @@ -207,19 +215,8 @@ public class ImageAudio : IPlayground _image.ProcessPixelRows(accessor => accessor.GetRowSpan(y).CopyTo(_rowBuf)); } private void SetCurrent() private bool AdvancePixel() { _current = _rowBuf[_x]; _toLeft = (_pixelIdx & 1) != 0; _colorWritten = false; _bitInPixel = 0; } private bool Advance() { _bitInPixel++; if (_bitInPixel < BitsPerPixel) return true; _pixelIdx++; _x++; if (_x >= _image.Width) Loading @@ -229,55 +226,61 @@ public class ImageAudio : IPlayground if (_rowY >= _image.Height) return false; LoadRow(_rowY); } SetCurrent(); return true; } public int Read(float[] buffer, int offset, int count) { int written = 0; int limit = count - (Channels - 1); // 保证能写满一个立体声帧 while (written < limit) while (written + 1 < count) { if (_rowY >= _image.Height) break; if (_showColor && !_colorWritten) { AnsiConsole.Write(new Text(" ", new Style(null, new Color(_current.R, _current.G, _current.B)))); _colorWritten = true; } // 偶数像素 → 右声道 Rgba32 evenPixel = _rowBuf[_x]; float right = PixelToFloat(evenPixel); if (_showColor) AnsiConsole.Write(new Text(" ", new Style(null, new Color(evenPixel.R, evenPixel.G, evenPixel.B)))); float v = GetBitValue(_current, _bitInPixel); int idx = offset + written; if (_toLeft) if (!AdvancePixel()) { buffer[idx] = v; buffer[idx + 1] = 0f; // 没有奇数像素了,只有右声道 buffer[offset + written++] = 0f; buffer[offset + written++] = right; break; } else if (_rowY >= _image.Height) { buffer[idx] = 0f; buffer[idx + 1] = v; buffer[offset + written++] = 0f; buffer[offset + written++] = right; break; } written += Channels; if (!Advance()) break; // 奇数像素 → 左声道 Rgba32 oddPixel = _rowBuf[_x]; float left = PixelToFloat(oddPixel); if (_showColor) AnsiConsole.Write(new Text(" ", new Style(null, new Color(oddPixel.R, oddPixel.G, oddPixel.B)))); // 立体声帧顺序:[left, right] buffer[offset + written++] = left; buffer[offset + written++] = right; if (!AdvancePixel()) break; } return written; } private static float GetBitValue(Rgba32 px, int bit) { // bit 0..7: R, 8..15: G, 16..23: B, 24..31: A // 二进制 0 → -1,二进制 1 → 1,使波形在 [-1, 1] 间波动 byte channel = (bit >> 3) switch /// <summary> /// RGBA 4字节 → float(小端序,与 BitConverter.Int32BitsToSingle 对应) /// </summary> private static float PixelToFloat(Rgba32 px) { 0 => px.R, 1 => px.G, 2 => px.B, _ => px.A, }; return ((channel >> (bit & 7)) & 1) != 0 ? 1f : -1f; int bits = px.R | (px.G << 8) | (px.B << 16) | (px.A << 24); return BitConverter.Int32BitsToSingle(bits); } } } Loading
TestV/Playgrounds/AudioImage.cs +62 −53 Original line number Diff line number Diff line Loading @@ -14,10 +14,9 @@ public class AudioImage : IPlayground { public string LaunchCmd { get; } = "ai"; public string? Name { get; } = "Audio Image"; public string? Description { get; } = "将音频采样反向变换为图片(默认PNG,可选JPG/BMP,可设置宽度或高度)"; public string? Description { get; } = "将音频采样反向变换为图片(可逆转换,默认PNG,可选JPG/BMP,可设置宽度或高度)"; private const int BitsPerPixel = 32; // 与 ImageAudio 对应:R/G/B/A 各 8 bit private const int FramesPerPixel = BitsPerPixel; private const int Channels = 2; // 立体声(与 ImageAudio 对应) public async Task Run(string[] args) { Loading @@ -31,6 +30,12 @@ public class AudioImage : IPlayground return; } if (File.Exists(opts.OutputPath)) { AnsiConsole.MarkupLine($"[red]输出文件已存在: {opts.OutputPath}[/]"); return; } await Task.Run(() => Decode(opts)); } Loading @@ -46,7 +51,6 @@ public class AudioImage : IPlayground private static Options? ParseOptions(string[] args) { var audioPath = args[0]; // 默认输出路径:与音频同目录,扩展名 png string outPath = Path.ChangeExtension(audioPath, ".png"); var opts = new Options { AudioPath = audioPath, OutputPath = outPath }; Loading Loading @@ -78,7 +82,6 @@ public class AudioImage : IPlayground } } // 若未显式指定 --format,按输出扩展名推断 if (!IsFormatArg(args)) { var ext = Path.GetExtension(opts.OutputPath).TrimStart('.').ToLowerInvariant(); Loading @@ -99,15 +102,17 @@ public class AudioImage : IPlayground { using var reader = new AudioFileReader(opts.AudioPath); int audioChannels = reader.WaveFormat.Channels; if (audioChannels < 1) if (audioChannels != 2) { AnsiConsole.MarkupLine("[red]无效的音频声道数。[/]"); AnsiConsole.MarkupLine($"[red]仅支持立体声音频(当前: {audioChannels} 声道)。ImageAudio 生成的音频为立体声。[/]"); return; } long totalSamples = reader.Length / sizeof(float); // float 采样数 long totalFrames = totalSamples / audioChannels; long totalPixels = totalFrames / FramesPerPixel; // 每2个float采样=1个立体声帧=2个像素 long totalSamples = reader.Length / sizeof(float); long totalFrames = totalSamples / Channels; long totalPixels = totalFrames * 2; if (totalPixels <= 0) { Loading @@ -124,28 +129,51 @@ public class AudioImage : IPlayground (usedPixels < totalPixels ? $" [yellow](丢弃 {totalPixels - usedPixels:N0} 像素采样)[/]" : "") + "[/]"); using var image = new Image<Rgba32>(width, height); var rowBuf = new float[width * FramesPerPixel * audioChannels]; long decodedPixels = 0; var sampleBuf = new float[8192]; int bufPos = 0; int bufLen = 0; long pixelIdx = 0; for (int y = 0; y < height; y++) image.ProcessPixelRows(accessor => { int read = reader.Read(rowBuf, 0, rowBuf.Length); if (read < rowBuf.Length) Array.Clear(rowBuf, read, rowBuf.Length - read); int curY = -1; Span<Rgba32> curRow = default; image.ProcessPixelRows(accessor => while (pixelIdx < usedPixels) { var row = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) // 确保缓冲区有至少2个采样(1帧) if (bufPos + 2 > bufLen) { long pixelIdx = (long)y * width + x; row[x] = DecodePixel(rowBuf, x * FramesPerPixel, audioChannels, pixelIdx); int remaining = bufLen - bufPos; if (remaining > 0) Array.Copy(sampleBuf, bufPos, sampleBuf, 0, remaining); bufPos = 0; bufLen = remaining + reader.Read(sampleBuf, remaining, sampleBuf.Length - remaining); if (bufLen < 2) break; } }); decodedPixels += width; if (read < rowBuf.Length) break; // EOF:剩余行保持默认(透明) // 立体声帧:[left, right] float left = sampleBuf[bufPos]; float right = sampleBuf[bufPos + 1]; bufPos += 2; // 偶数像素 → right int y0 = (int)(pixelIdx / width); int x0 = (int)(pixelIdx % width); if (y0 != curY) { curY = y0; curRow = accessor.GetRowSpan(y0); } curRow[x0] = FloatToPixel(right); pixelIdx++; if (pixelIdx >= usedPixels) break; // 奇数像素 → left int y1 = (int)(pixelIdx / width); int x1 = (int)(pixelIdx % width); if (y1 != curY) { curY = y1; curRow = accessor.GetRowSpan(y1); } curRow[x1] = FloatToPixel(left); pixelIdx++; } }); Save(image, opts.OutputPath, opts.Format); AnsiConsole.MarkupLine($"[green]已写入: {opts.OutputPath} ({opts.Format})[/]"); Loading @@ -153,19 +181,15 @@ public class AudioImage : IPlayground private static (int width, int height) ComputeDimensions(long totalPixels, int? reqWidth, int? reqHeight) { // 两者都设置:固定尺寸,多余采样丢弃 if (reqWidth is { } w && reqHeight is { } h) return (w, h); // 只设置宽度:高度自动 = totalPixels / width if (reqWidth is { } w2) return (w2, (int)Math.Max(1, totalPixels / w2)); // 只设置高度:宽度自动 = totalPixels / height if (reqHeight is { } h2) return ((int)Math.Max(1, totalPixels / h2), h2); // 都未设置:尽量正方形 int side = (int)Math.Ceiling(Math.Sqrt(totalPixels)); side = Math.Max(1, side); int autoHeight = (int)Math.Ceiling((double)totalPixels / side); Loading @@ -173,32 +197,17 @@ public class AudioImage : IPlayground } /// <summary> /// 从 32 个音频帧还原 1 个 Rgba32 像素。 /// 奇数像素取左声道,偶数像素取右声道(与 ImageAudio 正向映射对应); /// 采样值 > 0 → bit 1,否则 → bit 0。 /// float → RGBA 4字节(与 ImageAudio.PixelToFloat 互逆) /// </summary> private static Rgba32 DecodePixel(float[] buf, int frameBase, int audioChannels, long pixelIdx) { bool useLeft = (pixelIdx & 1) != 0; int channelOffset = useLeft ? 0 : (audioChannels >= 2 ? 1 : 0); byte r = 0, g = 0, b = 0, a = 0; for (int j = 0; j < FramesPerPixel; j++) { float v = buf[(frameBase + j) * audioChannels + channelOffset]; if (v > 0f) { byte mask = (byte)(1 << (j & 7)); switch (j >> 3) { case 0: r |= mask; break; case 1: g |= mask; break; case 2: b |= mask; break; case 3: a |= mask; break; } } } return new Rgba32(r, g, b, a); private static Rgba32 FloatToPixel(float v) { int bits = BitConverter.SingleToInt32Bits(v); return new Rgba32( (byte)(bits & 0xFF), (byte)((bits >> 8) & 0xFF), (byte)((bits >> 16) & 0xFF), (byte)((bits >> 24) & 0xFF) ); } private static void Save(Image image, string path, string format) Loading
TestV/Playgrounds/ImageAudio.cs +54 −51 Original line number Diff line number Diff line Loading @@ -12,9 +12,8 @@ public class ImageAudio : IPlayground { public string LaunchCmd { get; } = "ia"; public string? Name { get; } = "Image Audio"; public string? Description { get; } = "将32位像素转换成音频采样(奇数像素→左声道,偶数像素→右声道,支持WAV/MP3输出、Ctrl+C停止)"; public string? Description { get; } = "将像素RGBA字节直接作为音频采样(奇数像素→左声道,偶数像素→右声道,可逆转换,支持WAV/MP3输出、Ctrl+C停止)"; private const int BitsPerPixel = 32; // R/G/B/A 各 8 bit private const int Channels = 2; // 立体声 private const int DefaultSampleRate = 44100; private const int DefaultMp3BitRate = 128; // kbps Loading @@ -31,9 +30,13 @@ public class ImageAudio : IPlayground return; } if (!EnsureOutputAvailable(opts.WavPath)) return; if (!EnsureOutputAvailable(opts.Mp3Path)) return; using var image = Image.Load<Rgba32>(opts.ImagePath); int pixelCount = image.Width * image.Height; long totalFrames = (long)pixelCount * BitsPerPixel; // 每2个像素=1个立体声帧(偶数像素→右声道,奇数像素→左声道) long totalFrames = (pixelCount + 1) / 2; long totalSamples = totalFrames * Channels; long totalBytes = totalSamples * sizeof(float); Loading Loading @@ -74,6 +77,16 @@ public class ImageAudio : IPlayground } } private static bool EnsureOutputAvailable(string? path) { if (path is not null && File.Exists(path)) { AnsiConsole.MarkupLine($"[red]输出文件已存在: {path}[/]"); return false; } return true; } private sealed class Options { public required string ImagePath { get; init; } Loading Loading @@ -171,8 +184,8 @@ public class ImageAudio : IPlayground } /// <summary> /// 按需生成采样的 ISampleProvider。 /// 奇数像素 → 左声道,偶数像素 → 右声道;每像素 32 个立体声帧。 /// 可逆映射:像素的 RGBA 4字节直接作为一个 float 采样的4字节。 /// 偶数像素 → 右声道,奇数像素 → 左声道;每2个像素产生1个立体声帧。 /// 仅缓存一行像素,避免一次性持有全部采样。 /// </summary> private sealed class PixelSampleProvider : ISampleProvider Loading @@ -182,11 +195,7 @@ public class ImageAudio : IPlayground private readonly Rgba32[] _rowBuf; private int _rowY; private int _x; private int _bitInPixel; private long _pixelIdx; private Rgba32 _current; private bool _toLeft; private bool _colorWritten; public WaveFormat WaveFormat { get; } Loading @@ -199,7 +208,6 @@ public class ImageAudio : IPlayground _rowY = 0; _x = 0; LoadRow(0); SetCurrent(); } private void LoadRow(int y) Loading @@ -207,19 +215,8 @@ public class ImageAudio : IPlayground _image.ProcessPixelRows(accessor => accessor.GetRowSpan(y).CopyTo(_rowBuf)); } private void SetCurrent() private bool AdvancePixel() { _current = _rowBuf[_x]; _toLeft = (_pixelIdx & 1) != 0; _colorWritten = false; _bitInPixel = 0; } private bool Advance() { _bitInPixel++; if (_bitInPixel < BitsPerPixel) return true; _pixelIdx++; _x++; if (_x >= _image.Width) Loading @@ -229,55 +226,61 @@ public class ImageAudio : IPlayground if (_rowY >= _image.Height) return false; LoadRow(_rowY); } SetCurrent(); return true; } public int Read(float[] buffer, int offset, int count) { int written = 0; int limit = count - (Channels - 1); // 保证能写满一个立体声帧 while (written < limit) while (written + 1 < count) { if (_rowY >= _image.Height) break; if (_showColor && !_colorWritten) { AnsiConsole.Write(new Text(" ", new Style(null, new Color(_current.R, _current.G, _current.B)))); _colorWritten = true; } // 偶数像素 → 右声道 Rgba32 evenPixel = _rowBuf[_x]; float right = PixelToFloat(evenPixel); if (_showColor) AnsiConsole.Write(new Text(" ", new Style(null, new Color(evenPixel.R, evenPixel.G, evenPixel.B)))); float v = GetBitValue(_current, _bitInPixel); int idx = offset + written; if (_toLeft) if (!AdvancePixel()) { buffer[idx] = v; buffer[idx + 1] = 0f; // 没有奇数像素了,只有右声道 buffer[offset + written++] = 0f; buffer[offset + written++] = right; break; } else if (_rowY >= _image.Height) { buffer[idx] = 0f; buffer[idx + 1] = v; buffer[offset + written++] = 0f; buffer[offset + written++] = right; break; } written += Channels; if (!Advance()) break; // 奇数像素 → 左声道 Rgba32 oddPixel = _rowBuf[_x]; float left = PixelToFloat(oddPixel); if (_showColor) AnsiConsole.Write(new Text(" ", new Style(null, new Color(oddPixel.R, oddPixel.G, oddPixel.B)))); // 立体声帧顺序:[left, right] buffer[offset + written++] = left; buffer[offset + written++] = right; if (!AdvancePixel()) break; } return written; } private static float GetBitValue(Rgba32 px, int bit) { // bit 0..7: R, 8..15: G, 16..23: B, 24..31: A // 二进制 0 → -1,二进制 1 → 1,使波形在 [-1, 1] 间波动 byte channel = (bit >> 3) switch /// <summary> /// RGBA 4字节 → float(小端序,与 BitConverter.Int32BitsToSingle 对应) /// </summary> private static float PixelToFloat(Rgba32 px) { 0 => px.R, 1 => px.G, 2 => px.B, _ => px.A, }; return ((channel >> (bit & 7)) & 1) != 0 ? 1f : -1f; int bits = px.R | (px.G << 8) | (px.B << 16) | (px.A << 24); return BitConverter.Int32BitsToSingle(bits); } } }