Loading TestV/Playgrounds/AudioImage.cs 0 → 100644 +214 −0 Original line number Diff line number Diff line using NAudio.Wave; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using Spectre.Console; using TestV.Tools; namespace TestV.Playgrounds; public class AudioImage : IPlayground { public string LaunchCmd { get; } = "ai"; public string? Name { get; } = "Audio Image"; public string? Description { get; } = "将音频采样反向变换为图片(默认PNG,可选JPG/BMP,可设置宽度或高度)"; private const int BitsPerPixel = 32; // 与 ImageAudio 对应:R/G/B/A 各 8 bit private const int FramesPerPixel = BitsPerPixel; public async Task Run(string[] args) { if (!Errors.ValidArgsLength(args, 1)) return; var opts = ParseOptions(args); if (opts is null) return; if (!File.Exists(opts.AudioPath)) { AnsiConsole.MarkupLine("[red]文件不存在。[/]"); return; } await Task.Run(() => Decode(opts)); } private sealed class Options { public required string AudioPath { get; init; } public required string OutputPath { get; set; } public string Format { get; set; } = "png"; public int? Width { get; set; } public int? Height { get; set; } } 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 }; for (int i = 1; i < args.Length; i++) { var a = args[i]; if (a is "--out" or "-o") { if (i + 1 >= args.Length) { Errors.NoArgs(1); return null; } opts.OutputPath = args[++i]; } else if (a.StartsWith("--out=")) opts.OutputPath = a["--out=".Length..]; else if (a.StartsWith("--format=")) opts.Format = a["--format=".Length..].ToLowerInvariant(); else if (a is "--format") { if (i + 1 >= args.Length) { Errors.NoArgs(1); return null; } opts.Format = args[++i].ToLowerInvariant(); } else if (a.StartsWith("--width=") && int.TryParse(a["--width=".Length..], out var w) && w > 0) opts.Width = w; else if (a.StartsWith("--height=") && int.TryParse(a["--height=".Length..], out var h) && h > 0) opts.Height = h; else { Errors.NotArrow("参数", "--out", "-o", "--out=", "--format", "--format=", "--width=", "--height="); return null; } } // 若未显式指定 --format,按输出扩展名推断 if (!IsFormatArg(args)) { var ext = Path.GetExtension(opts.OutputPath).TrimStart('.').ToLowerInvariant(); if (ext is "png" or "jpg" or "jpeg" or "bmp") opts.Format = ext; } return opts; } private static bool IsFormatArg(string[] args) { foreach (var a in args) if (a is "--format" || a.StartsWith("--format=")) return true; return false; } private void Decode(Options opts) { using var reader = new AudioFileReader(opts.AudioPath); int audioChannels = reader.WaveFormat.Channels; if (audioChannels < 1) { AnsiConsole.MarkupLine("[red]无效的音频声道数。[/]"); return; } long totalSamples = reader.Length / sizeof(float); // float 采样数 long totalFrames = totalSamples / audioChannels; long totalPixels = totalFrames / FramesPerPixel; if (totalPixels <= 0) { AnsiConsole.MarkupLine("[red]音频太短,不足以还原出 1 个像素。[/]"); return; } var (width, height) = ComputeDimensions(totalPixels, opts.Width, opts.Height); long usedPixels = (long)width * height; AnsiConsole.MarkupLine($"[grey]音频: {Path.GetFileName(opts.AudioPath)} ({audioChannels}ch, {reader.WaveFormat.SampleRate}Hz)[/]"); AnsiConsole.MarkupLine($"[grey]可用像素: {totalPixels:N0}[/]"); AnsiConsole.MarkupLine($"[grey]输出尺寸: {width}x{height} = {usedPixels:N0} 像素" + (usedPixels < totalPixels ? $" [yellow](丢弃 {totalPixels - usedPixels:N0} 像素采样)[/]" : "") + "[/]"); using var image = new Image<Rgba32>(width, height); var rowBuf = new float[width * FramesPerPixel * audioChannels]; long decodedPixels = 0; for (int y = 0; y < height; y++) { int read = reader.Read(rowBuf, 0, rowBuf.Length); if (read < rowBuf.Length) Array.Clear(rowBuf, read, rowBuf.Length - read); image.ProcessPixelRows(accessor => { var row = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) { long pixelIdx = (long)y * width + x; row[x] = DecodePixel(rowBuf, x * FramesPerPixel, audioChannels, pixelIdx); } }); decodedPixels += width; if (read < rowBuf.Length) break; // EOF:剩余行保持默认(透明) } Save(image, opts.OutputPath, opts.Format); AnsiConsole.MarkupLine($"[green]已写入: {opts.OutputPath} ({opts.Format})[/]"); } 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); return (side, autoHeight); } /// <summary> /// 从 32 个音频帧还原 1 个 Rgba32 像素。 /// 奇数像素取左声道,偶数像素取右声道(与 ImageAudio 正向映射对应); /// 采样值 > 0 → bit 1,否则 → bit 0。 /// </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 void Save(Image image, string path, string format) { IImageEncoder encoder = format switch { "jpg" or "jpeg" => new JpegEncoder(), "bmp" => new BmpEncoder(), _ => new PngEncoder(), }; image.Save(path, encoder); } } TestV/Program.cs +2 −1 Original line number Diff line number Diff line using System.Diagnostics; using System.Diagnostics; using System.Globalization; using System.Numerics; using System.Reflection; Loading Loading @@ -167,6 +167,7 @@ class Program .Add<Factorial>() .Add<SubProgram>() .Add<ImageAudio>() .Add<AudioImage>() .Add<Exp1>(); play.Startup(args); Loading Loading
TestV/Playgrounds/AudioImage.cs 0 → 100644 +214 −0 Original line number Diff line number Diff line using NAudio.Wave; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using Spectre.Console; using TestV.Tools; namespace TestV.Playgrounds; public class AudioImage : IPlayground { public string LaunchCmd { get; } = "ai"; public string? Name { get; } = "Audio Image"; public string? Description { get; } = "将音频采样反向变换为图片(默认PNG,可选JPG/BMP,可设置宽度或高度)"; private const int BitsPerPixel = 32; // 与 ImageAudio 对应:R/G/B/A 各 8 bit private const int FramesPerPixel = BitsPerPixel; public async Task Run(string[] args) { if (!Errors.ValidArgsLength(args, 1)) return; var opts = ParseOptions(args); if (opts is null) return; if (!File.Exists(opts.AudioPath)) { AnsiConsole.MarkupLine("[red]文件不存在。[/]"); return; } await Task.Run(() => Decode(opts)); } private sealed class Options { public required string AudioPath { get; init; } public required string OutputPath { get; set; } public string Format { get; set; } = "png"; public int? Width { get; set; } public int? Height { get; set; } } 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 }; for (int i = 1; i < args.Length; i++) { var a = args[i]; if (a is "--out" or "-o") { if (i + 1 >= args.Length) { Errors.NoArgs(1); return null; } opts.OutputPath = args[++i]; } else if (a.StartsWith("--out=")) opts.OutputPath = a["--out=".Length..]; else if (a.StartsWith("--format=")) opts.Format = a["--format=".Length..].ToLowerInvariant(); else if (a is "--format") { if (i + 1 >= args.Length) { Errors.NoArgs(1); return null; } opts.Format = args[++i].ToLowerInvariant(); } else if (a.StartsWith("--width=") && int.TryParse(a["--width=".Length..], out var w) && w > 0) opts.Width = w; else if (a.StartsWith("--height=") && int.TryParse(a["--height=".Length..], out var h) && h > 0) opts.Height = h; else { Errors.NotArrow("参数", "--out", "-o", "--out=", "--format", "--format=", "--width=", "--height="); return null; } } // 若未显式指定 --format,按输出扩展名推断 if (!IsFormatArg(args)) { var ext = Path.GetExtension(opts.OutputPath).TrimStart('.').ToLowerInvariant(); if (ext is "png" or "jpg" or "jpeg" or "bmp") opts.Format = ext; } return opts; } private static bool IsFormatArg(string[] args) { foreach (var a in args) if (a is "--format" || a.StartsWith("--format=")) return true; return false; } private void Decode(Options opts) { using var reader = new AudioFileReader(opts.AudioPath); int audioChannels = reader.WaveFormat.Channels; if (audioChannels < 1) { AnsiConsole.MarkupLine("[red]无效的音频声道数。[/]"); return; } long totalSamples = reader.Length / sizeof(float); // float 采样数 long totalFrames = totalSamples / audioChannels; long totalPixels = totalFrames / FramesPerPixel; if (totalPixels <= 0) { AnsiConsole.MarkupLine("[red]音频太短,不足以还原出 1 个像素。[/]"); return; } var (width, height) = ComputeDimensions(totalPixels, opts.Width, opts.Height); long usedPixels = (long)width * height; AnsiConsole.MarkupLine($"[grey]音频: {Path.GetFileName(opts.AudioPath)} ({audioChannels}ch, {reader.WaveFormat.SampleRate}Hz)[/]"); AnsiConsole.MarkupLine($"[grey]可用像素: {totalPixels:N0}[/]"); AnsiConsole.MarkupLine($"[grey]输出尺寸: {width}x{height} = {usedPixels:N0} 像素" + (usedPixels < totalPixels ? $" [yellow](丢弃 {totalPixels - usedPixels:N0} 像素采样)[/]" : "") + "[/]"); using var image = new Image<Rgba32>(width, height); var rowBuf = new float[width * FramesPerPixel * audioChannels]; long decodedPixels = 0; for (int y = 0; y < height; y++) { int read = reader.Read(rowBuf, 0, rowBuf.Length); if (read < rowBuf.Length) Array.Clear(rowBuf, read, rowBuf.Length - read); image.ProcessPixelRows(accessor => { var row = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) { long pixelIdx = (long)y * width + x; row[x] = DecodePixel(rowBuf, x * FramesPerPixel, audioChannels, pixelIdx); } }); decodedPixels += width; if (read < rowBuf.Length) break; // EOF:剩余行保持默认(透明) } Save(image, opts.OutputPath, opts.Format); AnsiConsole.MarkupLine($"[green]已写入: {opts.OutputPath} ({opts.Format})[/]"); } 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); return (side, autoHeight); } /// <summary> /// 从 32 个音频帧还原 1 个 Rgba32 像素。 /// 奇数像素取左声道,偶数像素取右声道(与 ImageAudio 正向映射对应); /// 采样值 > 0 → bit 1,否则 → bit 0。 /// </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 void Save(Image image, string path, string format) { IImageEncoder encoder = format switch { "jpg" or "jpeg" => new JpegEncoder(), "bmp" => new BmpEncoder(), _ => new PngEncoder(), }; image.Save(path, encoder); } }
TestV/Program.cs +2 −1 Original line number Diff line number Diff line using System.Diagnostics; using System.Diagnostics; using System.Globalization; using System.Numerics; using System.Reflection; Loading Loading @@ -167,6 +167,7 @@ class Program .Add<Factorial>() .Add<SubProgram>() .Add<ImageAudio>() .Add<AudioImage>() .Add<Exp1>(); play.Startup(args); Loading