Click here to Skip to main content
13,145,068 members (55,022 online)
Click here to Skip to main content
Add your own
alternative version

Stats

2.9K views
7 bookmarked
Posted 24 Aug 2017

DryWetMIDI: High-level processing of MIDI files

, 24 Aug 2017
Rate this:
Please Sign up or sign in to vote.
Overview of how to use the DryWetMIDI library for high-level managing of a MIDI file data.

Introduction

DryWetMIDI is a .NET library to work with Standard MIDI Files (SMF) – read, write, create and modify them. Although there are a lot of .NET libraries which provide parsing of MIDI files there are some features that make the DryWetMIDI special:

  • ability to read files with some corruptions like missed End of Track event;
  • ability to finely adjust process of reading and writing which allows, for example, to specify Encoding of text stored in text-based meta events like Lyric;
  • set of high-level classes that allow to manage content of a MIDI file in more understandable way like Note or MusicalTime.

The last point is the most important part of the library since many users ask about managing notes or conversion of MIDI time and length to more human understandable representation like seconds. To solve these problems they are forced to write the same code again and again. DryWetMIDI frees them from the necessity to reinvent the wheel providing built-in tools to perform described tasks.

This article gives a quick overview of high-level data managing capabilities provided by the DryWetMIDI. It is not an API reference. The library also has a low-level layer of interaction with MIDI file content but it is not a subject of the article.

There are examples at the end of the article that show how you can solve real tasks using features provided by the DryWetMIDI. So you can move to them right now if you want to get overall impression of the library.

[^] top

Background

It is recommended to be familiar with SMF format but not necessary if you are going to work only with high-level API of the DryWetMIDI.

[^] top

Contents

  1. Absolute time
  2. Notes
  3. Chords
  4. Tempo map
  5. Time representations
  6. Length representations
  7. MathTime and MathLength
  8. Pattern
  9. Examples
  10. Links

[^] top

Absolute time

All events inside a MIDI file have a delta-time attached to them. Delta-time is an offset from previous event. Units of this offset defined by the time division of the file. According to SMF specification there are two possible time divisions:

  • ticks per quarter note defines amount of time the quarter note lasts (this time division used by 99.999% of all MIDI files so we can assume that all files have it);
  • SMPTE time division defines times as numbers of subdivisions of SMPTE frame along with the specified frame rate.

In practice it is often more convinient to operate by absolute time rather than relative one. DryWetMIDI provides TimedEventsManager class designed to manage events by their absolute times:

using Melanchall.DryWetMidi.Smf;
using Melanchall.DryWetMidi.Smf.Interaction;

// Read a MIDI file

var midiFile = MidiFile.Read("Cool song.mid");

// Manage timed events of the first track chunk of the file

using (TimedEventsManager timedEventsManager = midiFile.Chunks
                                                       .OfType<TrackChunk>()
                                                       .First()
                                                       .ManageTimedEvents())
{
    // Get timed events ordered by time

    TimedEventsCollection events = timedEventsManager.Events;
    
    // Set absolute time of the first Lyric event

    TimedEvent firstLyricEvent = events.Where(e => e.Event is LyricEvent).FirstOrDefault();
    if (firstLyricEvent != null)
        firstLyricEvent.Time = 2000;

    // Add new Pitch Bend event with absolute time = 3000

    events.Add(new TimedEvent(new PitchBendEvent(8000), 3000));
}

After exiting from the using section all events contained in the managing track chunk will be replaced with ones contained in the events collection updating all delta-times. Also you can call SaveChanges method of the TimedEventsManager to save all changes. This method especially useful if you are working with TimedEventsManager across multiple methods:

private TimedEventsManager _timedEventsManager;

private void BeginManageEvents(TrackChunk trackChunk)
{
    _timedEventsManager = trackChunk.ManageTimedEvents();
}

private void ShiftEvents(long shift)
{
    foreach (TimedEvent timedEvent in _timedEventsManager.Events)
    {
        timedEvent.Time += shift;
    }
}

private void EndManageEvents()
{
    _timedEventsManager.SaveChanges(); // or you can call Dispose
}

All other managers described below have the same saving logic.

Also there are some useful extension methods contained in the TimedEventsManagingUtilities. For example, you can easily remove all System Exclusive events with time of 400 from a file:

midiFile.RemoveTimedEvents(e => e.Event is SysExEvent && e.Time == 400);

Or you can divide times of all events by 2 to shrink a MIDI file:

midiFile.ProcessTimedEvents(e => e.Time /= 2);

Other managers also have utility methods similar to those for timed events. It is worth to take a look at classes where these extensions methods are placed. Also all managers provided by the DryWetMIDI can be obtained via constructor rather than via utility methods for low-level entities.

[^] top

Notes

To present notes a MIDI file uses pairs of Note On and Note Off events. But often people want to work with notes without messing with low-level MIDI events. DryWetMIDI provides NotesManager class for this purpose:

using (NotesManager notesManager = midiFile.GetTrackChunks() // shortcut method for
                                                             // Chunks.OfType<TrackChunk>()
                                           .First()
                                           .ManageNotes())
{
    // Get notes ordered by time

    NotesCollection notes = notesManager.Notes;

    // Get all C# notes

    IEnumerable<Note> cSharpNotes = notes.Where(n => n.NoteName == NoteName.CSharp);

    // Reduce length of all C# notes by 100

    foreach (var note in cSharpNotes)
    {
        note.Length -= 100;
    }
    
    // Add new note: C# of octave with number of 2
    // Note: DryWetMIDI uses scientific pitch notation which means middle C is C4

    notes.Add(new Note(NoteName.CSharp, 2)
    {
        Channel = (FourBitNumber)2,
        Velocity = (SevenBitNumber)95
    });
}

As with timed events there are useful utilities for notes managing which are contained in the NotesManagingUtilities class. One of the most useful ones is GetNotes method that allows to get all notes contained in a track chunk or entire MIDI file:

IEnumerable<Note> notes = midiFile.GetNotes();

Note that if you'll do any changes on returned notes, they will not be applied. All manipulations with notes must be done via NotesManager or you can use ProcessNotes method from the NotesManagingUtilities. For example, to transpose all F notes up by one octave you can use this code:

f.ProcessNotes(n => n.NoteNumber += (SevenBitNumber)12,
               n => n.NoteName == NoteName.F);

[^] top

Chords

Chord is just a group of notes. To work with chords you need to use ChordsManager class:

using (ChordsManager chordsManager = midiFile.GetTrackChunks()
                                             .First()
                                             .ManageChords(100)) // 100 is a notes tolerance
                                                                 // that defines maximum distance
                                                                 // of notes from the start of the
                                                                 // first note of a chord. Notes
                                                                 // within this tolerance will be
                                                                 // considered as a chord
{
    // Get chords ordered by time

    ChordsCollection chords = chordsManager.Chords;

    // Get all chords that have C# note

    IEnumerable<Chord> cSharpChords = chords.Where(c => c.Notes
                                                         .Any(n => n.NoteName == NoteName.CSharp));
}

