Make a Simple Command Line Music Player With C# #4


Hello and welcome to the 4th and final part if this tutorial. This time, as discussed last time we are going to add user input. We are also going to add the ability for the user to remove file paths from the player. Why? Well you may have for example podcasts in your music folder; however you may not want to use them when shuffling your music, so this feature will allow the optional exclusion of folders and files.

Let’s begin with the folder exclusion as it is very simple and is shown below:

...
static void Main(string[] args)
{
	GetPaths();
	RemovePaths();
	...
}

private static void RemovePaths()
{
	Console.WriteLine("Enter Paths to exclude (type \"done\" when finished):"); 
 
	
	List<string> excluded = new List<string>();

	while (true)
	{
		string input = Console.ReadLine();
			
		if (input == "done")
			break;
		
		excluded.Add(input);
	}

	for (int i = 0; i < excluded.Count; i++)
	{
		for (int j = musicPath.Count - 1; j >= 0; j--)
		{
			if (musicPath[j].Contains(excluded[i]))
				musicPath.RemoveAt(j);
		}
	}
}

Firstly we have also added a call to this method after the GetPaths() method has been called, so there will be paths to remove.

This will remove all the paths that contain the words input into the excluded list. This is however, not actually very good code. Why? Well what if the user types in “Done”? That won’t work because that is not exactly equal to “done”. You may not see this as a problem, sometimes it may also be desired, but a user may find this unintuative so I want to change it.

Also, what if we don’t actually know how strings work? Why? Because they are hard. They contain culture specific information that can sometimes affect the way comparisons are done (Why we dont know how strings work). Eg Turky has no uppercase I... for some reason.

We can also condense this for loop down using the C# Linqs libaray so let’s to that:

...
using System.Linq;
...

private static void RemovePaths()
{
	Console.WriteLine("Enter Paths to exclude (type \"done\" when finished):");
	
	List<string> excluded = new List<string>();
	
	while (true)
	{
		var input = Console.ReadLine();
	
		if (input.Equals("done", StringComparison.InvariantCultureIgnoreCase))    
			break;
	}

	musicPath.RemoveAll(s => excluded.Any(e => s.Contains(e)));
}

Well that is a lot smaller now. but what does it do?

We have changed the if so now we use the Equals() method which is a static method built into the string type. We then pass in the “done” string and after an enum. This enum means that the comparison will be done ignoring the current “culture” (where the code is being run and it's respective language rules) also ignoring capitalization.

Next, we have the magic Linqs method:
 musicPath.RemoveAll(s => excluded.Any(e => s.Contains(e))); 
Firstly I will say I am not an expert with Linqs, so please don’t inundate me with questions about it.

Now with that out of the way:
musicPath.RemoveAll()
This method takes in a List<T> goes that it and removes everything that matched an item in the list. The "s" variable that is declared is then set to each item in the musicpath list in turn. Then the "e" variable is set to values of the excluded list, and checked against the current value of the "s" variable and if "e" is contained within "s" it is added to a new list. "e" is cycled through all the values excluded before "s" is then incremented. Effectively we have written the nested for loop we had before in one line. Not only does it make you look smart, but it also makes you feel good, doesn’t it? Sort of knowing how it works . This method also doesn’t force the user to type in the whole path just the folder and everything past that folder is excluded.
Why use .Contains() for path removal but not "done"
This is because a folder name could contain the word "done". So this is just to prevent a small edge case possibility of someone not being able to remove a file path with the word "done" in it.

Now we have done that, let’s also improve our path addition code as currently it will give an error if we input an invalid file path
So let's fix that

...
string inputPath = "";

do
{
	Console.WriteLine("Please input a valid file path: ");  
	inputPath = Console.ReadLine();
} while (!Directory.Exists(inputPath));

foreach (var path in Directory.GetFiles(inputPath, "*.*", SearchOption.AllDirectories))
{
...

Now this will allow the user to make a mistake when inputting the path without the program crashing \o/. The first time the loop is run the condition is not checked so the WriteLine and the input will be taken then it will be checked. If the file path does not exist then it will loop until the user input a valid file path.

Now let’s add the method to allow for user control:

static void Main(string[] args)
{
	...
	musicThread.Start();
	GetUserInput();
	...
}

private static void GetUserInput()
{
	while (true)
	{
		var input = Console.ReadLine();
	}
}

We have created a new method that contains an infinite loop to get the use input. The loop will pause whilst it waits for the input and will not affect the playing of songs as discussed previously (Previous tutorial if you need a refresher). We are also then calling the method after the thread that plays the music is started. Next lets add the volume control:

...
private static Thread musicThread;
private static int volume = 2;
...

private static void GetUserInput()
{
	while (true)
	{
		var input = Console.ReadLine();
		
		if (int.TryParse(input, out var vol) && vol >= 0 && vol <= 10)    
			player.settings.volume = volume = vol;
	}
}

private static void PlaySong()
{
	player.URL = musicPath[RandomSong()];
	player.settings.volume = volume;
	player.controls.play();

	while (true)
	{
		if (player.controls.currentPosition == 0)
		{
			player.URL = musicPath[RandomSong()];
			player.settings.volume = volume;
			player.controls.play();
			...
}

The volume variable will only take a type of int so we first check that the user has input a number, as int.TryParse() will return false if it is not a number or not a number that can be implicitly cast to an int, we also initialize the volume variable with a value of 2 but you can change this if you want. It is then checked that is not below 0 or above 10 as having a negative volume would not make sense and above 10 could hurt someone .

In the if statement the player.settings.volume is set to the volume variable which is set to the vol variable. We set the volume variable so that the volume changes will stay when the next song is played. This is also why we apply the volume each time the song is changed. This is also why we have changed the PlaySong() method slightly.

Now here’s a few more controls to finishup:

...
private static int volume = 2;
private static double pausePosition;
...

private static void GetUserInput()
{
	while (true)
	{
		var input = Console.ReadLine();

		if (int.TryParse(input, out var vol) && vol >= 0 && vol <= 10)
		{
			player.settings.volume = volume = vol;
		}
		else if (input.Equals("skip", StringComparison.InvariantCultureIgnoreCase))
		{
			musicThread.Abort();
			musicThread = new Thread(() => PlaySong());
			musicThread.Start();
		}
		else if (input.Equals("mute", StringComparison.InvariantCultureIgnoreCase))
		{
			player.settings.mute = !player.settings.mute;
		}
		else if (input.Equals("unmute", StringComparison.InvariantCultureIgnoreCase))
		{
			player.settings.mute = false;
		}
		else if (input.Equals("pause", StringComparison.InvariantCultureIgnoreCase))
		{
			pausePosition = player.controls.currentPosition;
			player.controls.pause();
		}
		else if (input.Equals("play", StringComparison.InvariantCultureIgnoreCase))
		{
			player.controls.currentPosition = pausePosition;
			player.controls.play();
		}
		else if (input.Equals("exit", StringComparison.InvariantCultureIgnoreCase))
		{
			musicThread.Abort();
			Environment.Exit(0);
		}
		else
		{
			Console.WriteLine($"Invalid Input: {input}");
			Console.WriteLine("Please Try Again");
		}
	}
}

Now this should add the controls that most users will want to have in their music shuffling programs. I will not go over all of them as some of this can be left as an exercise for the student so we will only go over parts of this. Firstly this:
pausePosition = player.controls.currentPosition
The player.controls.pause() method is supposed to save the current play position of the track however, I have found that it will sometimes not and the song will change upon unpausing the song, so this is added to prevent that.

The other thing we will discuss is this:
Environment.Exit(0);
This is how you can exit a C# program, not the most graceful of ways to be honest; it is more like stabbing the program to death rather than gracefully allowing it to leave, but it works so never mind. The Exit() function requires a parameter you may use any int value that you with however a value of 0 means that this was intended to happen so that is why we are using it.

The rest of the else if’s as stated before will be left as an exercise for the student (you) to figure out how it works.

But here we have a problem. The user has no indication of what song is playing and does not know any of the controls so let’s fix that as the final part of this tutorial.

We will add these 2 methods:

private static void CleanAndDisplayName()
{
	Console.Clear();
	PrintControls();
	Console.WriteLine($"Currently Playing: {player.currentMedia?.name}");
}

private static void PrintControls()
{
	var controls = "===========\nCommands:\nPause \nPlay\nSkip\nMute\nUnMute\n===========\nType a number for Volume Control: 1 - 10\n==========="; 
	Console.WriteLine(controls);
}
The first will clean the console and call the second which displays the controls and then displays the name of the currently playing song.

They are split into 2 methods so that they can easily be used separately if you wish to expand on the functionality yourself.

Now we add it into the code:

private static void GetUserInput()
{
	while (true)
	{
		...
		
		if (int.TryParse(input, out var vol) && vol >= 0 && vol <= 10)
		{
			player.settings.volume = volume = vol;
			CleanAndDisplayName();
		}
		else if (input.Equals("skip", StringComparison.InvariantCultureIgnoreCase))
		{
			musicThread.Abort();
			musicThread = new Thread(() => PlaySong());
			musicThread.Start();
			CleanAndDisplayName();
		}
		else if (input.Equals("mute", StringComparison.InvariantCultureIgnoreCase))
		{
			player.settings.mute = !player.settings.mute;
			CleanAndDisplayName();
		}
		else if (input.Equals("unmute", StringComparison.InvariantCultureIgnoreCase))
		{
			player.settings.mute = false;
			CleanAndDisplayName();
		}
		else if (input.Equals("pause", StringComparison.InvariantCultureIgnoreCase))
		{
			pausePosition = player.controls.currentPosition;
			player.controls.pause();
			CleanAndDisplayName();
		}
		else if (input.Equals("play", StringComparison.InvariantCultureIgnoreCase))
		{
			player.controls.currentPosition = pausePosition;
			player.controls.play();
			CleanAndDisplayName();
		}
		...
		else
		{
			CleanAndDisplayName();
			Console.WriteLine($"Invalid Input: {input}");
			...
}

private static void PlaySong()
{
	...
	player.controls.play();
	CleanAndDisplayName();
	
	while (true)
	{
		Thread.Sleep(50);
	
		if (player.controls.currentPosition == 0)
		{
			...
			player.controls.play();
			CleanAndDisplayName();
			Thread.Sleep(50);
			...
}

As you can see all it does is remove the user input once the if branch has been completed. It is also added to the play song thread so it is updated when the song is changed.

Another Thread.Sleep(50) has also been added in the while loop as I noticed the memory usage keeps rising if it was not there so with it there it is not stable, a before and after example picture is below:
This is now what the command line looks like when the code is compiled:
And when a user inputs an invalid command:

Now that is it for this tutorial series. Short but I hope informative and engaging as this will give you something real and useful you can use yourself and show your friends. If you are wondering, the full source and compiled release is downloadable below. Hope you enjoyed yourself during this tutorial and don’t forget to check out my other series.
Source Download
Download Source
Compiled Download
Download Compiled Version (Windows)

Back To Collection | Previous | Next