C# programming isn’t all about enterprise development, databases, and web APIs. Most folks likely got into software development because of video games. We wanted to create our heroes, worlds, and adventures. Being a millennial myself, Super Mario holds a place near and dear in my heart. So I thought I would take some inspiration and create a console application that plays the Super Mario Bros. theme, focusing on being cross-platform friendly and capable.
In this post, we’ll see how to use a cross-platform command-line utility, SoX, to play synthesized sounds from a console application.
Sound eXchange (SoX)
SoX is a cross-platform command-line utility that we can use to interact with various formats and audio files. Users can use the utility to apply effects and synthesize audio.
For the sake of this post, SoX is the best utility I could find that would work on all .NET platforms. For folks following along, they should be able to run this code sample regardless of their development environment. Before running the example, install the operating system-specific version of the utility on your development machine.
Since I run primarily on macOS, I used Homebrew to install SoX.
brew install sox
Windows users can download the latest SoX executable from the SourceForge release page. Windows users need to make sure they install SoX into a directory part of PATH
, as we’ll be invoking the utility via Process
calls.
Tones and Silence Records
We can break musical tones down into two parts: Frequency and Duration. There is also a particular instance of a musical tone, which we’ll define as silence or note with duration, but no frequency. We’ll use C# 9 records to define a structure for Tone
and Silence
, which will inherit from a Sound
base record.
public abstract record Sound(int Duration = 125)
{
public abstract void Play();
}
public record Silence(int Duration = 125) : Sound(Duration)
{
public override void Play()
{
// TODO: Implement Silence
}
}
public record Tone(int Frequency, int Duration = 125) : Sound(Duration)
{
public override void Play()
{
// TODO: Call SoX
}
}
Let’s start by defining the Play
method of Silence
. We have two options here; the first option is to call Thread.Sleep
for the duration of silence.
public record Silence(int Duration = 125) : Sound(Duration)
{
public override void Play()
{
Thread.Sleep(Duration);
}
}
The second option for silence is to use SoX to play a 0
frequency, which has no sound.
public record Silence(int Duration = 125) : Sound(Duration)
{
public override void Play()
{
var length = TimeSpan.FromMilliseconds(Duration).TotalSeconds;
var info = new ProcessStartInfo("play",
$"-n synth {length} sine 0") {RedirectStandardOutput = true};
var result = Process.Start(info);
result?.WaitForExit(Duration);
}
}
Let’s now implement the Play
method of our Tone
record.
public record Tone(int Frequency, int Duration = 125) : Sound(Duration)
{
public override void Play()
{
var length = TimeSpan.FromMilliseconds(Duration).TotalSeconds;
var info = new ProcessStartInfo("play",
$"-n -c1 synth {length} sine {Frequency}") {
RedirectStandardOutput = true
};
var result = Process.Start(info);
result?.WaitForExit(Duration);
}
}
We can pass the Frequency
in Hertz (Hz)
to our Tone
record’s positional parameter. It just a matter of waiting for the SoX process to complete, and then we can move on to the next tone.
It’s A Me, Mario! C# Code
Now that we have the building blocks of music let’s compose our Super Mario theme. Here is the entire example in a top-level statement file.
using System;
using System.Collections.Generic;
using System.Diagnostics;
// Super Mario Bros
// (Ported from http://www.portal42.net/mario.txt)
var music = new List<Sound> {
new Tone(659), new Tone(659), new Silence(), new Tone(659), new Silence(167), new Tone(523),
new Tone(659), new Silence(), new Tone(784), new Silence(375), new Tone(392), new Silence(375),
new Tone(523), new Silence(250), new Tone(392), new Silence(250), new Tone(330), new Silence(250),
new Tone(440), new Silence(), new Tone(494), new Silence(), new Tone(466), new Silence(42),
new Tone(440), new Silence(), new Tone(392), new Silence(), new Tone(659), new Silence(),
new Tone(784), new Silence(), new Tone(880), new Silence(), new Tone(698), new Tone(784),
new Silence(), new Tone(659), new Silence(), new Tone(523), new Silence(), new Tone(587),
new Tone(494), new Silence(), new Tone(523), new Silence(250), new Tone(392), new Silence(250),
new Tone(330), new Silence(250), new Tone(440), new Silence(), new Tone(494), new Silence(),
new Tone(466), new Silence(42), new Tone(440), new Silence(), new Tone(392), new Silence(),
new Tone(659), new Silence(), new Tone(784), new Silence(), new Tone(880), new Silence(),
new Tone(698), new Tone(784), new Silence(), new Tone(659), new Silence(), new Tone(523),
new Silence(), new Tone(587), new Tone(494), new Silence(375), new Tone(784), new Tone(740),
new Tone(698), new Silence(42), new Tone(622), new Silence(), new Tone(659), new Silence(167),
new Tone(415), new Tone(440), new Tone(523), new Silence(), new Tone(440),
new Tone(523), new Tone(587), new Silence(250), new Tone(784), new Tone(740), new Tone(698), new Silence(42),
new Tone(622), new Silence(), new Tone(659), new Silence(167), new Tone(698), new Silence(),
new Tone(698), new Tone(698), new Silence(625), new Tone(784), new Tone(740),
new Tone(698), new Silence(42), new Tone(622), new Silence(), new Tone(659), new Silence(167), new Tone(415),
new Tone(440), new Tone(523), new Silence(), new Tone(440), new Tone(523),
new Tone(587), new Silence(250), new Tone(622), new Silence(250), new Tone(587), new Silence(250), new Tone(523),
new Silence(1125), new Tone(784), new Tone(740), new Tone(698), new Silence(42), new Tone(622),
new Silence(), new Tone(659), new Silence(167), new Tone(415), new Tone(440), new Tone(523),
new Silence(), new Tone(440), new Tone(523), new Tone(587), new Silence(250), new Tone(784),
new Tone(740), new Tone(698), new Silence(42), new Tone(622), new Silence(), new Tone(659),
new Silence(167), new Tone(698), new Silence(), new Tone(698), new Tone(698), new Silence(625),
new Tone(784), new Tone(740), new Tone(698), new Silence(42), new Tone(622), new Silence(),
new Tone(659), new Silence(167), new Tone(415), new Tone(440), new Tone(523),
new Silence(), new Tone(440), new Tone(523), new Tone(587), new Silence(250),
new Tone(622), new Silence(250), new Tone(587), new Silence(250), new Tone(523),
new Silence(625),
};
music.ForEach(s => s.Play());
Console.Write(
@"
____▒▒▒▒▒
—-▒▒▒▒▒▒▒▒▒
—–▓▓▓░░▓░
—▓░▓░░░▓░░░
—▓░▓▓░░░▓░░░
—▓▓░░░░▓▓▓▓
——░░░░░░░░
—-▓▓▒▓▓▓▒▓▓
–▓▓▓▒▓▓▓▒▓▓▓
▓▓▓▓▒▒▒▒▒▓▓▓▓
░░▓▒░▒▒▒░▒▓░░
░░░▒▒▒▒▒▒▒░░░
░░▒▒▒▒▒▒▒▒▒░░
—-▒▒▒ ——▒▒▒
–▓▓▓———-▓▓▓
▓▓▓▓———-▓▓▓▓
");
// data structures for sounds
public abstract record Sound(int Duration = 125)
{
public abstract void Play();
}
public record Silence(int Duration = 125) : Sound(Duration)
{
public override void Play()
{
var length = TimeSpan.FromMilliseconds(Duration).TotalSeconds;
var info = new ProcessStartInfo("play",
$"-n synth {length} sine 0") {RedirectStandardOutput = true};
var result = Process.Start(info);
result?.WaitForExit(Duration);
}
}
public record Tone(int Frequency, int Duration = 125) : Sound(Duration)
{
public override void Play()
{
var length = TimeSpan.FromMilliseconds(Duration).TotalSeconds;
var info = new ProcessStartInfo("play",
$"-n synth {length} sine {Frequency}") {
RedirectStandardOutput = true
};
var result = Process.Start(info);
result?.WaitForExit(Duration);
}
}
Running the console application, we hear the Super Mario Bros. theme song.
Because @maartenballiauw nerd sniped me into it.
— Khalid 🎅🎄 (@buhakmeh) December 15, 2020
Here is @JetBrainsRider and @dotnet playing the Super Mario theme using SoX. https://t.co/1Oqpn6lMh3 pic.twitter.com/oXVIevRp68
To run this code on your development environment, you can clone this project from my GitHub repository.
I hope you enjoyed this blog post, and please share any compositions with me on Twitter by tagging me at @buhakmeh.