CREAZIONE DI UN SERVIZIO WINDOWS CON VISUAL STUDIO
In un sistema operativo Windows, un
servizio è un’entità, a cui è associato un eseguibile, che effettua operazioni
senza l’intervento di un utente specifico, perché il servizio “appartiene” al
sistema operativo.
Solitamente è utilizzato per
operazioni in cui non deve essere richiesto l’intervento dell’utente.
Ma vediamo come creare un servizio
windows con Visual Studio.
Visual Studio solitamente ha tra i
suoi Modelli di progetto anche il Servizio Windows, comunque è possibile creare
direttamente una classe che implementa la classe base System.ServiceProcess.ServiceBase. Questa
classe contiene un metodo astratto OnStart(string[] args) , che verrà chiamato
quando il servizio viene avviato.
Purtroppo però non è possibile
eseguire il debug su Visual Studio come eseguiamo il debug con altri programmi
che presentano il metodo main. Per provare il nostro programma dobbiamo
compilare l’eseguibile exe che per funzionare dovrà essere installato sul
sistema. Ecco che la sovrastruttura .NET ci agevola il tutto aggiungendo un
installer al nostro progetto.
Un Installer per un servizio non è
altro che una classe che eredita da una classe della piattaforma .NET i cui
metodi verranno chiamati da un apposito tool (InstallUtil) per installare il
servizio.
Ecco i due modi possibili per aggiungere un
installer alla nostra classe:
1.Apriamo la
nostra classe in Design Mode, e cliccando col tasto destro su uno spazio vuoto,
scegliamo “Add Installer”
2.Aggiungendo
una classe che eredita daSystem.Configuration.Install.Installer
Seguendo il secondo metodo,
aggiungiamo manualmente una classe e la facciamo ereditare daSystem.Configuration.Install.Installer.
La prima proprietà è l’installer relativo al processo che verrà lanciato dal nostro
servizio, mentre la secondo è l’installer relativo al processo vero e proprio.
Impostiamo i campi dell’oggetto serviceProcessInstaller1:
·Parent: è il nome della classe di tipo System.Configuration.Install.Installerche
contiene il serviceInstaller
·Account: il tipo di “privilegio” che avrà il nostro
processo. Sono 4 i tipi:
oLocalService:
Utente con i più bassi privilegi
oNetworkService:
Medio privilegio
oLocalSystem:
Utente coi più alti privilegi
oUser: Avvio
del servizio con un utente in particolare
Per il
nostro esempio, scegliamo il secondo, quindi this.serviceProcessInstaller1.Account
= System.ServiceProcess.ServiceAccount.LocalSystem;
Impostiamo invece quelli dell’oggetto serviceInstaller1:
·L’unico campo rilevante è il ServiceName, ovvero il
nome del nostro servizio, quello che comparirà tra i servizi del tool mmc.exe
Adesso che il nostro servizio base è impostato, aggiungiamo
un qualcosa che ci dice che il nostro servizio funziona, altrimenti lo vedremo
con lo stato “in esecuzione” non trovando nessuna evidenza del fatto che stia
funzionando
APPLICAZIONE: AGGIUNTA DI UN TIMER
Aggiungiamo una proprietà di tipo System.Timers.Timeralla nostra
classe relativa al servizio.
Facendo un override del metodo OnStart(string[] args), che
verrà eseguito non appena viene avviato il processo, aggiungiamo il codice dove
inizializziamo il nostro timer:
protectedoverridevoid OnStart(string[] args)
{
timer1 = newTimer(5000);
timer1.Elapsed += Tick;
timer1.Enabled = true;
timer1.Start();
}
Il nostro è quindi partito, eseguendo il metodo Tick ogni
5000 millisecondi.
Nel metodo Tick metteremo del codice che scrive un file di
testo nella root del nostro hard disk. E’ importante che per fare questo, il
tipo di servizio venga lanciato coi privilegi di LocalSystem, come detto sopra.
StreamWriter t = newStreamWriter("C:\\DMTService_log.txt", true);
t.WriteLine(DateTime.Now + "-evento
tick");
t.Close();
}
Adesso che il nostro servizio è pronto, compiliamo il tutto e
passiamo all’installazione del servizio.
INSTALLAZIONE DEL SERVIZIO
L’installazione del servizio verrà fatta col metodo
installutil.exe, un tool della piattaforma che troviamo nella directory della
nostra installazione .Net.
Per esempio, aprendo cmd.exe, scriviamo sulla console:
Avevo in cantina una vecchia xbox, la prima serie. Essendo obsoleta, ho pensato di recuperarne almeno il sempre valido joystick, o gamepad che dir si voglia.
Ha 2 analogici, 2 triggers, 1 d-pad e 8 tasti: il gamepad della xbox 360 (nativamente supportato su windows, e con presa USB) ha solo un tasto in più.
Il modello in questione è il controller s, il secondo gamepad usato per questa console
Questo gamepad ha una presa proprietaria della Microsoft e credo che non appartenga a nessun standard, almeno stando alle informazioni che sono riuscito a racimolare sul web.
Il vantaggio è che questa presa è assolutamente analoga a quella USB: in poche parole, non basta essere un elettrotecnico per trasformare questa presa in una USB e quindi, collegarla al PC.
Infatti sia la presa USB che la presa del gamepad hanno lo stesso numero di fili aventi lo stesso colore (4 fili, in verità il gamepad ne ha uno in
La presa del gamepad
più, il giallo, che non viene utilizzato però). In fin dei conti, basta prendere un vecchio cavo USB e recidere in due sia quest'ultimo che il filo del gamepad, quindi collegare i fili; se si è bravi si può saldare ogni singolo filo e isolare singolarmente i quattro fili.
Fatto questo, collegare il gamepad al PC e Windows riconoscerà una nuova periferica, ma fallirà nel trovare i driver appropriati. Allora ecco che un nerd della rete è venuto in aiuto: ha sviluppato i driver necessari per far funzionare e riconoscere il gamepad al PC come una
I fili collegati
periferica di gioco. Se avete un sistema a 32 bit potete scaricare da qui l'installer (scegliete l'ultima versione) che installerà tutto automaticamente, riavviate e il gioco è fatto.
Se invece avete un sistema a 64bit questi driver, compilati per 32bit, non funzioneranno. Ma ecco che un altro bravo nerd della rete è venuto di nuovo in aiuto e ha compilato i driver anche per i sistemi a 64bit. Vi illustro in alcuni passaggi quello che ho fatto:
Da questa pagina ho copiato sugli appunti i caratteri strani che stanno tra "begin" e "end" (compresi), dunque ho aperto Notepad++ (dovrebbe andar bene anche il blocco note, ma se usate ancora quest'ultimo siete sfigati), incollato il tutto e salvato come file avente estensione .7z
Ho scaricato un encoder/decoder per decodificare il tutto da qui ed estratto il file UUDECODE.EXE sulla stessa cartella del file .7z
Ho aperto il prompt (dal menù Start, scrivere cmd e premere invio), mi sono spostato sulla cartella (cd "percorso cartella) e scritto UUDECODE <nomefile.7z> (con il nome del file .7z che avete salvato al punto 1) a premuto invio
Poi ho aperto il file xbcd_108.7z che si è creato (l'ho aperto con 7zip, ma va bene anche winrar) ed estratto il contenuto in una cartella.
Sono andato nel Pannello di Controllo->Sistema->Gestione dispositivi e individuato la periferica non riconosciuta (contrassegnata da un punto esclamativo su sfondo giallo), dunque cliccato con tasto destro->Proprietà, nella finestra sono andato nella sezione "Driver" e cliccato su "Aggiorna driver..." e poi "Cerca il software del driver nel computer", dunque ho scelto la sottocartella "x64" della cartella dove ho scompattato xbcd_108.7z
Dopo avere atteso un po', Windows ci chiede se siamo sicuri di voler installare un software senza firma; scegliamo si.
I sistemi Windows a 64bit controllano a ogni avvio se ci sono driver senza firma digitale, in tal caso li blocca e ne impedisce l'esecuzione. In poche parole,allo stato attuale, il gamepad ancora non vi funziona. Se voi avete un modo di mettere la firma digitale bene, altrimenti dovrete eludere il succitato controllo. All'avvio di Windows, premete continuamente F8 e scegliere l'ultima opzione, quella relativa al controllo dei driver non firmati. Questo farà si che all'avvio non verranno controllati i driver e finalmente il nostro gamepad funzionerà. Andate nel pannello di controllo e cercate "gioco", dovreste trovare "Configura dispositivi di gioco USB". Da lì dovreste vedere se il gamepad funziona bene.
In aggiuntiva, dovrete installare il software che gestisce le periferiche di gioco, cioè un software che sfrutta al meglio le potenzialità di questo gamepad con cui possiamo associare le funzioni di gioco a ogni tasto del gamepad; all'inizio io ho usato il molto valido Pinnacle Game Profiler, ma vista la sua natura shareware (ormai mi è scaduto, comunque il prezzo è molto basso) sono passato a una valida alternativa freeware: Xpadder. Potrete seguire le istruzioni di questo video per la configurazione.
Tempo fa ho scritto un post che descriveva il metodo per calcolare il determinante di una matrice in maniera ricorsiva (metodo di Laplace) e mostravo la funzione creata per C++ per calcolare il determinante data una matrice quadrata di un qualsiasi ordine.
Ho creato un'interfaccia grafica scritta con GTK+ dove si può comodamente inserire la matrice di cui si vuole calcolare il determinante. Per ora il programma è compilato con windows perché in questo momento non ho un linux a portata di mano, quindi l'eseguibile che potrete scaricare è per windows.
Il programma inizia con un prompt, chiedendo all'utente di inserire l'ordine della matrice:
subito dopo, inserendo un ordine di 3 per esempio, avremo la schermata:
in cui potremmo inserire i valori e calcolare il determinante:
Il programma non è limitato a un certo ordine, teoricamente si può inserire qualsiasi ordine, salvo tempi che si allungano per via dell'elaborazione prima della parte grafica, poi (più veloce) la parte computazionale. I problemi derivati dall'uso di GTK+ su windows è che queste librerie sono nate in ambiente linux, e il porting per windows è complesso e macchinoso rispetto altre librerie grafiche. Ma io sono affezionato a Ubuntu e Gnome :)
estraete i files in una cartella e lanciate "Determinante.bat", il quale lancerà a sua volta "Determinante.exe" contenuto nella cartella "lib". Ho dovuto fare questo trucchetto perché il programma per funzionare ha bisogno delle librerie GTK+.
Preso dalla nostalgia dei primi anni di programmazione sul vecchio Borland Pascal (e il suo fantastico IDE in modalità testo in DOS), ho pensato di ricreare la vecchia libreria "crt" della Borland sul C++, per la console di Windows. Ho creato una classe "Console", che scrive e legge sullo stdin e stout e non faccio uso delle librerie standard del C++ (quindi è una classe windows-dipendente), fornendo alcuni scarni metodi per un minimo di I/O. Invero faccio uso di una libreria standard del c/c++ (stdarg) per consentire alla funzione printf (scritta da me) di ricevere un numero indefinito di parametri. Header della classe Console:
Nota bene: libreria fortemente ancorata a Windows, senza il quale non può funzionare. Qui potete leggere la documentazione, mentre da qui potete scaricare i file sorgente.
Per completare il precedente post, ecco questa parte dedicata al codice.
Il “Risolutore solitario Klondike” è stato scritto in C++, con alcune
parti scritte in C (è questa la potenza e versatilità del C++…mantenere la
compatibilità col caro vecchio C), soprattutto quelle parti di codice inerenti
all’interfaccia grafica.
La collezione di classi principale è in C++ standard quindi OS
indipendente, le parti di codice della GUI sono molto limitate. Quindi il
programma è stato pensato per funzionare su Windows (Win32) e
Linux(GTK+/Gnome).
Analizziamo prima il cuore del programma, ovvero la collezione di classi
OS indipendente che creano la parte “logica” dell’applicazione,
Comincio a presentare la gerarchia, e poi analizzo le classi una per
una.
e invece questa è la classificazione fatta da me dei gruppi di carte che
sono sul tavolo:
p1,p2,p3 e p4 sono di tipo Pila.
master è di tipo MazzoMaster.
terra è di tipo GruppoDiCarte.
s2,s3,s4,s5,s6 ed s7(ovvero le
carte sotto le scale, coperte) sono di tipo GruppoDiCarte.
g1,g2,g3,g4,g5,g6,g7 (ovvero le scale) sono di tipo GruppoOrdinato.
La classe fondamentale di cui si servono tutte le altre è Carta, di cui
riporto il file "Carta.h" a cui appartiene:
#pragma once
#include
#include
using namespace std;
enum Seme {
CUORI,QUADRI,PICCHE,FIORI,
};
enum Colore {
ROSSO,NERO,
};
class Carta {
friend ostream& operator<<(ostream&,Carta&);
public:
Carta(unsigned int,Seme,string);
Carta(Carta&);
unsigned int getValore() const;
Seme getSeme() const;
Colore getColore() const;
void stampa() const;
string getResource() const;
private:
unsigned int valore;
Seme seme;
string risorsa;
string getStringaSeme() const;
};
La classe presenta due costruttori, uno che crea la carta con il suo valore e il suo seme, e un altro che è un costruttore di copie (che comunque potrei togliere perché non ne faccio uso).
Da notare l'importante metodo string getResource() const; che restituisce il percorso del file che contiene l'immagine associata alla carta, scelta appositamente in base al valore e il seme. L'overloading del'operatore << non fa altro che passare una sua stringa rappresentativa a uno stream (per esempio "2 di cuori").
Adesso presento la classe GruppoDiCarte che è una classe quasi-astratta, che rappresenta un gruppo generico di carte messe
alla rinfusa.
Da notare già quanti metodi virtuali ho inserito. Sono dei metodi "vuoti", potevo anche farli virtuali puri, ma poi non mi avrebbe permesso di instanziare oggetti dalla classe. Ad ogni modo questi metodi verranno chiamati solamente dalle classi figlie.
Presenta due costruttori, uno che crea un gruppo di carte con 0 elementi (vuoto) e un altro che crea un gruppo di carte con un numero determinato di elementi, che saranno ovviamente inizializzati al valore delle carte tramite il metodo void setCarta(unsigned int);
Poi ci sono i metodi void mescola(); e virtual void ordina(); inseriti per completezza, visto che non ne faccio uso nel programma. Per adesso tralasciamo i metodi virtual bool attaccaGruppo(GruppoOrdinato*); virtual bool attacca(Carta*); che saranno specializzati nelle classi figlie.
Particolare importanza hanno i metodi Carta* pop(); void push(Carta*); e Carta* vediCarta(); visto che saranno utilizzati frequentemente in tutto il programma. push(Carta*) aggiunge una carta al mazzetto, mentre pop() la preleva, invece vediCarta() mostra la carta che si trova in cima alla pila (infatti ogni gruppo di carte, eccetto alcuni, viene trattato come se fosse una struttura di tipo LIFO), senza modificare il mazzetto, ovvero senza prelevarla.
Una regola categorica per il corretto uso di queste classi è che ad ogni istruzione pop() eseguita su qualsiasi gruppo di carte, deve seguire una istruzione push() su un altro gruppo di carte, per conservare il numero totale di carte. Infatti se tolgo una carta a un gruppo senza inserirla in un altro gruppo, succede che perdo quella carta, scalando di uno il numero totale di carte. Un classico uso che ne faccio è questo:
supponiamo un gruppo di carte di nome "g1" e una ltro di nome "g2", e che io voglia passare una carta da g2 a g1.
g1.push(g2.pop());
oppure, verificando se effettivamente, stando alle regole del gioco, posso eseguire la suddetta operazione:
Carta* c = g2.vediCarta(); //guardo la carta
if (c==NULL) //se il mazzo è vuoto ritorno
return false;
bool result = g1.attacca(c); //verifico se posso fare l'operazione
if (result) { // in caso affermativo...
g2.pop(); //prelevo la carta da g2, visto che l'ho già attaccata a g1
return true;
}
Adesso analizziamo le classi che ereditano da GruppoDiCarte, ovvero GruppoOrdinato,MazzoMaster,Pila,Mazzo.
Analizziamo prima mazzo, che rappresenta un mazzo intero di 52 carte.
#pragma once
#include "GruppoDiCarte.h"
#include
using namespace std;
class Mazzo : public GruppoDiCarte {
public:
Mazzo();
void ordina();
private:
string toString(int);
};
La classe (che ricordiamo eredita da GruppoDiCarte) non fa altro che creare un gruppo di carte di 52 elementi e mescolarli. Riporto il costruttore di questa classe che crea il mazzo e simula il mescolamento delle carte:
#include "Mazzo.h"
#include
#include
Mazzo::Mazzo() {
nCarte = 52;
for (int i=0;i<13;i++){
carte[i] = new Carta(i+1,CUORI,"carte\\"+toString(i+1)+"c.bmp");
carte[i+13] = new Carta(i+1,QUADRI,"carte\\"+toString(i+1)+"q.bmp");
carte[i+26] = new Carta(i+1,PICCHE,"carte\\"+toString(i+1)+"p.bmp");
carte[i+39] = new Carta(i+1,FIORI,"carte\\"+toString(i+1)+"f.bmp");
}
/* mescolo le carte */
srand(time(NULL));
int c;
int d;
Carta* app; //carta di "appoggio"
for (int i=0;i<208;i++){
c = rand() % 52;
d = rand() % 52;
app = carte[c];
carte [c] = carte[d];
carte [d] = app;
}
}
Passiamo a MazzoMaster, ovvero il mazzo posto in alto a sinistra:
#pragma once
#include "GruppoDiCarte.h"
class MazzoMaster : public GruppoDiCarte {
public:
Carta* daiCarta();
void setTerra(GruppoDiCarte*);
private:
GruppoDiCarte* terra;
};
che aggiunge i metodi Carta* daiCarta(); e void setTerra(GruppoDiCarte*); dove il primo serve per scoprire una carta e il secondo serve per passare il gruppo di carte già scoperte alla classe, in modo che se MazzoMaster si esaurisce, "capovolga" le carte che ci sono a terra.
Passiamo ancora a Pila (il nome non ha il noto significato in informatica, ma non sapevo che nome scegliere). Le pile sul tavolo sono quattro, si trovano in alto a destra e raggruppano le carte di uno stesso seme in ordine crescente.
#pragma once
#include "GruppoDiCarte.h"
#include
#include
class Pila : public GruppoDiCarte {
friend ostream& operator<<(ostream&, Pila&);
public:
Pila();
bool attacca(Carta* c);
Seme getSeme();
Carta* getPrimo(){return NULL;}
bool attaccaGruppo(GruppoOrdinato*){return false;}
private:
Seme seme;
};
bool attacca(Carta* c); è un metodo specializzato che è virtual della classe madre, e restituisce true se la carta può effettivamente essere attaccata al gruppo (stando alle regole del gioco), false altrimenti. Seme getSeme(); ritorna il seme del gruppo (visto che sono tutte carte di uno stesso seme),Carta* getPrimo() è una funzione ridefinita in modo che ritorni sempre NULL, e allo stesso modo ridefinisco bool attaccaGruppo() visto che non ha senso attaccare un gruppo di carte a questo, dato che andranno attaccate una per una.
Passiamo all'ultima classe che rappresenta un gruppo di carte, ovvero GruppoOrdinato, quei gruppi di carte scoperti (7 in tutto) e ordinati in ordine decrescenti a colori alterni:
#pragma once
#include "GruppoDiCarte.h"
#include
using namespace std;
class GruppoOrdinato : public GruppoDiCarte {
friend ostream& operator<<(ostream&, GruppoOrdinato&);
public:
GruppoOrdinato();
bool attacca(Carta*);
bool attaccaGruppo(GruppoOrdinato*);
Carta* getPrimo();
void setAttaccaPrimaVolta(); //uso riservato solamente a Tavolo
private:
bool attaccaPrimaVolta;
};
bool attacca(Carta*); e bool attaccaGruppo(GruppoOrdinato*); sappiamo già a cosa servono, bool attaccaPrimaVolta; è una variabile usata per "capire" se la carta che verrà attaccata è attaccata dal primo utilizzo o meno. void setAttaccaPrimaVolta(); resetta questa variabile al valore originario, pertanto non andrebbe utilizzata e il suo utilizzo è riservato alla classe Tavolo. Utilizzare questo metodo sarebbe deleterio e il programma non funzionerebbe come dovrebbe.
Rimane l'ultima classe da analizzare (oltre a Risolutore), ovvero Tavolo.
Praticamente il tavolo contiene i puntatore a tutti i gruppi di carte che sono presenti sul tavolo di un giocatore che gioca al solitario. Potete confrontare i membri di questa classe con l’immagine postata sopra.
Da notare i metodi info(), che stampa su console informazioni sulle carte che contiene ogni gruppo; checkGruppiOrdinati() che controlla se c’è qualche GruppoOrdinato che non contiene alcuna carta, e quindi deve essere scoperta una carta del GruppoDiCarte che sta proprio sotto lo stesso; contacarte() che conta il numero totale di carte, mi è stato utile in debug per evitare la “scomparsa” (o l’”apparizione”) di alcune carte.
Ultima classe fondamentale: il Risolutore, ovvero colui che secondo una logica ben precisa, muove le carte per vincere il giuoco.
#pragma once
#include "costanti.h"
#include "Tavolo.h"
#include "Interfaccia.h"
class InterfacciaWin;
class Risolutore {
public:
Risolutore(Tavolo* t,Interfaccia*);
bool mossa();
bool getVincita(){return vincita;}
private:
Tavolo* tavolo;
int chiamateMaster; //tiene conto del numero di volte che io prendo una carta dal master
int chiamateCiclo; //tiene conto del numero di volte che io chiamo mossa()
GruppoDiCarte* gruppi[11];
bool gruppi_pile();
bool terra_gruppi();
bool gruppi_gruppi();
bool vincita;
InterfacciaWin* interfaccia;
};
Il risolutore per giocare ha bisogno dell’oggetto Tavolo, e di un’interfaccia grafica, per comunicare gli spostamenti di carte.
Il cuore di questa classe è contenuta nel metodo mossa(), che chiama ripetutamente i tre metodi privati della stessa classe. Ok, abbiamo presentato la parte logica della classe, passiamo alla parte fisica, o meglio, alla parte visiva, quella che ci fa vedere che cosa sta facendo il computer:
INTERFACCIA GRAFICA
L’interfaccia grafica per ora è stata implementata per Win32, ma sto lavorando per la versione Linux (GTK+).
Come vedete, siccome è OS dipendente, ho provveduto a creare le direttive per il precompilatore in modo tale che se WINDOWS_APP è definito, compila per windows, altrimento se LINUX_APP è definito, compila per LINUX.
Nota che PER ORA, la funzione della classe Interfaccia è abbastanza futile, visto che potrei anche farne a meno. Ma l’ho messa nel caso mi servisse più avanti, quando continuerò a sviluppare questo programma permettendo di fare giocare anche un umano (potete infatti notare il metodo aggiungiRisolutore() , che come dice il nome, attiva la modalità automatica, altrimenti se non c’è il risolutore, vuol dire che sarà un umano a giocare. Stay connected!
Per finire…beh, ecco il codice sorgente per voi: sono dei fottutissimi file .h e .cpp, quindi compilabili con qualunque compilatore all’altezza. Nessun progetto, nessun vincolo di sistema, only raw material.
P.S. ci sono solo 2 file che non ho analizzato: “WinMain.c” e “Windows.h”, che sono troppo specifici del sistema operativo, ma comunuque lo trovate nel codice sorgente ;)
Fine del post chilometrico.
Alberto
Ho creato un risolutore automatico del famoso solitario di Windows, Apple, Gnome, KDE e chi più ne ha più ne metta. Scritto in C++ per Win32.
La mia intenzione era di crere un risolutore in grado di vincere ogni partita, ma mi sbagliavo di grosso; infatti sappiamo che dipende tutto dalle carte, e possono presentarsi situazioni (frequenti) in cui è impossibile vincere, come in questo caso, per esempio:
Le carte per "sbloccare" il gioco sono 2: il nove di picche e il quattro di fiori, che quarda caso si trovano sotto il dieci di cuori e il nove di quadri.
Comunque, il computer vince 1 volta su 5, e non sono poche!
Metto il file exe scaricabile per Windows, conm poche modifiche ne farò anche una versione per KDE. Le mosse le fa il computer ad ogni click del mouse (mi è stato utile per seguire le mosse del computer). Risoluzione consigliata: almeno 800x600 pixel.
Scusate le carte ritagliate male e prese da wikipedia XD. Nel prossimo post darò i dettagli tecnici di questo programma. Download
Alberto
Per continuare l'argomento intrapreso due settimane fa, ovvero Un semplice programma in C per leggere i file RIFF WAVE, passiamo adesso alla riproduzione di un'onda PCM. Intanto, a differenza del precedente post, dimentichiamoci della struttura del file WAV e di come aprirlo e suonarlo (rimanderò ancora più avanti l'argomento), ma preoccupiamoci di generare un'onda qualsiasi (per semplicità genereremo un'onda sinusoidale), e comunicare in qualche modo con la scheda audio per inviarle dei dati grezzi e quindi, sentire un suono dalle casse del proprio computer.
La piattaforma in cui faremo ciò è Windows, e useremo alcune sue API di basso livello per comunicare con la scheda audio (come al solito, rimanderò più avanti nel caso di Linux). Sebbene utilizzeremo le API di Win32, scordatevi comunque di vedere una finestra bella colorata con tanti tasti e caselle di input: quel che vedremo è una scarna finestra di console che si chiude non appena finisce di riprodurre il suono.
Il linguaggio è sempre il C.
Prima cosa essenziale: creiamoci una bella enumerazione che ci consente di manipolare le caratteristiche dell'onda senza andare a cercare fra il codice:
enum {
TIME = 5, // Durata della traccia (secondi)
CHANNELS = 1, // Numero dei canali
SAMPLE_RATE = 44100, // Frequenza di campionamento (Hz)
BITS_PER_SAMPLE = 8, // Bits per campione
BLOCK_ALIGN = CHANNELS * BITS_PER_SAMPLE / 8, // Allineamento del blocco
BYTES_PER_SECOND SAMPLE_RATE * BLOCK_ALIGN, // Frequenza di campionamento in Hz
SAMPLES = CHANNELS * SAMPLE_RATE * TIME, // Numero di campioni richiesti per la durata scelta
};
E' necessario che abbiate letto almeno il precedente post per capire cosa sia per esempio la frequenza di campionamento o le altre cose (se non lo sapete già).
PASSO 1: APRIRE IL DEVICE
Cominciamo a scrivere la nostra main e a creare le variabili necessarie per aprire il device.
int main() { HWAVEOUT hWaveOut; /* device handle */ WAVEFORMATEX wfx; /* una struttura che riassume le caratteristiche dell'audio */ MMRESULT result;/* per il valore di ritorno di waveOut */ char* block;/* il puntatore ai nostri campioni */ /* * inizializzo la struttura */ wfx.nSamplesPerSec = SAMPLE_RATE; wfx.wBitsPerSample = BITS_PER_SAMPLE; wfx.nChannels = CHANNELS; wfx.nBlockAlign = BLOCK_ALIGN; wfx.nAvgBytesPerSec = BYTES_PER_SECOND; wfx.cbSize = 0; /* lunghezza delle informazioni extra */ wfx.wFormatTag = WAVE_FORMAT_PCM /* onda PCM; */
if(waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, 0,0,CALLBACK_NULL) != MMSYSERR_NOERROR) { fprintf(stderr, "Impossibile aprire il device.\n"); return 1; } else printf("Il device è stato aperto correttamente.\n"); waveOutClose(hWaveOut); }
Questo programma per ora non fa niente, si limita ad aprire il device e a richiuderlo, ma comunque introduce molte caratteristiche interessanti. La funzione-chiave è waveOutOpen che accetta sei parametri:
LPHWAVEOUT phwo : in questo parametro passato per indirizzo, verrà "depositato" l'handle del dispositivo ((hWaveOut)
UINT uDeviceID : il valore viene impostato a "WAVE_MAPPER" in modo che la funzione scelga il dispositivo adatto a riprodurre il formato specificato
LPWAVEFORMATEX pwfx : il puntatore alla struttura, ovvero &wfx
DWORD dwCallback : indirizzo dell'eventuale funzione CALLBACK, che in questo contesto non ci serve (più avanti si). Lo impostiamo a 0
DWORD dwInstance : specifica l'istanza del processo. Lo impostiamo a 0
DWORD fdwOpen : specifica alcuni comportamenti della funzione (callback, flags) che per ora ignoreremo. Lo impostiamo a CALLBACK_NULL(nessuna chiamata di callback)
Benissimo, adesso che abbiamo aperto il device, dobbiamo trovare un modo per comunicare con lui. Ci arriveremo al passo 3, per ora creiamo l'onda.
PASSO 2 : CREIAMO UN'ONDA SINUSOIDALE
int tone = 440; float volume = 10.0f; float pulsazione = ( 2.0f * 3.14f / (float) SAMPLE_RATE / CHANNELS ) * (float) tone; int i; if ((block = (char*)malloc(SAMPLES))==NULL) return 1; for (i=0 ; i < SAMPLES; i++ ) block[i] = (char) ( cos( (float) i * oscillation ) * volume + 128.0f );
Questo pezzetto di codice alloca la memoria necessaria per memorizzare i campioni e la inizializza con i valori dell'onda.
Sappiamo che questa è l'equazione di un'onda sinusoidale di ampiezza A, pulsazione w e fase f. Possiamo dire lecitamente che l'ampiezza non è altro che il volume(intensità) dell'onda, la fase è nulla (non è altro che una traslazione orizzontale dell'onda che non ci interessa) e la pulsazione è una funzione della frequenza secondo questa equazione:
dove f questa volta è la frequenza dell'onda. Non ho studiato teoria dei segnali, ma secondo il teorema del campionamento di Nyquist c'è una condizione: la frequenza di campionamento deve essere almeno il doppio della massima frequenza dello spettro dell'onda. Lo spettro acustico che un essere umano percepisce va da 16 Hz a 20000 Hz più o meno, quindi come frequenza di campionamento per memorizzare i campioni viene scelta 44100 Hz, che è diciamo uno standard, utilizzata negli ormai comuni CD Audio. Sempre secondo il teorema del campionamento, l'equazione esatta dell'onda è questa:
dove n, un numero intero, rappresenta l'indice del campione (da 0 a SAMPLES nel codice sopra), Tc è il tempo che intercorre fra un campione e un altro, ossia l'inverso della frequenza di campionamento, fc è la frequenza di campionamento e f0 è la frequenza del segnale analogico.
In base a tutto ciò si spiega facilmente il codice sopra: float pulsazione = ( 2.0f * 3.14f / (float) SAMPLE_RATE / CHANNELS ) * (float) tone; definisce la pulsazione costante mentre il ciclo for memorizza campione per campione tramite l'espressione block[i] = (char) ( cos( (float) i * oscillation ) * volume + 128.0f ); .
Da notare che il codice che utilizzo utilizza il puntatore a char* per memorizzare i campioni. char è un byte sostanzialmente, quindi se modifico BITS_PER_SAMPLE nell'enumerazione e metto 16, per esempio come valore, si avrà uno stravolgimento totale. Meglio non modificarlo! In C++ potremmo creare una funzione che memorizza i dati passandogli un tipo tramite un template, in modo da ovviare alla cosa. Ma non preoccupiamoci, almeno per ora.
PASSO 3 : INVIAMO I DATI AL DEVICE
void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size) { WAVEHDR header; /* * Inizializza l'intestazione con la lunghezza dei dati * e naturalmente il puntatore. */ ZeroMemory(&header, sizeof(WAVEHDR)); header.dwBufferLength = size; header.lpData = block; /* * crea l'intestazione */ waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR)); /* * scrive i dati in maniera asincrona * (il controllo passerà subito al programma) */ waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR)); /* * Aspetta un po' */ Sleep(500); while(waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR)) == WAVERR_STILLPLAYING) Sleep(100); }
Una bella funzione. Il codice è tutto commentato quindi non ci dovrebbero essere problemi. In soldoni: preparo l'header, cancello tutta la memoria occupata dall'header, inizializzo l'header, creo l'intestazione, scrivo i dati, faccio un ciclo fin quando la riproduzione finisce, e chiudo.
Quindi, in definitiva, ecco il codice completo:
TIME = 5, // Durata della traccia (secondi) FMT_SIZE = 16, // Dimensione dell'Fmt Chunk CHANNELS = 1, // Numero dei canali SAMPLE_RATE = 44100, // Frequenza di campionamento (Hz) BITS_PER_SAMPLE = 8, // Bits per campione
BLOCK_ALIGN = CHANNELS * BITS_PER_SAMPLE / 8, // Allineamento del blocco BYTES_PER_SECOND = SAMPLE_RATE * BLOCK_ALIGN, // Frequenza di campionamento in Hz
SAMPLES = CHANNELS * SAMPLE_RATE * TIME, // Numero di campioni richiesti per la durata scelta };
void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size) { WAVEHDR header; /* * Inizializza l'intestazione con la lunghezza dei dati * e naturalmente il puntatore. */ ZeroMemory(&header, sizeof(WAVEHDR)); header.dwBufferLength = size; header.lpData = block; /* * crea l'intestazione */ waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR)); /* * scrive i dati in maniera asincrona * (il controllo passerà subito al programma) */ waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR)); /* * Aspetta un po' */ Sleep(500); while(waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR)) == WAVERR_STILLPLAYING) Sleep(100); }
int main() { HWAVEOUT hWaveOut; /* device handle */ WAVEFORMATEX wfx; /* una struttura che riassume le caratteristiche dell'audio */ MMRESULT result;/* per il valore di ritorno di waveOut */ char* block;/* il puntatore ai nostri campioni */ /* * inizializzo la struttura */ wfx.nSamplesPerSec = SAMPLE_RATE; wfx.wBitsPerSample = BITS_PER_SAMPLE; wfx.nChannels = CHANNELS; wfx.nBlockAlign = BLOCK_ALIGN; wfx.nAvgBytesPerSec = BYTES_PER_SECOND; wfx.cbSize = 0; /* lunghezza delle informazioni extra */ wfx.wFormatTag = WAVE_FORMAT_PCM /* onda PCM; */
if(waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, 0,0,CALLBACK_NULL) != MMSYSERR_NOERROR) { fprintf(stderr, "Impossibile aprire il device.\n"); return 1; } else printf("Il device è stato aperto correttamente.\n");
int tone = 440; //"La" centrale float volume = 10.0f; float pulsazione = ( 2.0f * 3.14f / (float) SAMPLE_RATE / CHANNELS ) * (float) tone; int i; if ((block = (char*)malloc(SAMPLES))==NULL) return 1; for (i=0 ; i < SAMPLES; i++ ) block[i] = (char) ( cos( (float) i * oscillation ) * volume + 128.0f ); } writeAudioBlock(hWaveOut, block, SAMPLES); waveOutClose(hWaveOut); }
Non dimentichiamoci di linkare al compilatore la libreria winmm altrimenti il linker ci da errore.
Per dubbi, problemi e chiarimenti commentate pure.
Alberto