Skip to content

Instantly share code, notes, and snippets.

@elbruno
Created January 25, 2026 20:40
Show Gist options
  • Select an option

  • Save elbruno/d321b5b12adf99a2b46dc4fbb51bce03 to your computer and use it in GitHub Desktop.

Select an option

Save elbruno/d321b5b12adf99a2b46dc4fbb51bce03 to your computer and use it in GitHub Desktop.
#!/usr/bin/env dotnet run
#:package [email protected]
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Spectre.Console;
const int BufferSize = 80 * 1024; // 80KB buffer for chunked I/O
const string InputFolder = "audios";
const string OutputFile = "joined_output.mp3";
// Display banner
AnsiConsole.Write(new FigletText("Audio Joiner").Color(Color.Cyan1));
AnsiConsole.MarkupLine("[grey]Concatenates MP3 files into a single output[/]\n");
// Check if input folder exists
if (!Directory.Exists(InputFolder))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Folder '[yellow]{InputFolder}[/]' not found!");
AnsiConsole.MarkupLine("[grey]Create the folder and add MP3 files to join.[/]");
return 1;
}
// Get and sort MP3 files numerically
var mp3Files = Directory.GetFiles(InputFolder, "*.mp3")
.OrderBy(f => ExtractNumber(Path.GetFileNameWithoutExtension(f)))
.ThenBy(f => f)
.ToArray();
if (mp3Files.Length == 0)
{
AnsiConsole.MarkupLine($"[red]Error:[/] No MP3 files found in '[yellow]{InputFolder}[/]'!");
return 1;
}
AnsiConsole.MarkupLine($"[green]Found {mp3Files.Length} MP3 files to join[/]\n");
long totalBytesWritten = 0;
long totalInputSize = mp3Files.Sum(f => new FileInfo(f).Length);
// Process files with progress bar
await AnsiConsole.Progress()
.AutoRefresh(true)
.AutoClear(false)
.HideCompleted(false)
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn())
.StartAsync(async ctx =>
{
var task = ctx.AddTask("[cyan]Joining audio files[/]", maxValue: totalInputSize);
using var outputStream = new FileStream(OutputFile, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, FileOptions.SequentialScan);
var buffer = new byte[BufferSize];
foreach (var file in mp3Files)
{
task.Description = $"[cyan]Processing:[/] [white]{Path.GetFileName(file)}[/]";
using var inputStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.SequentialScan);
// Skip ID3v2 tag if present (starts with "ID3")
long skipBytes = GetId3v2TagSize(inputStream);
if (skipBytes > 0)
{
inputStream.Seek(skipBytes, SeekOrigin.Begin);
}
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer.AsMemory(0, buffer.Length))) > 0)
{
await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalBytesWritten += bytesRead;
task.Increment(bytesRead);
}
}
task.Description = "[green]Complete![/]";
});
// Display summary table
var outputInfo = new FileInfo(OutputFile);
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn(new TableColumn("[cyan]Property[/]").LeftAligned())
.AddColumn(new TableColumn("[cyan]Value[/]").RightAligned());
table.AddRow("[white]Output File[/]", $"[green]{OutputFile}[/]");
table.AddRow("[white]Output Size[/]", $"[yellow]{FormatBytes(outputInfo.Length)}[/]");
table.AddRow("[white]Files Joined[/]", $"[blue]{mp3Files.Length}[/]");
table.AddRow("[white]Total Input Size[/]", $"[grey]{FormatBytes(totalInputSize)}[/]");
AnsiConsole.WriteLine();
AnsiConsole.Write(new Panel(table)
.Header("[bold green]✓ Summary[/]")
.BorderColor(Color.Green));
AnsiConsole.MarkupLine("\n[green]Done![/] Audio files joined successfully.");
return 0;
// Extract numeric portion from filename for proper sorting
static int ExtractNumber(string filename)
{
var match = Regex.Match(filename, @"(\d+)");
return match.Success ? int.Parse(match.Groups[1].Value) : int.MaxValue;
}
// Get ID3v2 tag size (returns 0 if no tag present)
static long GetId3v2TagSize(FileStream stream)
{
byte[] header = new byte[10];
int bytesRead = stream.Read(header, 0, 10);
stream.Seek(0, SeekOrigin.Begin);
if (bytesRead < 10) return 0;
// Check for "ID3" signature
if (header[0] != 'I' || header[1] != 'D' || header[2] != '3') return 0;
// ID3v2 size is stored in bytes 6-9 as syncsafe integer (7 bits per byte)
long size = ((header[6] & 0x7F) << 21) |
((header[7] & 0x7F) << 14) |
((header[8] & 0x7F) << 7) |
(header[9] & 0x7F);
// Add 10 bytes for the header itself
return size + 10;
}
// Format bytes to human-readable string
static string FormatBytes(long bytes)
{
string[] suffixes = ["B", "KB", "MB", "GB"];
int i = 0;
double size = bytes;
while (size >= 1024 && i < suffixes.Length - 1)
{
size /= 1024;
i++;
}
return $"{size:F2} {suffixes[i]}";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment