[Continued] Read mp3 tags (ID3v1, ID3v2) (no library used) + List in DataGridView / Rewrite ID3v1 tags according to command line arguments

18 minute read

Overview

I made the following two types of software.

■ 1. I tried to make it possible to display a list using DataGridView. The save function (reflection processing to the file) is not supported.
How to compile: csc ID3Util.cs Mp3TagEditor.cs

■ 2. Rewrite the ID3v1 tag data according to the command line arguments.
* Please use at your own risk. Overwrite the mp3 file. It is not a smart implementation to create a backup file, so it is recommended to make a backup of the target mp3 file when using it.
How to compile: csc ID3Util.cs Mp3TagSetter.cs

Reference site

mp3 ID3v1, ID3v2 tags

The above reference site

DataGridView

  • https://garafu.blogspot.com/2016/09/cs-datagridview-customdata.html
  • https://qiita.com/lusf/items/dcce573787e808ccb0ea
    –How to automatically notify the control when property changes
    • https://teratail.com/questions/122430
    • https://tnakamura.hatenablog.com/entry/20091228/notify_property_changed

Screen capture

■1.
image.png

■2.
image.png

When using multi-byte characters such as Japanese, pay attention to the character code (code page) setting of the command prompt (you can check and change it with the chcp command of the command prompt). If the character code of the file and the command prompt do not match, such as when using a batch file, an unintended character string may be passed to the program.

Source code ID3Util.cs

ID3Util.cs



using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace ID3Util
{
    public class ID3v1
    {
        const int ID3v1Size = 128;

        public static readonly string[] Genres = {
            "Blues","ClassicRock","Country","Dance",
            "Disco","Funk","Grunge","Hip-Hop",
            "Jazz","Metal","NewAge","Oldies",
            "Other","Pop","R&B","Rap",
            "Reggae","Rock","Techno","Industrial",
            "Alternative","Ska","DeathMetal","Pranks",
            "Soundtrack","Euro-Techno","Ambient","Trip-Hop",
            "Vocal","Jazz+Funk","Fusion","Trance",
            "Classical","Instrumental","Acid","House",
            "Game","SoundClip","Gospel","Noise",
            "Alt.Rock","Bass","Soul","Punk",
            "Space","Meditative","InstrumentalPop","InstrumentalRock",
            "Ethnic","Gothic","Darkwave","Techno-Industrial",
            "Electronic","Pop-Folk","Eurodance","Dream",
            "SouthernRock","Comedy","Cult","Gangsta",
            "Top40","ChristianRap","Pop/Funk","Jungle",
            "NativeAmerican","Cabaret","NewWave","Psychadelic",
            "Rave","Showtunes","Trailer","Lo-Fi",
            "Tribal","AcidPunk","AcidJazz","Polka",
            "Retro","Musical","Rock&Roll","HardRock",
            "Folk","Folk/Rock","NationalFolk","Swing",
            "Fusion","Bebob","Latin","Revival",
            "Celtic","Bluegrass","Avantgarde","GothicRock",
            "ProgressiveRock","PsychedelicRock","SymphonicRock","SlowRock",
            "BigBand","Chorus","EasyListening","Acoustic",
            "Humour","Speech","Chanson","Opera",
            "ChamberMusic","Sonata","Symphony","BootyBass",
            "Primus","PornGroove","Satire","SlowJam",
            "Club","Tango","Samba","Folklore",
            "Ballad","Power Ballad","Rhytmic Soul","Freestyle",
            "Duet","Punk Rock","Drum Solo","Acapella",
            "Euro-House","Dance Hall","Goa","Drum & Bass",
            "Club-House","Hardcore","Terror","Indie",
            "BritPop","Negerpunk","Polsk Punk","Beat",
            "Christian Gangsta Rap","Heavy Metal","Black Metal","Crossover",
            "Contemporary Christian","Christian Rock","Merengue","Salsa",
            "Trash Metal","Anime","JPop","SynthPop",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "(reserved)","(reserved)","(reserved)","(reserved)",
            "Sacred","Northern Europe","Irish & Scottish","Scotland",
            "Ethnic Europe","Enka","Children's Song","(reserved)",
            "Heavy Rock(J)","Doom Rock(J)","J-POP(J)","Seiyu(J)",
            "Tecno Ambient(J)","Moemoe(J)","Tokusatsu(J)","Anime(J)", // 255 ("Anime(J)")Is originally assigned "not used"(?)
        };

        Encoding _enc;
        byte[] TitleInBytes;
        byte[] ArtistInBytes;
        byte[] AlbumInBytes;
        byte[] YearInByte;
        byte[] CommentInByte;
        public byte GenreNumber; //Genre
        byte track;

        public int Year {
            get { 
                try {
                    return Convert.ToInt32(Encoding.ASCII.GetString(YearInByte));
                }
                catch(Exception){return 0;}
            }
            set {
                if ( value < 0 ) {
                    value = 0;
                }
                if ( value > 9999 ) {
                    value = 9999;
                }
                YearInByte[0] = (byte)(0x30 + (value/1000));
                YearInByte[1] = (byte)(0x30 + (value/ 100)%10);
                YearInByte[2] = (byte)(0x30 + (value/  10)%10);
                YearInByte[3] = (byte)(0x30 +  value      %10);
            }
        }

        string TryToGetString(byte[] t)
        {
            try {
                return _enc.GetString(t).TrimEnd(new char[]{'\0',' '});
            }
            catch(Exception){return "";}
        }

        byte[] TryToGetBytes(string s, int length)
        {
            byte[] b = new byte[length];
            int n = _enc.GetByteCount(s);
            if ( n <= length ) {
                byte[] tmp = _enc.GetBytes(s);
                Array.Copy(tmp, 0, b, 0, n);
            }
            else {
                //over time
                //I want to return a truncated string that fits in length.
                //I want to avoid leaving some bytes of multibyte characters.
                //Since it is troublesome, it returns all 0x00 equivalent to an empty string.
            }
            return b;
            // GetBytes (string s, int charIndex, int charCount, byte[] bytes, int byteIndex);
            // https://docs.microsoft.com/ja-jp/dotnet/api/system.text.encoding.getbytes?view=netcore-3.1#System_Text_Encoding_GetBytes_System_String_System_Int32_System_Int32_System_Byte___System_Int32_
        }

        public string Title   { get { return TryToGetString(TitleInBytes);  } set { TitleInBytes  = TryToGetBytes(value, 30); } }
        public string Artist  { get { return TryToGetString(ArtistInBytes); } set { ArtistInBytes = TryToGetBytes(value, 30); } }
        public string Album   { get { return TryToGetString(AlbumInBytes);  } set { AlbumInBytes  = TryToGetBytes(value, 30); } }
        public string Comment { get { return TryToGetString(CommentInByte); } set { CommentInByte = TryToGetBytes(value, 30); } }
        public string Genre {
            get {return Genres[GenreNumber];}
        }
        public int Track {
            get { return track; }
            set {
                if ( value < 0   ) {value =   0;}
                if ( value > 255 ) {value = 255;}
                track = (byte)value;
            }
        }

        public static ID3v1 CreateDefault(Encoding enc)
        {
            ID3v1 ret = new ID3v1();
            ret._enc = enc;
            ret.TitleInBytes = new byte[30];
            ret.ArtistInBytes = new byte[30];
            ret.AlbumInBytes = new byte[30];
            ret.YearInByte = new byte[4];
            ret.CommentInByte = new byte[30];
            ret.GenreNumber = 255;
            ret.track = 0;
            return ret;
        }

        public static ID3v1 ParseFromFile(string fileName, Encoding enc)
        {
            using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) {
                if ( fs.Length < ID3v1Size ) {
                    return null;
                }
                fs.Seek(-ID3v1Size, SeekOrigin.End);
                byte[] buffer = new byte[ID3v1Size];
                fs.Read(buffer,0,ID3v1Size);
                return ID3v1.Parse(buffer, 0, enc);
            }
        }

        public static ID3v1 ParseFromFoot(byte[] buffer, Encoding enc)
        {
            return Parse(buffer, buffer.Length-ID3v1Size, enc);
        }

        public static ID3v1 Parse(byte[] buffer, int offset, Encoding enc)
        {
            ID3v1 ret = new ID3v1();
            
            if ( offset < 0 ) {
                return null;
            }
            if (buffer.Length < offset + ID3v1Size ) {
                return null;
            }

            if ( enc == null ) {
                enc = Encoding.GetEncoding(932); // Shift_JIS code page= 932;
            }
            ret._enc = enc;

            //Header check("TAG")
            if ( buffer[offset] != 0x54 || buffer[offset+1] != 0x41 || buffer[offset+2] != 0x47) {
                return null;
            }
            ret.TitleInBytes = new byte[30];
            ret.ArtistInBytes = new byte[30];
            ret.AlbumInBytes = new byte[30];
            ret.YearInByte = new byte[4];
            Array.Copy(buffer, offset+ 3, ret.TitleInBytes,  0, 30);
            Array.Copy(buffer, offset+33, ret.ArtistInBytes, 0, 30);
            Array.Copy(buffer, offset+63, ret.AlbumInBytes,  0, 30);
            Array.Copy(buffer, offset+93, ret.YearInByte,    0,  4);
            if ( buffer[offset+125] == 0x00 ) { //Track information available
                ret.CommentInByte = new byte[30];
                Array.Copy(buffer, offset+97, ret.CommentInByte, 0, 28);
                ret.CommentInByte[28] = 0;
                ret.CommentInByte[29] = 0;
                ret.Track = buffer[offset+126];
            }
            else {
                ret.CommentInByte = new byte[30];
                Array.Copy(buffer, offset+97, ret.CommentInByte, 0, 30);
                ret.Track = 0;
            }
            ret.GenreNumber = buffer[offset+127];

            return ret;
        }

        public bool WriteToFile(string fileName)
        {
            bool commentErrorFlag;
            byte[] a = ToByteArray(out commentErrorFlag);

            if ( commentErrorFlag ) {
                return false;
            }

            using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite)) {
                bool hasID3v1Tag = false;
                
                //Find ID3v1
                if ( fs.Length >= ID3v1Size ) {
                    fs.Seek(-ID3v1Size, SeekOrigin.End);
                    byte[] buffer = new byte[ID3v1Size];
                    fs.Read(buffer,0,ID3v1Size);
                    //Header check("TAG")
                    if ( buffer[0] == 0x54 && buffer[1] == 0x41 && buffer[2] == 0x47) {
                        hasID3v1Tag = true;
                    }
                }

                if ( hasID3v1Tag ) {
                    fs.Seek(-ID3v1Size, SeekOrigin.End);
                }
                else {
                    fs.Seek(0, SeekOrigin.End);
                }

                fs.Write(a, 0, a.Length);
            }
            return true;
        }

        public byte[] ToByteArray(out bool commentErrorFlag)
        {
            byte[] a = new byte[128];
            commentErrorFlag = false;

            a[0] = 0x54;
            a[1] = 0x41;
            a[2] = 0x47;
            
            //Unify the trailing whitespace character to 0x00.(get/set is called.)
            Title = Title;
            Artist = Artist;
            Album = Album;
            Comment = Comment;
            
            Array.Copy(TitleInBytes,  0, a,  3, 30);
            Array.Copy(ArtistInBytes, 0, a, 33, 30);
            Array.Copy(AlbumInBytes,  0, a, 63, 30);
            Array.Copy(YearInByte,    0, a, 93,  4);
            if ( Track != 0 ) { //Track information available
                if ( CommentInByte[28] != 0 || CommentInByte[29] != 0 ) { //Comments of unusable length are used with Track
                    commentErrorFlag = true;
                    //Do not copy(Remains 0x00)
                }
                else {
                    Array.Copy(CommentInByte, 0, a, 97, 28);
                }
                a[125] = 0;
                a[126] = (byte)Track;
            }
            else {
                Array.Copy(CommentInByte, 0, a, 97, 30);
            }
            a[127] = GenreNumber;

            return a;
        }
    }

    // -------------------------------------------------------------------

    public class ID3v2
    {
        Encoding _enc;
        //const int CodePage_Shift_JIS = 932;
        
        static int SynchsafeIntFrom4Bytes(byte[] buffer, int offset) {
            return ((buffer[offset  ]&0x7F)<<21) |
                ((buffer[offset+1]&0x7F)<<14) | 
                ((buffer[offset+2]&0x7F)<< 7) | 
                ((buffer[offset+3]&0x7F)    ) ;
        }

        public class Frame
        {
            public string ID{get;private set;}
            int MajorVer;
            int Size;
            int EncodingID;
            byte Flags1;
            byte Flags2;
            byte[] Data; //Excluding Encoding ID

            public bool PreferedToDiscardWhenTagChanged  { get{return ((Flags1&0x40)!=0);} }
            public bool PreferedToDiscardWhenFileChanged { get{return ((Flags1&0x20)!=0);} }
            public bool PreferedToBeReadOnly             { get{return ((Flags1&0x10)!=0);} }

            public bool RelatedWithOtherFrame            { get{return ((Flags2&0x40)!=0);} }
            public bool Compressed                       { get{return ((Flags2&0x08)!=0);} }
            public bool Encrypted                        { get{return ((Flags2&0x04)!=0);} }
            public bool AsyncFlag                        { get{return ((Flags2&0x02)!=0);} }
            public bool DataLengthFlag                   { get{return ((Flags2&0x01)!=0);} }

            public bool UnknownFlagsAreSet               { get{return ((Flags1&0x8F)!=0 || (Flags2&0xB0)!=0) ;} }

            public string ToString(Encoding defaultEncoding)
            {
                int offset = 0;

                if ( ID == "PIC" || ID == "APIC" ) {
                    return "";
                }

                if ( ID == "COM" || ID == "COMM" ) {
                    return ""; //Unimplemented
                    /*
                    if ( Data.Length >= 3+1 && 'a' <= Data[0] && Data[0] <= 'z' ) {
                        //If there is something that looks like a country code
                        offset = 3;
                        if ( EncodingID == 1 ) {
                            //BOM 2 bytes Search for 2 bytes at a time to find a null-terminated 0x00 00
                        }
                        else if ( EncodingID == 2 ) {
                            //Search 2 bytes at a time to find a null-terminated 0x00 00
                        }
                        else if ( EncodingID == 0 || EncodingID == 3 ) {
                            //Find null-terminated 0x00
                        }
                    }
                    else {
                        return ""; //Illegal format
                    }
                    */
                }

                try {
                    if ( EncodingID == 3 ) {
                        // UTF-8 without BOM
                        return (new System.Text.UTF8Encoding(false)).GetString(Data, offset, Data.Length-offset);
                    }
                    else if ( EncodingID == 2 ) {
                        // UTF-16BE without BOM
                        return (new System.Text.UnicodeEncoding(true,false)).GetString(Data, offset, Data.Length-offset);
                    }
                    else if ( EncodingID == 1 ) {
                        // UTF-16 with BOM
                        return (new System.Text.UnicodeEncoding()).GetString(Data, offset, Data.Length-offset);
                    }
                    else if ( EncodingID == 0 ) {
                        if ( defaultEncoding == null ) {
                            //According to the MPEG standard
                            //   ISO-8859-1 (CodePage=28591 Western European language(ISO))
                            return Encoding.GetEncoding(28591).GetString(Data, offset, Data.Length-offset);
                        }
                        else {
                            //Shift in Japan_JIS seems to be rampant(?)
                            //   shift_jis(CodePage=932)
                            // return Encoding.GetEncoding(CodePage_Shift_JIS).GetString(Data, offset, Data.Length-offset);
                            return defaultEncoding.GetString(Data, offset, Data.Length-offset);
                        }
                    }
                    else {
                        // unknown
                        return "";
                    }
                }
                catch ( DecoderFallbackException ) {
                    //Unreadable
                    return "";
                }
            }

            public static Frame Parse(int majorVer, byte[] buffer, ref int pos, int endPos)
            {
                Frame ret = new Frame();

                if ( endPos > buffer.Length ){ return null; }

                if ( endPos < pos+6 ) { return null; }
                if ( majorVer >= 3 && endPos < pos+10 ) { return null; }

                ret.MajorVer = majorVer;
                try {
                    ret.ID = Encoding.ASCII.GetString(buffer, pos, (majorVer<=2)?3:4 );
                }
                catch ( DecoderFallbackException ) {
                    //Unreadable
                    Console.WriteLine("Failed to parse at address 0x" + pos.ToString("X"));
                    return null;
                }

                if ( majorVer <= 2 ) {
                    ret.Size = (buffer[pos+3]<<16) | (buffer[pos+4]<<8) | (buffer[pos+5]);
                    pos += 6;
                }
                else {
                    if ( majorVer <= 3 ) {
                        ret.Size = (buffer[pos+4]<<24) |
                                   (buffer[pos+5]<<16) | 
                                   (buffer[pos+6]<<8) |
                                   (buffer[pos+7]);
                    }
                    else {
                        ret.Size = SynchsafeIntFrom4Bytes(buffer, pos+4);
                    }
                    ret.Flags1 = buffer[pos+8];
                    ret.Flags2 = buffer[pos+9];
                    pos += 10;
                }
                if ( endPos < pos + ret.Size ) {
                    Console.WriteLine("Failed to parse at address 0x" + pos.ToString("X"));
                    Console.WriteLine("Address over");
                    return null;
                }
                if ( ret.Size > 0 ) {
                    ret.Data = new byte[ret.Size-1];
                    ret.EncodingID = buffer[pos];
                    Array.Copy(buffer, pos+1, ret.Data, 0, ret.Size-1);
                }
                else {
                    ret.Data = new byte[0];
                    ret.EncodingID = 0;
                }
                pos += ret.Size;
                
                return ret;
            }
        }

        public int TagVerMajor{get;private set;}
        public int TagVerMinor{get;private set;}
        public int Flags{get;private set;}
        public int TagSize{get;private set;}
        public int ExtSize{get;private set;}

        public bool HasExtendedHeader{get{return ((Flags&0x40)!=0);}}
        public bool HasFooter{get{return ((Flags&0x10)!=0);}}

        List<Frame> Frames;
        
        int FindFirstFrameByID(string FrameID)
        {
            for ( int i=0 ; i<Frames.Count ; i++ ) {
                if ( Frames[i].ID == FrameID ) {
                    return i;
                }
            }
            return -1;
        }

        public string Artist  { get { return GetStringByID("TP1","TPE1"); } }
        public string Title   { get { return GetStringByID("TT2","TIT2"); } }
        public string Album   { get { return GetStringByID("TAL","TALB"); } }
        public string Track   { get { return GetStringByID("TRK","TRCK"); } }
        public string Year    { get { return GetStringByID("TYE","TYER"); } }
        public string Genre   { get { return GetStringByID("TCO","TCON"); } }
        public string Comment { get { return GetStringByID("COM","COMM"); } }
        // public Bitmap Jacket { get { return GetJacketByID("PIC","APIC"); } }

        string GetStringByID(string idForV3p2, string idForV3p3)
        {
            string id = (TagVerMajor<=2)?idForV3p2:idForV3p3;
            if ( id == null ) { return ""; }
            int index = FindFirstFrameByID(id);
            if ( index < 0 ) { return ""; }
            return Frames[index].ToString(_enc);
        }
        
        public static ID3v2 CreateDefault(Encoding enc)
        {
            ID3v2 ret = new ID3v2();
            ret._enc = enc;

            ret.Frames = new List<Frame>();
            ret.TagVerMajor = 4;
            ret.TagVerMinor = 0;
            ret.Flags   = 0;
            ret.TagSize = 0; //dummy
            ret.ExtSize = 0; //dummy
            
            return ret;
        }

        public static ID3v2 ParseFromFile(string fileName, Encoding enc)
        {
            using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) {
                
                byte[] buffer = new byte[10];
                fs.Read(buffer,0,10); //Read until you know the size

                //Header check"ID3"
                if ( buffer[0] != 0x49 || buffer[1] != 0x44 || buffer[2] != 0x33) {
                    return null;
                }
                
                int size = 10 + SynchsafeIntFrom4Bytes(buffer,6);
                Array.Resize(ref buffer, size);
                fs.Read(buffer,10,size-10);

                return ID3v2.Parse(buffer, 0, enc);
            }
        }


        //enc not implemented
        public static ID3v2 Parse(byte[] buffer, int offset, Encoding enc)
        {
            ID3v2 ret = new ID3v2();

            if ( offset < 0 ) {
                return null;
            }
            //ID3v2 is at least 10 bytes or more, so check that it is 10 bytes or more
            if (buffer.Length < offset + 10 ) {
                return null;
            }

            //Header check"ID3"
            if ( buffer[offset] != 0x49 || buffer[offset+1] != 0x44 || buffer[offset+2] != 0x33) {
                return null;
            }

            ret._enc = enc;
            ret.TagVerMajor = buffer[offset+3];
            ret.TagVerMinor = buffer[offset+4];
            ret.Flags = buffer[offset+5];
            ret.TagSize = SynchsafeIntFrom4Bytes(buffer, offset+6);

            int pos = offset+10;
            int endPos = pos + ret.TagSize;
            if ( endPos > buffer.Length ) {
                return null;
            }
            if ( ret.HasFooter ) {
                endPos -= 10; // Footer(10byte)Set the minute end position to the front
            }

            if ( ret.HasExtendedHeader ) {
                //There is a minimum of 6 bytes
                if ( buffer.Length < pos+6 ) {
                    return null;
                }
                if ( ret.TagVerMajor <= 3 ) {
                    // IDv2.3.x or less
                    ret.ExtSize = (buffer[pos  ]<<24) |
                                  (buffer[pos+1]<<16) |
                                  (buffer[pos+2]<< 8) |
                                  (buffer[pos+3]    );
                }
                else {
                    ret.ExtSize = SynchsafeIntFrom4Bytes(buffer, pos);
                }
                pos += 4 + ret.ExtSize;
            }

            // parsing Frame
            ret.Frames = new List<Frame>();
            while ( pos < endPos ) {
                if ( buffer[pos] == 0 ) {
                    break;
                }
                Frame t = Frame.Parse(ret.TagVerMajor, buffer, ref pos, endPos);
                if ( t == null ) {
                    Console.WriteLine("Failed to parse at address 0x" + pos.ToString("X"));
                    return null;
                }
                else {
                    ret.Frames.Add(t);
                }
            }
            return ret;
        }
    }

}

Source code Mp3TagEditor.cs

The name is Editor, but there is no save function yet.

Mp3TagEditor.cs



using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

using ID3Util;


class MainForm : Form
{
    DataGridView dgv;
    BindingList<Mp3Item> items;
    BindingSource wrapper;


    //Items to display in DataGridView
    public class Mp3Item : INotifyPropertyChanged
    {
        static readonly string TextForEdited = "Change";

        //Implemented INotifyPropertyChanged as a countermeasure that may not be updated even if the data in the target content of DataSource is changed
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        //The field is not displayed. Must be a property.
        public string FolderPath     {get{return _folderPath;     } set{if(value != _folderPath     ){                          _folderPath     =value;NotifyPropertyChanged();}}}
        public string FileNameWoExt  {get{return _fileNameWoExt;  } set{if(value != _fileNameWoExt  ){                          _fileNameWoExt  =value;NotifyPropertyChanged();}}}

        public string ID3v1Edited    {get{return _id3v1edited;    } set{if(value != _id3v1edited    ){                          _id3v1edited    =value;NotifyPropertyChanged();}}}
        public bool   ID3v1Enabled   {get{return _id3v1enabled;   } set{if(value != _id3v1enabled   ){ID3v1Edited=TextForEdited;_id3v1enabled   =value;NotifyPropertyChanged();}}}
        public string V1Artist       {get{return _v1artist;       } set{if(value != _v1artist       ){ID3v1Edited=TextForEdited;_v1artist       =value;NotifyPropertyChanged();}}}
        public string V1Album        {get{return _v1album;        } set{if(value != _v1album        ){ID3v1Edited=TextForEdited;_v1album        =value;NotifyPropertyChanged();}}}
        public string V1Title        {get{return _v1title;        } set{if(value != _v1title        ){ID3v1Edited=TextForEdited;_v1title        =value;NotifyPropertyChanged();}}}
        public string V1Track        {get{return _v1track;        } set{if(value != _v1track        ){ID3v1Edited=TextForEdited;_v1track        =value;NotifyPropertyChanged();}}}
        public string V1Year         {get{return _v1year;         } set{if(value != _v1year         ){ID3v1Edited=TextForEdited;_v1year         =value;NotifyPropertyChanged();}}}
        public string V1Genre        {get{return _v1genre;        } set{if(value != _v1genre        ){ID3v1Edited=TextForEdited;_v1genre        =value;NotifyPropertyChanged();}}}
        public string V1Comment      {get{return _v1comment;      } set{if(value != _v1comment      ){ID3v1Edited=TextForEdited;_v1comment      =value;NotifyPropertyChanged();}}}

        public string ID3v2Edited    {get{return _id3v2edited;    } set{if(value != _id3v2edited    ){                          _id3v2edited    =value;NotifyPropertyChanged();}}}
        public bool   ID3v2Enabled   {get{return _id3v2enabled;   } set{if(value != _id3v2enabled   ){ID3v2Edited=TextForEdited;_id3v2enabled   =value;NotifyPropertyChanged();}}}
        public string V2Artist       {get{return _v2artist;       } set{if(value != _v2artist       ){ID3v2Edited=TextForEdited;_v2artist       =value;NotifyPropertyChanged();}}}
        public string V2Album        {get{return _v2album;        } set{if(value != _v2album        ){ID3v2Edited=TextForEdited;_v2album        =value;NotifyPropertyChanged();}}}
        public string V2Title        {get{return _v2title;        } set{if(value != _v2title        ){ID3v2Edited=TextForEdited;_v2title        =value;NotifyPropertyChanged();}}}
        public string V2Track        {get{return _v2track;        } set{if(value != _v2track        ){ID3v2Edited=TextForEdited;_v2track        =value;NotifyPropertyChanged();}}}
        public string V2Year         {get{return _v2year;         } set{if(value != _v2year         ){ID3v2Edited=TextForEdited;_v2year         =value;NotifyPropertyChanged();}}}
        public string V2Genre        {get{return _v2genre;        } set{if(value != _v2genre        ){ID3v2Edited=TextForEdited;_v2genre        =value;NotifyPropertyChanged();}}}


