Home   |   Archivio   |   Chi siamo   |   Contatti
Stefano Leone Monni
Introduzione al MIDI in Java
IV parte: un esempio completo per la creazione, riproduzione, modifica, registrazione e salvataggio di un file MIDI con il comodo supporto delle SWING
di
Stefano Leone Monni
In questa quarta puntata analizzeremo a fondo un software completo per la gestione del MIDI completamente scritto in Java grazie al quale potremo, fra le altre cose, modificare un qualsiasi file MIDI introducendo diversi effetti in tempo reale; il tutto questa volta con il comodo ausilio delle classi Swing.

Introduzione

Come suggerito dal titolo di questo capitolo, l'esempio che di qui a poco andrò ad illustrare risulterà decisamente più complesso ed esteso di quelli presentati finora. Tuttavia ci si accorgerà che una buona parte del codice è dedicata essenzialmente ad una corretta gestione dei vari componenti grafici utilizzati (JButton, JComboBox, JTextField, JSlider, etc) e degli eventi ad essi relativi.

Noi ovviamente ci concentreremo principalmente sull'aspetto che riguarda più da vicino il MIDI, tralasciando una dettagliata descrizione sulla costruzione della nostra GUI, con lo scopo di non appesantire inutilmente la trattazione.

Ma veniamo subito al nostro programma. Esso risulta essere costituito da due file principali: MidiLab.java e MIDIFileSound.java.

  • \MidiLab.java\ si occupa di costruire e gestire la vera e propria interfaccia grafica utente; sarà quindi costituito da una serie di classi per la gestione dei vari componenti.
  • \MIDIFileSound.java\ gestisce tutto ciò che riguarda la gestione del MIDI: inizializzazione e apertura dei dispositivi, caricamento dei file MIDI, riproduzione, modifica e salvataggio. Inoltre possiede due classi interne (MIDIInfo e MIDIData) che contengono le informazioni sul file correntemente aperto.

Oltre a questi due file principali, abbiamo anche i seguenti: MIDITable.java, MidiSynth.java e ExampleFileFilter.java.

  • \ExampleFileFilter.java\ è un file che organizza la gestione dei filtri relativi alla finestra di dialogo "Apri" per l'apertura dei file MIDI.
  • \MIDITable.java\ è una classe il cui costruttore riceve una matrice di dati MIDI (o, per essere proprio pignoli, un array di array di dati...:) ), i quali vengono poi visualizzati su una tabella.
  • \MidiSynth.java\ è un file di esempio proposto dalla Sun. Questo file consente di creare una sequenza Midi, ascoltarla, e registrarla sotto forma di file MIDI. A questo punto ci si potrebbe chiedere quale sia l'utilità di aggiungere a tale file nuove classi di supporto.

Metto subito le cose in chiaro: MidiSynth non consente di aprire un file MIDI preesistente di ascoltarlo, ma solo di scrivere una sequenza a partire da zero e quindi di salvarla in un file in formato MIDI. Inoltre non possiede alcuna opzione che consenta di introdurre (e salvare nella sequence) qualche particolare effetto, come la regolazione del pan, del sustain, o anche solo del volume. Ancora di più, non è possibile spostarsi in un punto preciso della propria composizione, ma si è sempre costretti ad ascoltarla dall'inizio... E così via... Queste carenze (non me ne voglia la Sun!!!) ho cercato di colmarle io... pessimamente...:)).

Il codice sorgente di tutti i files sopra menzionati potranno essere scaricati da qui (MIDILAB.zip).

Prima di addentrarmi nella spiegazione del codice, penso sia bene chiarire più nel dettaglio il modo in cui l'utente possa utilizzare "MidiLab", una volta aperto il file MIDI desiderato.

Funzionamento ed utilizzo del programma da parte dell'utente

Figura 1 - Schermata principale di MidiLab
Figura 1 - Schermata principale di MidiLab

Una volta aperto un file MIDI dal menu "File-apri" apparirà in basso al centro sullo schermo un pannello (che nel codice è rappresentato da Jpanel playerPanel) che conterrà, tra le altre cose, un secondo pannello (Jpanel soundPanel contenente i classici pulsanti JToogle playBut, pauseBut, stopBut, recBut, forBut, backBut) per la riproduzione, registrazione e posizionamento della nostra sequenza (Sequence mySequence) musicale.

Inoltre il nostro playerPanel conterrà due campi di testo (JtextField tickPos, secPos) che informeranno l'utente, rispettivamente, della corrente posizione della sequence caricata sotto forma di numero di battuta e di posizione all'interno di essa e ancora sottoforma di minuti, minuti secondi, centesimi di secondo.