You can see that ManageChords (and ChordsManager's constructor) can take notes tolerance that will be taken into an account to build chords. For example, two notes where first one has time of 10 and second one has time of 100 will represent a chord if the tolerance will be greater than or equal to 90. For smaller values of the tolerance the second note will be out of it and will not fall into the chord. The default tolerance is 0 which means notes must start at the same time to make up a chord.

As with managing of notes there are utility methods for chords manipulations. No prizes for guessing what the name of the class that holds these methods. It is ChordsManagingUtilities.

[^] top

Tempo map

Tempo map is a list of all changes of the tempo and time signature in a MIDI file. With TempoMapManager you can set new values of these parameters at the specified time and obtain current tempo map:

using (TempoMapManager tempoMapManager = midiFile.ManageTempoMap())
{
    // Get current tempo map

    TempoMap tempoMap = tempoMapManager.TempoMap;

    // Get time signature at 2000

    TimeSignature timeSignature = tempoMap.TimeSignatureLine.AtTime(2000);

    // Set new tempo (230,000 microseconds per quarter note) at the time of
    // 20 seconds from the start of the file. See "Time representations"
    // section below to learn about time classes

    tempoMapManager.SetTempo(new MetricTime(0, 0, 20),
                             new Tempo(230000));
}

TempoMap also holds an instance of TimeDivision in order to use it for time and length conversions. To get tempo map of a MIDI file just call GetTempoMap extension method from the TempoMapManagingUtilities:

TempoMap tempoMap = midiFile.GetTempoMap();

Also you can easy replace the tempo map of a MIDI file with another one using ReplaceTempoMap method. For example, to change tempo of a file to the 50 BPM and time signature to 5/8 you can write this code:

midiFile.ReplaceTempoMap(TempoMap.Create(Tempo.FromBeatsPerMinute(50),
                                         new TimeSignature(5, 8)));

Tempo map is a very important object since it allows to perform time and length conversions as you'll see in the next sections.

[^] top

Time representations

As you could notice all times in code samples above are presented as some long values in units defined by the time division of a file. In practice it is much more convenient to operate by "human understandable" times like seconds or bars/beats. DryWetMIDI provides the following time classes:

  • MetricTime that represents time in terms of microseconds;
  • MusicalTime that represents time in terms of number of bars, beats and/or a fraction of the whole note's length.

All time classes implement ITime interface. To convert time between different representations you should use TimeConverter class:

// Tempo map is needed in order to perform time conversions

TempoMap tempoMap = midiFile.GetTempoMap();

// Some time in MIDI ticks (we assume time division of a MIDI file is "ticks per quarter note")

long ticks = 123;

// Convert ticks to metric time

MetricTime metricTime = TimeConverter.ConvertTo<MetricTime>(ticks, tempoMap);

// Convert ticks to musical time

MusicalTime musicalTimeFromTicks = TimeConverter.ConvertTo<MusicalTime>(ticks, tempoMap);

// Convert metric time to musical time

MusicalTime musicalTimeFromMetricTime = TimeConverter.ConvertTo<MusicalTime>(metricTime, tempoMap);

// Convert musical time back to ticks

long ticksFromMusicalTime = TimeConverter.ConvertFrom(musicalTimeFromTicks, tempoMap);

Also there are some useful methods in the TimedObjectUtilities class. This class contains extension methods for types that implement the ITimedObject interface – TimedEvent, Note and Chord. For example, you can get time of a timed event in hours, minutes, seconds with TimeAs method:

var metricTime = timedEvent.TimeAs<MetricTime>(tempoMap);

Or you can find all notes of a MIDI file that start at time of 10 bars and 4 triplet eighth note lengths:

TempoMap tempoMap = midiFile.GetTempoMap();
IEnumerable<Note> notes = midiFile.GetNotes()
                                  .AtTime(new MusicalTime(10, 0, 4 * MusicalFraction.TripletEighth),
                                          tempoMap);

Some examples of how you can create an instance of specific time class:

// 100,000 microseconds

var metricTime1 = new MetricTime(100000 /* microseconds */);

// 0 hours, 1 minute and 55 seconds

var metricTime2 = new MetricTime(0, 1, 55);

// Zero time

var metricTime3 = new MetricTime();

// 2 bars and 7 beats

var musicalTime1 = new MusicalTime(2, 7);

// 0 bars, 5 beats and 1/8

var musicalTime2 = new MusicalTime(0, 5, MusicalFraction.Eighth);

// Five 5/17

var musicalTime3 = new MusicalTime(5 * new Fraction(5, 17));

If you want, for example, to know length of a MIDI file in minutes and seconds, you can use this code:

var tempoMap = midiFile.GetTempoMap();
var midiFileDuration = midiFile.GetTimedEvents()
                               .LastOrDefault(e => e.Event is NoteOffEvent)
                               ?.TimeAs<MetricTime>(tempoMap)
                               ?? new MetricTime();

[^] top

Length representations

As with time all lengths are presented as some long values by default since, in fact, length is just the difference between two times. Length units defined by the time division of a file as well as time units. DryWetMIDI provides several classes to represent length in another way:

  • MetricLength that represents length in terms of microseconds;
  • MusicalLength that represents length in terms of a fraction of the whole note's length.

All length classes implement ILength interface. To convert length between different representations you should use LengthConverter class:

// Tempo map is needed in order to perform length conversions

TempoMap tempoMap = midiFile.GetTempoMap();

// Some length in MIDI ticks (we assume time division of a MIDI file is "ticks per quarter note")

long ticks = 123;

// Convert ticks to metric length

MetricLength metricLength = LengthConverter.ConvertTo<MetricLength>(ticks, time, tempoMap);

// Convert ticks to musical length

MusicalLength musicalLengthFromTicks = LengthConverter.ConvertTo<MusicalLength>(ticks, time, tempoMap);

// Convert metric length to musical length

MusicalLength musicalLengthFromMetricTime = LengthConverter.ConvertTo<MusicalLength>(metricLength,
                                                                                     time,
                                                                                     tempoMap);

// Convert musical length back to ticks

long ticksFromMusicalLength = LengthConverter.ConvertFrom(musicalLengthFromTicks, time, tempoMap);

You could notice that LengthConverter's methods take another one argument – time. In general case MIDI file has changes of the tempo and time signature. Thus the same long value can represent different amount of seconds, for example, depending on the time of an object with length of this value. The methods above can take time either as long or as ITime.

There are some helpful methods in the LengthedObjectUtilities class. This class contains extension methods for types that implement the ILengthedObject interface – Note and Chord. For example, you can get length of a note as a fraction of the whole note with LengthAs method:

var musicalTime = note.LengthAs<MusicalLength>(tempoMap);

Or you can get all notes of a MIDI file that end exactly at 30 seconds from the start of the file:

var tempoMap = midiFile.GetTempoMap();
var notesAt30sec = midiFile.GetNotes()
                           .EndAtTime(new MetricTime(0, 0, 30), tempoMap);

[^] top

Pattern

For purpose of simple MIDI file creation that allows you to focus on the music, there is the PatternBuilder class. This class provides a fluent interface to build a musical composition that can be exported to a MIDI file. A quick example of what you can do with the builder:

var patternBuilder = new PatternBuilder()
     
    // Insert a pause of 5 seconds

    .StepForward(new MetricLength(0, 0, 5))

    // Insert an eighth C# note of the 4th octave

    .Note(OctaveDefinition.Get(4).CSharp, (MusicalLength)MusicalFraction.Eighth)

    // Set default note length to triplet eighth and default octave to 5

    .SetNoteLength((MusicalLength)MusicalFraction.EighthTriplet)
    .SetOctave(5)

    // Now we can add triplet eighth notes of the 5th octave in a simple way

    .Note(NoteName.A)
    .Note(NoteName.B)
    .Note(NoteName.GSharp);

Build method will return an instance of the Pattern class containing all actions performed with the builder. Pattern then can be exported to a MIDI file:

Pattern pattern = patternBuilder.Build();

// Export the pattern to a MIDI file using default tempo map (4/4, 120 BPM)

MidiFile midiFile = pattern.ToFile(TempoMap.Default);

It is only a small part of the PatternBuilder features. It has much more ones including specifying note velocity, inserting of chords, setting time anchors, moving to specific time and repeating previous actions. So the Pattern is a sort of music programming bound to MIDI. See example at the end of the article that shows how to build the first four bars of the Beethoven's "Moonlight Sonata".

[^] top

MathTime and MathLength

In fact, the DryWetMIDI has another one time class – MathTime. The purpose of this class is to represent simple mathematical operation on arbitrary ITime and ILength: summation or subtraction.

For example, you want to add a note with time of some unknown ITime plus 30 seconds. Without MathTime your code will be like this:

ITime currentTime = ...;

var convertedCurrentTime = TimeConverter.ConvertFrom(currentTime, tempoMap);
var convertedStep = LengthConverter.ConvertFrom(new MetricLength(0, 0, 30),
                                                convertedCurrentTime,
                                                tempoMap);
var noteTime = convertedCurrentTime + convertedStep;

var note = new Note(NoteName.A, 2, length, noteTime);

This code is slightly verbose but it works.

Now you want to add a note with time of some unknown ITime minus 30 seconds. You may decide to use the code above replacing + with - for noteTime calculation. But it will not work in general case. If there are changes of tempo within 30 seconds area of currentTime, you'll get a wrong result since this code takes tempo changes that go after currentTime into consideration. But for correct result an algorithm have to deal with tempo map changes that go before currentTime. So here you'll encounter a serious problem. You can use ugly code with downcasts and iterating through tempo map but there is a simpler solution – MathTime:

ITime currentTime = ...;

var noteTime = TimeConverter.ConvertFrom(new MathTime(currentTime,
                                                      new MetricLength(0, 0, 30),
                                                      MathOperation.Subtract),
                                         tempoMap);

var note = new Note(NoteName.A, 2, length, noteTime);

But you should understand that it is impossible to convert time to MathTime, because there are tons of variants how some long or ITime can be represented as sum or difference between ITime and ILength. So you can perform conversion only from MathTime to another time representation.

In addition to MathTime there is the MathLength class which solves the same problems but for arbitrary ILength objects instead of ITime. Example below shows how to create a note with length of some ILength minus eighth note length:

long noteTime = ...;
ILength currentLength = ...;

var noteLength = LengthConverter.ConvertFrom(new MathLength(currentLength,
                                                            (MusicalLength)MusicalFraction.Eighth,
                                                            MathOperation.Subtract),
                                             noteTime,
                                             tempoMap);

var note = new Note(NoteName.A, 2, noteLength, noteTime);

As with MathTime you cannot convert length to MathLength.

[^] top

Examples

Let's see how you can use the DryWetMIDI for some real tasks.

Notes quantization

One of the most common operations with notes is quantization, or snapping to a grid with the specified step. We are going to write the method that takes an instance of the MidiFile along with a grid step as arguments and performs quantization of notes times (don't touching lengths). For simplicity, we will work with musical grid steps only.

The method is quite simple:

public static void QuantizeNotes(MidiFile midiFile, MusicalLength step)
{
    // Convert step to MIDI ticks

    long stepAsTicks = LengthConverter.ConvertFrom(step,
                                                   0,
                                                   midiFile.GetTempoMap());

    midiFile.ProcessNotes(note =>
    {
        long oldTime = note.Time;

        // To find nearest grid point we take note's time (expressed in ticks)
        // and divide it by grid step. Then we just round result of division
        // and multiply it by grid step

        note.Time = (long)Math.Round(oldTime / (double)stepAsTicks) * stepAsTicks;
    });
}

Let's check its results. Our input file contains notes as shown on image below (the picture shows single bar):

If we call the method with half step, we'll get following result:

QuantizeNotes(midiFile, (MusicalLength)MusicalFraction.Half);

For eighth step the picture will be like that:

QuantizeNotes(midiFile, (MusicalLength)MusicalFraction.Eighth);

[^] top

Notes merging

Sometimes in real MIDI files notes can overlap each other. Following method merges overlapping notes of the same channel and pitch:

public static void MergeNotes(MidiFile midiFile)
{
    foreach (var trackChunk in midiFile.GetTrackChunks())
    {
        MergeNotes(trackChunk);
    }
}

private static void MergeNotes(TrackChunk trackChunk)
{
    using (var notesManager = trackChunk.ManageNotes())
    {
        var notes = notesManager.Notes;

        // Create dictionary for storing currently merging notes of each channel (key)
        // and each pitch (key of dictionary used as value for channel)

        var currentNotes = new Dictionary<FourBitNumber, Dictionary<SevenBitNumber, Note>>();

        foreach (var note in notes.ToList())
        {
            var channel = note.Channel;

            // Get currently merging notes of the channel

            if (!currentNotes.TryGetValue(channel, out var currentNotesByNoteNumber))
                currentNotes.Add(channel, currentNotesByNoteNumber =
                                              new Dictionary<SevenBitNumber, Note>());

            // Get the currently merging note

            if (!currentNotesByNoteNumber.TryGetValue(note.NoteNumber, out var currentNote))
            {
                currentNotesByNoteNumber.Add(note.NoteNumber, currentNote = note);
                continue;
            }

            var currentEndTime = currentNote.Time + currentNote.Length;

            // If time of the note is less than end of currently merging one,
            // we should update length of currently merging note and delete the
            // note from the notes collection

            if (note.Time <= currentEndTime)
            {
                var endTime = Math.Max(note.Time + note.Length, currentEndTime);
                currentNote.Length = endTime - currentNote.Time;

                notes.Remove(note);
            }

            // If the note doesn't overlap currently merging one, the note become
            // a currently merging note

            else
                currentNotesByNoteNumber[note.NoteNumber] = note;
        }
    }
}

If we take input file like that:

and perform merging with the method above, we'll get following result:

MergeNotes(midiFile);

[^] top

Building of "Moonlight Sonata"

This example shows how you can use Pattern to build a musical composition. First four bars of the Beethoven's "Moonlight Sonata" will help us with this.

public static Pattern BuildMoonlightSonata()
{
    // Define a chord for bass part which is just an octave

    var bassChord = new[] { IntervalDefinition.Twelve };

    // Build the composition

    return new PatternBuilder()

        // The length of all main theme's notes within four first bars is
        // triplet eight so set it which will free us from necessity to specify
        // the length of each note explicitly

        .SetNoteLength((MusicalLength)MusicalFraction.EighthTriplet)

        // Anchor current time (start of the pattern) to jump to it
        // when we'll start to program bass part

        .Anchor()

        // We will add notes relative to G#3.
        // Instead of OctaveDefinition.Get(3).GSharp it is possible to use
        // NoteDefinition.Get(NoteName.GSharp, 3)

        .SetRootNote(OctaveDefinition.Get(3).GSharp)

        // Add first three notes and repeat them seven times which will
        // give us two bars of the main theme

                                        // G#3
        .Note(IntervalDefinition.Zero)  // +0  (G#3)
        .Note(IntervalDefinition.Five)  // +5  (C#4)
        .Note(IntervalDefinition.Eight) // +8  (E4)
        .Repeat(3, 7)                   // repeat three previous notes seven times

        // Add notes of the next two bars

                                        // G#3
        .Note(IntervalDefinition.One)   // +1  (A3)
        .Note(IntervalDefinition.Five)  // +5  (C#4)
        .Note(IntervalDefinition.Eight) // +8  (E4)
        .Repeat(3, 1)                   // repeat three previous notes
        .Note(IntervalDefinition.One)   // +1  (A3)
        .Note(IntervalDefinition.Six)   // +6  (D4)
        .Note(IntervalDefinition.Ten)   // +10 (F#4)
        .Repeat(3, 1)                   // repeat three previous notes
                                        // reaching the end of third bar
        .Note(IntervalDefinition.Zero)  // +0  (G#3)
        .Note(IntervalDefinition.Four)  // +4  (C4)
        .Note(IntervalDefinition.Ten)   // +10 (F#4)
        .Note(IntervalDefinition.Zero)  // +0  (G#3)
        .Note(IntervalDefinition.Five)  // +5  (C#4)
        .Note(IntervalDefinition.Eight) // +8  (E4)
        .Note(IntervalDefinition.Zero)  // +0  (G#3)
        .Note(IntervalDefinition.Five)  // +5  (C#4)
        .Note(IntervalDefinition.Seven) // +7  (D#4)
        .Note(-IntervalDefinition.Two)  // -2  (F#3)
        .Note(IntervalDefinition.Four)  // +4  (C4)
        .Note(IntervalDefinition.Seven) // +7  (D#4)

        // Now we will program bass part. To start adding notes from the
        // beginning of the pattern we need to move to the anchor we set
        // above

        .MoveToFirstAnchor()

        // First two chords hal whole length

        .SetNoteLength((MusicalLength)MusicalFraction.Whole)

                                                            // insert a chord relative to
        .Chord(bassChord, OctaveDefinition.Get(2).CSharp) // C#2 (C#2, C#3)
        .Chord(bassChord, OctaveDefinition.Get(1).B)      // B1  (B1, B2)

        // Remaining four chords has half length

        .SetNoteLength((MusicalLength)MusicalFraction.Half)

        .Chord(bassChord, OctaveDefinition.Get(1).A)      // A1  (A1, A2)
        .Chord(bassChord, OctaveDefinition.Get(1).FSharp) // F#1 (F#1, F#2)
        .Chord(bassChord, OctaveDefinition.Get(1).GSharp) // G#1 (G#1, G#2)
        .Repeat()                                         // repeat the previous chord

        // Build a pattern that can be then saved to a MIDI file

        .Build();
}

[^] top

Links

NuGet package: https://www.nuget.org/packages/Melanchall.DryWetMidi

GitHub repository: https://github.com/melanchall/drywetmidi

Wiki of the library: https://github.com/melanchall/drywetmidi/wiki

[^] top

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Maxim Dobroselsky
Russian Federation Russian Federation
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionI Got following errors how to resolve it Pin
Member 1329069918-Sep-17 22:51
professionalMember 1329069918-Sep-17 22:51 
AnswerRe: I Got following errors how to resolve it Pin
Maxim Dobroselsky21-Sep-17 0:27
memberMaxim Dobroselsky21-Sep-17 0:27 
GeneralRe: I Got following errors how to resolve it Pin
Member 132906991 hr 58mins ago
professionalMember 132906991 hr 58mins ago 
GeneralRe: I Got following errors how to resolve it Pin
Maxim Dobroselsky1 hr 25mins ago
memberMaxim Dobroselsky1 hr 25mins ago 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.170915.1 | Last Updated 24 Aug 2017
Article Copyright 2017 by Maxim Dobroselsky
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid