Monday, 4 November 2013

Announcing “Audio Programming with NAudio”

I’m really excited to announce the release of my latest Pluralsight course “Audio Programming with NAudio”. This is the follow-on course to my introductory “Digital Audio Fundamentals” course, and is intended to give a thorough and systematic run-through of how to use all the major features of NAudio.
The modules are as follows:
  • Introducing NAudio in which I go through all the things you can find in NAudio, and explain what the demo applications do as well. This module also introduces the three base classes/interfaces for all signal chain components in NAudio (IWaveProvider, WaveStream and ISampleProvider), which are crucial to understanding how to work with NAudio.
  • Audio File Playback explains how to decide which of NAudio’s playback classes you should use, and how they are configured. I cover lots of important playback related tasks such as how to change volume, reposition, and even how to stop (yes, that can require more thought than you might imagine).
  • Working with Files deals with the various file readers in NAudio, as well as showing how to create wave files with WaveFileWriter.
  • Changing Audio Formats might seem longwinded and a bit boring, but is actually one of the most important modules in the course. It explains how to change the sample rate, bit depth and channel count of PCM audio. This is something that you need to do regularly when dealing with sampled audio, and there are lots of different approaches you can take.
  • Working with Codecs explains how you can use the ACM and MediaFoundation codecs on your computer, and also gives a couple of other techniques for working with codecs that you might find helpful.
  • Recording Audio shows how to use the various NAudio recording classes, including loopback capture, and a brief demo of high performance low level capture using ASIO.
  • Visualizations is not really about NAudio, but gives some useful strategies for creating peak meters, waveform displays and spectrum analyzers in both WinForms and WPF. I included it because this is something lots of NAudio users wanted to know how to do.
  • Mixing and Effects is probably my favourite module in the course. To show how to do mixing and effects I create a software piano, and turn it into a software synthesizer.
  • Audio Streaming covers another topic that NAudio doesn’t directly address, but that lots of NAudio users are interested in. This module shows how you can implement playback of streaming media, and how you would go about creating a network chat application.
I really hope that everyone planning to create a serious application with NAudio will take the time to watch this course (and the previous one if necessary). You’ll find you will be a lot more productive once you know the basics of digital audio, and understand the design philosophy behind the NAudio library.
I know Pluralsight is not free, but receiving some financial compensation for the time I put into this course enabled me to spend a lot more time on it than I would have been able to otherwise. The Pluralsight library is ridiculously well stocked with courses on all kinds of development technologies and practices, so you can’t fail to get good value for the monthly subscription fee. Having said that, please be assured that I remain committed to provide good training materials on NAudio, and I plan to keep blogging and updating the main site with more documentation.
I have actually reached the point with NAudio that I am getting far more requests for help than I can keep up with. I’m averaging 10 questions/emails per day, and so if I miss even a few days I get hopelessly behind. If I’ve failed to answer your question, please accept my apologies, and part of the goal in creating this course is having something I can point people to. So if I answer your question with a link to this course, please don’t be offended.

Sunday, 3 November 2013

Finding Prime Factors in F#

After having seen a few presentations on F# over the last few months, I’ve been looking for some really simple problems to get me started, and this code golf question inspired me to see if I could factorize a number in F#. I started off by doing it in C#, taking the same approach as the answers on the code golf site (which are optimised for code brevity, not performance):

var n = 502978;
for (var i = 2; i <= n; i++)
{
    while (n%i < 1)
    {
        Console.WriteLine(i);
        n /= i;
    }
}

Obviously it would be possible to try to simply port this directly to F#, but it felt wrong to do so, because there are two mutable variables in this approach (i and n). I started wondering if there was a more functional way to do this working entirely with immutable types.

The algorithm we have starts by testing if 2 is a factor of n. If it is, it divides n by 2 and tries again. Once we’ve divided out all the factors of 2, we increment the test number and repeat the process. Eventually we get down to to the final factor when i equals n.

So the F# approach I came up with, uses a recursive function. We pass in the number to be factorised, the potential factor to test (so we start with 2), and an (immutable) list of factors found so far, which starts off as an empty list. Whenever we find a factor, we create a new immutable list with the old factors as a tail, and call ourselves again. This means n doesn’t have to be mutable – we simply pass in n divided by the factor we just found. Here’s the F# code:

let rec f n x a = 
    if x = n then
        x::a
    elif n % x = 0 then 
        f (n/x) x (x::a)
    else
        f n (x+1) a
let factorise n = f n 2 []
let factors = factorise 502978

The main challenge to get this working was figuring out where I needed to put brackets (and remembering not to use commas between arguments). You’ll also notice I created a factorise function, saving me passing in the initial values of 2 and an empty list. This is one of F#’s real strengths, making it easy to combine functions like this.

Obviously there are performance improvements that could be made to this, and I would also like at some point to work out how to make a non-recursive version of this that still uses immutable values. But at least I now have my first working F# “program”, so I’m a little better prepared for the forthcoming F# Tutorial night with Phil Trelford at devsouthcoast.

If you’re an F# guru, feel free to tell me in the comments how I ought to have solved this.

Tuesday, 29 October 2013

NAudio 1.7 Release Notes

It’s been just over a year since the last release, so an updated release of NAudio is long overdue and there’s a lot of great new features added over the last year which I’m really excited to make official. In particular, this release adds Media Foundation support, allowing you to play from a much wider variety of audio file types, including extracting audio from video files. There’s also been significant progress on Windows 8 store app support although it still isn’t quite ready for official release.

As usual you can get it via Nuget, or download it from Codeplex. Last year NAudio 1.6 was downloaded 29616 times from Codeplex and 7089 times on nuget.

What’s new in NAudio 1.7?

  • MediaFoundationReader allows you to play any audio files that Media Foundation can play, which on Windows 7 and above means playback of AAC, MP3, WMA, as well as playing the audio from video files.
  • MediaFoundationEncoder allows you to easily encode audio using any Media Foundation Encoders installed on your machine. The WPF Demo application shows this in action, allowing you to encode AAC, MP3 and WMA files in Windows 8.
  • MediaFoundationTransform is a low-level class designed to be inherited from, allowing you to get direct access to Media Foundation Transforms if that’s what you need.
  • MediaFoundationResampler direct access to the Media Foundation Resampler MFT as an IWaveProvider, with the capability to set the quality level.
  • NAudio is now built against .NET 3.5. This allows us to make use of language features such as extension methods, LINQ and Action/Func parameters.
  • You can enumerate Media Foundation Transforms to see what’s installed. The WPF Demo Application shows how to do this.
  • WasapiCapture supports exclusive mode, and a new WASAPI capture demo has been added to the WPF demo application, allowing you to experiment more easily to see what capture formats your soundcard will support.
  • A new ToSampleProvider extension method on IWaveProvider now makes it trivially easy to to convert any PCM WaveProvider to an ISampleProvider. There is also another extension method allowing an ISampleProvider to be passed directly into any IWavePlayer implementation without the need for converting back to an IWaveProvider first.
  • WaveFileWriter supports creating a 16 bit WAV file directly from an ISampleProvider with the new CreateWaveFile16 static method.
  • IWavePosition interface implemented by several IWavePlayer classes allows greater accuracy of determining exact position of playback. Contribution courtesy of ioctlLR
  • AIFF File Writer (courtesy of Gaiwa)
  • Added the ability to add a local ACM driver allowing you to use ACM codecs without installing them. Use AcmDriver.AddLocalDriver
  • ReadFully property allows you to create never-ending MixingSampleProvider, for use when dynamically adding and removing inputs.
  • WasapiOut now allows setting the playback volume directly on the MMDevice.
  • Support for sending MIDI Sysex messages, thanks to Marcel Schot
  • A new BiQuadFilter for easy creation of various filter types including high pass, low pass etc
  • A new EnvelopeGenerator class for creating ADSR envelopes based on a blog post from Nigel Redmon.
  • Lots of bugfixes (see the commit history for more details). Some highlights include…
      • Fixed a long-standing issue with MP3FileReader incorrectly interpreting some metadata as an MP3 frame then throwing an exception saying the sample rate has changed.
      • WaveFileReader.TryReadFloat works in stereo files
      • Fixed possible memory exception with large buffer sizes for WaveInBuffer and WaveOutBuffer
  • Various code cleanups including removal of use of ApplicationException, and removal of all classes marked as obsolete.
  • Preview Release of WinRT support. The NAudio nuget package now includes a WinRT version of NAudio for Windows 8 store apps. This currently supports basic recording and playback. This should still very much be thought of as a preview release. There are still several parts of NAudio (in particular several of the file readers and writers) that are not accessible, and we may need to replace the MFT Resampler used by WASAPI with a fully managed one, as it might mean that Windows Store certification testing fails.
    • Use WasapiOutRT for playback
    • Use WasapiCaptureRT for record (thanks to Kassoul for some performance enhancement suggestions)
    • There is a demo application in the NAudio source code showing record and playback