        string _folderPath;
        string _fileNameWoExt;//Excluding folders and extensions
        
        string _id3v1edited;
        bool   _id3v1enabled;
        string _v1artist;
        string _v1album;
        string _v1title;
        string _v1track;
        string _v1year;
        string _v1genre;
        string _v1comment;
        
        string _id3v2edited;
        bool   _id3v2enabled;
        string _v2artist;
        string _v2album;
        string _v2title;
        string _v2track;
        string _v2year;
        string _v2genre;


        public static Mp3Item ConvertToMp3Item(string mp3FileName)
        {
            bool _tmp_id3v1enabled = true;
            bool _tmp_id3v2enabled = true;

            if ( ! ( File.Exists(mp3FileName) && mp3FileName.EndsWith(".mp3", true, null) ) ) {// Note:The second argument of EndsWith is ignoreCase
                return null;
            }

            Encoding enc = Encoding.GetEncoding(CodePage_Shift_JIS);

            ID3v1 id3v1 = ID3v1.ParseFromFile(mp3FileName, enc);
            if ( id3v1 == null ) {
                _tmp_id3v1enabled = false;
                id3v1 = ID3v1.CreateDefault(enc);
            }

            ID3v2 id3v2 = ID3v2.ParseFromFile(mp3FileName, enc);
            if ( id3v2 == null ) {
                _tmp_id3v2enabled = false;
                id3v2 = ID3v2.CreateDefault(enc);
            }

            var item = new Mp3Item(){
                FolderPath = Path.GetDirectoryName(Path.GetFullPath(mp3FileName)),
                FileNameWoExt = Path.GetFileNameWithoutExtension(mp3FileName),

                ID3v1Enabled = _tmp_id3v1enabled,
                V1Artist  = id3v1.Artist,
                V1Album   = id3v1.Album,
                V1Title   = id3v1.Title,
                V1Track   = id3v1.Track.ToString(),
                V1Year    = id3v1.Year.ToString(),
                V1Genre   = id3v1.Genre,
                V1Comment = id3v1.Comment,

                ID3v2Enabled = _tmp_id3v2enabled,
                V2Artist  = id3v2.Artist,
                V2Album   = id3v2.Album,
                V2Title   = id3v2.Title,
                V2Track   = id3v2.Track,
                V2Year    = id3v2.Year,
                V2Genre   = id3v2.Genre,
            };
            item.ID3v1Edited = "";
            item.ID3v2Edited = "";

            return item;
        }
    }

