/// <summary> /// Checks if a given <see cref="MelodyTrackIndex"/> is valid within the given midi file. /// </summary> /// <param name="melodyTrackIndex">The one-based ordinal track index number to be checked. </param> /// <param name="midiFile"> The midi file that the track is checked against.</param> /// <param name="errorMessage"> Returned error message in case the track is invalid. </param> /// <returns> True if the track number is null or valid, false otherwise. </returns> public static bool IsMelodyTrackIndexValid(int?melodyTrackIndex, IMidiFile midiFile, out string errorMessage) { if (melodyTrackIndex.HasValue && melodyTrackIndex >= midiFile?.Tracks.Count) { errorMessage = $"Error: Invalid index for {nameof(melodyTrackIndex)}." + $"The given MIDI file has only {midiFile.Tracks.Count - 1} tracks."; return(false); } errorMessage = null; return(true); }
/// <summary> /// Creates a midi playback file by copying an existing midi file or stream /// and removing it's melody track, if such exists (according to the specified /// track number in the <paramref name="MelodyTrackNumber"/> parameter. /// <para> If the given midi is already a pure playback with no melody tracks, /// then a new copy of this midi would be returned. /// </para> /// </summary> /// <param name="midiStream"> The midi input that the playback should be created from. </param> /// <param name="MelodyTrackNumber">Number of the melody track in the given midi, if such a track exists.</param> /// <returns></returns> public static IMidiFile CreateMidiPlayback(Stream midiStream, MelodyTrackIndex?melodyTrackNumber) { // create a new midi file instance IMidiFile midifile = MidiFactory.CreateMidiFile(midiStream); // remove melody track is such exists on this midi file if (melodyTrackNumber.HasValue && melodyTrackNumber != MelodyTrackIndex.NoMelodyTrackInFile) { midifile.ExtractMelodyTrack((byte)melodyTrackNumber); } // return the resulted playback midi file return(midifile); }
public static bool IsMidiFileValid(HttpPostedFileBase midiFileHandler, out IMidiFile midiFile, out string errorMessage) { errorMessage = null; try { // fetch midi content from stream midiFile = CompositionContext.ReadMidiFile(midiFileHandler.InputStream); return(true); } catch (Exception ex) { midiFile = null; errorMessage = ex.Message; return(false); } }
private void btnUploadChords_Click(object sender, EventArgs e) { if (_chordsFileDialog.ShowDialog() == DialogResult.OK) { _chordsFilePath = _chordsFileDialog.FileName; _chordsFileName = _chordsFileDialog.SafeFileName; lblChordsFileName.Text = _chordsFileName; if (!string.IsNullOrWhiteSpace(_chordsFilePath) && !string.IsNullOrWhiteSpace(_midiFilePath)) { _composition = new CompositionContext(_chordsFilePath, _midiFilePath, melodyTrackIndex: MelodyTrackIndex.TrackNumber1); _midiFile = _composition.Compose()[0]; lblMidiTitle.Text += ": " + _midiFile.Title; lblMidiBpm.Text += ": " + _midiFile.BeatsPerMinute.ToString(); lblMidiTimeSignature.Text += ": " + _midiFile.KeySignature.ToString(); lblMidiBars.Text += ": " + _midiFile.NumberOfBars.ToString(); } } }
/// <summary> /// Checks that bar structure of the musical peace is compatible between it's description in the /// MIDI file and it's description in the chord progression read from the chords file. /// </summary> /// <param name="chordProgression"> Bar sequence which contains the chord progressionn from chords file. </param> /// <param name="midiFile"> MIDI file instance containning the bars music peace description. </param> /// <param name="errorMessage"> Returned error message in case the bars are not compatible. </param> /// <returns></returns> public static bool AreBarsCompatible(IList <IBar> chordProgression, IMidiFile midiFile, out string errorMessage) { // validate that total num of bars in MIDI matches num of bars in chord progression if (chordProgression?.Count != midiFile?.NumberOfBars) { errorMessage = $"Error: Number of bars mismatch: \n" + $"chord file has {chordProgression?.Count} bars," + $" while midi file has {midiFile?.NumberOfBars}!"; return(false); } // validate that each bar duration from CHORD progression matches the duration in MIDI IDuration midiDuration = null; byte barNumerator = 0; byte barDenominator = 0; for (int barIndex = 0; barIndex < chordProgression.Count; barIndex++) { // get bar's duration from chord progression barNumerator = chordProgression[barIndex].TimeSignature.Numerator; barDenominator = chordProgression[barIndex].TimeSignature.Denominator; // get bar's duration from midi file midiDuration = midiFile.GetBarDuration(barIndex); // validate equality if (barNumerator != midiDuration.Numerator || barDenominator != midiDuration.Denominator) { errorMessage = $"Error: Time signature '{barNumerator}/{barDenominator}' " + $"of bar number {barIndex + 1} in the chord progression " + $"does not match the corresponding time signature " + $"'{midiDuration.Numerator}/{midiDuration.Denominator}' " + $"in the midi file."; } } errorMessage = null; return(true); }
/// <summary> /// Construct a composition from a chord progression, midi file handler, /// and an indicator determinning if this midi file is a pure playback or whether /// it contains a melody track that should be replaced when composing a new melody. /// </summary> /// <remarks> Number of bars in midi file and chord progression must match exactly, and so do their durations. </remarks> /// <param name="chordProgression"> The bar sequence which contains the chord progression.</param> /// <param name="midiFile"> The MIDI file handler. </param> /// <param name="melodyTrackIndex"> Index of the existing melody track in the midi file, if one exists. /// <para> /// When composing, the track with this index would be replaced by the new composed /// melody track. If the current midi file is a pure playback that contains no existing /// melody track, then this property should be set to null. </para></param> /// <exception cref="InvalidDataException"> Thrown when there is a mismatch /// between the chord progression and the midi file, either in the total number /// of bars, or the duration of individual bars. </exception> /// <exception cref="IndexOutOfRangeException"> Throw when <paramref name="melodyTrackIndex"/> /// has a value which equal or greater than the number of track in the given midi file. </exception> public CompositionContext(IList <IBar> chordProgression, IMidiFile midiFile, MelodyTrackIndex?melodyTrackIndex = null) { // set chord progression and midi properties MidiInputFile = midiFile; ChordProgression = chordProgression; _melodyTrackIndex = melodyTrackIndex; _midiInputFilePath = MidiInputFile.FilePath; _midiInputFileName = Path.GetFileNameWithoutExtension(MidiInputFile.FilePath); // place holder for an error message if validtions fail string errorMessage; // validate melody track index is not out of bounds if (!CompositionContext.IsMelodyTrackIndexValid((int?)melodyTrackIndex, MidiInputFile, out errorMessage)) { throw new IndexOutOfRangeException(errorMessage); } // validate that bars in CHORD progression are compatible with MIDI file if (!AreBarsCompatible(chordProgression, MidiInputFile, out errorMessage)) { throw new InvalidDataException(errorMessage); } }
public async Task <ActionResult> Create(SongViewModel songViewModel) { // if model is invalid return back the view again if (!ModelState.IsValid) { return(View(songViewModel)); } // get current timestamp DateTime timestamp = DateTime.Now; // Create a new song instance based on the form data from the view model Song song = new Song { Created = timestamp, Modified = timestamp, Title = songViewModel.Title, Artist = songViewModel.Artist, MidiFileName = songViewModel.MidiFileHandler.FileName, ChordsFileName = songViewModel.ChordsFileHandler.FileName, MelodyTrackIndex = songViewModel.MelodyTrackIndex, IsPublic = songViewModel.IsPublic && User.IsInRole(RoleName.Admin), UserId = this.User.Identity.GetUserId(), }; // set name for the playback file to be created based on the midi file song.SetPlaybackName(); // save the new song in the database _databaseGateway.Songs.Add(song); await _databaseGateway.CommitChangesAsync(); // save the files of the new song on the file server IMidiFile playbackFile = null; try { // create a new directory for the new song string directoryPath = await GetSongPath(song.Id); Directory.CreateDirectory(directoryPath); // save the midi file in the new directory string midiFileFullPath = directoryPath + song.MidiFileName; songViewModel.MidiFileHandler.SaveAs(midiFileFullPath); // save the midi playback file in the new directory string midiPlaybackFullPath = directoryPath + song.MidiPlaybackFileName; playbackFile = CompositionContext.CreateMidiPlayback(songViewModel.MidiFileHandler.InputStream, song.MelodyTrackIndex); playbackFile.SaveFile(outputPath: midiPlaybackFullPath, pathIncludesFileName: true); // save the chord progression file in the new directory string chordsFilefullPath = directoryPath + song.ChordsFileName; songViewModel.ChordsFileHandler.SaveAs(chordsFilefullPath); } catch (Exception ex) { // in case of failure, rollback DB changes and log error message _databaseGateway.Songs.Remove(song); await _databaseGateway.CommitChangesAsync(); HomeController.WriteErrorToLog(HttpContext, ex.Message); } finally { // release open unmanaged resources songViewModel.ChordsFileHandler?.InputStream?.Dispose(); songViewModel.MidiFileHandler?.InputStream?.Dispose(); playbackFile?.Stream?.Dispose(); } // If creation was successful, redirect to new song details page string successMessage = $"The Song '{song.Title}' by '{song.Artist}' was successfully uploaded."; return(RedirectToAction(nameof(Details), new { Id = song.Id, message = successMessage })); }
public async Task <ActionResult> Compose(CompositionViewModel model) { // validate that sum of weights is equal to 100 double weightSum = model.WeightSum; if (weightSum != 100) { string errorMessage = $"The total weights of all fitness function " + $"evaluators must sum up to 100.\n" + $"The current sum is {weightSum}"; this.ModelState.AddModelError(nameof(model.AccentedBeats), errorMessage); } // assure all other validations passed okay if (!ModelState.IsValid) { CompositionViewModel viewModel = new CompositionViewModel { SongSelectList = new SelectList(_databaseGateway.Songs.GetAll().OrderBy(s => s.Title), nameof(Song.Id), nameof(Song.Title)), PitchSelectList = new SelectList(_pitchSelectList, nameof(PitchRecord.Pitch), nameof(PitchRecord.Description)), }; return(View(viewModel)); } // fetch song from datasource Song song = _databaseGateway.Songs.Get(model.SongId); // get the chord and file paths on the file server string chordFilePath = await SongsController.GetSongPath(song.Id, _databaseGateway, User, SongFileType.ChordProgressionFile); string midiFilePath = await SongsController.GetSongPath(song.Id, _databaseGateway, User, SongFileType.MidiOriginalFile); // create a compositon instance CompositionContext composition = new CompositionContext( chordProgressionFilePath: chordFilePath, midiFilePath: midiFilePath, melodyTrackIndex: song.MelodyTrackIndex); // build evaluators weights MelodyEvaluatorsWeights weights = new MelodyEvaluatorsWeights { AccentedBeats = model.AccentedBeats, ContourDirection = model.ContourDirection, ContourStability = model.ContourStability, DensityBalance = model.DensityBalance, ExtremeIntervals = model.ExtremeIntervals, PitchRange = model.PitchRange, PitchVariety = model.PitchVariety, SmoothMovement = model.SmoothMovement, Syncopation = model.Syncopation }; // Compose some melodies and fetch the best one IMidiFile midiFile = composition.Compose( strategy: CompositionStrategy.GeneticAlgorithmStrategy, overallNoteDurationFeel: model.OverallNoteDurationFeel, musicalInstrument: model.MusicalInstrument, minPitch: model.MinPitch, maxPitch: model.MaxPitch, useExistingMelodyAsSeed: model.useExistingMelodyAsSeed, customParams: weights) .FirstOrDefault(); // save the midifile output internally _midiFile = midiFile; ViewBag.MidiFile = midiFile; // save file on the file server string directoryPath = HomeController.GetFileServerPath() + "Outputs" + Path.DirectorySeparatorChar + song.Id + Path.DirectorySeparatorChar; Directory.CreateDirectory(directoryPath); string filePath = _midiFile.SaveFile(outputPath: directoryPath); // return the file to the client client for downloading string fileName = Path.GetFileName(filePath); byte[] fileBytes = System.IO.File.ReadAllBytes(filePath); return(File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName)); }
/// <summary> /// Composes a solo-melody over this composition's midi playback file and chord progression, /// using the additional preferences and constraints parameters. /// </summary> /// <returns> A new midi file containing the composed solo-melody. </returns> /// <param name="strategy">Requested composition strategy algorithm for composing the melody. </param> /// <param name="overallNoteDurationFeel">Requested generic density overall feeling of the outcome melody.</param> /// <param name="musicalInstrument">Requested virtual music instrumen tha would play the output melody.</param> /// <param name="pitchRangeSource">Determins whether to set the pitch range from the original melody in the /// midi file, or from the custom values mentiones in <paramref name="minPitch"/> /// and <paramref name="maxPitch"/>.</param> /// <param name="minPitch">Requested lowest available pitch that would can be used for the composition. /// This is relevant only if <paramref name="pitchRangeSource"/> was set to /// <see cref="PitchRangeSource.Custom"/>.</param> /// <param name="maxPitch">Requested highest available pitch that would can be used for the composition. /// This is relevant only if <paramref name="pitchRangeSource"/> was set to /// <see cref="PitchRangeSource.Custom"/>.</param> /// <param name="useExistingMelodyAsSeed">If set, then the original melody might be used in initialization phase /// of the compositon strategy algorithm. This is relevant only if the /// midi file contains an existing melody to begin with. /// Moreover, this feature is implementation dependent: some strategies /// might make high use of original melody as the main initialization seed /// and some may not use it at all.</param> /// <param name="customParams"> Currently unused. This a placeholder for additional parameters that could be used /// in the future to extened existing implementations and developing new ones without /// breaking the existing interface. </param> /// <returns></returns> public IMidiFile[] Compose( CompositionStrategy strategy = CompositionStrategy.GeneticAlgorithmStrategy, OverallNoteDurationFeel overallNoteDurationFeel = OverallNoteDurationFeel.Medium, MusicalInstrument musicalInstrument = MusicalInstrument.AcousticGrandPiano, PitchRangeSource pitchRangeSource = PitchRangeSource.Custom, NotePitch minPitch = DefaultMinPitch, NotePitch maxPitch = DefaultMaxPitch, bool useExistingMelodyAsSeed = true, params object[] customParams) { // set compositor according to composition strategy _composer = ComposerFactory.CreateComposer(strategy); // make a copy of the input midi file for the output file MidiOutputFile = MidiFactory.CreateMidiFile(_midiInputFilePath); /* if the midi file already contains a melody track, * extract it out of the intended midi output file * and if requested, save it in a separate bar sequence for further usage * as a melody initialization seed for the composition if needed. */ if (_melodyTrackIndex.HasValue && _melodyTrackIndex.Value != MelodyTrackIndex.NoMelodyTrackInFile) { /* if the existing melody should serve as a seed, * initialize a bar-sequence place-holder for it, * based on the chord progression structure */ _melodySeed = useExistingMelodyAsSeed ? CompositionContext.CloneChordProgressionBars(ChordProgression) : null; /* extract/remove the existing melody from midi file and * save it in the place holder if it was initialized */ MidiOutputFile.ExtractMelodyTrack((byte)_melodyTrackIndex, _melodySeed); } // initialize pitch range from midi file if requested if (pitchRangeSource == PitchRangeSource.MidiFile && _melodyTrackIndex.HasValue) { NotePitch?lowestPitch, highestPitch; MidiInputFile.GetPitchRangeForTrack((int)_melodyTrackIndex, out lowestPitch, out highestPitch); if (lowestPitch.HasValue && highestPitch.HasValue) { minPitch = (NotePitch)lowestPitch; maxPitch = (NotePitch)highestPitch; } } // validate pitch range is at least one octave long (12 semi-tones) if (!IsPitchRangeValid((int)minPitch, (int)maxPitch, out string errorMessage)) { throw new ArgumentException(errorMessage); } // compose a new melody IList <IBar>[] composedMelodies = _composer.Compose( chordProgression: ChordProgression, melodyInitializationSeed: _melodySeed, overallNoteDurationFeel: overallNoteDurationFeel, minPitch: minPitch, maxPitch: maxPitch, customParams: customParams) .ToArray(); // Embed each new generated melody into a new separate midi file IMidiFile[] midiOutputs = new IMidiFile[composedMelodies.Length]; for (int i = 0; i < composedMelodies.Length; i++) { midiOutputs[i] = MidiFactory.CreateMidiFile(_midiInputFilePath); midiOutputs[i].ExtractMelodyTrack((byte)_melodyTrackIndex); midiOutputs[i].EmbedMelody(composedMelodies[i], musicalInstrument); midiOutputs[i].FadeOut(); } // save first output in dedicated placeholder MidiOutputFile = midiOutputs[0]; // return the first composed melody from the output return(midiOutputs); }