In ogni momento, sia in fase di riproduzione che non, sarà possibile inserire nel campo di testo posTick la posizione desiderata per la nostra sequence. Alternativamente potremo servirci dello slider orizzontale sottostante il campo di testo (Jslider posSlider) per spostarci nella posizione desiderata.

Gli altri due campi di testo (JtextField leftLock, rigthLoc, posti sulla sinistra del pannello) serviranno a delimitare una arbitraria porzione di sequenza: quando il pulsante JToggleButton cicloBut risulterà essere selezionato, verrà riprodotta sotto forma di ciclo solo la porzione di sequenza specificata da questi due campi di testo.

Infine, un ultimo campo di testo (JtextField txtTempo) consentirà all'utente di impostare una arbitraria velocità di esecuzione per la propria sequenza. Il tempo dovrà essere espresso in BPM (ovvero in Beats Per Minuto).

L'ultimo componente del nostro soundPanel, rappresentato da uno slider verticale (Jslider conSlider) ci consentirà di modificare in tempo reale (ovvero durante la fase di riproduzione della sequenza) la nostra musica, inserendo nella posizione corrente della nostra sequenza un certo effetto (controller), specificato di volta in volta dall'utente.

I Controllers disponibili per questa prima versione di MidiLab verranno visualizzati nella JcomboBox comboCon visibile nella parte superiore sinistra della nostra finestra.

Dico subito che, sebbene i controllers disponibili risulteranno piuttosto pochi, ci sarà facilissimo (direi quasi immediato) aumentare il numero e la tipologia di controllers che vorremo utilizzare nella nostra applicazione. Il modo in cui poter fare ciò sarà spiegato in seguito. Per ora, per chi fosse interessato ulteriormente all'utilizzo e alla tipologia dei controllers esistenti, rimando alla voce [3] relativa ai riferimenti bibliografici, che contiene, fra le altre cose, un elenco completo su tutti i tipi di controllers utilizzabili per un file MIDI.

Nella finestra della nostra applicazione compariranno altri due JcomboBox comboTrack, comboChan che conterrano, rispettivamente, il numero della traccia e del canale MIDI correntemente selezionati dall'utente.

Come già accennato in precedenza, la particolarità di questo software consisterà nella possibilità di modificare un file MIDI preesistente inserendovi nella posizione e nell'istante desiderati un certo effetto rappresentato dal tipo di Controller selezionato. Tali modifiche potranno essere poi caricate sulla nostra sequenza (tramite l'uso del pulsante Jbutton appBut, etichettato "Apply") e quindi salvate sotto forma di file MIDI. In ogni momento, inoltre, sarà possibile visualizzare le modifiche apportate alla propria sequenza (selezionando la voce "Elenco eventi MIDI" del menu "Visualizza") e, all'accorrenza, annullare l'ultima modifica apportata (tramite l'uso del pulsante Jbutton undoBut, etichettato "Undo") oppure annullare tutte le modifiche apportate fino a quel momento (tramite l'uso del pulsante Jbutton resetBut, etichettato "Reset").

Veniamo ora alle modalità con cui l'utente potrà inserire i propri effetti nella propria composizione.

La prima cosa da fare sarà, ovviamente, selezionare l'effetto (il Controller) desiderato ed il canale MIDI cui vorremo indirizzare l'effetto stesso.

Dalla fig.1 possiamo notare - affianco all'elenco dei Controllers - la presenza dei seguenti campi di testo:

  • JTextField startLoc (etichettato "Start"): consentirà all'utente di specificare la posizione esatta in cui inserire il proprio effetto.
  • JTextField inVal (etichettato "Initial Value"): consentirà di specificare l'entità desiderata dell'effetto stesso: i valori consentiti in questo campo, indipendentemente dal controller selezionato, dovranno essere compresi nell'intervallo [0,127].

Ora, se si desiderasse inserire nella propria sequenza musicale un effetto progressivo (come, ad esempio, un aumento graduale del volume, oppure uno spostamento progressivo del pan da sinistra verso destra, etc.) sarà possibile farlo aggiungendo i valori desiderati nei seguenti campi di testo:

  • JTextField endLoc (etichettato "End"): Questo campo consentirà di specificare l'esatta posizione in cui inserire l'ultimo valore relativo all'effetto progressivo desiderato.
  • JTextField finVal (etichettato "Final Value"): (analogo a 'inVal') rappresenterà il campo in cui inserire il valore dell'effetto nella posizione specificata dal campo 'endLoc'.
  • JTextField stepLoc (etichettato "step(risoluzione)"): consentirà di specificare la distanza spaziale fra due eventi MIDI consecutivi relativi ad un effetto progressivo. Se ne parlerà più diffusamente in seguito. Anticipo solo che il valore step = 0 indicherà l'inserimento di un certo effetto nella sola posizione precisata nel campo "Start" col valore "initial Value".