    const int CodePage_Shift_JIS = 932;

    MainForm(string filePath)
    {
        items = new BindingList<Mp3Item>();

        Text = "Mp3TagViewer";
        ClientSize = new Size(840,450);


        var menuStrip1 = new MenuStrip(); // https://dobon.net/vb/dotnet/control/menustrip.html

        SuspendLayout();
        menuStrip1.SuspendLayout();

        var fileMenuItem = new ToolStripMenuItem(){ Text = "File(&F)"};
        var editMenuItem = new ToolStripMenuItem(){ Text = "Edit(&E)"};
        menuStrip1.Items.Add(fileMenuItem);
        menuStrip1.Items.Add(editMenuItem);

//        fileMenuItem.DropDownItems.Add( new ToolStripMenuItem("open(&O)...", null, (s,e)=>{OpenTemplateWithDialog();}, Keys.Control | Keys.O) );
//        fileMenuItem.DropDownItems.Add( new ToolStripMenuItem("Save(&S)...", null, (s,e)=>{SaveTemplateWithDialog();}, Keys.Control | Keys.S) );

//        editMenuItem.DropDownItems.Add( new ToolStripMenuItem("icon(.ico)Save as(&I)...", null, (s,e)=>{SaveImageWithDialog("ico");}, Keys.Control | Keys.I) );
//        editMenuItem.DropDownItems.Add( new ToolStripMenuItem("image(.png)Save as(&P)...",     null, (s,e)=>{SaveImageWithDialog("png");}, Keys.Control | Keys.P) );



        Controls.Add(
            dgv = new DataGridView() {
                //Location = new Point(0, 0),
                //Size = new Size(800, 400),
                Dock = DockStyle.Fill,
                AllowUserToAddRows = false,
                AutoGenerateColumns = false,
                AllowDrop = true,
            }
        );

        //https://dobon.net/vb/dotnet/datagridview/addcolumn.html
        
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 100, DataPropertyName = "FolderPath",    Name = "FolderPath",    HeaderText = "place",       ReadOnly=true});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 110, DataPropertyName = "FileNameWoExt", Name = "FileNameWoExt", HeaderText = "file name", ReadOnly=true});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  30, DataPropertyName = "ID3v1Edited",   Name = "ID3v1Edited",   HeaderText = "v1 edit status", ReadOnly=true});
        dgv.Columns.Add(new DataGridViewCheckBoxColumn(){Width=  50, DataPropertyName = "ID3v1Enabled",  Name = "ID3v1Enabled",  HeaderText = "ID3v1"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  80, DataPropertyName = "V1Artist",  Name = "V1Artist",  HeaderText = "[v1]Artist"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  80, DataPropertyName = "V1Album",   Name = "V1Album",   HeaderText = "[v1]album"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 110, DataPropertyName = "V1Title",   Name = "V1Title",   HeaderText = "[v1]Song title"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  35, DataPropertyName = "V1Track",   Name = "V1Track",   HeaderText = "[v1]truck"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  45, DataPropertyName = "V1Year",    Name = "V1Year",    HeaderText = "[v1]Year"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  60, DataPropertyName = "V1Genre",   Name = "V1Genre",   HeaderText = "[v1]Genre"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  50, DataPropertyName = "V1Comment", Name = "V1Comment", HeaderText = "[v1]comment"});

        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  30, DataPropertyName = "ID3v2Edited",   Name = "ID3v2Edited",   HeaderText = "v2 edit status", ReadOnly=true});
        dgv.Columns.Add(new DataGridViewCheckBoxColumn(){Width=  50, DataPropertyName = "ID3v2Enabled",  Name = "ID3v2Enabled",  HeaderText = "ID3v2"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  80, DataPropertyName = "V2Artist",  Name = "V2Artist",  HeaderText = "[v2]Artist"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  80, DataPropertyName = "V2Album",   Name = "V2Album",   HeaderText = "[v2]album"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 110, DataPropertyName = "V2Title",   Name = "V2Title",   HeaderText = "[v2]Song title"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  35, DataPropertyName = "V2Track",   Name = "V2Track",   HeaderText = "[v2]truck"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  45, DataPropertyName = "V2Year",    Name = "V2Year",    HeaderText = "[v2]Year"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  60, DataPropertyName = "V2Genre",   Name = "V2Genre",   HeaderText = "[v2]Genre"});

        wrapper = new BindingSource() {
            DataSource = items
        };
 
        dgv.DataSource = wrapper;

        dgv.DragEnter += Control_DragEnter;
        dgv.DragDrop += Control_DragDrop;


        this.AllowDrop = true;
        this.DragEnter += Control_DragEnter;
        this.DragDrop += Control_DragDrop;


        Controls.Add(menuStrip1);
        MainMenuStrip = menuStrip1;
        menuStrip1.ResumeLayout(false);
        menuStrip1.PerformLayout();
        ResumeLayout(false);
        PerformLayout();


        if ( filePath != null ) {
            RegisterID3FromFile(filePath);
        }
    }
    
    void Control_DragEnter(Object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
            e.Effect = DragDropEffects.Copy;
        }
        else {
            e.Effect = DragDropEffects.None;
        }
    }
    
    void Control_DragDrop(Object sender, DragEventArgs e)
    {
        var fileNames = (string[])e.Data.GetData(DataFormats.FileDrop, false);
        if ( fileNames != null && fileNames.Length >= 1 ) {
            foreach ( var s in fileNames ) {
                RegisterID3FromFile(s);
            }
        }
    }

    void RegisterID3FromFile(string filePath)
    {
        var item = Mp3Item.ConvertToMp3Item(filePath);
        if ( item != null ) {
            wrapper.Add(item);
        }
    }

    

    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new MainForm((args.Length==1)?args[0]:null));
    }
}

