1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【続】 mp3のタグ(ID3v1, ID3v2)を読み込む(ライブラリ不使用) + DataGridViewで一覧表示する / コマンドライン引数に応じてID3v1タグを書き換える

Last updated at Posted at 2020-08-09

概要

下記2種類のソフトを作ってみました。

■1.DataGridViewを使って一覧表示できるようにしてみた。保存機能(ファイルへの反映処理)は未対応。
 コンパイル方法:csc ID3Util.cs Mp3TagEditor.cs

■2.コマンドライン引数に応じて、ID3v1タグのデータを書き換える。
ご利用は自己責任でお願いします。mp3ファイルを上書きします。バックアップファイルを作成するような気の利いた実装にはしていませんので、ご利用の際は対象のmp3ファイルのバックアップを取っておくことをお勧めします。
 コンパイル方法:csc ID3Util.cs Mp3TagSetter.cs

参考サイト

mp3のID3v1, ID3v2タグ

前記事の参考サイト

DataGridView

画面キャプチャ

■1.
image.png

■2.
image.png

日本語などのマルチバイト文字を使う場合は、コマンドプロンプトの文字コード(コードページ)設定に注意してください(コマンドプロンプトのchcpコマンドで確認・変更が可能です)。バッチファイルなどを使用した場合などにおいて、ファイルとコマンドプロンプトの文字コードが整合しないと、意図しない文字列がプログラムに渡ってしまう可能性があります。

