前に使っていたmp3タグエディタがWindows10でなぜか正常に動作しなくなったので、自力で編集ツールを作ろうと思って調べてみた。
ID3v1は割と簡単に編集できそうだが、ID3v2は結構めんどくさそう・・・。ID3v2.2, v2.3, v2.4の微妙な仕様差異もめんどくさい・・・。
今回は、まずは単一ファイルの読み込みだけに対応してみた。
参考サイト ID3v1
- http://eleken.y-lab.org/report/other/mp3tags.shtml
- http://www.cactussoft.co.jp/Sarbo/divMPeg3UnmanageID3v1.html
参考サイト ID3v2
- http://www.cactussoft.co.jp/Sarbo/divMPeg3UnmanageID3v2.html
- http://eleken.y-lab.org/report/other/mp3tags.shtml
- http://tohka383.hatenablog.jp/entry/20120918/1347960578
- http://takaaki.info/wp-content/uploads/2013/01/ID3v2.3.0J.html#sec3.2
- http://www.takaaki.info/wp-content/uploads/2013/01/id3v2_4_0-frames_j.txt
キャプチャ
ソースコード
usingSystem;usingSystem.Collections;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Diagnostics;usingSystem.Drawing;usingSystem.Drawing.Drawing2D;usingSystem.Drawing.Imaging;usingSystem.IO;usingSystem.Reflection;usingSystem.Runtime.InteropServices;usingSystem.Text;usingSystem.Text.RegularExpressions;usingSystem.Threading;usingSystem.Threading.Tasks;usingSystem.Windows.Forms;publicclassID3v1{constintCodePage_Shift_JIS=932;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)") は本来「不使用」がアサインされている(?)};// null終端文字列の扱い// https://www.ipentec.com/document/csharp-null-terminate-string-trimmingbyte[]TitleInBytes;byte[]ArtistInBytes;byte[]AlbumInBytes;byte[]YearInByte;byte[]CommnetInByte;byteGenreNumber;// ジャンルpublicintYear{get{try{returnConvert.ToInt32(Encoding.ASCII.GetString(YearInByte));}catch(Exception){return0;}}}stringTryToGetString(byte[]t){try{returnEncoding.GetEncoding(CodePage_Shift_JIS).GetString(t).TrimEnd(newchar[]{'\0',' '});}catch(Exception){return"";}}publicstringTitle{get{returnTryToGetString(TitleInBytes);}}publicstringArtist{get{returnTryToGetString(ArtistInBytes);}}publicstringAlbum{get{returnTryToGetString(AlbumInBytes);}}publicstringComment{get{returnTryToGetString(CommnetInByte);}}publicstringGenre{get{returnGenres[GenreNumber];}}publicintTrack{get;privateset;}publicstaticID3v1PargeFromFoot(byte[]buffer){returnParge(buffer,buffer.Length-128);}publicstaticID3v1Parge(byte[]buffer,intoffset){ID3v1ret=newID3v1();if(offset<0){returnnull;}if(buffer.Length<offset+128){returnnull;}// ヘッダチェック ("TAG")if(buffer[offset]!=0x54||buffer[offset+1]!=0x41||buffer[offset+2]!=0x47){returnnull;}ret.TitleInBytes=newbyte[30];ret.ArtistInBytes=newbyte[30];ret.AlbumInBytes=newbyte[30];ret.YearInByte=newbyte[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.CommnetInByte=newbyte[28];Array.Copy(buffer,offset+97,ret.CommnetInByte,0,28);ret.Track=buffer[offset+126];}else{ret.CommnetInByte=newbyte[30];Array.Copy(buffer,offset+97,ret.CommnetInByte,0,30);ret.Track=0;}ret.GenreNumber=buffer[offset+127];returnret;}}publicclassID3v2{constintCodePage_Shift_JIS=932;staticintSynchsafeIntFrom4Bytes(byte[]buffer,intoffset){return((buffer[offset]&0x7F)<<21)|((buffer[offset+1]&0x7F)<<14)|((buffer[offset+2]&0x7F)<<7)|((buffer[offset+3]&0x7F));}publicclassFrame{publicstringID{get;privateset;}intMajorVer;intSize;intEncodingID;byteFlags1;byteFlags2;byte[]Data;// EncodingIDを除くpublicoverridestringToString(){intoffset=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(newSystem.Text.UTF8Encoding(false)).GetString(Data,offset,Data.Length-offset);}elseif(EncodingID==2){// UTF-16BE BOMなしreturn(newSystem.Text.UnicodeEncoding(true,false)).GetString(Data,offset,Data.Length-offset);}elseif(EncodingID==1){// UTF-16 BOMありreturn(newSystem.Text.UnicodeEncoding()).GetString(Data,offset,Data.Length-offset);}elseif(EncodingID==0){// MPEGの規格上は// ISO-8859-1 (CodePage=28591 西ヨーロッパ言語 (ISO))//return Encoding.GetEncoding(28591).GetString(Data);// 日本ではShift_JISが横行しているらしい(?)// shift_jis(CodePage=932)returnEncoding.GetEncoding(CodePage_Shift_JIS).GetString(Data,offset,Data.Length-offset);}else{// unknownreturn"";}}catch(DecoderFallbackException){// 読み取り不能return"";}}publicstaticFrameParse(intmajorVer,byte[]buffer,refintpos,intendPos){Frameret=newFrame();if(endPos>buffer.Length){returnnull;}if(endPos<pos+6){returnnull;}if(majorVer>=3&&endPos<pos+10){returnnull;}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"));returnnull;}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");returnnull;}if(ret.Size>0){ret.Data=newbyte[ret.Size-1];ret.EncodingID=buffer[pos];Array.Copy(buffer,pos+1,ret.Data,0,ret.Size-1);}else{ret.Data=newbyte[0];ret.EncodingID=0;}pos+=ret.Size;returnret;}}publicintTagVerMajor{get;privateset;}publicintTagVerMinor{get;privateset;}publicintFlags{get;privateset;}publicintTagSize{get;privateset;}publicintExtSize{get;privateset;}publicboolHasExtendedHeader{get{return((Flags&0x40)!=0);}}publicboolHasFooter{get{return((Flags&0x10)!=0);}}List<Frame>Frames;intFindFirstFrameByID(stringFrameID){for(inti=0;i<Frames.Count;i++){if(Frames[i].ID==FrameID){returni;}}return-1;}publicstringArtist{get{returnGetStringByID("TP1","TPE1");}}publicstringTitle{get{returnGetStringByID("TT2","TIT2");}}publicstringAlbum{get{returnGetStringByID("TAL","TALB");}}publicstringTrack{get{returnGetStringByID("TRK","TRCK");}}publicstringYear{get{returnGetStringByID("TYE","TYER");}}publicstringGenre{get{returnGetStringByID("TCO","TCON");}}publicstringComment{get{returnGetStringByID("COM","COMM");}}stringGetStringByID(stringidForV3p2,stringidForV3p3){stringid=(TagVerMajor<=2)?idForV3p2:idForV3p3;if(id==null){return"";}intindex=FindFirstFrameByID(id);if(index<0){return"";}returnFrames[index].ToString();}publicstaticID3v2Parge(byte[]buffer,intoffset){ID3v2ret=newID3v2();if(offset<0){returnnull;}// ID3v2は最低でも10byte以上なので10byte以上であることをチェックするif(buffer.Length<offset+10){returnnull;}// ヘッダチェック "ID3"if(buffer[offset]!=0x49||buffer[offset+1]!=0x44||buffer[offset+2]!=0x33){returnnull;}ret.TagVerMajor=buffer[offset+3];ret.TagVerMinor=buffer[offset+4];ret.Flags=buffer[offset+5];ret.TagSize=SynchsafeIntFrom4Bytes(buffer,offset+6);intpos=offset+10;intendPos=pos+ret.TagSize;if(endPos>buffer.Length){returnnull;}if(ret.HasFooter){endPos-=10;// Footer(10byte)分末尾位置を手前にセットする}if(ret.HasExtendedHeader){// 最小6byteあるif(buffer.Length<pos+6){returnnull;}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 Frameret.Frames=newList<Frame>();while(pos<endPos){if(buffer[pos]==0){break;}Framet=Frame.Parse(ret.TagVerMajor,buffer,refpos,endPos);if(t==null){Console.WriteLine("Failed to parse at address 0x"+pos.ToString("X"));returnnull;}else{ret.Frames.Add(t);}}returnret;}}classMainForm:Form{ListViewlsvID3v1;ListViewlsvID3v2;MainForm(stringfilePath){Text="Mp3TagViewer";Controls.Add(lsvID3v1=newListView(){Location=newPoint(0,0),Size=newSize(600,200),View=View.Details,FullRowSelect=true,GridLines=true,AllowDrop=true,});lsvID3v1.Columns.Add("ID3v1項目",150);lsvID3v1.Columns.Add("値",250);lsvID3v1.DragEnter+=Control_DragEnter;lsvID3v1.DragDrop+=Control_DragDrop;Controls.Add(lsvID3v2=newListView(){Location=newPoint(0,200),Size=newSize(600,400),View=View.Details,FullRowSelect=true,GridLines=true,AllowDrop=true,});lsvID3v2.Columns.Add("ID3v2項目",150);lsvID3v2.Columns.Add("値",250);lsvID3v2.DragEnter+=Control_DragEnter;lsvID3v2.DragDrop+=Control_DragDrop;this.AllowDrop=true;this.DragEnter+=Control_DragEnter;this.DragDrop+=Control_DragDrop;if(filePath!=null){LoadFile(filePath);}ClientSize=newSize(600,650);}voidControl_DragEnter(Objectsender,DragEventArgse){if(e.Data.GetDataPresent(DataFormats.FileDrop)){e.Effect=DragDropEffects.Copy;}else{e.Effect=DragDropEffects.None;}}voidControl_DragDrop(Objectsender,DragEventArgse){varfileNames=(string[])e.Data.GetData(DataFormats.FileDrop,false);if(fileNames!=null&&fileNames.Length==1){if(fileNames[0].EndsWith(".mp3",true,null)){// Note: 第2引数はignoreCaseLoadFile(fileNames[0]);}}}voidLoadFile(stringfilePath){if(filePath.EndsWith(".mp3",true,null)){// Note: 第2引数はignoreCasebyte[]data=File.ReadAllBytes(filePath);ID3v1id3v1=ID3v1.PargeFromFoot(data);ID3v2id3v2=ID3v2.Parge(data,0);RegisterID3v1ToControl(id3v1);RegisterID3v2ToControl(id3v2);}}voidRegisterID3v1ToControl(ID3v1id3v1){lsvID3v1.Items.Clear();if(id3v1!=null){lsvID3v1.BeginUpdate();try{lsvID3v1.Items.AddRange(newListViewItem[]{newListViewItem(newstring[]{"アーティスト",id3v1.Artist}),newListViewItem(newstring[]{"アルバム",id3v1.Album}),newListViewItem(newstring[]{"トラック",id3v1.Track.ToString()}),newListViewItem(newstring[]{"曲名",id3v1.Title}),newListViewItem(newstring[]{"年",id3v1.Year.ToString()}),newListViewItem(newstring[]{"ジャンル",id3v1.Genre}),newListViewItem(newstring[]{"コメント",id3v1.Comment}),});}finally{lsvID3v1.EndUpdate();}}}voidRegisterID3v2ToControl(ID3v2id3v2){lsvID3v2.Items.Clear();if(id3v2!=null){lsvID3v2.BeginUpdate();try{lsvID3v2.Items.AddRange(newListViewItem[]{newListViewItem(newstring[]{"アーティスト",id3v2.Artist}),newListViewItem(newstring[]{"アルバム",id3v2.Album}),newListViewItem(newstring[]{"トラック",id3v2.Track}),newListViewItem(newstring[]{"曲名",id3v2.Title}),newListViewItem(newstring[]{"年",id3v2.Year}),newListViewItem(newstring[]{"ジャンル",id3v2.Genre}),//new ListViewItem(new string[]{"コメント", id3v2.Comment}),});}finally{lsvID3v2.EndUpdate();}}}[STAThread]staticvoidMain(string[]args){Application.Run(newMainForm((args.Length==1)?args[0]:null));}}