Po uruchomieniu w przeglądarce starych, mówiących szachów, zatęskniłem do programu czytającego ekran, który kiedyś towarzyszył mi na co dzień. Podstawowy problem, że SCR pracował przy pomocy zewnętrznego syntezatora sprzętowego… Czy uda się go zastąpić? Eksperyment się powiódł, ale zrobiła się z tego programistyczna podróż w czasie do ostatniej dekady ubiegłego wieku, przez takie zagadnienia jak Assembler, Turbo Pascal, przerwanie zegarowe, programy rezydentne, port szeregowy i równoległy… Sympatycznie było podróżować i to opisać – szczegóły poniżej, a szybki start dla niecierpliwych umieszczam na końcu wpisu.
Czytniki ekranu dla DOS
Czytniki ekranu dla MS-DOS pracowały w oparciu o pamięć karty graficznej trybu tekstowego, z której mogły pobierać informacje o tym jaki tekst i w jakich kolorach jest aktualnie wyświetlany. W Polsce popularne były trzy: Hal produkcji brytyjskiej firmy Dolphin, Readboard z Altix i właśnie SCR autorstwa Pana Ryszarda Czubkowskiego, opracowany na Polskiej Akademii Nauk, a dystrybuowany przez firmę E.C.E. Readboard korzystał z programowego syntezatora mowy, używanego w prezentowanych wcześniej mówiących szachach, Hal i SCR wykorzystywały syntezatory sprzętowe, podłączane do komputera przez port szeregowy.
Zaletą tego rozwiązania był fakt, że generowanie mowy nie obciążało procesora, bo tekst po wysłaniu do zewnętrznego urządzenia był w nim zamieniany na sztuczną mowę. SCR był napisany w Assemblerze, czyli języku maszynowym, dzięki czemu jego nieco wcześniejsza od prezentowanej wersja w wariancie bez wbudowanego trybu pomocy zajmowała tylko dwadzieścia kilka kilobajtów pamięci, a cała reszta mogła być używana przez uruchamiane programy. Hal zajmował – jeśli dobrze pamiętam – ponad 100 KB. Różnice są niebagatelne, jeśli wziąć pod uwagę, że pamięci podstawowej uruchomione programy i system operacyjny miały do dyspozycji w sumie 640 KB.
Drugą zaletą programu SCR był innowacyjny sposób wprowadzania poleceń użytkownika za pomocą brajlowskiej klawiatury emulowanej na klawiszach fds i jkl – więcej o tym w szybkim starcie na końcu wpisu.
Czytnik mógł odczytywać linię, w której znajdował się kursor systemowy, albo dowolną inną linię ekranu; okno, czyli prostokątny obszar o zdefiniowanych współrzędnych, albo tylko tekst o określonym kolorze tła i pierwszego planu na całym ekranie, czy też w którymś oknie. Akcje mogły być powiązane z naciskanymi klawiszami, więc np. możliwe było skojarzenie odczytywania podświetlonego tekstu po naciśnięciu klawiszy strzałek – przydatne w managerze plików Norton Commander.
Zestawy ustawień programu czytającego, pomocne przy obsłudze konkretnych programów, można było zapisywać na dysku do użycia w przyszłości.
Inną przydatną funkcją była możliwość ręcznego przeglądania zawartości ekranu, czyli tzw. tryb czytania. W programach Hal i Readboard odbywało się to przy wstrzymanej pracy programów, a w SCR – bez jej wstrzymywania. Oba rozwiązania miały swoje plusy i minusy: wstrzymanie pracy programów pozwalało śledzić tekst wyświetlany przez krótki czas, a odczyt bez zamrażania uruchomionego programu dawał możliwość śledzenia bez opóźnień informacji zmieniających się na ekranie.
Nie istniały techniczne rozwiązania dające dostęp do programów pracujących w graficznym trybie, np. edytora tekstów TAG. Dlatego też w połowie lat 90-tych dość powszechne były obawy, że system Windows, pracujący wyłącznie w trybie graficznym, będzie stanowił barierę nie do przejścia w dostępie niewidomych do komputerów. Paradoksalnie, okazało się, że było dokładnie odwrotnie: interfejs Windows to nie tylko grafika wyświetlana na ekranie, ale też obiektowy model, z którego sam system, jak też czytniki ekranu dla Windowsa, mogły czerpać informację o rodzaju i zawartości kontrolek znajdujących się w oknie uruchomionego programu i na tej podstawie w sposób zautomatyzowany przedstawiać użytkownikowi istotne informacje.
Co ciekawe, żaden z trzech czytników ekranu dla DOS nie posiadał mechanizmów emulowania myszy, pozwalających np. wykonać kliknięcie na określonym tekście. Taka funkcja pojawiła się dopiero w czytnikach ekranu dla Windows.
Jak uruchomić SCR w DosBox?
W przypadku mówiących szachów, wystarczyło skorzystać z gotowego rozwiązania w postaci trybu emulacji urządzenia Covox, wbudowanego w DosBox.
SCR wysyłał tekst do wypowiedzenia na port szeregowy, a że nie dysponowałem sprzętowym syntezatorem mowy, to nie sprawdziłaby się możliwość oferowana w DosBox, aby którymś z portów szeregowych dostępnych w wirtualnej maszynie, był fizyczny port com komputera. Trudno z resztą byłoby dzisiaj o komputer z takim portem, a dostępne konwertery USB to Serial nie zawsze sprawdzają się w praktyce.
DosBox udostępniał jeszcze inną możliwość – przekierowania portu szeregowego na nullmodem i port sieciowy TCP jakiegoś komputera. Takim komputerem mogłaby być maszyna, na której uruchamiany jest DosBox.
Potrzebne było oprogramowanie nasłuchujące na jakimś porcie TCP i wypowiadające przysyłane tam teksty przez Windowsowy czytnik ekranu NVDA. Napisałem na kolanie takie narzędzie, sprawdziło się nadspodziewanie dobrze. Pojawił się problem przy konwersji polskich liter z Dosowego standardu Mazovia na unikod, ale w końcu machnąłem na to ręką, bo ten pomost i tak miał być użyty tylko tymczasowo do wygodnego uruchamiania różnych programów w emulowanym przez DosBox systemie.
DosBox uruchamiany w przeglądarce nie pozwoli na używanie nullmodemu i uruchamianie zewnętrznego komponentu nasłuchującego na porcie TCP, ale gdyby udało się spowodować, żeby SCR wysyłał teksty nie na port szeregowy, lecz do jakiegoś programowego syntezatora mowy, który również byłby uruchamiany w DosBox, wówczas wszystko mogłoby zadziałać.
Czy da się zmodyfikować SCR?
Wspominana zaleta SCR, że był napisany w czystym Assemblerze, powinna okazać się pomocna przy próbie jego modyfikacji.
Korzystając z kultowego niegdyś narzędzia „Sourcer 8.01 Commenting Disassembler”, znalezionego na jakimś portalu archiwizującym historyczne oprogramowanie, przeprowadzam próbę dekompilacji programu SCR. Powstaje ponad 300 tys. znakowy kod programu w języku Assembler. Warto sprawdzić, czy kompilując go, uzyskamy identyczny jak początkowy plik .com.
Ponowne poszukiwania w archiwach sieciowych prowadzą do innego znanego niegdyś narzędzia – Turbo Assembler. Nie używałem go od 20 lat, więc sporo czasu zajęło przypomnienie sposobu postępowania: po przetworzeniu pliku źródłowego narzędziem Tasm.exe, generowany jest plik .obj. Prócz kompilacji potrzebne jeszcze linkowanie, czyli zamiana pliku obj na com. Tlink produkuje plik exe z uwagą, że nie ma stosu, a potrzebny jest plik com.
Jaki parametr wywołania generuje .com? Okazało się, że /t.
Dosowe narzędzie fc /b w trybie binarnym potwierdza identyczność obu wersji pliku. Oznacza to, że z pozyskanego kodu źródłowego udało się wygenerować skompilowany rezultat identyczny bajt do bajtu z oryginałem. To istotne sprawdzenie, gdyż brak identyczności obu wersji oznaczałby jakieś błędy Sourcera i prawdopodobną nieprzydatność uzyskanego kodu źródłowego w dalszych działaniach, bo wytworzony program wynikowy mógłby częściowo lub wcale nie pracować. Skoro jednak można modyfikować program, to teraz potrzebne będzie przygotowanie programowego syntezatora mowy dla DOS.
Programowy syntezator mowy dla DOS
Jednym z ciekawszych moich osiągnięć z czasów licealnych, była biblioteka programistyczna, umożliwiająca generowanie mowy syntetycznej. Syntezator mógł mówić przez covox i kartę soundblaster, a w ostateczności przez głośniczek komputera z bardzo prymitywnym brzmieniem. Wykorzystane tam fonemy nie nadawały się do użytku, ale do tematu syntezy mowy wróciłem kilka lat temu, tworząc Windowsowy syntezator „Gregor”, oparty o materiał nagrany w profesjonalnym studio. Gregor działa w oparciu o segmenty fonemów, stara biblioteka bazuje na całych fonemach, ale udało się odnaleźć całe fonemy z pierwszej wersji Gregora. Konieczne było następnie ich skonwertowanie do 8 bitów i częstotliwości 11 KHz, ze względu na małą ilość pamięci DOS.
Ponowne nurkowanie w składnicach starych programów dla DOS w poszukiwaniu kompilatora Turbo Pascal 7.0 okazało się udane, więc pozostawała próba skompilowania starej biblioteki z nowymi fonemami, mając nadzieję, że się powiedzie. Prawie się udało, bo działało wszystko prócz litery „ż”, a po długich poszukiwaniach przyczyn problemu, okazał się nią być brak ostatniego elementu tablicy indeksującej fonemy.
Sprawdzenie brzmienia na dłuższych tekstach i najniezbędniejsze korekty bez wgryzania się za bardzo w różne moje dziwne pomysły programistyczne z przed 20 lat, pozwoliły w końcu uzyskać dość akceptowalny rezultat.
Kolejny etap, to przetworzenie biblioteki programistycznej do postaci samoistnego syntezatora mowy, działającego na jednym z przerwań użytkownika.
Programy rezydentne i przerwania
W systemie DOS nie było wielozadaniowości, ale istniały tzw. programy rezydentne – zresztą właśnie SCR czy inne czytniki ekranu są ich dobrym przykładem.
Program rezydentny po uruchomieniu podłączał się do jednego lub kilku przerwań sprzętowych lub programowych, rozszerzając ich obsługę o własny kod, a następnie przekazywał kontrolę do systemu operacyjnego. Przykładowo, programowe generowanie mowy czy dźwięku odbywało się na przerwaniu zegarowym, wywoływanym domyślnie co 55 milisekund. Korzystając z odpowiednich komend wysyłanych na porty zegara, można było spowodować, że przerwanie to mogło być wywoływane np. 11025 razy na sekundę. W każdym takim wywołaniu należało ustawić wartość portu równoległego na wartość kolejnej pojedynczej próbki dźwięku, co powodowało, że w urządzeniu Covox w tym momencie na podłączone do niego słuchawki lub głośnik było wysyłane proporcjonalne napięcie elektryczne. Prócz tego, co 55 milisekund należało wywoływać starą procedurę obsługi przerwania zegarowego, z której korzystał sam system, albo inne programy rezydentne.
Poza przerwaniami sprzętowymi, takimi jak wspomniane przerwanie zegarowe, albo przerwanie klawiatury, system oferował kilka przerwań użytkownika, które mogły być dowolnie oprogramowane. Przykładowo, na przerwaniu 62h pracowały serwery mowy REZ i Syntech, przez które inne programy mogły generować mowę. To – również polskie – rozwiązanie, było z sukcesem wykorzystywane m.in. w edytorze tekstu QR-Tekst czy managerze plików Foltyn Commander, dla zapewnienia bardzo wysokiej dostępności tych programów dla niewidomych użytkowników. Rez produkcji Altixu, generował mowę przez programowy syntezator na Covox, a Syntech autorstwa Pana Wojciecha Cagary – wysyłał teksty na sprzętowe syntezatory.
Problem z Dosowymi rezydentami, to skomplikowany algorytm ich usuwania z pamięci, który zresztą chyba nie działa w Dosbox. Najlepiej więc na początek testować mechanizm jednym programem, w którym oprogramowana jest funkcja przerwania, a główna pętla je wywołuje. Synteza działa w testach, ale nie w obrębie przerwania int62h. Próba zaradzenia problemowi zawiesza system, więc jest jak 20 lat temu: restart maszyny (tyle, że dzisiaj wirtualnej w Dosbox), no i poszukiwanie kolejnych pomysłów: może flagi procesora, może przechowanie wszystkich rejestrów… Jedno jest pewne: raczej nie ma sensu szukać odpowiedzi w sieci, bo podobnych problemów najprawdopodobniej nikt nie rozwiązywał w ciągu ostatnich kilkunastu lat. Wreszcie któreś z kolejnych próbowanych rozwiązań okazuje się skuteczne.
Można teraz pójść krok dalej i wyodrębnić obsługę przerwania do samoistnego programu rezydentnego. Na tym etapie wszystko odbywa się o dziwo bez niespodzianek.
Następny krok, to modyfikacja programu scr.
Modyfikacja SCR
SCR to program napisany w Assemblerze, czyli języku maszynowym, w którym każda instrukcja to pojedynczy rozkaz procesora. Instrukcje języków wysokiego poziomu, takich jak C czy Pascal, to kilkanaście lub kilkadziesiąt instrukcji assemblera.
Modyfikowanie własnego programu napisanego lata temu jest wyzwaniem, większym, jeśli modyfikowany program napisał ktoś inny, a jeszcze większym, gdy kod źródłowy został pozyskany automatycznie i zawiera numerowane funkcje i zmienne zamiast nazywane w sposób, który by coś mówił. Udaje się odnaleźć fragment odpowiedzialny za przesyłanie danych na port szeregowy (funkcja o nazwie sub_24).
Usunięcie tego fragmentu i zamiana wywołaniem funkcji przerwania 62h spowodują, że uzyskany program będzie miał trochę inną wielkość, co może się okazać zgubne w skutkach, jeśli Sourcer nieprawidłowo zidentyfikował wszystkie miejsca w kodzie, do których następują odwołania z innych miejsc programu. A zatem po wprowadzeniu zmian, różnicę należy uzupełnić np. przy pomocy instrukcji procesora NOP (nic nie rób).
Kompilacja przebiega bez problemów, ale uruchomienie nie powoduje oczekiwanego komunikatu wypowiadanego mową. Co więcej, znowu się wszystko zawiesza. Dalsze badania pokazują, że nastąpiło przepełnienie stosu.
Przepełnienie stosu
Stos jest zarezerwowaną przestrzenią pamięci, w której programy mogą na chwilę odkładać jakieś dane. Przykładowo, jeśli procedura przerwania potrzebuje użyć jakiegoś rejestru procesora, to jego pierwotną wartość powinna najpierw zapamiętać na stosie, a na końcu swojej pracy – ją ze stosu przywrócić. Dzięki temu program główny, do którego powróci procesor po wykonaniu procedury przerwania, nie spotka się z nieoczekiwaną sytuacją, że w pewnym momencie zawartość jednego lub kilku rejestrów procesora uległa zmianie.
Informacje na stosie należy pobierać w odwrotnej kolejności, niż były umieszczane, chociaż jeśli korzysta się z kompilatora języków wysokiego poziomu, to ze stosem nie ma się bezpośrednio do czynienia. Kompilatory wykorzystują stos np. do przekazywania parametrów dla wywoływanych funkcji, albo tworzenia zmiennych lokalnych wewnątrz nich.
I tu może się pojawić drugi problem związany ze stosem, a wynikający z faktu, że jego przestrzeń pamięci nie jest nieograniczona, więc po jej przekroczeniu, następuje krytyczny błąd przepełnienia stosu.
Przepełnienie stosu czyli stack overflow jest na tyle znanym programistom zjawiskiem, że popularny portal pytań i odpowiedzi dla programistów nosi właśnie tę nazwę. Najprostszym sposobem spowodowania tego błędu jest nieskończenie rekurencyjne wywoływanie jakiejś funkcji przez nią samą bez warunku zakończenia kolejnych wywołań, ale w opisanym przypadku nie mogło o to chodzić.
Skoro stos jest niewystarczający to zwiększenie jego przestrzeni powinno rozwiązać problem, chociaż trochę niepokoi, że przy wcześniejszych testach za pomocą dwóch własnych programów, nie dał on o sobie znać, a pojawił się dopiero przy próbie uruchomienia scr. Może to oznaczać, że w trybie obsługi przerwań, działamy w przestrzeni stosu procesu, który przerwanie wywołał. Coś jest na rzeczy, bo przypomina mi się z dawnych czasów zachowywanie i odtwarzanie segmentu i wskaźnika stosu, które na czas obsługi przerwania, były podmieniane własnymi. Ale szczegółów nie pamiętam, więc jednak trzeba poszukać w sieci.
Znajduję w końcu plik programu rezydentnego w Pascalu na jakimś mirrorze archiwum Simtel, zawierającego składnicę różnych plików przydatnych dla Dos. Szkoda, że w treści nie zawiera daty, ale jego nagłówki HTTP pokazują 5 marca 1994. Osobliwość, to wstawki Assemblera w formie binarnej (jakby były pisane w czasach, gdy kompilator nie obsługiwał jeszcze polecenia asm pozwalającego umieszczać fragmenty kodu maszynowego w formie tekstowej). Pozostaje więc poszukać nowszej informacji, a najlepiej specyfikacji Turbo pascala 7.
Ani znaleziony „Programmers’ reference” ani „Language guide”, nawet w rozdziale o tworzeniu programów rezydentnych, nie zawierały informacji w jaki sposób przełączyć się na własny stos. Ustawienie rejestrów SS i SP na odpowiednie wartości i ich przywrócenie na końcu procedury obsługi przerwania, nie rozwiązało problemu.
Zmniejszenie użycia stosu w module mówiącym przez rezygnację z części nadmiarowych zmiennych lokalnych, a może ponowna kompilacja modułu z wyłączonym sprawdzaniem stosu, które już wcześniej było wyłączone dla całego programu, sprawiło w każdym razie, że uruchomienie powiodło się i mowa jest wreszcie generowana.
Dopinanie ostatnich guzików
Spakowanie serwera mowy i programu SCR, rozruch w przeglądarce pod js-dos i okazuje się, że działa, więc można dalej poprawiać.
Główny problem: niemożność przerwania wypowiadania tekstu. Podczas, gdy odtwarzanie dźwięku odbywa się opisaną wyżej metodą na przerwaniu zegarowym int8h, główny wątek czeka na zakończenie, a nie musi. Wystarczyło oprogramować kolejkę fifo dla fonemów do wypowiedzenia, a następnie zmodyfikować przerwanie zegarowe, by odtwarzało albo następną próbkę bieżącego fonemu, albo rozpoczynało następny fonem z kolejki,a gdy kolejka jest pusta – nie robiło nic poza wywoływaniem starej procedury obsługi przerwania zegarowego z odpowiednią częstotliwością.
Ile cykliw DosBox?
Aby uruchomić mówiące szachy dla DOS, potrzebne było zmniejszenie ilości cykli DosBox do 400, bo przy innej wartości mowa nie była generowana prawidłowo. W zainstalowanym DosBox mój moduł mówiący działa dobrze nawet przy 20 tys. cykli, ale okazuje się, że nie w przeglądarce. Tu z jakichś powodów pojawiają się dziwne artefakty przy zbyt dużych lub zbyt małych wartościach, więc w drodze eksperymentów ustawiam domyślną prędkość na 2 tys. cykli. I tak czasem dźwięk się lekko zacina, albo słychać, że jakaś jego drobna część została pominięta. Drugim efektem ubocznym tych 2 tys. cykli, który nie występuje w rzeczywistym DosBox przy 20 tys. cyklach, jest zauważalny spadek wysokości mowy przy próbie wypowiedzenia większej ilości tekstu. Ma to swój urok, bo podobne zjawisko zdarzało się czasem przy pracy ze sprzętowym syntezatorem SMP po wysłaniu do niego dużej partii tekstu, np. zawartości całego ekranu z dużą ilością interpunkcji.
Która wersja SCR?
Tytułowy 1995 rok widnieje na wersji programu numer 2.4 – to właśnie jej używałem przez lata. W roku 1998 pojawiła się wersja 3.1 – „Ulepszona lecz spóźniona” parafrazując tytuł omawiającego ją artykułu autorstwa Sylwestra Piekarskiego, opublikowany w czasopiśmie „Biuletyn informacyjny” nr 4/1998.
Zastanawiałem się, czy w przeglądarkowym DosBox umieścić znaną mi wersję 2.4, czy najnowszą dostępną 3.1, w której m.in. pojawił się mechanizm definiowania markerów i akcji do nich przypisanych. Zwyciężyło nowsze.
Szybki start
Po uruchomieniu poniższego linku, należy kliknąć „Click to start”.
Następnie, jeśli używamy czytnika ekranu – wyłączyć tryb czytania i przejść do trybu formularzy.
Po odczekaniu kilkunastu sekund, powinien dać się słyszeć komunikat „SCR zainstalowany”.
Od tej pory możemy wydawać czytnikowi polecenia tworzone na bazie sześciopunktu brajlowskiego, symulowanego w oparciu o klawisze FDS i JKL, na których trzyma się palce pisząc na klawiaturze systemem 10-palcowym. Polecenia czytnikowi wydaje się przez jednoczesne naciśnięcie kombinacji tych klawiszy, więc dostęp do nich jest możliwy bez odrywania rąk z bazowego położenia.
Najważniejsze komendy SCR:
- Klawisze s+f – brajlowska litera k – odczytanie linii kursora.
- Klawisze f+k – brajlowska litera e – odczytanie całego ekranu.
- Lewy Shift i strzałki – ręczne przeglądanie zawartości ekranu liniami albo znakami.
- Klawisze f+d+k – brajlowska litera h – help, czyli tryb pomocy. W trybie pomocy spacja przenosi do informacji o następnym poleceniu programu, Enter umożliwia uzyskanie informacji, jakie klawisze na klawiaturze należy nacisnąć, aby wywołać aktualnie prezentowaną komendę, lewy shift+spacja cofa nas na liście 10 pozycji wstecz.
- Klawisze f+d+s+k – brajlowska litera r – określanie reakcji programu na naciśnięty klawisz. Można zdefiniować dwie akcje i opóźnienie ich wykonania.