ソースコード 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)") は本来「不使用」がアサインされている(?)
        };

        Encoding _enc;
        byte[] TitleInBytes;
        byte[] ArtistInBytes;
        byte[] AlbumInBytes;
        byte[] YearInByte;
        byte[] CommentInByte;
        public byte GenreNumber; // ジャンル
        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時
                // lengthにおさまる切り詰めた文字列を返したい。
                // 多バイト文字の一部バイトが残るのは避けたい。
                // 面倒なので空列相当のall 0x00を返す。
            }
            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;

            // ヘッダチェック ("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情報あり
                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;
                
                // ID3v1を探す
                if ( fs.Length >= ID3v1Size ) {
                    fs.Seek(-ID3v1Size, SeekOrigin.End);
                    byte[] buffer = new byte[ID3v1Size];
                    fs.Read(buffer,0,ID3v1Size);
                    // ヘッダチェック ("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;
            
            // 終端の空白文字を0x00に統一する。(get/setが呼ばれる。)
            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情報あり
                if ( CommentInByte[28] != 0 || CommentInByte[29] != 0 ) { // Trackありの場合に使用不可能な長さのコメントが使用されている
                    commentErrorFlag = true;
                    // copyしない(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; // EncodingIDを除く

            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 ""; // 未実装
                    /*
                    if ( Data.Length >= 3+1 && 'a' <= Data[0] && Data[0] <= 'z' ) {
                        // 国別コードらしきものがある場合
                        offset = 3;
                        if ( EncodingID == 1 ) {
                            // BOM 2byteすてて 2byteずつサーチしてNULL終端0x00 00を探す
                        }
                        else if ( EncodingID == 2 ) {
                            // 2byteずつサーチしてNULL終端0x00 00を探す
                        }
                        else if ( EncodingID == 0 || EncodingID == 3 ) {
                            // NULL終端0x00を探す
                        }
                    }
                    else {
                        return ""; // 不正なフォーマット
                    }
                    */
                }

                try {
                    if ( EncodingID == 3 ) {
                        // UTF-8 BOMなし
                        return (new System.Text.UTF8Encoding(false)).GetString(Data, offset, Data.Length-offset);
                    }
                    else if ( EncodingID == 2 ) {
                        // UTF-16BE BOMなし
                        return (new System.Text.UnicodeEncoding(true,false)).GetString(Data, offset, Data.Length-offset);
                    }
                    else if ( EncodingID == 1 ) {
                        // UTF-16 BOMあり
                        return (new System.Text.UnicodeEncoding()).GetString(Data, offset, Data.Length-offset);
                    }
                    else if ( EncodingID == 0 ) {
                        if ( defaultEncoding == null ) {
                            // MPEGの規格上は
                            //   ISO-8859-1 (CodePage=28591 西ヨーロッパ言語 (ISO))
                            return Encoding.GetEncoding(28591).GetString(Data, offset, Data.Length-offset);
                        }
                        else {
                            // 日本ではShift_JISが横行しているらしい(?)
                            //   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 ) {
                    // 読み取り不能
                    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 ) {
                    // 読み取り不能
                    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; // ダミー
            ret.ExtSize = 0; // ダミー
            
            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); // サイズが分かるところまで読み込む

                // ヘッダチェック "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未実装
        public static ID3v2 Parse(byte[] buffer, int offset, Encoding enc)
        {
            ID3v2 ret = new ID3v2();

            if ( offset < 0 ) {
                return null;
            }
            // ID3v2は最低でも10byte以上なので10byte以上であることをチェックする
            if (buffer.Length < offset + 10 ) {
                return null;
            }

            // ヘッダチェック "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)分末尾位置を手前にセットする
            }

            if ( ret.HasExtendedHeader ) {
                // 最小6byteある
                if ( buffer.Length < pos+6 ) {
                    return null;
                }
                if ( ret.TagVerMajor <= 3 ) {
                    // IDv2.3.x以下
                    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;
        }
    }

}

ソースコード Mp3TagEditor.cs

名前は先走ってEditorにしてますが、まだ保存機能無いです。

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;


    // DataGridViewに表示する項目
    public class Mp3Item : INotifyPropertyChanged
    {
        static readonly string TextForEdited = "変更";

        // DataSourceの対象の中身のデータを変更しても更新されない場合がある対策としてINotifyPropertyChangedを実装
        public event PropertyChangedEventHandler PropertyChanged;

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

        // フィールドは表示されない。プロパティにする必要がある。
        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;// フォルダと拡張子除く
        
        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: EndsWithの第2引数は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 = "ファイル(&F)"};
        var editMenuItem = new ToolStripMenuItem(){ Text = "編集(&E)"};
        menuStrip1.Items.Add(fileMenuItem);
        menuStrip1.Items.Add(editMenuItem);

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

//        editMenuItem.DropDownItems.Add( new ToolStripMenuItem("アイコン(.ico)として保存(&I)...", null, (s,e)=>{SaveImageWithDialog("ico");}, Keys.Control | Keys.I) );
//        editMenuItem.DropDownItems.Add( new ToolStripMenuItem("画像(.png)として保存(&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 = "場所",       ReadOnly=true});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 110, DataPropertyName = "FileNameWoExt", Name = "FileNameWoExt", HeaderText = "ファイル名", ReadOnly=true});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  30, DataPropertyName = "ID3v1Edited",   Name = "ID3v1Edited",   HeaderText = "v1編集状態", 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]アーティスト"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  80, DataPropertyName = "V1Album",   Name = "V1Album",   HeaderText = "[v1]アルバム"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 110, DataPropertyName = "V1Title",   Name = "V1Title",   HeaderText = "[v1]曲名"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  35, DataPropertyName = "V1Track",   Name = "V1Track",   HeaderText = "[v1]トラック"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  45, DataPropertyName = "V1Year",    Name = "V1Year",    HeaderText = "[v1]年"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  60, DataPropertyName = "V1Genre",   Name = "V1Genre",   HeaderText = "[v1]ジャンル"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  50, DataPropertyName = "V1Comment", Name = "V1Comment", HeaderText = "[v1]コメント"});

        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  30, DataPropertyName = "ID3v2Edited",   Name = "ID3v2Edited",   HeaderText = "v2編集状態", 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]アーティスト"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  80, DataPropertyName = "V2Album",   Name = "V2Album",   HeaderText = "[v2]アルバム"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width = 110, DataPropertyName = "V2Title",   Name = "V2Title",   HeaderText = "[v2]曲名"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  35, DataPropertyName = "V2Track",   Name = "V2Track",   HeaderText = "[v2]トラック"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  45, DataPropertyName = "V2Year",    Name = "V2Year",    HeaderText = "[v2]年"});
        dgv.Columns.Add(new DataGridViewTextBoxColumn(){Width =  60, DataPropertyName = "V2Genre",   Name = "V2Genre",   HeaderText = "[v2]ジャンル"});

        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));
    }
}

ソースコード 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);
        }
    }
}

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?