PDA

View Full Version : [C++] QT threads - un'idea migliore ?


trallallero
26-01-2009, 12:27
Avrei bisogno di qualche consiglio su un programma che sto facendo (C++ - Linux Ubuntu).

In pratica devo creare un record/playback client da inserire nel programma mumble che già usiamo per i nostri software di comunicazione.

In fase "record" il client deve registare l'audio e questo l'ho già fatto usando l'ottima libreria free "libsndfile".

In fase "playback" ho qualche problemino ... devo sincronizzare un thread "transport" ed uno "playback" in modo che quando quello "transport" riceve un comando (via tcp) tipo play, stop o set-position, quello "playback" esegua la giusta operazione.

Devo usare i threads di QT (QThread) perchè mumble li usa quindi mi ritrovo con 2 threads che hanno un metodo run() con un ciclo infinito.
Il thread "transport" controlla periodicamente se c'è un comando da eseguire e, se si, informa il thread "playback".

Non mi piace la soluzione che ho trovato ma è l'unica ... un QT mutex per ogni comando:
es: se arriva un comando stop, il thread "transport" locka il mutex stop e il thread "playback", quando prova a lockarlo, si ferma in attesa che venga sbloccato.
Non mi piace perchè ogni 1/4 di secondo i threads devono bloccare/sbloccare un mutex per ogni comando per non parlare poi del rischio deadlock visto il numero di mutex.

Qualcuno ha idee migliori ?

cionci
26-01-2009, 13:22
Difficile da dire non conoscendo nei dettagli l'architettura. Ad esempio potresti usare una coda circolare in modo da fare il playback dei comandi solo tutti insieme (nota che inserimento in coda e prelievo lavorano su semafori diversi).
Serve comunque una mutex per l'inserimento perché ci possono essere più thread che tentano di incrementare il contatore.
C'è un solo thread che preleva, quindi di fatto non c'è bisogno di alcuna mutex ed il thread che preleva, se non ci sono dati da prelevare, può tornare a fare altro. Di fatto il thread che preleva non ha alcuna attesa. Quelli che scrivono attendono solo di prendere possesso della mutex in scrittura sul contatore oppure attendono in caso di coda piena.

trallallero
26-01-2009, 13:51
Difficile da dire non conoscendo nei dettagli l'architettura. Ad esempio potresti usare una coda circolare in modo da fare il playback dei comandi solo tutti insieme (nota che inserimento in coda e prelievo lavorano su semafori diversi).
Serve comunque una mutex per l'inserimento perché ci possono essere più thread che tentano di incrementare il contatore.
C'è un solo thread che preleva, quindi di fatto non c'è bisogno di alcuna mutex ed il thread che preleva, se non ci sono dati da prelevare, può tornare a fare altro. Di fatto il thread che preleva non ha alcuna attesa. Quelli che scrivono attendono solo di prendere possesso della mutex in scrittura sul contatore oppure attendono in caso di coda piena.
Non ho capito molto :stordita:
Che ci sia bisogno del mutex lo so ma non ho capito come fare per usarne solo uno ...

trallallero
26-01-2009, 13:55
Forse ho capito ...

Potrei lockare una struttura con una variabile che indica l'azione da eseguire e una che indica il valore (per set-position, per esempio).

Se è "lockata per scrivere" dal thread transport, la leggo dal thread "playback" per sapere cosa deve fare.


Non so se è quello che intendi ma grazie dell'idea :)

cionci
26-01-2009, 14:13
Non ho capito molto :stordita:
Che ci sia bisogno del mutex lo so ma non ho capito come fare per usarne solo uno ...
Siamo d'accordo che ci siano N produttori e un consumatore ?
La coda circolare avrà M risorse.
Inizializzerai due QSemaphore vuote(M), piene(0), una mutex(unlocked) da condividere fra i produttori.
Un vettore:
Risorse buffer[M]:
int inserimento = 0;
int prelievo = 0;

Per l'inserimento:
1 - acquisisci vuote
2 - acquisisci la mutex
3 - buffer[inserimento] = miaRisorsa
4 - inserimento = (inserimento + 1) % M;
5 - release sulla mutex
6 - release su piene

Per il prelievo:
1 - tento l'acquisizione di piene, se fallisco ritorno al chiamante, non ci sono risorse disponibili
2 - se sono qui significa che ci sono risorse disponibili
3 - risorsaDaRitornare = buffer[prelievo];
4 - prelievo = (prelievo + 1) % M;
5 - release su vuote

Se noti prelievo e inserimento non si troveranno mai ad operare sullo stesso elemento di buffer, di conseguenza non c'è concorrenza su buffer. Non c'è concorrenza nemmeno sugli incrementi.
La concorrenza è solo fra inserimenti diversi (dato che non ci sono prelievi contemporanei) e viene gestita dalla mutex.

Questo è il classico esempio di coda circolare che è presente nella letteratura per la sincronizzazione fra processi ;)

trallallero
26-01-2009, 14:27
Mi sa che non fa per il caso mio ...
Io ho un singolo processo (che poi saranno 15 perchè avremo 15 playback clients) che ha solo 2 threads, uno capo, che decide cosa fare, ed uno "schiavo", che esegue.

Quindi ci deve essere concorrenza perchè se il capo decide che lo schiavo deve fermarsi, questo si deve fermare fino a cambio ordine.
Però è interssante questa coda circolare, ogni volta che intervieni tu ne imparo una nuova.


Siamo d'accordo che ci siano N produttori e un consumatore ?
Ecco, mi sa che non siamo sincronizzati o non seguo il tuo linguaggio tecnico :D

cionci
26-01-2009, 14:43
E' merito delle tue domande che sono sempre interessanti ;)

Per produttore si intende colui che produce una risorsa (i comandi nel tuo caso), per consumatore si intende colui che consuma la risorsa (esegue i comandi).

Quindi hai un solo produttore e un solo consumatore, è ancora più semplice !!!
Inizializzerai due QSemaphore vuote(M), piene(0):

Comando buffer[M]:
int inserimento = 0;
int prelievo = 0;

Per l'inserimento:
1 - tento l'acquisizione di vuote, se fallisco ritorno al chiamante perché non ci sono risorse disponibili
2 - buffer[inserimento] = mioComando
3 - inserimento = (inserimento + 1) % M;
4 - release su piene

