2008.11_Radio w GNOME_[Programowanie].pdf

(731 KB) Pobierz
439033055 UNPDF
Programowanie
Programowanie/GNOME
Radio w GNOME
Marek Sawerwain
Jądro systemu Linux już od dawna oferuje wsparcie dla multimediów, począwszy od kart graicznych,
poprzez muzyczne, a także różnego rodzaju tunery telewizyjne i radiowe. Innym problemem jest
wsparcie producentów, przede wszystkim poprzez dostarczanie sterowników bądź dokumentacji do
swoich kart, szczególnie tunerów FM/TV – z tym niestety bywa różnie.
J ednak gdy karta jest obsługiwana, można zapytać,
cujemy go jak zazwyczaj za pomocą języka C, ale przy uży-
ciu nowego języka programowania, jakim jest VALA. Jest to
dość młody projekt, ale rozwija się dość dynamicznie.
Język VALA jest podobny do języka C#, ale w prze-
ciwieństwie do niego jest to język kompilowany, choć nie
wprost do assemblera , lecz do języka C. Mimo tego dodatko-
wego etapu programy napisane w języku VALA niemal za-
wsze są bardziej wydajne od C#.
Zastosowanie języka VALA zamiast C spowoduje, że
kod źródłowy naszego programu niewątpliwie będzie bar-
dziej obiektowy niż siermiężne C, które nie oferuje przecież
zbyt dużej czytelności w przypadku programowania obiek-
towego.
Użycie innego języka w miejsce C naturalnie nie zwala-
nia nas z obowiązku utworzenia interfejsu graicznego. Inter-
fejs ten utworzymy w programie GLADE. Może to być wer-
sja zarówno 2.X bądź nowsza 3.x, gdyż będziemy interfejs
wczytywać dynamicznie. VALA zapewnia łatwy dostęp do
funkcji obsługujących pliki w formacie glade .
Zestawianie wszystkich najważniejszych zdarzeń poka-
zuje schemat na Rysunku 1. Pierwsza czynność w naszym
programie to budowa interfejsu użytkownika, kolejny krok
Krok pierwszy – planowanie
Program, który opracujemy nie jest zbyt wielki, ale mi-
mo to warto zastanowić się, jakie problemy możemy na-
potkać, dlatego wykonanie schematu naszego programu
z pewnością się przyda. Pierwszy problem to naturalnie
obsługa tunera, gdzie podstawową funkcją jest zmiana
częstotliwości, na której słuchamy określonej stacji. Nasz
program będzie obsługiwał częstotliwość w zakresie od 87
do 108 Mhz.
Uzupełnieniem tej niewątpliwie podstawowej własności
jest badanie siły sygnału oraz sprawdzenie, czy odbierany sy-
gnał jest stereofoniczny. Dokładniejsze informacje związane
z obsługą tunera przedstawimy już w następnym punkcie.
Nasz program ma być też wygodny w użyciu, więc zo-
stanie napisany dla środowiska GNOME. Jednakże nie opra-
44
listopad 2008
czy trudnym zadaniem jest opracowanie nieskom-
plikowanego programu do obsługi np. tunera radio-
wego. Jak to często bywa, nie jest to trudne zadanie,
ponieważ podstawowy zestaw do obsługi tunera to zaledwie
kilka funkcji.
439033055.043.png 439033055.044.png 439033055.045.png
 