Source code Mp3TagSetter.cs

Mp3TagSetter.cs



using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
//using System.Drawing;
using System.IO;
using System.Text;
//using System.Text.RegularExpressions;

using ID3Util;

class Mp3TagSetterCmd
{

    static void ShowHelp()
    {
        var enc = Console.OutputEncoding;
        Console.WriteLine("Current code page is: "+enc.CodePage.ToString()+"("+enc.EncodingName+")");
        Console.WriteLine("To change code page, use chcp command:");
        Console.WriteLine("  chcp CODEPAGE");
        Console.WriteLine("CODEPAGE");
        Console.WriteLine("  932 = Shift_JIS");
        Console.WriteLine("  65001 = UTF-8");
        Console.WriteLine("");
        Console.WriteLine("Mp3TagSetter OPTIONS...");
        Console.WriteLine("");
        Console.WriteLine("OPTIONS");
        Console.WriteLine(" [--file] TEXT");
        Console.WriteLine("   mp3 file.");
        Console.WriteLine("");
        Console.WriteLine(" --track NUMBER");
        Console.WriteLine("   NUMBER = 0 to 255.");
        Console.WriteLine("   0 means track number does not exist.");
        Console.WriteLine("");
        Console.WriteLine(" --title TEXT");
        Console.WriteLine("   max length of TEXT is 30 bytes.");
        Console.WriteLine("");
        Console.WriteLine(" --artist TEXT");
        Console.WriteLine("   max length of TEXT is 30 bytes.");
        Console.WriteLine("");
        Console.WriteLine(" --album TEXT");
        Console.WriteLine("   max length of TEXT is 30 bytes.");
        Console.WriteLine("");
        Console.WriteLine(" --comment TEXT");
        Console.WriteLine("   max length of TEXT is 28 bytes when track information exists.");
        Console.WriteLine("   max length of TEXT is 30 bytes when track information does not exist.");
        Console.WriteLine("");
        Console.WriteLine(" --year NUMBER");
        Console.WriteLine("   NUMBER = 0 to 9999.");
        Console.WriteLine("");
        Console.WriteLine(" --genre NUMBER");
        Console.WriteLine("   NUMBER = 0 to 255.");
        Console.WriteLine("");
        Console.WriteLine(" --showgenre");
        Console.WriteLine("   Shows genre list.");
    }