Per il prelievo:
1 - acquisisco piene (mi sembra di capire che se non ci sono comandi il consumatore si debba fermare)
2 - comandoDaEseguire = buffer[prelievo];
3 - prelievo = (prelievo + 1) % M;
4 - release su vuote
5 - ritorno comandoDaEseguire al chiamante

Credo che questa cosa sia perfetta per te. In realtà ci sono due mutex, ma sono nascoste nei due semafori, però solo un processo si può bloccare su ogni mutex e non succede mai in contemporanea.
Di fatto quando il consumatore ha finito i comandi si blocca autonomamente sul semaforo fino a quando il produttore non inserirà un nuovo comando.

trallallero
26-01-2009, 14:56
Scusa ma non ho capito questo
3 - inserimento = (inserimento + 1) % M;
...
3 - prelievo = (prelievo + 1) % M;

perchè % M :wtf: ?

cionci
26-01-2009, 15:18
Scusa ma non ho capito questo


perchè % M :wtf: ?
E' l'operatore di modulo o resto della divisione intera. M è il numero di risorse. Se il contatore raggiunge il valore M il modulo a M torna zero e quindi permette di rendere la coda circolare ;)

cionci
26-01-2009, 15:21
Considera il semaforo come un contatore, ogni volta che rilasci il semaforo lo incrementi, ogni volta che lo acquisisci lo decrementi, se il contatore è zero e tento di fare un'acquisizione il mio thread si blocca in attesa che torni maggiore di zero.

trallallero
26-01-2009, 15:25
E' l'operatore di modulo o resto della divisione intera. M è il numero di risorse. Se il contatore raggiunge il valore M il modulo a M torna zero e quindi permette di rendere la coda circolare ;)
Geniale :eek:

sapevo che è il resto della divisione ma non riuscivo a capire la sua utilità!

Considera il semaforo come un contatore, ogni volta che rilasci il semaforo lo incrementi, ogni volta che lo acquisisci lo decrementi, se il contatore è zero e tento di fare un'acquisizione il mio thread si blocca in attesa che torni maggiore di zero.
Adesso vado a casa, sto fuso perchè sto anche combattendo con un portatile nuovo, cercando di installare winxp :uh:
Domani a mente fresca riguardo il tutto.

Grazie mille ;)

cionci
26-01-2009, 15:27
Non ti preoccupare, il meccanismo è fine, ma è semplice ;)
Per una miglior comprensione sostituisci la release con nomeSemaforo++ e l'acquisizione con nomeSemaforo-- (tieni conto che non può mai arrivare sotto zero). Ovviamente solo per fare i conti su carta :)

trallallero
27-01-2009, 08:58
Ok, sono lucido e riposato :D

Mi sa che c'è qualcosa di troppo nella tua soluzione ...

Non è che "se non ci sono comandi il consumatore si deve fermare" ma dipende dall'ultimo comando ottenuto.
Immagina un cd player: premi "play" e il cd parte; fino a che non riceve un altro comando (o non finisce il cd) il cd viene riprodotto.
Poi viene premuto "pause", nuovo comando, e il cd si ferma - fino ad un nuovo comando il cd sta fermo.
etc etc.

Quindi non capisco l'utilità della coda circolare.

Probabilmente mi basta un mutex che blocca una variabile "comando"

cionci
27-01-2009, 10:14
Tu hai chiesto una soluzione per evitare che i due thread si sincronizzassero. Questa è una soluzione. Ad evitare che il consumatore si blocchi ci vuole poco (tryAcquire al posto di acquire).

Spiegami qualcosa di più sui comandi. Se il thread produttore produce molti comandi. E possibile che ci siano più comandi in attesa di essere eseguiti ? Se la risposta è sì, allora ti serve ad ogni costo una coda circolare. A meno che non voglia far bloccare il tuo produttore su una mutex fino a quando il consumatore non ha consumato il comando ;)

Io la vedo così. Quello che mi immagino è che il produttore possa produrre molti comandi e che il consumatore debba fare "qualcosa" con questi comandi, questo "qualcosa" occupa tempo e quindi non rende il consumatore reattivo al massimo ai comandi. Il produttore non si può permettere di fermarsi ad aspettare che il consumatore esegua il comando, ma nemmeno il consumatore (visto che dovrai fare un playback dovrai rispettare temporizzazioni strette). Per garantirti la massima reattività ai comandi:
Nuova architettura.
Thread A: produttore
Thread B: consumatore
Thread C: playback
Coda come sopra: bloccante per il consumatore e non bloccante per il produttore (la coda non si dovrebbe riempire mai, tranne per errori, quindi bene o male se la tryAcquire fallisce dovresti generare un'eccezione).

Con questa architettura è palese che quando non ci sono comandi il consumatore resta sempre in attesa sul semaforo del prelievo. Quindi il consumo di uno comando è veramente istantaneo.
Il produttore ha attesa zero sulla coda perché è praticamente sempre vuota. All'inserimento di un comando il consumatore viene risvegliato e preleva immediatamente il comando. All'uscita dal prelievo lo esegue su playback: se deve stoppare il playback acquisirà una mutex, se deve avviare il playback rilascerà la stessa mutex, se deve fare altre cose lo saprai te cosa deve fare ;)
Il consumatore non appena ha eseguito il comando si rimette in attesa sul prelievo fino a quando non riceve un altro comando.

L'interfaccia migliore fra consumatore e playback al di là di come te la ho descritta sopra dovrebbe essere questa:

consumatore:
prelevo comando dalla coda
acquisisco MutexM
setto il comando
se è il comando stop acquisisco MutexStop
se è il comando play rilascio MutexStop
rilascio MutexM

playback:
acquisisco MutexM
se non ci sono comandi continuo ad eseguire quello attuale, salto al rilascio
recupero il comando
se è il comando di stop, rilascio MutexM, acquisisco MutexStop (qui si ferma), acquisisco MutexM, comando = Play
rilascio MutexM
eseguo il comando che mi è stato impartito

Alla fine: se per il tuo produttore è accettabile l'attesa fra l'acquisizione ed il rilascio di MutexM allora puoi tranquillamente sostituire il produttore al consumatore mantenendo un'architettura a 2 thread.

trallallero
27-01-2009, 10:32
Mamma mia, come al solito mi hai dato parecchi input ;)

