public async Task Play() { using (var settings = new Settings()) { settings[ConfigurationKeys.AudioDriver].StringValue = "pulseaudio"; settings[ConfigurationKeys.SynthAudioChannels].IntValue = 2; settings[ConfigurationKeys.AudioRealtimePrio].IntValue = 0; settings[ConfigurationKeys.SynthVerbose].IntValue = 0; settings[ConfigurationKeys.AudioPeriodSize].IntValue = 1024; settings[ConfigurationKeys.SynthReverbActive].IntValue = ReverbEnabled ? 1 : 0; settings[ConfigurationKeys.SynthChorusActive].IntValue = ChorusEnabled ? 1 : 0; using (var syn = new Synth(settings)) { syn.LoadSoundFont("/usr/share/sounds/sf2/FluidR3_GM.sf2", true); using (var adriver = new AudioDriver(syn.Settings, syn)) { if (ReverbEnabled) { syn.SetReverb(ReverbRoomSize, ReverbDamping, ReverbWidth, ReverbLevel); } if (ChorusEnabled) { syn.SetChorus(ChorusNumVoices, ChorusLevel, ChorusSpeed, ChorusDepthMS, ChorusMod); } // Hardcoded, will be changed in the future syn.ProgramChange(1, (int)GeneralMidi.Violin); if (Midi != null) { // Meanwhile we are cheating a little bit and using the build-in FluidSynth MIDI player // In the future it would be nice to read the events from the MIDI file and play them ourselves // That way we could mix MIDI files with our own music expressions, and maybe even transform MIDI files using (var player = new Player(syn)) { player.Add(Midi); player.Play(); // Synchronous join. Thankfully this code runs in a separate thread. Would be nice to have a async version player.Join(); } } else { var sw = new System.Diagnostics.Stopwatch(); sw.Start(); // Explaining the timer process used // All note commands (NoteOn/NoteOff) are ordered by their noteCommand.Timestamp (value in milliseconds relative to the beginning // of the music). Let's imagine that two of them are, for example, one second apart. Ideally, we would want to do a // Thread.Sleep( 1000 ) and hope that the thread would unblock exactly 1000 milliseconds after. However, the system's clock // resolution varies between different Operating Systems and different physical Processors. So we could not be sure when the thread // would be woken. Instead we do a Thread.Sleep( 1000 - minResolution ), which hopefully means that our thread will awake before the note is due // Three things can happen now: // - We can be early, i.e. we still have to wait a bit, and for that we use a simple loop with a high-precision Stopwatch // - We can be on time, in which case we just play the note // - We can be late, in wich case we play the note right away and add to the drift variable how late we are // Every subsequent timestamp will have the drift variable added to it to compensate int minResolution = 30; int drift = 0; foreach (INoteCommand noteCommand in BuildCommands(Notes)) { int timestamp = noteCommand.Timestamp + drift; int elapsed = (int)sw.Elapsed.TotalMilliseconds; if (timestamp - minResolution > elapsed) { await Task.Delay(Math.Max(0, timestamp - elapsed - minResolution)); } while (timestamp > sw.Elapsed.TotalMilliseconds) { Thread.Sleep(0); } elapsed = (int)sw.Elapsed.TotalMilliseconds; noteCommand.Apply(syn); if (timestamp < elapsed) { drift += elapsed - timestamp; } } sw.Stop(); Console.WriteLine($"Total drift: {drift}ms out of {sw.Elapsed.TotalMilliseconds}ms"); } } } } }