domenica 17 aprile 2016

Flacreader, lettore di commenti (tag metadata) dei file flac

Come un surplus per JMusicMan, il progetto che avevo iniziato in java per la catalogazione della musica sul mio PC e che avevo abbandonato per l'impossibilità di gestire i file .flac, ho sviluppato questa classe, ancora da migliorare, per leggere e modificare i commenti (si chiamano così, più precisamente: vorbis comments) di questi file lossless. 

Però è una classe scritta in C#, dovrei convertirla di nuovo in java, o creare un binding. 

Da questa pagina ho consultato come è formato l'header dei file flac, dunque con delle operazioni sui file binari bit a bit sono riuscito a creare questa classe che attualmente legge bene i file, modifica bene anche i file che hanno già una sezione vorbis comment, ma ancora non ho testato la modifica dei file che non hanno la suddetta sezione, e quindi sarà la caratteristica che implementerò al prossimo commit su git. 

La pagina git è questa: https://github.com/standuptall/Flacreader, con due branch, uno quello principale (la classe è Flacreader.cs) e un'altro branch (grafica) che include una piccola interfaccia grafica. Anche se con git devo prenderci ancora la mano. 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace it.albe
{
    public class FlacReader
    {
        public static string vendor_string;
        public static Int32 user_comment_list_length;
        private string filepath;
        private bool data_written = false;
        private class _metadata {
            public struct _streaminfo
            {
                public bool presente;
                public const int code = 0x0;
                public bool ultimo;
            }
            public struct _padding
            {
                public bool presente;
                public const int code = 0x1;
                public bool ultimo;
            }
            public struct _application
            {
                public bool presente;
                public const int code = 0x2;
                public bool ultimo;
            }
            public struct _seektable
            {
                public bool presente;
                public const int code = 0x3;
                public bool ultimo;
            }
            public struct _vorbis_comment
            {
                public bool presente;
                public const int code = 0x4;
                public bool ultimo;
            }
            public struct _cuesheet
            {
                public bool presente;
                public const int code = 0x5;
                public bool ultimo;
            }
            public struct _picture
            {
                public bool presente;
                public const int code = 0x6;
                public bool ultimo;
            }

            public _streaminfo streaminfo;
            public _padding padding;
            public _application application;
            public _seektable seektable;
            public _vorbis_comment vorbis_comment;
            public _cuesheet cuesheet;
            public _picture picture;
            public const int ending_metadata_code = 0xF0;
            const int NUM_METADATA = 7;
            public _metadata()
            {
                streaminfo.presente = false;
                streaminfo.ultimo = false;
                padding.presente = false;
                padding.ultimo = false;
                application.presente = false;
                application.ultimo = false;
                seektable.presente = false;
                seektable.ultimo = false;
                vorbis_comment.presente = false;
                vorbis_comment.ultimo = false;
                cuesheet.presente = false;
                cuesheet.ultimo = false;
                picture.presente = false;
                picture.ultimo = false;
            }
            public void setMetadata(int code)
            {
                switch ((code<<1)>>1)  //tolgo il bit più significativo
                {
                    case _streaminfo.code: streaminfo.presente = true; break;
                    case _padding.code: padding.presente = true; break;
                    case _application.code: application.presente = true; break;
                    case _seektable.code: seektable.presente = true; break;
                    case _vorbis_comment.code: vorbis_comment.presente = true; break;
                    case _cuesheet.code: cuesheet.presente = true; break;
                    case _picture.code: picture.presente = true; break;
                }
            }
            public int getFlag(int flag)  //devo controllare se è l'ultimo, così setto il bit più significativo a 1
            {
                flag = ((flag << 1) >> 1); //tolgo il bit più significtivo
                if (picture.presente)
                    picture.ultimo = true;
                else if (cuesheet.presente)
                    cuesheet.ultimo = true;
                else if (vorbis_comment.presente)
                    vorbis_comment.ultimo = true;
                else if (seektable.presente)
                    seektable.ultimo = true;
                else if (application.presente)
                    application.ultimo = true;
                else if (padding.presente)
                    padding.ultimo = true;
                else if (streaminfo.presente)
                    streaminfo.ultimo = true;
                switch (flag)
                {
                    case _streaminfo.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                    case _padding.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                    case _application.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                    case _seektable.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                    case _vorbis_comment.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                    case _cuesheet.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                    case _picture.code: if (streaminfo.presente) flag += ending_metadata_code; break;
                }
                return flag;
            }
        } ;
        _metadata metadata;
        private int metadata_comments_length;
        public Dictionary comments;
        public FlacReader(string filepath)
        {
            metadata_comments_length = 0;
            metadata = new _metadata();
            comments = new Dictionary();
            this.filepath = filepath;
            user_comment_list_length = 0;
            FileStream stream;
            stream = File.OpenRead(filepath);
            if (stream == null)
                throw new FileNotFoundException("Il file non esiste");
            byte[] array = {0,0,0,0};
            stream.Read(array,0,4);
            if (!((array[0]==0x66)&&(array[1]==0x4C)&&(array[2]==0x61)&&(array[3]==0x43)))   //fLaC
                throw new Exception("Il file non è un file flac!");
            byte flag;
            do 
            {
                stream.Read(array,0,1);  //mi sposto di 1 byte
                flag = array[0];
                metadata.setMetadata(flag);
                stream.Read(array, 0, 3);  //leggo tre byte per la lunghezza del metadata
                Int32 metadata_length = array[0] * 65536 + array[1] * 256 + array[2];
                if (flag == 4 || flag == 132)   //se è un vorbis comment cioè 00000100 oppure 10000100
                {
                    metadata_comments_length = metadata_length;
                    caricaCommenti(stream);
                }
                else
                    stream.Seek(metadata_length, SeekOrigin.Current); //mi sposto della lunghezza del metadata
            } while (!((flag>>7)==0x1)); //se il bit più significativo è uguale a uno vuol dire ch enon ci sono più metadata
            stream.Close();
        }
        public void setVendor(string vendorName)
        {
            int vendor_string_length_old = vendor_string.Length;  
            metadata_comments_length -= vendor_string_length_old;//tolgo la lunghezza iniziale
            vendor_string = vendorName;
            metadata_comments_length += vendor_string.Length;  //e metto la nuova lunghezza del vendor string
        }
        public string getVendor()
        {
            return vendor_string;
        }
        public void addComment(string field, string value)
        {
            try
            {
                if (comments[field] != null)   //se il commento già esiste
                {
                    string val = comments[field];
                    metadata_comments_length -= (field.Length + val.Length + 1);   //tolgo la lunghezza originaria
                    metadata_comments_length -= 4;
                }
            }
            catch(KeyNotFoundException e)
            {
                comments[field] = value;
                user_comment_list_length++;
                metadata_comments_length += 4; //aggiungo 4 byte per memorizzare la lunghezza del comment sul file
                metadata_comments_length += field.Length + value.Length + 1;  //aggiungo la lunghezza del commento più il simbolo uguale
            }
            
            
        }
        public void writeAll()  //file esistente quindi riverso il contenuto su un file _temp
        {
            FileStream stream,streamWrite;
            stream = File.OpenRead(filepath);
            streamWrite = File.OpenWrite(filepath + "_temp");
            byte[] array = { 0, 0, 0, 0 };
            stream.Read(array, 0, 4);
            streamWrite.Write(array, 0, 4);
            byte flag;
            do
            {
                stream.Read(array, 0, 1);  //mi sposto di 1 byte
                streamWrite.Write(array, 0, 1);  //mi sposto di 1 byte
                flag = array[0];
                stream.Read(array, 0, 3);  //leggo tre byte per la lunghezza del metadata
                Int32 metadata_length = array[0] * 65536 + array[1] * 256 + array[2];
                if (flag == 4 || flag == 132)  //se è un vorbis comment cioè 00000100 oppure 10000100
                {
                    stream.Seek(metadata_length, SeekOrigin.Current); //mi sposto sul lettore della lunghezza del metadata
                    array[0] = (byte) (metadata_comments_length >> 16);
                    array[1] = (byte) (metadata_comments_length >> 8);
                    array[2] = (byte)(metadata_comments_length);
                    streamWrite.Write(array, 0, 3);  //scrivo la nuova lunghezza del metadata
                    scriviCommenti(streamWrite);
                }
                else
                {
                    streamWrite.Write(array, 0, 3);  //scrivo la lunghezza del metadata
                    int i = 0;
                    for (i = 0; (i+4)< metadata_length; i+=4)
                    {
                        stream.Read(array, 0, 4);
                        streamWrite.Write(array, 0, 4);
                    }
                    /* scrivo i byte rimanenti */
                    stream.Read(array, 0, metadata_length-i);
                    streamWrite.Write(array, 0, metadata_length -i);
                }
            } while (!((flag >> 7) == 0x1)); //se il bit più significativo è uguale a uno vuol dire ch enon ci sono più metadata
            /* scrivo tutto il rimanente */
            byte[] chunk = new byte[10240];
            
               
            int num = 0;
            while (true)
            {
                num = stream.Read(chunk, 0, 10240);
                streamWrite.Write(chunk, 0, num);
                if (num < 10240)
                {
                    stream.Close();
                    streamWrite.Close();
                    return;
                }
            }
            
        }
        public void writeAll(String filename)  //nuovo file
        {
        }
        private void caricaCommenti(Stream stream)
        {
            byte[] array = { 0, 0, 0, 0 };
            stream.Read(array, 0, 4);
            uint vendor_length = (uint)((array[0]));  //non capisco perché ma conta solo il primo byte
            byte[] stringa = new byte[1024];
            stream.Read(stringa, 0, (int)vendor_length);
            vendor_string = System.Text.Encoding.UTF8.GetString(stringa,0,(int)vendor_length);
            stream.Read(array, 0, 4); //leggo il numero dei commenti
            uint number_of_comments = (uint)array[0];
            for (uint i = 0; i < number_of_comments; i++)
            {
                stream.Read(array, 0, 4); //leggo il numero di caratteri da leggere
                uint comment_length = (uint)array[0];
                stream.Read(stringa, 0, (int)comment_length);
                String comment = System.Text.Encoding.UTF8.GetString(stringa, 0,(int) comment_length);
                comments[comment.Split('=')[0]] = comment.Split('=')[1];
                Array.Clear(stringa, 0, stringa.Length);
            }
        }
        private void scriviCommenti(Stream stream)
        {
            Int32 length = vendor_string.Length;
            byte[] array = { 0, 0, 0, 0 };
            array[0] = (byte)(length);
            array[1] = 0;
            array[2] = 0;
            array[3] = 0;
            stream.Write(array, 0, 4);
            byte[] arrayName = System.Text.Encoding.UTF8.GetBytes(vendor_string.ToCharArray());
            stream.Write(arrayName, 0, length);
            byte numcommenti = (byte)comments.Count;
            array[0] = (byte)(numcommenti);
            stream.Write(array, 0, 4);
            /* scrivo i commenti */
            foreach (KeyValuePair entry in comments)
            {
                string comment = entry.Key + "=" + entry.Value;
                length = comment.Length;
                arrayName = System.Text.Encoding.UTF8.GetBytes(comment.ToCharArray());
                array[0] = (byte)length;
                array[1] = 0;
                array[2] = 0;
                array[3] = 0;
                stream.Write(array, 0, 4);
                stream.Write(arrayName, 0, length);
            }
            data_written = true;
        }
    }
}