NAudio Training

Another thing I’ve wanted to do for ages is to create some really good training materials for NAudio. I began this with the release of my Digital Audio Fundamentals course on Pluralsight, which is all about giving you the background understanding you need in order to work effectively with NAudio. And I’m on the verge of releasing the follow-on course which is around 6 hours of in-depth training on how to use NAudio. Keep an eye on my Pluralsight Author page and you should see it in a couple of weeks. I’ll also announce it here on my blog. You do need to be a Pluralsight subscriber to watch these videos, but it’s only $29 for a month’s subscription, which also gives you access to their entire library, making it a great deal.

What’s next?

I’ve still got lots of plans for NAudio. Here’s a few of the things near the top of my list for NAudio 1.8:

  • Finish Windows Store support. In particular, this means ensuring it passes Windows App Certification
  • I’d really like to add a fully managed resampler, which would help out in a lot of scenarios. I’ve got one ported from C already, but sadly it’s license (LGPL) isn’t compatible with NAudio’s.
  • More Media Foundation features. In particular, supporting reading from and writing to streams.
  • I’ll probably obsolete the DirectX Media Object components, as they serve little purpose now that we have MediaFoundation support
  • More managed effects implemented as ISampleProvider
  • I have some ideas for making a fluent API for NAudio to make it much easier to construct complex signal chains in a single line.

Hope you find NAudio useful. Don’t forget to get in touch and tell me what you’ve built with it.

Monday, 30 September 2013

The Five Rules of ReSharper

I recently managed to persuade my work to get ReSharper licences for our development team. I’ve been using it since I won a free copy at Developer South Coast, and after being initially opposed to the idea of such a heavy-duty Visual Studio extension, it has won me over and I’m relying on it increasingly in my daily work.

Last week I ran some training sessions for the developers at work to introduce them to its features, but also to warn against some ways in which the tool can be misused. Here’s my top five rules for using ReSharper (or any productivity tool for that matter). Let me know in the comments if you have any additional rules you’d add to the list.

1. It’s Your Code

ReSharper can offer to delete unused methods or fields, or to refactor a loop into a single LINQ statement. It provides several automatic refactorings such as renaming things, extracting methods etc. On the whole these seem to be extremely reliable, and before long you’ll be triggering these refactorings without a second thought.

But when you commit, it is your responsibility to ensure that the code works fully. Of course, the best way of doing this is having unit tests that you can run, so you can prove that when ReSharper deletes unused code, or refactors a loop, that everything still runs as expected. If you check in broken code, it is your responsibility alone - you can’t blame the tool. ReSharper sometimes gets it wrong (especially when detecting “redundant” code), but it never forces you to commit to source control. It’s your job to test that everything is still working as expected before committing your changes.

2. They’re Only Suggestions

imageimage

ReSharper has a superb way of presenting the issues that it has found with the source file you are currently editing, by presenting a bar showing where in the file various errors and warnings are.

If you manage to eliminate them all, you are rewarded with a nice green tick, telling you that your code is now perfect. But this can lead to problems for people with an OCD tendency. They become obsessed by an overwhelming desire to make all of the warnings go away.

What you need to constantly bear in mind is that these are only suggestions. And sometimes the code is actually better like it is. Resist the urge to make a change that you don’t agree with. If seeing the warning sign really upsets you then R# helpfully allows you to supress warnings by use of a comment.

3. Keep code cleanup commits separate from regular commits

Fixing up the warnings that R# finds is so quick and easy, that you can find yourself wandering through a source file fixing all its suggestions without even thinking about it. The trouble comes when you ask someone to code review your bug-fix. They end up having to examine a huge diff, mostly nothing to do with fixing the bug. The solution here is simply to keep your bug-fix changes and your code cleanup changes separate. Or at least restrict the code cleanup to the immediate vicinity of the bug-fix.

4. Avoid code cleanup in maintenance branches