Programowanie
Programowanie/GNOME
to uzyskanie dostępu do tunera. Po tym eta-
pie wchodzimy w pętlę, w której reagujemy
na czynności podejmowane przez użytkowni-
ka, do których należą m. in. kliknięcie na przy-
cisk kończący pracę naszej aplikacji albo zmia-
na częstotliwości. Ważną akcją jest też samo-
dzielnie szukanie następnej bądź poprzedniej
stacji. Program zawiera także funkcję skano-
wania całego dostępnego pasma po kliknięciu
na przycisk Szukaj ( ang. Find ).
Program, który napiszemy nie jest duży,
ale kod źródłowy zostanie podzielony na dwie
części. Pierwszą część stanowią pliki tunerfm.c
oraz tunerfm.h zawierające funkcje do obsługi
tunera. Zostały ono celowo opracowane w ję-
zyku C, ponieważ z poziomu tego języka ła-
twiej korzystać np. z funkcji ioctl oraz plików
nagłówkowych jądra systemu, do których do-
stęp jest niezbędny, aby sterować tunerem ra-
diowym. Druga część kodu źródłowego to wła-
ściwy kod aplikacji napisany w języku VALA
– jest to plik o nazwie minituner.vala .
Wprowadzimy też dwie klasy. Pierwsza
z nich, RadioTuner, zgodnie ze swoją nazwą
będzie odpowiedzialna za dostęp do tunera ra-
diowego poprzez funkcje opracowane w języ-
ku C. Druga klasa, MiniTunerWindow, repre-
zentuje nasz program. W tej klasie znajduje się
statyczna oraz publiczna metoda main, to od tej
metody rozpoczyna się wykonywanie naszego
programu.
diowy nie oferuje dużej liczby funkcji oraz roz-
wiązania zastosowane w API dodatkowo uła-
twiają sterowanie tego typu urządzeniem.
Wszystkie podstawowe funkcje do obsłu-
gi tunera zostały zgromadzone w plikach tu-
nerfm.c oraz tunerfm.h . Należy też dołączyć
odpowiednie pliki nagłówkowe = najważniej-
sze są dwa pliki. Pierwszy z nich
#include <linux/videodev.h>
zawiera deinicje potrzebnych struktur do ob-
sługi tunera nie tylko radiowego, ale też tu-
nerów telewizyjnych. Można także dołączyć
drugi plik do obsługi karty dźwiękowej
Funkcje do obsługi tunera
Tworzenie funkcji, czy też całego API do obsłu-
gi sprzętu w większości przypadków nie jest ła-
twym zadaniem. Jednakże w przypadku Linuksa
sterowanie np. tunerem radiowym jest względ-
nie łatwe do zrealizowania. Ostatecznie tuner ra-
#include <sys/soundcard.h>
co pozwoli na np. sterowanie głośnością.
Pierwsza funkcja, którą trzeba opisać to
FMTuner_Init , przedstawiona na Listingu 1.
Uzyskanie dostępu do urządzenia realizuje-
my za pomocą funkcji open. Różnego rodza-
ju karty są reprezentowane przez pliki w ka-
talogu /dev . Domyślna nazwa tunera radiowe-
go to /dev/radio0 . Po udanym dostępie do
tego pliku uzyskujemy uchwyt (ang. handle )
reprezentujący urządzenie. Zastosowanie funk-
cji close powoduje naturalnie zamknięcie do-
stępu do pliku oraz w naszym przypadku – do
urządzenia.
Użycie funkcji open na pliku /dev/radio0
lub innym podanym przez użytkownika zała-
twia sprawę, jednakże trzeba uzyskać jeszcze
jedną istotną informację. Jest to współczyn-
nik, przez jaki należy podzielić lub pomnożyć
częstotliwość, aby uzyskać poprawną wartość.
Do komunikacji z tunerem stosujemy funkcję
ioctl , potrzebne informacje odczytamy wysy-
łając komunikat VIDIOCGTUNER. W ostat-
nim argumencie umieszczamy adres struktu-
ry, w której zostaną umieszczone wszystkie
informacje:
�����
�����������������
�����������
���������
�����������������
������������������
�����������
���������������������
������������������
����������������
��������������������
������
���
����������������
������
��������������
�����
ioctl (fd, VIDIOCGTUNER, &tuner_fm)
���
Odczytanie informacji sprowadza się do spraw-
dzenia, czy jeden z bitów (reprezentowany przez
deinicję VIDEO_TUNER_LOW ) w polu o nazwie
���
������������
���������
���
����������
�����������������
������
Rysunek 1. Schemat programu do obsługi tunera radiowego
Rysunek 2. Interfejs naszej aplikacji
www.lpmagazine.org
45
439033055.001.png 439033055.002.png 439033055.003.png 439033055.004.png 439033055.005.png 439033055.006.png 439033055.007.png 439033055.008.png 439033055.009.png 439033055.010.png 439033055.011.png 439033055.012.png 439033055.013.png 439033055.014.png 439033055.015.png 439033055.016.png 439033055.017.png 439033055.018.png 439033055.019.png
Programowanie
Programowanie/GNOME
lag jest odpowiednio ustawiony, przy czym jeśli
ioctl zwróci wartość mniejszą niż zero, to za-
kłada się, że podzielnik jest równy 16.
Zamknięcie dostępu do tunera to zadanie
dla funkcji FMTuner_Stop . Jednak w przeci-
wieństwie do funkcji FMTuner_Init jest ona
bardzo krótka, gdyż są to zaledwie dwie linie.
w przypadku tunera zapisana jako typ int, tym-
czasem argument funkcji FMTuner_SetFreq to
liczba zmiennoprzecinkowa. Odpowiedni wzór
z wykorzystaniem wcześniej odczytanego po-
dzielnika częstotliwości zapiszemy w odpo-
wiedni sposób:
Ustalenie nowej częstotliwości wykonamy za
pomocą funkcji ioctl oraz stałej VIDIOCSFREQ
– przedstawia się to następująco:
ioctl(fd, VIDIOCSFREQ, &int_freq);
Ważnym aspektem są potencjalne błędy. Li-
sting 2. zawiera pełną implementację funkcji
do zmiany częstotliwości. Przed zmianą często-
i f (fd >= 0) close(fd);
return 0;
int_freq = (frequency+1.0 /
32)*frequency_fact;
Właściwe zamknięcie wykonuje funkcja clo-
se , zamykająca dostęp do pliku urządzenia.
Plik tunerfm.c zawiera jeszcze kilka innych
funkcji, jak np. FMTuner_IsStereo sprawdza-
jącą, czy odbierany sygnał jest stereofoniczny.
Realizacja takiej funkcji nie jest trud-
nym zadaniem. W pierwszej kolejności two-
rzy strukturę, w której zostaną umieszczone da-
ne o tunerze, a do pola mode wpisujemy war-
tość -1, która oznacza, że sygnał nie jest ste-
reofoniczny:
Kilka informacji o języku VALA
Język VALA to jeden z najmłodszych projektów związanych
ze środowiskiem GNOME. Jest on bardzo podobny do języka C# i powstał głównie po to,
aby zaoferować programistom GNOME nowoczesny i obiektowy język programowania.
Dlatego też język VALA jest bardzo ściśle zintegrowany z biblioteką GLib , która stanowi
podstawę modelu obiektowego GNOME.
Język oferuje nowoczesne konstrukcje językowe dostępne również w Javie czy C#
oraz C++, czyli interfejsy, własności, sygnały, wyrażenia lambda, a nawet typy uogólnione
(odpowiednik template z języka C++). Autorom zależało też na wysokiej wydajności, two-
rzenie programów w C# z pewnością jest łatwiejsze niż np. w C, ale ostatecznie uzyskany
program, ze względu na obecność maszyny wirtualnej traci sporo na wydajności. Dlatego
autorzy postanowili, że programy w języku VALA będą tłumaczone na kod w języku C. Mi-
mo tego dodatkowego etapu podczas kompilacji, VALA oferuje wysoką wydajność, wyższą
niż programy pisane w C#.
Aktualnie dostępna wersja pakietu (w chwili pisania artykułu) to 0.3.5, ale ten niski nu-
mer jest nieco mylący – w rzeczywistości możemy już całkiem swobodne pisać normalne
oprogramowanie przy zastosowaniu tego języka. Pierwsza wersja stabilna powinna uka-
zać się w niedalekiej przyszłości, co z pewnością przyniesie z sobą większą popularność
tego języka programowania.
struct video_audio va;
va.mode=-1;
Odczytanie danych wykonamy za pomocą
funkcji ioctl:
ioctl(fd, VIDIOCGAUDIO, &va);
Ostatecznie sprawdzenie, czy odbierana sta-
cja nadaje sygnał stereofoniczny, sprowa-
dzić można do następującej instrukcji wa-
runkowej:
Listing 1. Uzyskanie dostępu do tunera radiowego
int FMTuner_Init ( char * dev ) {
int fd ;
struct video_tuner tuner_fm ;
if (va.mode == VIDEO_SOUND_STEREO) {
} else { }
if ( dev != NULL ) {
if (( fd = open ( dev , O_RDONLY )) < 0 )
return - 1 ;
}
else {
if (( fd = open (& default_device [ 0 ] , O_RDONLY )) < 0 )
return - 1 ;
}
Ustalanie częstotliwości oraz siła sygnału
Zmiana częstotliwości jest dla naszego progra-
mu bardzo ważna. Chcemy aby użytkownik
w programie samodzielnie przeszukiwał pa-
smo. Istotna jest również funkcja sprawdzają-
ca siłę sygnału. Zastosowanie prostej pętli oraz
sprawdzanie siły sygnału daje nam możliwość
automatycznego przeszukiwania całego pasma.
Tym problemem zajmiemy się jednak w dal-
szej części artykułu. Nagłówki funkcji są na-
stępujące:
tuner_fm . tuner = 0 ;
if ( ioctl ( fd , VIDIOCGTUNER , & tuner_fm ) < 0 )
frequency_fact = 16 ;
else {
if ( ( tuner_fm . lags & VIDEO_TUNER_LOW ) == 0 )
frequency_fact = 16 ;
else
frequency_fact = 16000 ;
}
int FMTuner_SetFreq(int fd, loat
frequency);
int FMTuner_GetSignal(int fd);
Ważna uwaga dotyczy argumentu frequency .
Będziemy podawać wartość częstotliwości w
megahercach, ale podaną wartość należy od-
powiednio zmienić. Wartość częstotliwości jest
return fd ;
}
46
listopad 2008
439033055.020.png 439033055.021.png 439033055.022.png 439033055.023.png
 
Programowanie
Programowanie/GNOME
tliwości upewniamy się, czy wartość uchwytu
hd jest większa niż zero, sprawdzana jest także
wartość argumentu frequency – czy znajduje
się w zakresie od 65 do 108 MHZ. Przy czym
nasz program oferuje dostęp do częstotliwości
w zakresie od 87.5 do 108 Mhz.
Gdy użytkownik naszego programu bę-
dzie zmieniał częstotliwość, to jakość sygna-
łu niewątpliwie będzie oceniał na ucho , ale
w trakcie automatycznego wyszukiwania stacji
należy korzystać z możliwości sprzętu, który
samodzielnie potrai ocenić siłę sygnału. Spo-
sób odczytania siły sygnału jest podobne do re-
alizacji np. funkcji sprawdzającej, czy sygnał
jest nadawany w STEREO. Rozpoczniemy od
utworzenia struktury z informacjami o aktual-
nym stanie tunera (za pomocą funkcji memset
wypełniamy całą strukturę zerami):
Listing 2. Ustalenie częstotliwości dla tunera radiowego
struct video_tuner vt;
memset(&vt, 0, sizeof(vt));
int FMTuner_SetFreq ( int fd , loat frequency ) {
int int_freq , out_v ;
Odczytanie informacje realizujemy identycznie
jak w poprzednich przypadkach, choć potrzeb-
ne informacje uzyskamy używając deinicji VI-
DIOCGTUNER.
if ( fd < 0 ) return - 1 ;
if (( frequency > 108 ) || ( frequency < 65 ))
return - 2 ;
ioctl (fd, VIDIOCGTUNER, &vt);
int_freq = ( frequency + 1.0 / 32 )* frequency_fact ;
out_v = ioctl ( fd , VIDIOCSFREQ , & int_freq );
Siła sygnału jest zapisana w polu signal struk-
tury vt. Jest to wartość szesnastobitowa. Brak
sygnału to zero, silny sygnał to np.: 32768, sy-
gnał o pełnej sile to 655536. Dlatego lepiej
byłoby wykorzystać np. wartości procentowe.
Konwersja na taki typ opisu siły sygnały wy-
maga tylko wykonania odpowiedniego dziele-
nia i mnożenia przez 100:
return out_v ;
}
Listing 3. Fragmenty konstruktora odpowiedzialnego za utworzenie interfejsu
construct {
this . xml = new Glade . XML ( "gr_mainwin.glade" , null , null );
MainWin = ( Gtk . Window ) this . xml . get_widget ( "MainWin" );
MainWin . destroy += Gtk . main_quit ;
(vt.signal/65536.0f)*100;
Klasa tunera w języku VALA
Funkcje obsługujące tuner przedstawione
w poprzednim akapicie można w sposób bez-
pośredni zastosować na poziomie języka VA-
LA. Wprowadzimy klasę RadioTuner, która
będzie oferować kilka metod do obsługi tune-
ra. Metody te naturalnie będą korzystać z funk-
cji napisanych w języku C znajdujących się
w pliku tunerfm.c. Należy tylko odpowiednio je
zadeklarować, np. funkcja z Listingu 1 na po-
ziom języka VALA jest wprowadzana w nastę-
pujący sposób:
ConigWin = ( Gtk . Window ) this . xml . get_widget ( "ConigWin" );
ConigWin . hide ();
var btn1 = ( Gtk . Button ) this . xml . get_widget ( "CloseWinBTN" );
btn1 . clicked += on_CloseWinBTN_clicked ;
...
var RadioStationList = ( Gtk . ComboBoxEntry ) this . xml . get_widget ( "RadioSta
tionList" );
list_stations_model = new Gtk . ListStore ( 2 , typeof ( string ) ,
typeof ( string ) );
RadioStationList . set_model ( list_stations_model );
RadioStationList . set_text_column ( 0 );
[CCode (cname="FMTuner_Init")]
private static int FMTuner_Init(string
dev);
var RadioStationsView = ( Gtk . TreeView ) this . xml . get_widget ( "RadioStation
sView" );
RadioStationsView . set_model ( list_stations_model );
RadioStationsView . insert_column_with_attributes (- 1 , "Station Name" ,
new Gtk . CellRendererText () , "text" , 0 , null );
...
Typ string jest odpowiednikiem typu char* ,
wprowadzenie funkcji FM_Tuner_SetFreq jest
bardzo podobne:
tuner_ctr = new RadioTuner ();
tuner_ctr . RadioTunerUp ();
[CCode (cname="FMTuner_SetFreq")]
private static int FMTuner_SetFreq(int
fd, loat frequency);
TuneScale . set_range ( tuner_ctr . begin_mhz , tuner_ctr . end_mhz );
TuneScale . set_digits ( 2 );
TuneScale . set_increments ( 0.05 , 0.1 );
mute = 0 ;
}
Sekcja CCod e jest potrzebna, aby podać orygi-
nalną nazwę funkcji, VALA będzie samodziel-
nie tłumaczyć nazwy. W ten sposób możemy
opracować API o bardziej obiektowym charak-
terze, bez względu na oryginalne nazwy funk-
cji. Wszystkie wprowadzane funkcje zostały
www.lpmagazine.org
47
439033055.024.png 439033055.025.png 439033055.026.png 439033055.027.png 439033055.028.png 439033055.029.png 439033055.030.png 439033055.031.png 439033055.032.png 439033055.033.png 439033055.034.png 439033055.035.png 439033055.036.png
 
Programowanie
Programowanie/GNOME
zadeklarowane jako statyczne oraz prywatne,
więc nie będą one dostępne dla użytkownika
klasy. Dlatego wprowadzimy metody, za po-
mocą których użytkownik sterował tunerem.
Ustalenie częstotliwości to zadanie dla meto-
dy SetFrequency:
kowe czynności związane z interfejsem użyt-
kownika zostały zgromadzone w konstruktorze
głównej klasy aplikacji (fragmenty kodu przed-
stawia Listing 3).
Jakiekolwiek czynności musi poprzedzić
proces wczytania pliku glade , z opisem inter-
fejsu:
modelu wymaga zastosowania słowa kluczo-
wego typeof informujące o zastosowanym
typie. Sama nazwa typu to niestety za mało,
aby utworzenie modelu danych przebiegło po-
prawnie.
list_stations_model = new
Gtk.ListStore(2, typeof(string),
typeof(string) );
public void SetFrequency(loat freq) {
FMTuner_SetFreq(handle, freq);
curr_mhz = freq;
}
this.xml = new Glade.XML ("gr_
mainwin.glade", null, null);
Tworzymy tylko dwa pola, w pierwszym znaj-
dzie się nazwa stacji, natomiast w drugim czę-
stotliwość, na której dana stacja nadaje. Doda-
wanie nowej kolumny do kontrolki ze spisem
stacji przedstawia się następująco:
Implementacja pozostałych metod jest rów-
nie krótka. Ważny jest również konstruktor
klasy RadioTuner. W konstruktorze ustalamy
przede wszystkim wartości początkowe kil-
ku zmiennych. Jest to uchwyt do urządzenia
(zmienna handle, wartość początkowa równa
jest -1), ścieżka dostępowa do tunera (zmien-
na device_name ), jej domyślna wartość to
/dev/radio0 . Natomiast, zakres pasma dla na-
szego odbiornika radiowego , oraz krok z jakim
będziemy przeszukiwać pasmo, jest ustalany za
pomocą trzech poniższych linii kodu:
Następnie możemy odczytać odniesienia do
poszczególnych widgetów, np: dostęp do kon-
trolki okna głównego uzyskamy w następują-
cy sposób:
MainWin = (Gtk.Window)this.xml.get_
widget("MainWin");
RadioStationsView.insert_column_with_
attributes (-1, "Station Name",
new Gtk.CellRendererText(),
"text", 0, null);
Bardzo wygodnie, względem oryginalnego API
GTK+, przedstawia się proces podłączania ob-
sługi sygnałów, np.: jeśli do zdarzenia destroy
dotyczącego okna chcemy podłączyć metodę
main_quit zamykającą całą aplikację, wystar-
czy napisać następujące wyrażenie:
Typ nowej kolumny tworzymy za pomocą sło-
wa kluczowego new, gdzie argumentem jest
potrzebny nam typ, dla uproszczenia przyjmie-
my, że będzie to Gtk.CellRendererText().
Pozostały kod jaki jest widoczny na Li-
stingu 3 dotyczy tunera radiowego. Tworzymy
obiekt tunera tuner_ctr , a następnie za po-
mocą metody RadioTunerUp , niejako włącza-
my tuner. Ostatnie linie kodu są odpowiedzial-
ne za ustalenie parametrów kontrolki TuneSca-
le , wbrew pozorom jest to dość istotny etap,
ponieważ za pomocą tej kontrolki użytkownik,
będzie ustalał częstotliwość pracy tunera.
begin_mhz = 87.5f;
inc_mhz = 0.20f;
end_mhz = 108.0f;
MainWin.destroy += Gtk.main_quit;
Budowa interfejsu
Po opisie problemów zawiązanych z tunerem
radiowym, trzeba zająć się interfejsem. Jedną
z zalet języka VALA jest fakt, iż odwzorowu-
je on wszystkie typowe zachowania biblioteki
GTK+ i GNOME. Dlatego, tworzenie interfej-
su jest typowe dla GTK+, i co ważne uwypu-
kla obiektowy charakter nowego języka oraz
bibliotek GTK+ i GNOME. Wszystkie począt-
Podobnie postępujemy w przypadku podłą-
czania obsługi sygnału clicked dla przycisków.
Nieco inaczej przedstawia się tworzenie mode-
lu danych dla listy. Model ten, wykorzystuje-
my do zbierania informacji o wykrytych stacji
w czasie skanowania. Sam proces tworzenie
Kompilacja programu
Kompilacja naszej aplikacji przebiega w dwóch etapach. Pierwszy krok, to kompilacja pliku
z funkcjami do obsługi tunera radiowego:
gcc -c tunerfm.c
Rezultatem jest plik obiektowy tunerfm.o , plik ten należy podać jako argument podczas
kompilacji programu w języku VALA. Całe polecenie przedstawia się następująco:
valac --pkg pango --pkg gtk+-2.0 --pkg libglade-2.0 --pkg gmodule-2.0 -o
minituner minituner.vala -X "tunerfm.o"
Rysunek 3. Projektowanie interfejsu w programie
GLADE
Za pomocą opcji --pkg określa się jakie pakiety będą stosowane w programie. Używa-
my standardowego zestawu dla programów pisanych w GTK+ i GLADE. Dołączenie pliku
obiektowego z funkcjami obsługi tunera zapewnia opcja -X.
Etapy pośrednie, jak np.: tłumaczenie do języka C, a następnie kompilacja takiego pliku
są nadzorowane przez polecenie valac . Naturalnie możliwe jest otrzymanie pliku w języku
C, aby dalej samodzielnie poddać go kompilacji, wydajemy wtedy następujące polecenie:
valac --pkg pango --pkg gtk+-2.0 --pkg libglade-2.0 --pkg gmodule-2.0 -C
minituner.vala
Rysunek 4. Główna strona poświęcona językowi VALA
Ogromną zaletą języka VALA jest fakt, iż programy pisane w tym języku nie wymagają do-
datkowych specjalnych bibliotek związanych z tym językiem. Powstały program, jeśli moż-
na tak powiedzieć, z punktu widzenia binarnego jest aplikacją opracowaną w języku C.
48
listopad 2008
439033055.037.png 439033055.038.png 439033055.039.png 439033055.040.png 439033055.041.png 439033055.042.png
 
Zgłoś jeśli naruszono regulamin