Infine, nella nostra finestra disporremo di due JCheckBox soloTrack, muteTrack che consentiranno, rispettivamente, di ascoltare o ammutire la traccia selezionata.

Queste sono le nozioni principali necessarie per l'utilizzo del programma; le altre opzioni comprese nel software risulteranno intuitive e pertanto se ne omette la descrizione. Tuttavia, l'uso del programma risulterà decisamente più chiaro in seguito alla descrizione del codice che andremo adesso ad affrontare.

Commento del codice del software

In effetti, l'intera gestione dei file MIDI (ovvero tutte quelle operazioni che vanno dall'apertura alla riproduzione, modifica e salvataggio di un file MIDI) è stata inserita nel file MIDIFileSound.java. Il file MidiLab.java si preoccupa invece di creare un ponte di collegamento fra l'utilizzo della interfaccia grafica per l'utente e le corrispondenti operazioni da eseguire sul file MIDI stesso. Pertanto, le uniche informazioni relative al MIDI presenti in MidiLab.java deriveranno da delle esplicite chiamate ai metodi messi a disposizione dal file MIDIFileSound.java.

È per questo motivo che ci concentreremo principalmente sulla descrizione di quest'ultimo file. Ci accorgeremo presto come molti dei metodi presenti in questa classe conterranno le medesime istruzioni contenute nei tre esempi presentati nelle puntate precedenti.

Tanto per cominciare, prima di aprire il file MIDI selezionato sarà necessario, come al solito, inizializzare ed aprire tutti i dispositivi necessari per la gestione del MIDI stesso. Tali operazioni sono state racchiuse, per comodità, nel metodo inizializza() che si riporta qui di seguito:

public void inizializza() {
    try {
        if(mySynthesizer == null) {
            if((mySynthesizer = MidiSystem.getSynthesizer()) == null) {
                System.out.println("getSynthesizer() fallito!");
                return;
            }
        }
        mySynthesizer.open();
    } catch(Exception ex) {
        ex.printStackTrace();
        return;
    }

    Soundbank sb = mySynthesizer.getDefaultSoundbank();
    if(sb != null) {
        instruments = sb.getInstruments();
    }
    channel = mySynthesizer.getChannels();
    try {
        mySequencer = MidiSystem.getSequencer();
        mySequencer.open();
    } catch(MidiUnavailableException ex) { } ;
}

Nel codice sopra riportato non c'è niente di nuovo rispetto a ciò che è stato spiegato nelle puntate precedenti.

In questo metodo non solo viene inizializzato un sequencer, ma anche un synthesizer. Gli ambiti di utilizzo di tali dispositivi sono stati spiegati nelle puntate precedenti.

In questa prima versione di MidiLab, ci sarebbe bastato inizializzare il solo Sequencer. Infatti, sappiamo che il sintetizzatore ha il solo scopo di generare un suono con un determinato strumento (prelevato a sua volta da un banco) su un certo canale. Nella nostra applicazione, tuttavia, tutte le modifiche apportate al nostro file saranno registrate sotto forma di eventi MIDI che verranno via via aggiunti ad una traccia (Track track).

Ancora sul codice

Una volta inizializzati i nostri dispositivi, possiamo aprire il nostro file MIDI. Sia la inizializzazione dei dispositivi che l'apertura del file MIDI è delegata al costruttore della classe MIDIFileSound.

MIDIFileSound(String nomeFile) throws IOException, InvalidMidiDataException {
    this.inizializza();
    try {
        if(!nomeFile.equals("")) {
            myMidiFile = new File(nomeFile);
            mySequence = MidiSystem.getSequence(myMidiFile);
            mySequencer.setSequence(mySequence);

            // Aggiungo una traccia a quelle preesistenti
            track = mySequence.createTrack();
            tracks = mySequence.getTracks();
            [...]
        }
    } catch(Exception ex) { } ;
}

Anche in questo metodo non c'è praticamente quasi nulla che non sia stato spiegato nelle puntate precedenti. L'unica precisazione da fare riguarda le due istruzioni seguenti:

// Aggiungo una traccia a quelle preesistenti nella sequenza:
track = mySequence.createTrack();
tracks = mySequence.getTracks();

dove Track[] tracks risulterà cioè essere un array i cui elementi conterranno, ciascuno, una delle tracce presenti nella sequenza appena caricata da file.

Track track invece è una traccia che aggiungiamo noi a quelle preesistenti. Tutte le modifiche che vorremo introdurre al nostro file verranno scritte su tale traccia.

Gestione dei Controllers

Per questa versione di MidiLab, avremo a disposizione controlli come la regolazione del volume, del pan, l'introduzione del sustain, e così via.

// Per la prima versione di MidiLab ci serviremo dei seguenti Controllers:

String [] strCon = {"001 - Modulator Wheel",
                    "007 - Main Volume",
                    "010 - Pan Position",
                    "011 - Expression Controller",
                    "064 - Sustain Pedal"
                   };

private int [] numCon = { 1, 7, 10, 11, 64};

/* n.b: per aggiungere ulteriori Controllers
 * all'applicazione è sufficiente
 * aggiornare gli array strCon[] e numCon[]
 */

Come specificato nel commento finale, se volessimo aumentare la quantità di controllers da utilizzare nella nostra applicazione basterebbe eseguire le seguenti operazioni:

  • Aggiungere all'inizializzazione di strCon i nomi dei Controllers da aggiungere
  • Aggiungere all'inizializzazione di numCon il numero del controller che si vuole aggiungere

Effetti progressivi

Figura 2 - Esempio di valori per un effetto progressivo
Figura 2 - Esempio di valori per un effetto progressivo

L'utente ha sostanzialmente due possibilità per introdurre delle modifiche al proprio file: inserire un certo effetto con un certo valore in una determinata posizione della sequenza, oppure inserire un "effetto progressivo".

Uno Step = 0001.00.000 equivarrà ad imporre la scrittura di un nuovo evento MIDI sulla nostra traccia alla distanza di una battuta l'uno dall'altro. Più lo step risulterà essere un valore piccolo, tanti più eventi MIDI andremo a scrivere nell'intervallo musicale considerato.

Figura 3 - Effetto con risoluzione più bassa
Figura 3 - Effetto con risoluzione più bassa

Creazione degli eventi

Il metodo createConEvents() esamina le coordinate iniziali (t1,y1) e finali (t2,y2) per ricavarne la retta derivante dall'unione di tale coppia di punti:

boolean createConEvents(long t1, long t2, int y1, int y2, long step) {
    double m;
    int tmpY;
    long tmpT;
    int i;

    if((t2 == t1) || (y2 == y1) || (step == 0)) return false;
    m = Math.abs((double)(y2 - y1)) / Math.abs((double)(t2 - t1));
    clearConEvents();
    if(t1 > t2) {
        tmpT = t2;
        t2 = t1;
        t1 = tmpT;
        tmpY = y2;
        y2 = y1;
        y1 = tmpY;
    }
    i = 0;

    setConEvents(i, y1);
    setConTicks(i, t1);

    while(t1 < t2) {
        t1 = t1 + step;
        while(getConEvents(i) == (int) Math.floor((double)(m * t1))) {
            t1++;
            if(t1 > t2) break;
        }
        if(t1 <= t2) {
            i++;
            setConTicks(i, t1);
            setConEvents(i, (int)Math.floor((double)(m * t1)));
        }
    }
    setNumConEvents(i);
    return true;
}

Il metodo setControl si occupa di creare gli eventi tramite il metodo createEvent:

public void setControl(int type, int chan, int numTrack, int con, int val,
                       long tick) {
    if((val >= 0) && (val < 128))
        if(chan == 0) {
            for(int j = 0; j < 16; j++)
                createEvent(type, j, numTrack, con, val, tick);
        } else
            createEvent(type, chan - 1, numTrack, con, val, tick);
    setMySequence();
}

Il metodo createEvent:

private void createEvent(int type, int chan, int numTrack, int fun, int val,
                        long tick) {
    ShortMessage message = new ShortMessage();
    if((chan >= 0) && (chan < 16))
        try {
            message.setMessage(type, chan, fun, val);
            MidiEvent event = new MidiEvent(message, tick);
            track.add(event);
            if(numUndo < undoEvent.length)undoEvent[numUndo++] = event;
        } catch(Exception ex) {
            ex.printStackTrace();
        }
}

Gestione Undo

La rimozione degli eventi indesiderati avviene in conseguenza della pressione del pulsante "Undo":