    static void ShowGenres()
    {
        for ( int i=0 ; i<ID3v1.Genres.Length ; i++ ) {
            Console.WriteLine(i.ToString("D3")+":"+ID3v1.Genres[i]);
        }
    }

    static Dictionary<string,string> ParseCmdOptions(string[] args)
    {
        var d = new Dictionary<string,string>();
        
        for ( int i=0 ; i<args.Length ; i++ ) {
            switch ( args[i] ) {
            case "--track":
            case "--title":
            case "--artist":
            case "--album":
            case "--comment":
            case "--year":
            case "--genre":
            case "--file":
                if ( !d.ContainsKey(args[i]) && i+1 < args.Length ) {
                    d.Add(args[i], args[i+1]);
                    i++;
                }
                else {
                    ShowHelp();
                    return null;
                }
                break;
            case "--showgenre":
                ShowGenres();
                return null;
            default:
                if ( args[i].StartsWith("-") ) {
                    ShowHelp();
                    return null;
                }
                if ( !d.ContainsKey("--file") ) {
                    d.Add("--file", args[i]);
                }
                else {
                    ShowHelp();
                    return null;
                }
                break;
            }
        }
        return d;
    }

    [STAThread]
    static void Main(string[] args)
    {
        bool needToSave = false;

        var opts = ParseCmdOptions(args);
        if ( opts == null ) {
            return;
        }
        if ( !opts.ContainsKey("--file") ) {
            ShowHelp();
            return;
        }

        string fileName = opts["--file"];
        if ( !fileName.EndsWith(".mp3", true, null) ) {
            Console.WriteLine("FileName must end with \".mp3\".");
            return;
        }
        if ( !File.Exists(fileName) ) {
            Console.WriteLine("File \""+fileName+"\" does not exists.");
            return;
        }

        // 
        var enc = Encoding.GetEncoding(932);
        ID3v1 id3v1 = ID3v1.ParseFromFile(fileName, enc)??ID3v1.CreateDefault(enc);

        if ( opts.ContainsKey("--track") ) {
            needToSave = true;
            try { id3v1.Track = Convert.ToInt32(opts["--track"]); }
            catch (FormatException  ) { id3v1.Track = 0; Console.WriteLine("Warning: parameter of \"--track\" is invalid."); }
            catch (OverflowException) { id3v1.Track = 0; Console.WriteLine("Warning: parameter of \"--track\" is invalid."); }
            Console.WriteLine("Track overwrite");
        }
        if ( opts.ContainsKey("--genre") ) {
            needToSave = true;
            try { id3v1.GenreNumber = Convert.ToByte(opts["--genre"]); }
            catch (FormatException  ) { id3v1.GenreNumber = 0; Console.WriteLine("Warning: parameter of \"--genre\" is invalid."); }
            catch (OverflowException) { id3v1.GenreNumber = 0; Console.WriteLine("Warning: parameter of \"--genre\" is invalid."); }
        }
        if ( opts.ContainsKey("--year") ) {
            needToSave = true;
            try { id3v1.Year = Convert.ToInt32(opts["--year"]); }
            catch (FormatException  ) { id3v1.Year = 0; Console.WriteLine("Warning: parameter of \"--year\" is invalid."); }
            catch (OverflowException) { id3v1.Year = 0; Console.WriteLine("Warning: parameter of \"--year\" is invalid."); }
        }
        if ( opts.ContainsKey("--title") ) {
            needToSave = true;
            id3v1.Title = opts["--title"];
        }
        if ( opts.ContainsKey("--artist") ) {
            needToSave = true;
            id3v1.Artist = opts["--artist"];
        }
        if ( opts.ContainsKey("--album") ) {
            needToSave = true;
            id3v1.Album = opts["--album"];
        }
        if ( opts.ContainsKey("--comment") ) {
            needToSave = true;
            id3v1.Comment = opts["--comment"];
        }

        if ( needToSave ) {
            id3v1.WriteToFile(fileName);
        }
    }
}