Turnerj L'homme. Le mythe. Le programmeur.

Shuffle Fail: Fixing my car stereo with code!

Dec 28, 2018

Background

I have a 40-ish minute commute to work and love blasting music in my car (sorry if you were in the car next to me). I bought my car used 6 years ago and it still has the same stereo since I bought it.

Alpine CDA-9886 Alpine CDA-9886 - the stereo currently in my car

I have my music on a USB that I plug into my car and I like listening to the music on shuffle (I don't want to anticipate the next song). I had suspicions for a while that my car wasn't actually shuffling the music very well.

Some songs I have heard quite a lot (even back-to-back) and others I have never heard play. Turns out my stereo has an interesting quirk of only allowing the first 100 songs in a folder to play. This is definitely a problem but wasn't my only problem as I did have some of my music spread through dozens of folders which did occasionally play.

If I were to guess, from the few hundred songs I had loaded on my USB, I'd say there was probably 60 songs that I actually heard with shuffle enabled.

I'm a programmer dammit, we bend computers to our will! I'm sure there is something I can do...

!Random

xkcd: Random Number xkcd #221

You may or may not know this but many random number functions in our code are not as random as you might expect. These are pseudorandom number generators that are actually determined by an initial value, a seed.

The seed for a pseudorandom number generator is often based on the current time which can lead to an interesting problem if you're not careful. If you want a lot of random numbers in a short period of time, you want to use the same instance of the generator otherwise you might end up with the exact same number as it may have the same seed.

Pseudorandom numbers are useful for a lot of things but definitely shouldn't be used for cryptographic purposes which you will normally find noted in documentation for these functions.

With my car stereo, I don't know how the seed is generated or how it comes up with the random number. While it would be a fun job trying to reverse engineer the stereo to control an aspect like that, I was hoping for a simpler solution.

Debugging

Through some more repetitive trips to work and a bit more debugging, I discovered something interesting about playing without shuffle. Without shuffle, I thought the music would play in alphabetical order or maybe by date modified but it is far simpler than that - it uses the order that it was written to the file system.

This might seem like an obvious thing to others but was a bit of a surprise for me and gave me a way to potentially work around my randomness problem. If the songs are loaded up in a different order, I'd get to listen to my music in a different order.

Solution

The solution is straight forward - remove all music from the USB then re-add music to the USB in both a shuffled (to achieve my randomness goal) and "chunked" (to avoid the 100 files in a folder problem) fashion.

Here is what I cobbled together:

class Program
{
	static Random rand = new Random();

	static void Main(string[] args)
	{
		var source = @"C:\Path\To\My\Music\";
		var destination = @"E:\";

		Console.WriteLine($"Source: {source}");
		Console.WriteLine($"Destination: {destination}");

		Console.WriteLine("Press Enter to continue...");
		Console.ReadLine();

		RemoveAllFiles(destination);
		CopyFiles(source, destination);

		Console.WriteLine("Complete! Press Enter to close");
		Console.ReadLine();
	}

	static void RemoveAllFiles(string location)
	{
		foreach (var file in Directory.EnumerateFiles(location))
		{
			Console.WriteLine($"Deleteing {file}...");
			File.Delete(file);
		}
	}

	static void CopyFiles(string source, string destination)
	{
		var sourceFiles = Directory.EnumerateFiles(source).ToArray();

		Shuffle(rand, sourceFiles);

		var foldersOfFiles = ChunkBy(sourceFiles.ToList(), 25);

		for (int i = 0, l = foldersOfFiles.Count; i < l; i++)
		{
			var folderName = $"{i + 1}";
			var folderPath = Path.Combine(destination, folderName);

			Directory.CreateDirectory(folderPath);

			var files = foldersOfFiles[i];

			for (int i2 = 0, l2 = files.Count; i2 < l2; i2++)
			{
				var sourceFile = files[i2];
				var destFile = Path.Combine(folderPath, Path.GetFileName(files[i2]));
					
				Console.WriteLine($"Copying {sourceFile} to {destFile}...");
				File.Copy(sourceFile, destFile);
			}
		}
	}

	static void Shuffle<T>(Random rng, T[] array)
	{
		int n = array.Length;
		while (n > 1)
		{
			int k = rng.Next(n--);
			T temp = array[n];
			array[n] = array[k];
			array[k] = temp;
		}
	}

	static List<List<T>> ChunkBy<T>(List<T> source, int chunkSize)
	{
		return source
			.Select((x, i) => new { Index = i, Value = x })
			.GroupBy(x => x.Index / chunkSize)
			.Select(x => x.Select(v => v.Value).ToList())
			.ToList();
	}
}

Credit where credit is due, Stack Overflow was the source for both that shuffle and chunking implementation.

You might notice I'm also using the Random class which is a pseudorandom number generator but that is actually fine in this case. My problem stemmed from the car stereo having a bad pseudorandom generator (or seed) so with my computer randomising the music on upload to the USB, I have worked around that problem.

This doesn't eliminate that shuffle play on my car stereo still only plays about 60 songs but it does mean that I now don't need to have shuffle enabled as my music already is shuffled.