class UndoButtonHandler implements MouseListener {
    public void mouseClicked(MouseEvent e) {
        if(e.getSource().equals(undoBut)) {
            for(int i = 0; i < MyMIDIFileSound.numUndo; i++) {
                MyMIDIFileSound.track.remove(MyMIDIFileSound.undoEvent[i]);
            }
            MyMIDIFileSound.setMySequence();
            undoBut.setEnabled(false);
        }
    }
}

Aggiornamento della sequenza

Il metodo setMySequence() si occupa di aggiornare il sequencer:

public void setMySequence() {
    try {
        if(mySequencer.isRunning()) {
            long pos = mySequencer.getTickPosition();
            mySequencer.setSequence(mySequence);
            mySequencer.start();
            mySequencer.setTickPosition(pos);
        } else mySequencer.setSequence(mySequence);
    } catch(Exception ex) {
        ex.printStackTrace();
    }
}

Lettura dei dati MIDI

La classe MIDIData sfrutta i metodi offerti dalla libreria javax.sound.midi che consentono la lettura dei singoli bytes di dati costituenti un messaggio MIDI:

public int setMIDIData(int tracciaCorrente, int index) {
    int ct = tracciaCorrente;
    int i, l;
    conv = (long)(mySequence.getMicrosecondLength() /
                  mySequence.getTickLength());

    int val[] = { 0, 0, 0 } ;

    for(i = index; i < tracks[ct].size() + index; i++) {
        l = i - index;

        val[0] = (int)(tracks[ct].get(l).getMessage().getMessage()[0] & 0xFF);
        val[1] = (int)(tracks[ct].get(l).getMessage().getMessage()[1] & 0xFF);
        if(tracks[ct].get(l).getMessage().getMessage().length > 2) {
            val[2] = tracks[ct].get(l).getMessage().getMessage()[2];
        }

        data[i][0] = getTimePos(conv * tracks[ct].get(l).getTick());
        data[i][1] = getEventTime(ct, val[0], l);
        data[i][2] = new Integer(val[0]);
        data[i][3] = new Integer(val[1]);
        data[i][4] = new Integer(val[2]);
        data[i][5] = new Integer(ct + 1);
        data[i][6] = getChanIfShort(ct, val[0], l);

        val[2] = 0;
    }
    return i;
}

Conclusioni sul commento del software

Questo lungo articolo ha avuto lo scopo di illustrare nel dettaglio solo le funzionalità principali del software di esempio. Mi limiterò a riportare un breve elenco di alcune caratteristiche del codice del file MIDIFileSound.java non commentate in questa sede:

  • La presenza di una classe interna (SoundTimerHandler) che ha lo scopo di visualizzare all'utente informazioni in tempo reale come la corrente posizione del sequencer in fase di riproduzione
  • La presenza di una classe interna (ScorePos) che esegue tutti i tipi di conversione dai dati inseriti dall'utente nel formato long Tick e viceversa
  • La presenza di una classe interna (MIDIInfo) che fornisce all'utente le informazioni di base del file MIDI correntemente aperto

Conclusioni sul tutorial del MIDI in Java

In questi quattro articoli sul MIDI in Java si è cercato di spiegare le funzionalità principali offerte dal package javax.sound.midi della Sun. In particolare, in questo ultimo articolo, abbiamo ripreso ed esteso molti dei concetti esposti nei tre precedenti articoli attraverso l'analisi di un esempio d'implementazione compatto, funzionale ed organico.

Per il momento il nostro viaggio col MIDI si ferma qui. D'altra parte, molti altri argomenti relativi ad esso potrebbero essere affrontati, come ad esempio la realizzazione di un motore di ricerca MIDI, l'uso della master keyboard, e tanto altro ancora.

Dal prossimo articolo inizieremo invece ad occuparci dell'altro package della Sun relativo alla gestione del suono "a basso livello": stiamo parlando del package javax.sound.sampled, con il quale impareremo ad aprire, ascoltare e registrare file .wav e, in generale, a gestire complessi sistemi audio. Non mancate all'appuntamento!

Bibliografia

  • [1] S.L.Monni - "Introduzione al MIDI in Java", Tesina di Ing. del Software presso l'Università di Ing.Elettronica di Cagliari - gen 2001
  • [2] Documentazione fornita dalla Sun relativa al package javax.sound.midi consultabile all'indirizzo: http://java.sun.com/j2se/1.3.0/docs/api/javax/sound/midi/packasummary.html
  • [3] Ottimo sito sulla teoria relativa al MIDI: www.midi.org
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it