If like us you have to maintain multiple legacy versions of your software then you probably have to merge bug-fixes between branches on a regular basis. For those merges to go well, you want minimal impact to code, and code cleanup (especially renaming things) can end up causing widespread changes which can make merges painful. So the approach I have adopted for our team is to make minimal changes to legacy branches, but allow the code in the very latest version to be cleaned up as it is worked on.

5. Get it clean on first commit

Of course, the ideal is that when you write new code, you implement (or suppress) all of the R# suggestion before committing to source control. That way my rules #3 and #4 become redundant. Make sure you set up a team shared R# settings file so everyone is working to the same standards.

Saturday, 27 July 2013

Announcing Digital Audio Fundamentals

I get several emails every day asking me for help with NAudio, and often it is clear that the real issue is that many developers only have a rather vague grasp of the concepts of digital audio. So I wanted to create a really good training course that would introduce and clearly explain all the things I think are really important to developers working with digital audio in general, and NAudio in particular.

So I’m really pleased to announce the availability of my Digital Audio Fundamentals course on Pluralsight. The course begins by explaining the essential topic of sampling. In particular you need to understand sample rates and bit depths if you are to be successful with NAudio. I then talk about various file formats and lots of different codecs, before looking at several types of effects. The final module may in fact be the most important for NAudio developers as I explain the concept of “signal chains” or “pipelines”. To get things done with NAudio you need to have a clear grasp of the signal chain that you are working with. NAudio tries to make it easy for you to construct a signal chain that does exactly what you need out of many simple components.

So I’d really encourage anyone who wants to use NAudio and is able to view this course to do so. It’s just under 3 hours, but it will be a very worthwhile investment of your time. I’m hoping that maybe later this year there will be a follow-up course focusing specifically on using NAudio.

I know some of you may be disappointed that this is not a free course, but I think you’ll find the Pluralsight pricing to be very reasonable. You could always sign up for just one month to watch this course, and get the added benefit of tapping into their vast library of excellent courses on a wide variety of programming topics.

Thursday, 27 June 2013

Lunchtime LINQ Challenge Answers

I had a number of people attempt the Lunchtime LINQ Challenge I set yesterday, and so I thought I’d follow up with a post about each problem, and what my preferred approach was. With each problem I saw a wide variety of different approaches, and the majority were correct (apart from the odd off-by-one error). LINQ is very powerful, and chaining operators can let you achieve a lot, but you can risk creating incomprehensible code. So I was looking for answers that were succinct but also readable.

Problem 1: Numbering Players

1. Take the following string "Davis, Clyne, Fonte, Hooiveld, Shaw, Davis, Schneiderlin, Cork, Lallana, Rodriguez, Lambert" and give each player a shirt number, starting from 1, to create a string of the form: "1. Davis, 2. Clyne, 3. Fonte" etc

This one wasn’t too hard, and was designed to highlight the fact that the LINQ Select method lets you pass in a lambda that takes two parameters – the first is the item from the input IEnumerable, and the second is an index (zero based). Here was my solution to this problem:

String.Join(", ",
    "Davis, Clyne, Fonte, Hooiveld, Shaw, Davis, Schneiderlin, Cork, Lallana, Rodriguez, Lambert"
        .Split(',')
        .Select((item, index) => index+1 + "." + item)
        .ToArray())

Problem 2: Order by Age

2. Take the following string "Jason Puncheon, 26/06/1986; Jos Hooiveld, 22/04/1983; Kelvin Davis, 29/09/1976; Luke Shaw, 12/07/1995; Gaston Ramirez, 02/12/1990; Adam Lallana, 10/05/1988" and turn it into an IEnumerable of players in order of age (bonus to show the age in the output).

This basic problem isn’t too hard to solve, but calculating the age proved rather tricky. Various attempts were made such as dividing the days old by 365.25, but the only one that worked for all test cases came from this StackOverflow answer. The trouble is, inserting this code snippet into a LINQ statement would make it quite cumbersome, so my preference would be to create a small helper method or even an extension method:

public static int GetAge(this DateTime dateOfBirth)
{
    DateTime today = DateTime.Today;
    int age = today.Year - dateOfBirth.Year;
    if (dateOfBirth > today.AddYears(-age)) age--;
    return age;
}

With that extension method in place, we can create much more readable code. Note that to sort players by age properly, the OrderBy should operate on the date of birth, rather than on the age.

"Jason Puncheon, 26/06/1986; Jos Hooiveld, 22/04/1983; Kelvin Davis, 29/09/1976; Luke Shaw, 12/07/1995; Gaston Ramirez, 02/12/1990; Adam Lallana, 10/05/1988"
.Split(';')
.Select(s => s.Split(','))
.Select(s => new { Name = s[0].Trim(), Dob = DateTime.Parse(s[1].Trim()) })
.Select(s => new { Name = s.Name, Dob = s.Dob, Age = s.Dob.GetAge() })
.OrderByDescending (s => s.Dob)

Problem 3: Sum Timespans

3. Take the following string "4:12,2:43,3:51,4:29,3:24,3:14,4:46,3:25,4:52,3:27" which represents the durations of songs in minutes and seconds, and calculate the total duration of the whole album

The main challenges here are first turning the strings into TimeSpans, and then summing TimeSpans, since you can’t use LINQ’s Sum method on an IEnumerable<TimeSpan>. The first can be done with TimeSpan.ParseExact, or you could add an extra “0:” on the beginning to get it into the format that TimeSpan.Parse expects. Several people ignored timespans completely, and simply parsed out the minutes and seconds themselves, and added up the total number of seconds. This is OK, although not as extensible for changes to the input string format such as the introduction of millisecond components. The summing of TimeSpans can be done quite straightforwardly with the LINQ Aggregate method. Here’s what I came up with:

"4:12,2:43,3:51,4:29,3:24,3:14,4:46,3:25,4:52,3:27"
    .Split(',')
    .Select(s => TimeSpan.Parse("0:" + s))
    .Aggregate ((t1, t2) => t1 + t2)

Problem 4: Generate Coordinates

4. Create an enumerable sequence of strings in the form "x,y" representing all the points on a 3x3 grid. e.g. output would be: 0,0 0,1 0,2 1,0 1,1 1,2 2,0 2,1 2,2

This one is nice and easy. I expected this one to be solved using a SelectMany, but I did get one answer that just used a Select:

Enumerable.Range(0,9)
    .Select(x => string.Format("{0},{1}", x / 3, x % 3))

This works fine, although it would be a little more involved to change the maximum x and y values. The answer I was expecting from most people just uses two Enumerable.Range and a SelectMany

Enumerable.Range(0,3)
.SelectMany(x => Enumerable.Range(0,3)
    .Select(y => String.Format("{0},{1}",x,y)))

But I did get a few responses that use the alternative LINQ syntax, and although I tend to prefer the chained method calls approach, in this case I think it makes for easier to read code:

from i in Enumerable.Range(0, 3)
from j in Enumerable.Range(0, 3)
select String.Format("{0}, {1}", i, j);

Problem 5: Swim Length Times

5. Take the following string "00:45,01:32,02:18,03:01,03:44,04:31,05:19,06:01,06:47,07:35" which represents the times (in minutes and seconds) at which a swimmer completed each of 10 lengths. Turn this into an enumerable of timespan objects containing the time taken to swim each length (e.g. first length was 45 seconds, second was 47 seconds etc)

This one was by far the hardest to implement as a single LINQ statement, since you needed to compare the current value with the previous value to calculate the difference. Perhaps the easiest trick is to Zip the sequence with itself delayed by one:

("00:00," + splitTimes).Split(',')
.Zip(splitTimes.Split(','), (s,f) => new 
    { Start = TimeSpan.Parse("0:" + s), 
      Finish = TimeSpan.Parse("0:" + f) })
.Select (q =>  q.Finish-q.Start)      

The disadvantage of this approach is that you are enumerating the input sequence twice. Obviously for a string input it does not matter, but in some situations you cannot enumerate a sequence twice. I did receive two ingenious solutions that used the Aggregate function to avoid the double enumeration:

"00:45,01:32,02:18,03:01,03:44,04:31,05:19,06:01,06:47,07:35"
    .Split(new char[] { ','})
    .Select(x => "00:"+x)
    .Aggregate((acc, z) => acc + "," + TimeSpan.Parse(z.Dump()).Add(new TimeSpan(0,0,- 
        acc.Split(new char[] { ',' })
            .Sum(x => (int)TimeSpan.Parse(x).TotalSeconds))))    
    .Split(new char[] {','})
    .Select(x => TimeSpan.Parse(x))