Adesso provo un pò e poi vedo quale soluzione prendere (sicuramente ho già abbandonato l'orribile idea di un mutex per ogni comando, grazie).

Diciamo che l'attesa tra l'acquisizione ed il rilascio è accettabilissima (anche perchè ho carta bianca quindi decido io :D).
Prima facevo un blocco mutex e lettura da file ogni secondo ma poi sono arrivato ad 1/4 di secondo. Quindi dal momento in cui premo stop a quello in cui il playback si ferma passano 2.5 decimi di secondo, direi che è impercettibile.

Spiegami qualcosa di più sui comandi. Se il thread produttore produce molti comandi. E possibile che ci siano più comandi in attesa di essere eseguiti ? Se la risposta è sì, allora ti serve ad ogni costo una coda circolare. A meno che non voglia far bloccare il tuo produttore su una mutex fino a quando il consumatore non ha consumato il comando
Ecco, una cosa che dovrei prevedere è il militare che si mette a premere i tastini play/stop/setpos all'impazzata. Altrimenti non ci dobvrebbe essere una coda di comandi. Controllo ogni 1/4 di secondo, mica può essere così veloce un umano (militare poi :asd:)


PS: Ieri sera (come tutte le sere) suonavo la mia chitarra elettrica ed ho fatto caso al Cubase (programma professionale): quando premi start e stop c'è sempre un periodo piuttosto lungo di attesa; sullo start è ovvio perché ogni instrumento ha la sua latency, ma sullo stop immagino sia per lo stesso mio motivo (ovvero "attesa tra l'acquisizione ed il rilascio"). Quindi ho pensato: se lo fa una grossa società come la Steinberg lo posso fare anche io :D

cionci
27-01-2009, 10:42
Ok, allora vai di metodo a 2 thread.
E' importante la sequenza di acquisizione e rilascio delle mutex ;)
Evita deadlock ;) Viene chiamato "cambio del testimone".

cionci
27-01-2009, 10:50
Comuqnue io verificherei cosa avviene se vengono premuti due tasti contemporaneamente, probabilmente in questo modo rimani un po' indietro, mi spiego:
- tempo 0: premo play e stop (lo stop leggermente dopo)
- tempo 250ms: premo play e stop (lo stop leggermente dopo)
- tempo 500ms: premo tasto "Guerra termonucleare globale"

Il programma dovrebbe reagire in questo modo:
- tempo 20ms: leggo play
- tempo 270ms: leggo stop
- tempo 520ms: leggo play
- tempo 1020ms: leggo stop
- tempo 1270ms: leggo "Guerra termonucleare globale"

Ho ritardato di 3/4 di secondo la fine del mondo ;)

trallallero
27-01-2009, 11:19
Ok, allora vai di metodo a 2 thread.
E' importante la sequenza di acquisizione e rilascio delle mutex ;)
Evita deadlock ;) Viene chiamato "cambio del testimone".
Veramente mi ritrovo con 3 threads ma mi sa che ne posso eliminare uno (a furia di testare mi perdo ...)

class RingBuffer
class WavReader : public RingBuffer, QThread
class WavTransport : public QThread
class WavPlayer : public QThread


Comuqnue io verificherei cosa avviene se vengono premuti due tasti contemporaneamente, probabilmente in questo modo rimani un po' indietro, mi spiego:
- tempo 0: premo play e stop (lo stop leggermente dopo)
- tempo 250ms: premo play e stop (lo stop leggermente dopo)
- tempo 500ms: premo tasto "Guerra termonucleare globale"

Il programma dovrebbe reagire in questo modo:
- tempo 20ms: leggo play
- tempo 270ms: leggo stop
- tempo 520ms: leggo play
- tempo 1020ms: leggo stop
- tempo 1270ms: leggo "Guerra termonucleare globale"
Il tempo di risposta quando premo stop non è 1/4 di secondo ma di 1 secondo e non capisco perchè (forse è il mutex nella classe RingBuffer, devo controllare cosa ho fatto)

Ho ritardato di 3/4 di secondo la fine del mondo ;)
:D

trallallero
27-01-2009, 11:36
visto che ci siamo faccio un'altra domanda:

15 playback clients che riproducono 15 diversi audio file, sono perfettamente sincronizzati ?
non intendo l'audio in se ma proprio i tempi di esecuzione dei threads dei 15 processi.

cionci
27-01-2009, 15:03
Sopra era "passaggio del testimone" :doh:
15 playback clients che riproducono 15 diversi audio file, sono perfettamente sincronizzati ?
non intendo l'audio in se ma proprio i tempi di esecuzione dei threads dei 15 processi.
Credo proprio che non ci sia garanzia che avanzino in maniera sincronizzata. 15 sono tanti. In tal caso ti conviene sincronizzarli.

Puoi usare una QWaitCondition.

mutex.lock();
numberOfThreadInside++;
if(numberOfThreadInside < 15)
{
allThreadInside.wait(&mutex);
}
else
{
allThreadInside.wakeAll();
}
numberOfThreadInside--;
mutex.unlock();

In ogni casi bisogna vedere che siano mantenuti i requisiti temporali.

trallallero
27-01-2009, 20:15
Sopra era "passaggio del testimone" :doh:
Non è grave :D

Credo proprio che non ci sia garanzia che avanzino in maniera sincronizzata. 15 sono tanti. In tal caso ti conviene sincronizzarli.

Puoi usare una QWaitCondition.

mutex.lock();
numberOfThreadInside++;
if(numberOfThreadInside < 15)
{
allThreadInside.wait(&mutex);
}
else
{
allThreadInside.wakeAll();
}
numberOfThreadInside--;
mutex.unlock();

In ogni casi bisogna vedere che siano mantenuti i requisiti temporali.
Ma così non rischio di far andare l'audio a intermittenza ?
Voglio dire: se non sono sincronizzati in un margine di tempo relativo non mi frega niente, l'importante è che prima o poi si riallineino.

Non so se ricordi l'altro thread ma si parla di 4 ore di training quindi se non trovo il modo per sincronizzarli son ... acidi :stordita:

Però è interessante questo allThreadInside, grazie ;)

cionci
27-01-2009, 20:20
allThreadInside è un QWaitCondition eh ;)

Appunto parlavo di requisiti temporali. Dipende comunque tutto da come funziona il tuo playback. Mi spiego: tu mandi i dati Wav alla scheda audio, la scheda audio andrà a fare il play di questi dati.
Non so come funziona in questi casi, di solito i dati sono sufficienti per un periodo di tempo limitato ed il controllo torna subito al chiamante in modo che possa andare a recuperare altri dati mentre quelli precedenti vengono "suonati".
Che libreria usi per la scheda audio ?

trallallero
28-01-2009, 13:41
allThreadInside è un QWaitCondition eh ;)

Appunto parlavo di requisiti temporali. Dipende comunque tutto da come funziona il tuo playback. Mi spiego: tu mandi i dati Wav alla scheda audio, la scheda audio andrà a fare il play di questi dati.
Non so come funziona in questi casi, di solito i dati sono sufficienti per un periodo di tempo limitato ed il controllo torna subito al chiamante in modo che possa andare a recuperare altri dati mentre quelli precedenti vengono "suonati".
Che libreria usi per la scheda audio ?
(scusa ma pensavo non mi avessi ancora risposto perchè il 3d non era in grassetto :boh: )

Allora, è un pò diverso il meccanismo: faccio partire 15 clients (processi) playback che inviano lo stream audio via rete ad un server che decide a quali client (PC) inviarli. Quindi il processo playback non invia i dati alla scheda audio ma solo via rete.
Poi il PC che riceve lo stream dovrà inviarlo alla scheda audio.

Quindi io non dovrei sincronizzare 15 threads ma 15 processi che hanno almeno 2 threads ognuno.

trallallero
28-01-2009, 13:47
Una idea ce l'avrei ma mi sa che è una cazzata ...

se faccio una fseek sul file wav ogni secondo ? :stordita:

cionci
28-01-2009, 14:08
(scusa ma pensavo non mi avessi ancora risposto perchè il 3d non era in grassetto :boh: )

Allora, è un pò diverso il meccanismo: faccio partire 15 clients (processi) playback che inviano lo stream audio via rete ad un server che decide a quali client (PC) inviarli. Quindi il processo playback non invia i dati alla scheda audio ma solo via rete.
Poi il PC che riceve lo stream dovrà inviarlo alla scheda audio.

Quindi io non dovrei sincronizzare 15 threads ma 15 processi che hanno almeno 2 threads ognuno.
Allora alla sincronizzazione del playback ci dovrebbero pensare i client che ricevono i dati.
Una idea ce l'avrei ma mi sa che è una cazzata ...

se faccio una fseek sul file wav ogni secondo ? :stordita:
In che senso ? Spiega.

trallallero
28-01-2009, 14:21
Allora alla sincronizzazione del playback ci dovrebbero pensare i client che ricevono i dati.
Ma ogni client è un PC a parte. Da non confondere con i 15 clients (processi) che inviano gli stream ;)

In che senso ? Spiega.
Conosco la samplerate, i sample etc, quindi so esattamente a che posizione dovrebbe trovarsi il file ad ogni secondo.
Infatti con la mia miniinterfaccia riesco a posizionare il file dove voglio io, funziona.

Se, con un global timer, ogni secondo io faccio una fseek (che poi sarebbe una sf_seek visto che uso la libsndfile) son sicuro che i processi saranno sempre allineati.

Quello che non so è cosa rischio di sentire con un sistema del genere ...

cionci
28-01-2009, 15:01
Sinceramente è difficile analizzare cosa ti serva senza fare qualche prova o sapere i requisiti del problema.
Se leggi il file ogni secondo sicuramente il playback è in ritardo di un secondo più il tempo di esecuzione ed invio sulla rete. Imho devi leggerlo ad un rate più basso, tipo un decimo di secondo.
Poi imho devi anche trovare il modo per far sì che questa lettura avvenga esattamente ogni decimo di secondo, o con un timer o con un qualche rendezvous fra i thread (un punto del programma in cui i thread vengono raggruppati in attesa che un timer li sblocchi).

trallallero
28-01-2009, 15:23
Sinceramente è difficile analizzare cosa ti serva senza fare qualche prova o sapere i requisiti del problema.
Se leggi il file ogni secondo sicuramente il playback è in ritardo di un secondo più il tempo di esecuzione ed invio sulla rete. Imho devi leggerlo ad un rate più basso, tipo un decimo di secondo.
Poi imho devi anche trovare il modo per far sì che questa lettura avvenga esattamente ogni decimo di secondo, o con un timer o con un qualche rendezvous fra i thread (un punto del programma in cui i thread vengono raggruppati in attesa che un timer li sblocchi).

È esattamente quello che sto cercando in rete, qualche esempio.

cionci
28-01-2009, 15:26
Ce l'avevi sotto gli occhi...

mutex.lock();
numberOfThreadInside++;
if(numberOfThreadInside < 15)
{
allThreadInside.wait(&mutex);
}
else
{
allThreadInside.wakeAll();
}
numberOfThreadInside--;
mutex.unlock();

Basta far fare ad un timer il wakeAll ;) Il contatore non serve, ma la mutex sì.

trallallero
28-01-2009, 15:29
Ce l'avevi sotto gli occhi...

mutex.lock();
numberOfThreadInside++;
if(numberOfThreadInside < 15)
{
allThreadInside.wait(&mutex);
}
else
{
allThreadInside.wakeAll();
}
numberOfThreadInside--;
mutex.unlock();

Basta far fare ad un timer il wakeAll ;) Il contatore non serve, ma la mutex sì.
Si lo so, ma io devo sincronizzare 15 processi, non 3ds ;)

cionci
28-01-2009, 15:32
Si lo so, ma io devo sincronizzare 15 processi, non 3ds ;)
Ah già :D
Linux o Windows ?

trallallero
28-01-2009, 15:36
Ah già :D
Linux o Windows ?

Linux ovviamente :O

(sto per andare via - devo prendere mia figlia all'asilo ... mi sa che dovrò continuare a fare figli per poter andare via a quest'ora :asd:)

cionci
28-01-2009, 15:50
Ma usi fork per creare i processi ? In tal caso le possibilità sono multiple. Shared memory (shmget etc), pipe (comando pipe). Mentre se i processi sono creati in maniera diversa e non condividono la stessa immagine in memoria allora puoi usare named pipe (mkfifo) o i socket.

trallallero
28-01-2009, 20:01
Ma usi fork per creare i processi ? In tal caso le possibilità sono multiple. Shared memory (shmget etc), pipe (comando pipe). Mentre se i processi sono creati in maniera diversa e non condividono la stessa immagine in memoria allora puoi usare named pipe (mkfifo) o i socket.

Beh, qualcosa sulla interprocess communication in effetti la so anche se quando lavoravo per le società telefoniche facevo soltanto sistemi client/server dove i clients erano indipendenti, non comunicavano tra loro.

Però, adesso che ci penso, noi in società abbiamo un sistema (di cui sò poco) chiamato swrm, dove r sta per reflected e m per memory, che si basa proprio su shared memory. Ha anche un global timer.
Quindi forse prima di inventarmi qualcosa o rubare qualche tua idea mi conviene chiedere in ditta se c'è qualche tool, api o quant'altro.
Se poi hai qualche dritta, male non fa :D

Domani ti faccio vedere come ho risolto con il client playback così, se ti và e hai tempo, mi dici se può andare bene.

Mi sento un pò in debito ... se non ricordo male l'unica cosa che hai imparato da me è stata la "structure bit fields" :stordita:

cionci
29-01-2009, 08:27
Beh, qualcosa sulla interprocess communication in effetti la so anche se quando lavoravo per le società telefoniche facevo soltanto sistemi client/server dove i clients erano indipendenti, non comunicavano tra loro.

Però, adesso che ci penso, noi in società abbiamo un sistema (di cui sò poco) chiamato swrm, dove r sta per reflected e m per memory, che si basa proprio su shared memory. Ha anche un global timer.
Quindi forse prima di inventarmi qualcosa o rubare qualche tua idea mi conviene chiedere in ditta se c'è qualche tool, api o quant'altro.
Se poi hai qualche dritta, male non fa :D

Domani ti faccio vedere come ho risolto con il client playback così, se ti và e hai tempo, mi dici se può andare bene.

Mi sento un pò in debito ... se non ricordo male l'unica cosa che hai imparato da me è stata la "structure bit fields" :stordita:
Certo che mi interessa vederlo.
Dai dai ma che ci importa...siamo qui per parlare :D

trallallero
29-01-2009, 09:39
Ok, non sarò breve :D

Ho creato una struttura che contiene il comando, il valore del comando (se c'è), e il mutex (ho usato QReadWriteLock perchè pensavo di poter bloccare per scrivere e leggere ma poi ho cambiato, un semplice QMutex andrebbe benissimo).

typedef enum
{
NONE = 0,
PLAY = 1,
STOP = 2,
POS = 3,
CLOSE = 4,
} ACTIONS;

typedef struct _Actions
{
ACTIONS Action;
unsigned long Value;
QReadWriteLock qMutex;
} tActions;


Ho una classe RingBuffer che controlla via mutex i buffers che vengono scritti/letti da WavReader e WavPlayer.

Poi ho 3 classi: WavPlayer, WavTransport e WavReader.
Per creare un client playback basta fare così:
WavPlayer Player( "file.wav", TOT_BUFFERS, BUF_LEN);
TOT_BUFFERS indica quanti buffers deve usare la classe RingBuffer.
BUF_LEN ovviamente la lunghezza del buffer
BUF_LEN = RATE*CHANNELS/SECONDS_PARTS
RATE = sample rate, CHANNELS = 1 (mono) e SECONDS_PARTS corrisponde a quante frazioni di secondo.

Il WavPlayer apre il file audio, inizializza ALSA, crea un WavTransort ed un WavReader e li fa partire (sono derivati da QThread).
E aspetta un comando ...

Per adesso non ho segnali tcp quindi uso files, giusto per testare.
Questo è il ciclo del thread WavTransport:

void WavTransport::run()
{
while(1)
{
if ( stat( "/home/trallallero/LOG/play", &stFileInfo) == 0)
{
Actions.qMutex.lockForWrite();
Actions.Action = PLAY;
Actions.qMutex.unlock();
remove("/home/trallallero/LOG/play");
}
if ( stat( "/home/trallallero/LOG/pause", &stFileInfo) == 0)
{
Actions.qMutex.lockForWrite();
Actions.Action = STOP;
Actions.qMutex.unlock();
remove("/home/trallallero/LOG/pause");
}
else if ( stat( "/home/trallallero/LOG/close", &stFileInfo) == 0)
{
Actions.qMutex.lockForWrite();
Actions.Action = CLOSE;
Actions.qMutex.unlock();
remove("/home/trallallero/LOG/close");
}
else if ( stat( "/home/trallallero/LOG/pos", &stFileInfo) == 0)
{
Actions.qMutex.lockForWrite();
Actions.Action = POS;
Actions.Value = static_cast<unsigned long>(GetPosNum("/home/trallallero/LOG/pos"));
Actions.qMutex.unlock();
remove("/home/trallallero/LOG/pos");
}
usleep(3000);
}
}


Questo è il ciclo del thread WavReader:

void WavReader::run()
{
LOG();

short LocalBuffer[RingBuffer::GetBufferLen()];
bool bActive = true;
bool bExiting = false;

while(bActive)
{
Actions.qMutex.lockForWrite();

switch(Actions.Action)
{
case PLAY : m_bPaused = false; puts("PLAY" ); break;
case STOP : m_bPaused = true; puts("STOP" ); break;
case POS : SetPosition(Actions.Value); Reset(); puts("POS" ); break;
case CLOSE: bExiting = true; puts("CLOSE"); break;
default : break;
}
Actions.Value = 0;
Actions.Action = NONE;
Actions.qMutex.unlock();

if (bExiting)
bActive = false;

if (bActive && ! m_bPaused)
{
sf_read_short(m_WavFile, LocalBuffer, RingBuffer::GetBufferLen());

while ( ! m_bPaused && ! FillBuffer(LocalBuffer) )
usleep(3000);
}
else
usleep(3000);
}
}


Quindi il transport blocca, setta il comando (e il valore in caso di "set pos") e sblocca.
Il reader blocca, legge il comando (e il valore), resetta i valori della struct e sblocca.

In caso di "set position" fa una sf_seek (chiamando la funzione SetPosition) ed una Reset perchè i buffers devono essere azzerati visto che si cambia posizione del file.

Ho provato con QSemaphore ma ho visto che con un mutex andava più che bene. O almeno spero :D

cionci
29-01-2009, 10:33
Mmmmhhh...quella pausa in fondo è obbligatoria ? Nel senso: non vuoi che il tuo thread reagisca subito ai comandi ?

Imho puoi usare una QMutex al posto di una QReadWriteMutex (la seconda è fatta per problemi in cui c'è almeno un produttore ed almeno un produttore).

Nota che entrambi i thread avanzano in maniera diversa, mi spiego:
- istante 0: inizia l'esecuzione
- istante 0+K(0): finisce l'esecuzione
- istante 3000+K(0): inizia l'esecuzione
- istante 3000+K(0)+K(1): finisce l'esecuzione
...
- istante N*3000 + Somma Da 0 a N - 1 di K(i): inizia l'esecuzione
- istante N*3000 + Somma Da 0 a N di K(i): finisce l'esecuzione
Supponendo K1 la media di K(i) per transport e K2 la media di K(i) per WavReader, dK = |K1 - K2| > 0, i due thread si desincronizzano con N > 3000/dK. In questo modo perdi un comando, o ne duplichi uno, e il delay fra invio ed esecuzione del comando è variabile ;)
Quello che volevo rendere chiaro è che una sleep non implica che il tuo codice verrà eseguito ogni tot tempo, ma ogni tot + il tempo di esecuzione del codice. Per svegliare un thread ogni tot ms bisogna usare un timer che ogni tot ms esegue, ad esempio, una wake su una condition.

Mantenendo l'architettura attuale dei thread, devi semplicemente svegliare il thread ogni volta che depositi un comando ;)

typedef struct _Actions
{
ACTIONS Action;
unsigned long Value;
QSemaphore pieno;
QSemaphore vuoto;

//costruttore di default
_Actions():pieno(0), vuoto(1)
{
}
} tActions;

La sincronizzazione deve essere fatta così per Transport:

//qui sopra controllo l'esistenza del file, determino il comando da depositare
vuoto.acquire();
Actions.Action = PLAY;
pieno.release();

Non ci deve essere alcuna attesa nel thread Reader. E deve sincronizzarsi così:

ACTIONS localAction;
pieno.acquire();
localAction = Actions.Action;
vuoto.release();
//fai lo switch su localAction (anche se ci starebbe bene il pattern State o Command)

Ovviamente anche lo sleep di Transport deve essere eliminato, ma lo farai quando avrai i socket, ti metti in attesa sulla receive ;)

trallallero
29-01-2009, 10:38
Miii che spiegazione teorica da paura :D

Ok grazie, provo subito con 'sti semafori.

Io quella sleep l'ho messa soltanto perchè se premo STOP, l'eseguibile, mentre fa niente, mi ciuccia il 100% della cpu.

cionci
29-01-2009, 10:43
Io quella sleep l'ho messa soltanto perchè se premo STOP, l'eseguibile, mentre fa niente, mi ciuccia il 100% della cpu.
Chiaro, era un'attesa attiva. Invece con la soluzione sopra, se primi stop si mette in attesa di un nuovo comando e basta :D

trallallero
29-01-2009, 11:00
Chiaro, era un'attesa attiva. Invece con la soluzione sopra, se primi stop si mette in attesa di un nuovo comando e basta :D

Infatti non va :D

Cioè, non gira. Devo continuare a premere play per farlo muovere ma non sento comunque niente :boh:

edit: no, se premo rapidamente riesco a sentire qualcosa ...

cionci
29-01-2009, 11:02
Fammi vedere come l'hai scritto.

trallallero
29-01-2009, 11:07
Ho messo in inglese (full ed empty)

Il Transport
while(1)
{
if ( stat( "/home/mservadei/LOG/play", &stFileInfo) == 0)
{
//Actions.qMutex.lockForWrite();
Actions.empty.acquire();
Actions.Action = PLAY;
Actions.full.release();
//Actions.qMutex.unlock();
remove("/home/mservadei/LOG/play");
}
if ( stat( "/home/mservadei/LOG/pause", &stFileInfo) == 0)
{
//Actions.qMutex.lockForWrite();
Actions.empty.acquire();
Actions.Action = STOP;
Actions.full.release();
//Actions.qMutex.unlock();
remove("/home/mservadei/LOG/pause");
}
else if ( stat( "/home/mservadei/LOG/close", &stFileInfo) == 0)
{
//Actions.qMutex.lockForWrite();
Actions.empty.acquire();
Actions.Action = CLOSE;
Actions.full.release();
//Actions.qMutex.unlock();
remove("/home/mservadei/LOG/close");
}
else if ( stat( "/home/mservadei/LOG/pos", &stFileInfo) == 0)
{
//Actions.qMutex.lockForWrite();
Actions.empty.acquire();
Actions.Action = POS;
Actions.Value = static_cast<unsigned long>(GetPosNum("/home/mservadei/LOG/pos"));
Actions.full.release();
//Actions.qMutex.unlock();
remove("/home/mservadei/LOG/pos");
}
usleep(3000);
}


Il Reader

while(bActive)
{
puts("1");
//Actions.qMutex.lockForWrite();
ACTIONS localAction;

Actions.full.acquire();
localAction = Actions.Action;
Actions.empty.release();

switch(localAction)
{
case PLAY : m_bPaused = false; puts("PLAY" ); break;
case STOP : m_bPaused = true; puts("STOP" ); break;
case POS : SetPosition(Actions.Value); Reset(); puts("POS" ); break;
case CLOSE: bExiting = true; puts("CLOSE"); break;
default : break;
}
// Actions.Value = 0;
// Actions.Action = NONE;
//Actions.qMutex.unlock();

if (bExiting)
bActive = false;

if (bActive && ! m_bPaused)
{
sf_read_short(m_WavFile, LocalBuffer, RingBuffer::GetBufferLen());

while ( ! m_bPaused && ! FillBuffer(LocalBuffer) )
usleep(3000);
}
else
usleep(3000);
}

trallallero
29-01-2009, 11:12
Si blocca quì, sul Reader
Actions.full.acquire();

cionci
29-01-2009, 11:16
Ah ok, capito, comunque di facile risoluzione ;)

ACTIONS getAction(tActions &Actions, ACTIONS current, int &value)
{
ACTIONS localAction;

if(current == STOP)
{
Actions.full.acquire();
}
else
{
if(!Actions.full.tryAcquire())
return current;
}

localAction = Actions.Action;
value = Actions.Value;
Actions.empty.release();

return localAction;
}

Fa una acquire bloccante in caso sia stop ;)

cionci
29-01-2009, 11:17
Per chiamarla:

currentAction = getAction(Actions, currentAction, pos);

cionci
29-01-2009, 11:20
Ho modificato per tenere conto della posizione.

trallallero
29-01-2009, 11:27
Ok mitico! funziona :yeah:

Adesso controllo l'uso della cpu con top

grazie :)

trallallero
29-01-2009, 11:43
La cpu è a posto anche senza sleep.

A questo punto posso usare lo stesso meccanismo nel RingBuffer.

cionci
29-01-2009, 11:47
Aspetta...ce l'hai un meccanismo che ti faccia passare lo stato a STOP quando il play è terminato ?
Se il play termina quella diventa un'attesa attiva (perché non hai più dati da spedire al buffer).
Come identifichi lo stato in cui non hai più dati da spedire al buffer ?

trallallero
29-01-2009, 12:07
Aspetta...ce l'hai un meccanismo che ti faccia passare lo stato a STOP quando il play è terminato ?
Se il play termina quella diventa un'attesa attiva (perché non hai più dati da spedire al buffer).
Come identifichi lo stato in cui non hai più dati da spedire al buffer ?

No, in effetti non c'ho ancora pensato

cionci
29-01-2009, 12:11
Basta mettere lo stato a stop e poi sei a cavallo ;)
In ogni caso imho una usleep anche passando 1 come parametro ce la metterei, prova a verificare che il play continui a funzionare.
Questo ti garantisce che altre thread entrino in esecuzione.
In alternativa puoi mettere un'attesa minima nella tryAcquire ;)

trallallero
29-01-2009, 12:24
Basta mettere lo stato a stop e poi sei a cavallo ;)
Ma si, quando la sf_read_short mi da zero setto bActive a false.
Ma non mi preoccupa adesso perché non so cosa vuole il cliente.
Magari quando finisce vuole poter tornare indietro, riascoltare ... in quel caso deve essere ancora attivo.

In ogni caso imho una usleep anche passando 1 come parametro ce la metterei, prova a verificare che il play continui a funzionare.
Questo ti garantisce che altre thread entrino in esecuzione.
In alternativa puoi mettere un'attesa minima nella tryAcquire ;)
Ok, provo un po di casi, poi a vado giù a chiedere della swrm.

trallallero
29-01-2009, 12:52
Ok. la swrm non mi serve a niente quindi dovrò fare tutto da solo.
Mi hanno consigliato di usare boost per l'interprocess communication.

Ne sai qualcosa ?

cionci
29-01-2009, 14:08
Ma si, quando la sf_read_short mi da zero setto bActive a false.
Ma non mi preoccupa adesso perché non so cosa vuole il cliente.
Magari quando finisce vuole poter tornare indietro, riascoltare ... in quel caso deve essere ancora attivo.
Non bActive a false, ma localAction a STOP ;) Così si mette in attesa di un nuovo comando.

Riguardo a Boost è una libreria che per buona parte verrà adottata nella nuova libreria standard del futuro standard C++ (ad esempio per i thread e per le stringhe unicode).
A questo punto però la libreria boost si sovrappone a parte delle QT (per la sincronizzazione e la creazione dei thread)...imho non ci sta molto bene. O usi una tipologia o l'altra.
Per la comunicazione la boost ti permette di usare diverse cose, sia su shared memory (che suppongo tu non possa usare se i processi non sono stati creati tutti dallo stesso processo padre) o ad esempio named_semaphore, named_mutex e named_condition (puoi quindi riportare il tutto a quanto ti avevo fatto vedere qualche post fa per il rendezvous).

trallallero
29-01-2009, 14:24
Non bActive a false, ma localAction a STOP ;) Così si mette in attesa di un nuovo comando.
Apperò :D

Riguardo a Boost è una libreria che per buona parte verrà adottata nella nuova libreria standard del futuro standard C++ (ad esempio per i thread e per le stringhe unicode).
Si la conosco e la uso già per altre cose.

A questo punto però la libreria boost si sovrappone a parte delle QT (per la sincronizzazione e la creazione dei thread)...imho non ci sta molto bene. O usi una tipologia o l'altra.
Uhm ... giusta osservazione.

Per la comunicazione la boost ti permette di usare diverse cose, sia su shared memory (che suppongo tu non possa usare se i processi non sono stati creati tutti dallo stesso processo padre) o ad esempio named_semaphore, named_mutex e named_condition (puoi quindi riportare il tutto a quanto ti avevo fatto vedere qualche post fa per il rendezvous).
Ok, me lo studio un pò allora, grazie :)

trallallero
30-01-2009, 13:32
Ma usi fork per creare i processi ? In tal caso le possibilità sono multiple. Shared memory (shmget etc), pipe (comando pipe). Mentre se i processi sono creati in maniera diversa e non condividono la stessa immagine in memoria allora puoi usare named pipe (mkfifo) o i socket.

Mi sa che faccio partire un 3d che lancia 15 QProcess. Adesso c'è solo da capire come comunicare con loro.

cionci
30-01-2009, 14:54
Mi sa che faccio partire un 3d che lancia 15 QProcess. Adesso c'è solo da capire come comunicare con loro.
Mi dispiace, ma non ho idea se c'è o meno modo di far comunicare i QProcess, altre ai socket e alle chiamate di sistema chiaramente

trallallero
30-01-2009, 14:59
Mi dispiace, ma non ho idea se c'è o meno modo di far comunicare i QProcess, altre ai socket e alle chiamate di sistema chiaramente

C'`il modulo DBUS della QT ma non c'ho capito 'na ceppa :D
Il problema è che non si trovano esempi in rete ...

cionci
30-01-2009, 15:05
C'`il modulo DBUS della QT ma non c'ho capito 'na ceppa :D
Il problema è che non si trovano esempi in rete ...
Allora usa i socket ;)

trallallero
30-01-2009, 15:10
Allora usa i socket ;)

A sto punto mi sa di si, sono esausto a furia di cercare in rete :coffee:

trallallero
02-02-2009, 08:48
Ariecchime :stordita:

Adesso devo integrare il codice playback/record nel nuovo client mumble quindi non posso pensare alla sincronizzazione ma lo scenario sarà il seguente:

http://lh3.ggpht.com/_HH5byQu0gx4/SYayspO-eoI/AAAAAAAAAPQ/GYmd_4BLtlM/play.jpg

Logic è il sistema che ho fatto io tempo fa, gestisce la logica di mille mila pulsantini, led, comunicazioni, etc e sarà il processo che invierà i comandi start/stop/etc al server.

Il server comunica con i clients via tcp per i comandi (start/stop/etc) e udp per l'audio.

I clients sono processi che non si conoscono perchè vengono lanciati da script o linea di comando quindi impossile pensare ad un thread che li crea con fork o simile.

In pratica abbiamo solo un tick che il server invia e al quale i clients si dovranno allineare.



