Riporto di seguito lo studio che ho compiuto su O.O.S.:
Invio dati relativi agli eventi Midi con O.O.S.
Uso del dispositivo: /dev/sequencer
Gli eventi Midi devono essere inviati come flusso di dati (stream) al dispositivo " /dev/sequencer ".Tale dispositivo rappresenta, quindi, il dispositivo in grado di supportare il multimediale Midi, e che permette di programmare il sintetizzatore FM o wavetable o dispositivi esterni sul MIDI bus. Il parametro "device" serve proprio per indirizzare l'evento Midi su una certa periferica, corrisponde al MIDI output da usare ed il suo numero identificativo è 0.
Per la sintesi sonora, laddove le schede audio non posseggano il sintetizzatore interno, è necessario servirsi di un softsynth che gestisca la riproduzione sonora appunto a livello software. Softsynth sono, per esempio, Timidity e Fluidsynth. I Softsynth sono, dunque, programmi specifici (client) aventi il compito di interfacciare i sequencer con la scheda audio gestendo preliminarmente la parte sonora vera e propria.
Ovviamente è necessario che il dispositivo sia connesso al softsynth. Il comando per stabilire tale connessione è "aconnect" seguito dal numero identificativo del client " MidiThrough " e della sua porta, che solitamente è 14:0 , nonché del client softsynth e della sua porta. Il comando " aconnect " può essere attivato mediante il comando SHELL.
Senza questo accorgimento non si potrebbero inviare eventi Midi dal sequencer al softsynth, il quale sulla base dei dati Midi ricevuti gestisce i campioni audio del soundbank e li suona sul dispositivo PCM della scheda audio.
L'invio dei dati al device " /dev/sequencer " avviene mediante l'apertura del device medesimo in scrittura all'inizio del programma. In OSS il dispositivo non può essere aperto due volte.
Con " /dev/sequencer " il flusso di dati, che genera un evento midi, è distinguibile sostanzialmente in gruppi di byte costituiti da 4 valori. Di ciascun gruppo:
- il 1° valore è sempre = 5 e corrisponde a SEQ_MIDIPUTC.
- il 2° valore corrisponde al comando midi in sé:
- Evento Midi+canale per il 1°gruppo di dati;
- 2° parametro dell'evento per il 2° gruppo;
- 3° eventuale parametro dell'evento per il 3° gruppo.
- il 3° valore è sempre = 0 (è il parametro del "device").
- il 4° valore è sempre = 0.
Il numero complessivo di dati da inviare dipende dal tipo di evento Midi da generare. Solitamente il numero di Byte è 12.
Temporizzazione degli eventi Midi:
tre sono le possibili modalità per determinare dopo quanto tempo (Tempo Delta) un evento Midi avverrà rispetto a quello immediatamente precedente:
1 - Con un'istruzione da 4 byte
WRITE #file, Chr(2), Chr(valore1) & Chr(valore2) & Chr(valore3), 4
' Chr(2) = valore identificativo di TMR_WAIT_ABS (tempo assoluto)
' valore1 = da 0 a 255. Qui 255 dovrebbe essere = 2,6 secondi (255/96 tick).
' valore2 = da 0 a 255. Qui invece: 1 = 2,6 sec. , e 255 = 663 sec. (255 * 2,6)
' Esempio:
WRITE #file, Chr(2), Chr(255) & Chr(2) & Chr(0), 4
'durata dell'evento Midi = 2,6 + (2,6 * 2) = 7,8 secondi
2 - Con un'istruzione da 8 byte
WRITE #file, Chr(&h81), Chr(2) & Chr(0) & Chr(0), & Chr(valore1), & Chr(valore2), & Chr(valore3), & Chr(valore4), 8
NOTA: le due istruzioni, sopra descritte, si comportano diversamente da WAIT, e per questo risultano essere più vantaggiose. Infatti, il programma Midi continua comunque a fluire, e la CPU può svolgere relativamente ad esso ogni altra operazione. Questo perché la gestione delle due istruzioni è demandata specificatamente a "/dev/sequencer".
3 - Uso di Timer
In alternativa, si può usare un ciclo attivato da Timer, con il quale far accadere i vari eventi Midi ogni tot millesimi di secondo. Il Timer è abbastanza preciso, nonostante presenti, ad ogni tick, alcuni millesimi di imprecisione. Questo significa che ogni nota può iniziare o terminare con alcuni millisecondi di anticipo di o di ritardo, ma ciò può - volendo - essere considerato accettabile.
Un'altra idea potrebbe essere quella di combinare il TIMER con la funzione TIME, la quale ultima restituisce l'orario sino al millisecondo. Il programma assegnerebbe innanzitutto agli eventi Midi un vero e proprio orario, con definizione al millesimo di secondo, in cui ciascuno di essi deve accadere e, quindi, essere inviato al dispositivo /dev/sequencer. Timer si limiterebbe, in tal caso, ad effettuare semplicemente il ciclo per far verificare in ogni istante se è giunto l'orario prestabilito per l'invio dei dati che determineranno l'evento Midi. Ci si affiderebbe così alla precisione dell'orologio interno del computer; e la funzione TIME farebbe da punto di riferimento certo, superando in tal modo, qualora lo si richieda, l'incerta precisione millesimale della funzione Timer.
Esempio pratico dell'uso di /dev/sequencer :
' Gambas class file
PRIVATE midi AS file
PUBLIC SUB Form_open()
DIM device AS String
device = "/dev/sequencer"
' apre il flusso di dati per la scrittura
midi = OPEN device FOR WRITE
END
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
PUBLIC SUB ButtonConnessione_Click()
' presuppone il softsynth Timidity installato
' e connette MidiThrough 14:0 a Timidity 128:0
SHELL "aconnect 14:0 128:0"
END
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
PUBLIC SUB ButtonInvioDati_Click()
' Invio dei gruppi di dati:
WRITE #midi, Chr(5) & Chr(&hC0) & Chr(0) & Chr(0)& ' definisce l'evento Program Change al canale 1
Chr(5) & Chr(70) & Chr(0) & Chr(0), 8 ' definisce lo strumento musicale
WRITE #midi, Chr(5) & Chr(&h90) & Chr(0) & Chr(0)& ' definisce l'evento Note ON al canale 1
Chr(5) & Chr(60) & Chr(0) & Chr(0)& ' definisce la nota da suonare
Chr(5) & Chr(100) & Chr(0) & Chr(0), 12 ' definisce la velocità di tocco (velocity)
WRITE #midi, Chr(2) & Chr(255) & Chr(2) & Chr(0), 4 ' Temporizzazione con istruzione da 4 byte
WRITE #midi, Chr(5) & Chr(&h80) & Chr(0) & Chr(0)& ' definisce l'evento Note OFF al canale 1
Chr(5) & Chr(60) & Chr(0) & Chr(0)& ' definisce la nota da spegnere
Chr(5) & Chr(100) & Chr(0) & Chr(0), 12 ' definisce la velocità di tocco (velocity)
END
N O T A :
le cose con la versione 4.0 di OSS sono cambiate per "/dev/sequencer".
Da me specificamente contattato, Hannu Savolainen (che ha redatto soundcard.h) ha risposto:
<< The /dev/sequencer API is no longer part of OSS so it will not work. However you can use the /dev/midi API and write raw MIDI messages to it. The /dev/midi API can do the same than /dev/sequencer. All you need to do is to open /dev/midi (or /dev/midi##) and to write standard MIDI messages to it. The device doesn't provide any timing facilities but you can call usleep() to wait between messages. The /dev/sequencer API was removed because the whole concept was wrong and and it proved to be impossible to document it. There were some plans to develop a replacement but I don't think it will ever get implemented.>>
Inoltre, in http://manuals.opensound.com si legge:
<< The third change in OSS 4.0 is that the development of the previous sequencer (/dev/sequencer and /dev/music) API has been halted. >>
<< The /dev/sequencer device interface for MIDI and synthesizer devices has been
abandoned. This part of the OSS API was redundant with the official MIDI 1.0 specification by MMA. >>
Nel messaggio precedente si è fatto uso del dispositivo: /dev/sequencer, che - come già detto - dalla versione 4.0 di OSS è stato soppresso.
Nell'attesa di un adeguato rimpiazzamento del dispositivo /dev/sequencer con le future versioni successive alla 4.0, chi ha una
versione precedente può comunque compiere un piccolo salto di qualità nella gestione delle funzioni di OSS mediante l'uso del dispositivo " /dev/sequencer2 ".
Con la versione 4.0 anche lo sviluppo del dispositivo /dev/sequencer2 è stato bloccato, e pertanto è inutilizzabile.
Riporto di seguito il suo uso solo ormai a titolo storico.
USO di " /dev/sequencer2 " (oggi non più possibile)
" /dev/sequencer2 " ha i seguenti vantaggi:
- non è più necessario inserire il comando "aconnect" in SHELL per connettere il dispositivo con il softsynth;
- consente di utilizzare innanzitutto pacchetti (record) tutti da 8 byte, i quali, apparendo così più lineari ed omogenei, possono risultare più comodi nella fase di programmazione; e comunque sono consigliati in "soundcard.h";
- consente di utilizzare tipi di eventi di secondo livello, quindi istruzioni che faciltano ed abbreviano la definizione, la gestione ed il conseguente invio dei dati Midi;
- restituisce un errore, se gli vengono trasmessi argomenti errati;
[li]migliore e più ampio utilizzo delle istruzioni per la gestione della temporizzazione.
[/li][/list]
Come già detto, con il dispositivo " /dev/sequencer2 " non è più necessario l'uso dell'istruzione SHELL per connettere il dispositivo al softsynth: esso avviene automaticamente.
Bisogna premettere che il numero del "device, che prima con " /dev/sequencer " era impostato a 0, ora con " /dev/sequencer2 " è da impostarsi a 3 ovvero, qualora 3 non funzioni, a 1 .
Gli eventi richiamabili con " /dev/sequencer2 " sono suddivisi per Classi di eventi:
Classi di eventi | | Num. identif.vo |
EV_SEQ_LOCAL | | &h80 |
EV_TIMING | | &h81 |
EV_CHN_COMMON | | &h92 |
EV_CHN_VOICE | | &h93 |
EV_CHN_SYSEX | | &h92 |
Le Classi di eventi sono suddivise a loro volta in Sotto-eventi tipi (riporto le Classi ed i loro sotto-eventi più importanti):
Classe di evento | | Sotto-evento | | Num. identif.vo | | Tipo di evento |
EV_TIMING | | TMR_WAIT_REL | | 1 | | Tempo Delta espresso in tick |
| | TMR_STOP | | 3 | | Arresto dell'esecuzione |
| | TMR_START | | 4 | | Avvio del Timer assoluto |
| | TMR_CONTINUE | | 5 | | Continua l'esecuzione dopo l'arresto |
| | TMR_TEMPO | | 6 | | Tempo Metronomico espresso in bpm |
| | TMR_ECHO | | 8 | | Per la sincronizzazione con la marcatura temporale (Timestamp) |
|
EV_CHN_COMMON | | MIDI_CTL_CHANGE | | &hB0 | | Control change |
| | MIDI_PGM_CHANGE | | &hC0 | | Program change |
| | MIDI_CHN_PRESSURE | | &hD0 | | Channel Aftertouch |
| | MIDI_PITCH_BEND | | &hE0 | | Pitch Bend |
|
EV_CHN_VOICE | | MIDI_NOTEOFF | | &h80 | | Note OFF |
| | MIDI_NOTEON | | &h90 | | Note ON |
| | MIDI_KEY_PRESSURE | | &hA0 | | Aftertouch Polyphonic |
L'utilizzo di questi dati avviene, come detto, mediante istruzioni formate da 8 byte.
Modello di istruzione per gli eventi EV_CHN_VOICE:
WRITE #flusso_dati, Chr(Classe) & Chr(device) & Chr(Sotto-evento) & Chr(canale) & Chr(nota) & Chr(velocità) & Chr(0) & Chr(0), 8
Legenda:
- flusso_dati AS file
- Classe = il numero identificativo della Classe di eventi
- device = il numero del device (= 1 oppure 3)
- Sotto-evento = il numero identificativo del Sotto-evento
- canale = il numero del canale (da 0 a 15)
- nota = numero della nota
- velocità = velocity
NOTA: I valori del 7° e dell'8° byte sono sempre posti a zero.
Esempi pratici:
1) Per l'invio dell'istruzione Note ON:
WRITE, #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h90) & Chr(0) & Chr(60) & Chr(100) & Chr(0) & Chr(0), 8
2) Per l'nvio dell'istruzione Note OFF:
WRITE, #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h80) & Chr(0) & Chr(60) & Chr(100) & Chr(0) & Chr(0), 8
3) Per l'invio dell'istruzione Aftertouch Polyphonic:
WRITE, #flusso_dati, Chr(&h93) & Chr(1) & Chr(&hA0) & Chr(0) & Chr(60) & Chr(100) & Chr(0) & Chr(0), 8
Modello di istruzione per gli eventi EV_CHN_COMMON:
WRITE #flusso_dati, Chr(Classe) & Chr(device) & Chr(Sotto-evento) & Chr(canale) & Chr(valore1) & Chr(valore2) & Chr(0) & Chr(0), 8
Legenda:
- flusso_dati AS file
- Classe = il numero identificativo della Classe di eventi
- device = il numero del device (= 1 oppure 3)
- Sotto-evento = il numero identificativo del Sotto-evento
- canale = il numero del canale (da 0 a 15)
- valore1, 2 = valori dei parametri specifici dei Sotto-eventi
NOTA: I valori del 7° e dell'8° byte sono sempre posti a zero.
Alcuni esempi pratici:
1) Per l'invio dell'istruzione Control Change:
WRITE, #flusso_dati, Chr(&h92) & Chr(1) & Chr(&hB0) & Chr(0) & Chr(7) & Chr(100) & Chr(0) & Chr(0), 8
(In questo esempio il valore del Volume è stato posto a 100)
2) Per l'invio dell'istruzione Program Change:
WRITE, #flusso_dati, Chr(&h92) & Chr(1) & Chr(&hC0) & Chr(0) & Chr(70) & Chr(0) & Chr(0) & Chr(0), 8
(In questo esempio è stato scelto lo strumento n. 70: Fagotto)
Modello di istruzione per l'evento EV_TIMING:
WRITE, #flusso_dati, Chr(Classe) & Chr(Sotto-evento) & Chr(0) & Chr(0) & Chr(valore1) & Chr(valore2) & Chr(valore3) & Chr(0), 8
Legenda:
- flusso_dati AS file
- Classe = il numero identificativo della Classe di eventi
- Sotto-evento = il numero identificativo del Sotto-evento
- valore1,2,3 = valori degli eventuali parametri specifici dei Sotto-eventi
NOTE:
- Hanno almeno un parametro specifico: TMR_WAIT_REL, TMR_TEMPO, TMR_ECHO;
- il 3° ed il 4° byte sono sempre posti a zero;
- si conferma quanto già affermato nel precedente messaggio: gli EV_TIMING risultano più vantaggiosi del comando WAIT, poiché essi, diversamente da WAIT, consentono al programma di continuare comunque a fluire nella lettura, gestione ed esecuzione del proprio codice. Infatti, essendo la gestione delle istruzioni di temporizzazione demandata specificatamente a "/dev/sequencer2 ", la CPU può svolgere ogni altra operazione.
Alcuni esempi pratici:
1) Per l'invio dell'istruzione TMR_WAIT_REL:
WRITE, #flusso_dati, Chr(&h81) & Chr(1) & Chr(0) & Chr(0) & Chr(96) & Chr(0) & Chr(0) & Chr(0), 8
(In questo esempio il valore di TMR_WAIT_REL è stato posto a 96 tick)
2) Per l'invio dell'istruzione TMR_START:
WRITE, #flusso_dati, Chr(&h81) & Chr(4) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0), 8
(TMR_START non ha parametri specifici)
Uso pratico delle istruzioni da 8 byte
Ogni qual volta si inviano gruppi di istruzioni per l'esecuzione di uno o più eventi Midi, è indispensabile assolutamente inviare all'inizio un'istruzione TMR_START, richiamata dal valore 4, la quale provvede ad avviare il Timer assoluto.
Invio istruzioni per suonare e far cessare due note:
' definisce la variabile flusso_dati come file (stream)
Private flusso_dati AS FILE
PUBLIC SUB Form_Open()
flusso_dati = OPEN "/dev/sequencer2" FOR WRITE
END
PUBLIC SUB ButtonAvvio_Click()
' Invio di TMR_START
WRITE, #flusso_dati, Chr(&h81) & Chr(4) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0), 8
' E' possibile impostare il tempo metronomico: 120 bmp
WRITE, #flusso_dati, Chr(&h81) & Chr(6) & Chr(0) & Chr(0) & Chr(120) & Chr(0) & Chr(0) & Chr(0), 8
' 1° evento Midi – Program Change: Fagotto.
WRITE #flusso_dati, Chr(&h92) & Chr(1) & Chr(&hC0) & Chr(0) & Chr(70) & Chr(0) & Chr(0) & Chr(0), 8
' 2° evento Midi – EV_CHN_VOICE - Note ON
WRITE #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h90) & Chr(0) & Chr(60) & Chr(100) & Chr(0) & Chr(0), 8
' Temporizzazione (tempo delta)– TMR_REL = 40 tick
WRITE #flusso_dati, Chr(&h81) & Chr(1) & Chr(0) & Chr(0) & Chr(40) & Chr(0) & Chr(0) & Chr(0), 8
' 3° evento Midi – EV_CHN_VOICE - Note OFF
WRITE #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h80) & Chr(0) & Chr(60) & Chr(100) & Chr(0) & Chr(0), 8
' - - -
' 4° evento Midi – EV_CHN_VOICE - Note ON
WRITE #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h90) & Chr(0) & Chr(72) & Chr(100) & Chr(0) & Chr(0), 8
' Temporizzazione (tempo delta) – TMR_REL = 40 tick
WRITE #flusso_dati, Chr(&h81) & Chr(1) & Chr(0) & Chr(0) & Chr(40) & Chr(0) & Chr(0) & Chr(0), 8
' 5° evento Midi – EV_CHN_VOICE - Note OFF
WRITE #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h80) & Chr(0) & Chr(72) & Chr(100) & Chr(0) & Chr(0), 8
END
Se si vuole mettere in "Pausa" l'esecuzione degli eventi Midi, basta lanciare l'istruzione: TMR_STOP, per esempio mediante un altro tasto, e inviare contemporaneamente un'istruzione Control Change di All Note OFF.
PUBLIC SUB Pausa_Click()
WRITE #flusso_dati, Chr(&h81) & Chr(3) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0), 8
WRITE #flusso_dati, Chr(&92) & Chr(1) & Chr(&hB0) & Chr(0) & Chr(123) & Chr(0) & Chr(0) & Chr(0), 8
END
Per riavviare dopo la pausa, utilizzare l'istruzione TMR_CONTINUE (sotto-evento = 5). L'esecuzione ripartirà dall'evento successivo a quello in cui l'esecuzione è stata posta in pausa:
PUBLIC SUB Continua_Click()
WRITE #flusso_dati, Chr(&h81) & Chr(5) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0) & Chr(0), 8
END
Il Timestamp con il sotto-evento TMR_ECHO
Nella programmazione Midi l'uso di una marcatura temporale (timestamp) risulta utile se è richiesta la verifica dell'avvenimento di uno o più eventi.
Tale marca temporale può essere ottenuta mediante il sotto-evento TMR_ECHO (numero identificativo: 8 ):
WRITE #flusso_dati, Chr(&h81) & Chr(8) & Chr(0) & Chr(0) & Chr(valore1) & Chr(valore2) & Chr(valore3) & Chr(valore4), 8
NOTA: il 3° ed il 4° byte sono sempre posti a zero.
I valori contenuti nei byte 5, 6, 7 ed 8 (ossia: Chr(valore1) & Chr(valore2) & Chr(valore3) & Chr(valore4) ) dell'istruzione di TMR_ECHO costituiscono la "key" indicata in "soundcard.h", ossia la chiave inviata a "/dev/sequencer2" con TMR_ECHO. Poiché in un flusso di istruzioni Midi potrebbero essere necessarie moltissime marcature temporali (timestamp), con valori numerici ovviamente crescenti, si rende necessario l'uso di un dato integer passato al dispositivo con TMR_ECHO come parametro. TMR_ECHO accetta una "chiave" che è un intero definito a scelta del programmatore. Gli ultimi 4 byte dell'istruzione contengono, appunto, i quattro valori numerici utili per la ricostruzione dell'integer.
Dunque con l'invio del sotto-evento TMR_ECHO "/dev/sequencer2" ci restituisce il valore della "chiave" integer, spezzettato nei valori numerici degli ultimi 4 byte del sotto-evento.
E' quindi necessario poter leggere dal dispositivo "/dev/sequencer2" tali valori dopo l'invio del sotto-evento TMR_ECHO mediante la funzione READ
Semplice esempio pratico del funzionamento di TMR_ECHO:
' definisce la variabile flusso_dati come file (stream)
Private flusso_dati AS FILE
PUBLIC SUB Form_Open()
' il dispositivo viene aperto sia per leggere che per scrivere
eco_dati = OPEN "/dev/sequencer2" FOR READ WRITE
END
PUBLIC SUB ButtonProvaTmrEcho_Click()
Dim eco_dati AS STRING
Dim j AS INTEGER
' Invia un Note ON
WRITE #flusso_dati, Chr(&h93) & Chr(1) & Chr(&h90) & Chr(0) & Chr(72) & Chr(100) & Chr(0) & Chr(0), 8
' Quindi invia TMR_ECHO. Nei 4 ultimi byte dovranno essere inseriti 4 numeri a piacere
WRITE #flusso_dati, Chr(&h81) & Chr(8) & Chr(0) & Chr(0) & Chr(valore1) & Chr(valore2) & Chr(valore3) & Chr(valore4), 8
' Legge gli otto dati da "/dev/sequencer2"
READ #flusso_dati, eco_dati, -8
' ' ' '
' Verifica empirica della "chiave" marca temporale (timestamp)
FOR j = 5 to 8
PRINT Asc(eco_dati, j) ' Legge dal 5° valore fino all'8°, cioè appunto i 4 valori costituenti l'integer "chiave"
NEXT
END
NOTA: L'istruzione TMR_ECHO funziona. E' necessario avere l'accortezza di instaurare due cicli diversi: uno per mandare gli eventi Midi, l'altro per leggere da "/dev/sequencer2" i dati di ritorno da TMR_ECHO. Il ciclo può essere, per esempio, attivato con un Timer con delay posto fra i 20 ed i 50 ms.
E' opportuno, per non creare intasamento, inviare un certo numero di eventi Midi insieme con il numero scelto di eventi TMR_ECHO, e poi ad ogni evento di Timer leggere da "/dev/sequencer2" i dati di ritorno di TMR_ECHO. Il ciclo virtuoso, insomma, sarebbe: invio di un gruppo di dati, poi lettura continua delle "chiavi" TMR_ECHO di ritorno afferenti a questo gruppo di eventi Midi; poi, prima che l'esecuzione del gruppo degli eventi Midi termini, inviare un altro gruppo di eventi Midi, quindi lettura continua delle "chiavi" TMR_ECHO di ritorno di quest'altro gruppo; e così via fino alla fine.
Tutto ciò è possibile, poiché bisogna non dimenticare che gli eventi Midi temporizzati mediante l'evento di timing TMR_WAIT_REL (che rappresenta il Tempo Delta) vengono "accodati" e gestiti dal dispositivo l'uno dopo l'altro secondo l'ordine temporale della loro attivazione imposto da TMR_WAIT_REL (Tempo Delta), ossia attivati ciascuno "a suo tempo". Mentre, dunque, "/dev/sequencer2" gestisce in modo pienamente autonomo (è utile ricordare anche questa caratteristica) gli eventi Midi, il programma può effettuare tranquillamente la lettura della "eco" di ritorno della "chiave" di TMR_ECHO proveniente da "/dev/sequencer2".
Un ringraziamento ancora all'amico Doriano per la preziosa consulenza tecnica.