and

.Split(',')
.Aggregate("00:00:00", (lapsString, lapTime) => 
    string.Format("{0}-00:{1},00:{1}", lapsString, lapTime), result => result.Substring(0, result.Length - 9))
.Split(',')
.Select(lap => DateTime.Parse(lap.Substring(9)) - DateTime.Parse(lap.Substring(0,8)))

But to be honest, I think that this is another case for a helper extension method. Suppose we had a ZipWithSelf method that allowed you to operate on the sequence in terms of the current and previous values:

public static IEnumerable<X> ZipWithSelf<T,X>(this IEnumerable<T> inputSequence, Func<T,T,X> selector)
{
    var last = default(T);
    foreach(var input in inputSequence)
    {
        yield return selector(last, input);
        last = input;
    }
}

then we could make the LINQ statement read very naturally and only require a single enumeration of the sequence:

"00:45,01:32,02:18,03:01,03:44,04:31,05:19,06:01,06:47,07:35"
    .Split(',')    
    .Select(x => TimeSpan.Parse("00:"+x))
    .ZipWithSelf((a,b) => b - a)

Problem 6: Ranges

6. Take the following string "2,5,7-10,11,17-18" and turn it into an IEnumerable of integers: 2 5 7 8 9 10 11 17 18

This one represents a real problem I had to solve a while ago, and after a few iterations, found that it could be solved quite succinctly in LINQ. The trick is to turn it into an enumeration of start and end values. For the entries that aren’t ranges, just have the same value for start and end. Then you can do a SelectMany combined with Enumerable.Range:

"2,5,7-10,11,17-18"
    .Split(',')
    .Select(x => x.Split('-'))
    .Select(p => new { First = int.Parse(p[0]), Last = int.Parse(p.Last()) })
    .SelectMany(r => Enumerable.Range(r.First, r.Last - r.First + 1))

Wednesday, 26 June 2013

Lunchtime LINQ Challenge

I do regular lunchtime developer training sessions at my work, and this week I created a short quiz for the developers to try out their LINQ skills. They are not too hard (although number 5 is quite tricky) and most are based on actual problems I needed to solve recently. I thought I’d post them here too in case anyone fancies a short brain-teaser. If you attempt it, why not post your answers to a Github gist and link to them in the comments. I’ll hopefully do a followup post with some thoughts on how I would attempt these. Also, feel free to suggest additional interesting LINQ problems of your own…

The Challenge…

Each of these problems can be solved using a single C# statement by making use of chained LINQ operators (although you can use more statements if you like). You'll find the String.Split function helpful to get started on each problem. Other functions you might need to use at various points are String.Join, Enumerable.Range, Zip, Aggregate, SelectMany. LINQPad would be a good choice to try out your ideas.

1. Take the following string "Davis, Clyne, Fonte, Hooiveld, Shaw, Davis, Schneiderlin, Cork, Lallana, Rodriguez, Lambert" and give each player a shirt number, starting from 1, to create a string of the form: "1. Davis, 2. Clyne, 3. Fonte" etc

2. Take the following string "Jason Puncheon, 26/06/1986; Jos Hooiveld, 22/04/1983; Kelvin Davis, 29/09/1976; Luke Shaw, 12/07/1995; Gaston Ramirez, 02/12/1990; Adam Lallana, 10/05/1988" and turn it into an IEnumerable of players in order of age (bonus to show the age in the output)

3. Take the following string "4:12,2:43,3:51,4:29,3:24,3:14,4:46,3:25,4:52,3:27" which represents the durations of songs in minutes and seconds, and calculate the total duration of the whole album

4. Create an enumerable sequence of strings in the form "x,y" representing all the points on a 3x3 grid. e.g. output would be: 0,0 0,1 0,2 1,0 1,1 1,2 2,0 2,1 2,2

5. Take the following string "00:45,01:32,02:18,03:01,03:44,04:31,05:19,06:01,06:47,07:35" which represents the times (in minutes and seconds) at which a swimmer completed each of 10 lengths. Turn this into an enumerable of timespan objects containing the time taken to swim each length (e.g. first length was 45 seconds, second was 47 seconds etc)

6. Take the following string "2,5,7-10,11,17-18" and turn it into an IEnumerable of integers: 2 5 7 8 9 10 11 17 18