Per adesso aggiorno mumble col nuovo codice per fare il record e playback, poi dovrò pensare alla sincronizzazione dei processi.
Se hai qualche idea ... non essere timido :D

cionci
02-02-2009, 09:20
In pratica abbiamo solo un tick che il server invia e al quale i clients si dovranno allineare.
:read:

Non ti basta ?

trallallero
02-02-2009, 09:32
:read:

Non ti basta ?

ehm ... si ma come ? :stordita:

cionci
02-02-2009, 09:35
ehm ... si ma come ? :stordita:
Ogni quanto invia il tick ed ogni quanto dovresti invece processare i dati ?
Se i client si mettono in attesa del tick per "svegliarsi" allora si sincronizzano automaticamente.

trallallero
02-02-2009, 09:44
Ogni quanto invia il tick ed ogni quanto dovresti invece processare i dati ?
Se i client si mettono in attesa del tick per "svegliarsi" allora si sincronizzano automaticamente.

Penso un tick al secondo.

Come fanno a mettersi in attesa del tick ? sono un pò stordito stamani ma non riesco ad immaginare una lettura da file a tempo ...

cionci
02-02-2009, 09:47
Certo un tick al secondo è un po' pochino.
Il tick viene inviato insieme ai dati o su un altra porta ? Se non viene inviato sulla stessa porta dei dati allora basterebbe un receive bloccante.

trallallero
02-02-2009, 09:51
Certo un tick al secondo è un po' pochino.
Il tick viene inviato insieme ai dati o su un altra porta ? Se non viene inviato sulla stessa porta dei dati allora basterebbe un receive bloccante.

:doh: sono decisamente stordito

Pensavo ad un secondo per ridurre il traffico di rete, 15 clients sono tantini ...

grazie della dritta ;)

trallallero
12-02-2009, 14:56
cucù :D

Allora, sfrutto questo 3d tanto fa sempre parte dello stesso progetto.

Non so se ti ricordi la logica che ho dovuto disegnare/implemantare tempo fa (mi hai aiutato anche li - parlo direttamente con te tanto mi caghi solo tu :p ).

Quì c'è il link all'immagine (troppo grande, non la posto) http://lh3.ggpht.com/_HH5byQu0gx4/SSLQ7L_YMKI/AAAAAAAAAJg/JCjsItR1M5c/s800/a.jpg
dei pannelli e pulsantini vari della logica, giusto per far capire la sua mole.

Bene, il problema è il seguente:
quando i militari vogliono posizionare la simulazione ad un determinato secondo nel tempo, oltre a settare i wave files (a proposito, mi resta da sincronizzare ma per il resto funge tutto, grazie di nuovo ;)) devo fare in modo che la logica sia esattamente come lo era in quel dato momento.

La logica riceve un input da un hwserver (pulsante premuto), fa i suoi calcoli e risponde con un "accendi pusanti/led + apri comunicazione" o "spegni e chiudi" etc.
Ma ad ogni messaggio potrebbe settare n mila stati di tutti i suoi oggetti. (la sua stuttura è: unit -> panel -> device -> element (a piramide)

Un'idea potrebbe essere di registare tutti i messaggi e poi rieseguirli dall'inizio fino al dato momento, ma visto che la simulazione dura 4 ore non mi sembra una buona idea.

C'è qualche diavoleria che possa aiutare ? devo modificare tutte le classi in modo da poter settare gli stati da "fuori" ???

PS: non potevano dirmelo prima almeno avrei preparato il sistema a questo uso ?
odio mettere le mani nel "kernel" del sistema :muro:

trallallero
12-02-2009, 15:12
Fumare fà male ma aiuta a comunicare :D

Durante una pausa sigaretta mi hanno cosigliato questo: http://www.boost.org/doc/libs/1_38_0/libs/serialization/doc/index.html

cionci
12-02-2009, 15:15
Faccio un esempio, poi magari sarai te a perfezionarlo.
Ogni X minuti (ad esempio 5), scrivi lo stato completo del sistema su un file. Da quel momento alla scadenza degli X minuti successivi scrivi solo le varizazioni di stato.
In questo modo per recuperare lo stato vai a prendere l'ultimo stato completo precedente e poi fai il playback degli altri eventi fino al momento interessato.

cionci
12-02-2009, 15:16
La serializzazione va benissimo per implementare quello che vuoi, ma in ogni caso serializzando un oggetto hai il suo stato completo ;) Non puoi salvare lo stato completo ad ogni eventi...è un po' troppo dispendioso ;)

trallallero
12-02-2009, 15:26
La serializzazione va benissimo per implementare quello che vuoi, ma in ogni caso serializzando un oggetto hai il suo stato completo ;) Non puoi salvare lo stato completo ad ogni eventi...è un po' troppo dispendioso ;)

Cioè ? vuoi dire che lo stato completo è tutto l'oggetto della classe e non solo certe variabili ?
Comunque potrei creare un 3d ad hoc.

Mi piacerebbe serializzare ogni secondo visto che l'unità di misura per i wave files è il secondo :stordita:

trallallero
12-02-2009, 15:27
Faccio un esempio, poi magari sarai te a perfezionarlo.
Ogni X minuti (ad esempio 5), scrivi lo stato completo del sistema su un file. Da quel momento alla scadenza degli X minuti successivi scrivi solo le varizazioni di stato.
In questo modo per recuperare lo stato vai a prendere l'ultimo stato completo precedente e poi fai il playback degli altri eventi fino al momento interessato.

uhm ... solo le variazioni ... ottimo :)

cionci
12-02-2009, 16:13
Però potresti fare anche così: serializzi tutto il sistema, mettiamo, una volta al minuto. Per gli eventi che occorrono fra i due stati completi serializzi solo gli oggetti che cambiano stato ;)
Dovrebbe essere più semplice.

trallallero
12-02-2009, 18:13
Però potresti fare anche così: serializzi tutto il sistema, mettiamo, una volta al minuto. Per gli eventi che occorrono fra i due stati completi serializzi solo gli oggetti che cambiano stato ;)
Dovrebbe essere più semplice.

Beh si, anche. Adesso che so che si può serializzare qualcosa mi inventerò, prima rancolavo nel buio.
Diciamo che non è una priorità, prima devo sbrigare altri piccoli dettagli (sincronizzazione, messaggi TCP - play/stop ...) poi passerò agli snapshot.

comunque ... grande boost! ha tutto! :eek:

grazie :)