L'inverse du neuromancien. Partie 4: Son, Animation, Huffman, Github

Bonjour, comme vous l'avez déjà compris, ceci est la continuation de mon histoire d'ingénierie inverse et de portage de Neuromant.



L'inverse du neuromancien. Partie 1: Sprites
L'inverse du neuromancien. Partie 2: police de rendu
L'inverse du neuromancien. Partie 3: Rendu fini, faites le jeu

Aujourd'hui, commençons par deux bonnes nouvelles:


  • premièrement, je ne suis plus seul - l’utilisateur viiri a déjà rejoint le projet et a déjà apporté une contribution significative;
  • deuxièmement, nous avons maintenant un référentiel ouvert sur github .

En général, les choses vont très bien, et peut-être que bientôt nous aurons au moins une version jouable. Et sous la coupe, comme d'habitude, parlons de ce qui a été réalisé et de la façon dont il a été réalisé en ce moment.




Il a commencé à gérer le son. Hélas, parmi les ressources du jeu, il n'y avait rien de similaire à l'audio, et comme je n'avais aucune idée du fonctionnement de la musique dans MS-DOS , il était extrêmement difficile de savoir par où commencer. Après avoir lu un peu sur toutes sortes de SoundBlaster , le mieux que j'ai trouvé est de faire défiler le code désassemblé dans l'espoir de voir des signatures familières. Et quiconque cherche, il trouve généralement, même si ce n'est pas tout à fait ce qu'il cherchait (commentaires dénoncés par Ida ):


sub_20416: ... mov ax, [si+8] out 42h, al ; 8253-5 (AT: 8254.2). mov al, ah out 42h, al ; Timer 8253-5 (AT: 8254.2). mov bx, [si+0Ah] and bl, 3 in al, 61h ; PC/XT PPI port B bits: ; 0: Tmr 2 gate ═╦═► OR 03H=spkr ON ; 1: Tmr 2 data ═╝ AND 0fcH=spkr OFF ; 3: 1=read high switches ; 4: 0=enable RAM parity checking ; 5: 0=enable I/O channel check ; 6: 0=hold keyboard clock low ; 7: 0=enable kbrd and al, 0FCh or al, bl out 61h, al ; PC/XT PPI port B bits: ; 0: Tmr 2 gate ═╦═► OR 03H=spkr ON ; 1: Tmr 2 data ═╝ AND 0fcH=spkr OFF ; 3: 1=read high switches ; 4: 0=enable RAM parity checking ; 5: 0=enable I/O channel check ; 6: 0=hold keyboard clock low ; 7: 0=enable kbrd 

Après avoir parcouru ce Timer 8253-5, je suis tombé sur un article qui est devenu la première clé pour comprendre ce qui se passait. Ci-dessous, je vais essayer d'expliquer quoi.


Ainsi, à l'ère d' IBM-PC , avant l'avènement des cartes son abordables, l'appareil de reproduction du son le plus courant était le soi-disant PC Speaker , également connu sous le nom de «beeper». Cet appareil n'est rien de plus qu'un haut-parleur ordinaire connecté à la carte mère, dans la plupart des cas, via un connecteur à quatre broches. Le beeper, selon l'idée, permettait de reproduire une impulsion rectangulaire à deux niveaux (correspondant à deux niveaux de tension, généralement 0V et + 5V) et était contrôlé via le 61ème port du contrôleur PPI (Programmable Peripheral Interface) . Plus précisément, les deux premiers bits de la valeur envoyée au port sont chargés de contrôler le «haut-parleur» (voir commentaires sur les lignes in al, 61h et out 61h, al ).


Comme je l'ai dit (dans des mots un peu différents), notre haut-parleur peut être dans deux états - "in" et "out" ( "low" - "high" , "off" - "on" , "off" - "on" , que ce soit). Pour créer une impulsion, il est nécessaire de changer l'état actuel à l'opposé et, après un certain temps, de revenir. Cela peut être fait directement en manipulant le premier bit (compter à partir de zéro) du port 61, par exemple, comme ceci:


 PULSE: in al, 61h ;    and al, 11111100b ;    ... or al, 00000010b ;     ... ; ,        0 out 61h, al ;    61-  mov cx, 100 ;   DELAY: loop DELAY ;    in al, 61h ;    and al, 11111100b ;     out 61h, al ;    61-  

Le résultat de l'exécution de ce code ressemblera à ceci:


  loop DELAY +5V +----------------------+ ! ! 0V ---+ +-------------------------- or al, 00000010b and al, 11111100b out 61h, al out 61h, al 

En répétant PULSE avec un retard, nous obtenons un signal rectangulaire:


  mov dx, 100 ;  100  PULSE: ... mov cx, 100 WAIT: loop WAIT dec dx jnz PULSE PULSE +5V +---------+ +---------+ +---------+ ! ! ! ! ! ! 0V ---+ +---------+ +---------+ +--- loop WAIT 

Si dans le premier cas, nous n'aurions presque rien entendu, dans le second, nous obtiendrons une tonalité de fréquence, en fonction de la vitesse de la machine sur laquelle ce code est exécuté. C'est formidable, mais associé à certaines difficultés. Dans tous les cas, il existe un moyen plus pratique de contrôler le haut-parleur.


Voici la minuterie à trois canaux programmable du jeu - Intel 8253 , dont le deuxième canal (à partir de zéro) est connecté au signal sonore. Ce temporisateur reçoit un signal de l'horloge Intel 8254 , envoyant 1193180 impulsions par seconde (~ 1,193 MHz), et peut être programmé pour une réaction spécifique après un nombre spécifié d'impulsions. L'une de ces réactions envoie une impulsion carrée au haut-parleur. En d'autres termes, le 8253 peut fonctionner sous la forme d'un générateur d'un signal rectangulaire de fréquence réglable, ce qui permet de synthétiser relativement facilement divers effets sonores sur le haut-parleur. Et voici ce dont vous avez besoin pour cela:


  1. Réglez le deuxième canal de la minuterie sur le mode de génération de signal rectangulaire. Pour ce faire, écrivez une valeur spéciale à un octet sur le port 43 ( registre de mode / commande 8253 ). Dans mon cas, c'est 10110110B (plus de détails ici ):

 Bits Usage 6 and 7 Select channel : 1 0 = Channel 2 4 and 5 Access mode : 1 1 = Access mode: lobyte/hibyte 1 to 3 Operating mode : 0 1 1 = Mode 3 (square wave generator) 0 BCD/Binary mode: 0 = 16-bit binary 

  1. Réglez la fréquence souhaitée sur le deuxième canal. Pour ce faire, octet par bit, du plus jeune au plus âgé, nous envoyons au 42e port ( 8253 canal de données du canal 2 ) une valeur égale à 1193180 / freq , où freq est la valeur de fréquence requise en Hertz.


  2. Laissez le haut-parleur recevoir des impulsions de la minuterie. Pour ce faire, définissez les deux premiers bits de la valeur du port 61 ( PPI ) sur l'unité. Le fait est que si le bit zéro est mis à 1, alors le premier est interprété comme un «interrupteur»:



 Bit 0 Effect ----------------------------------------------------------------- 0 The state of the speaker will follow bit 1 of port 61h 1 The speaker will be connected to PIT channel 2, bit 1 is used as switch ie 0 = not connected, 1 = connected. 

En conséquence, nous avons l'image suivante:


  mov al, 10110110b out 43h, al ;   mov ax, 02E9Bh ; 1193180 / 100 = ~0x2E9B out 42h, al ;      mov al, ah out 42h, al ;      in al, 61h ;    or al, 00000011b ;      1 out 61h, al ;   ... ;       ~100  in al, 61h and al, 11111100b out 61h, al ;   

Et c'est exactement ce que fait le code que j'ai cité au tout début (sauf pour l'initialisation, mais je l'ai trouvé dans une autre fonction): à si + 8 il y a un diviseur de fréquence envoyé au port 42, et à si + 0Ah - état du haut-parleur ( «on» - «off» ) enregistré dans le port 61.


Le mécanisme de lecture est simple et direct, mais vous avez ensuite dû faire face à des synchronisations. Après avoir examiné le code voisin, j'ai vu que dans la même fonction dans laquelle le temporisateur est initialisé ( sub_2037A , ci-après - init_8253 ), le huitième gestionnaire d' interruption est remplacé par la fonction sub_20416 (ci-après - play_sample ). Il est rapidement devenu évident que cette interruption est générée environ 18,2 fois par seconde et sert à mettre à jour l'heure du système. Le remplacement du gestionnaire de cette interruption est une pratique courante si vous devez effectuer une action 18 fois par seconde (en fait, le gestionnaire d'origine doit également être appelé à l'intérieur du hook, sinon l'heure du système s'arrêtera). Sur cette base, il s'avère que la fréquence suivante est chargée au générateur tous les (1 / 18.2) * 1000 ~ 55 .


Le plan était le suivant:


  • mettre un point d'arrêt dans la fonction play_sample , sur la ligne où le prochain diviseur de fréquence est extrait;
  • calculer la fréquence selon la formule freq = 1193180 / divisor ;
  • générer 55 ms de signal de freq carrée dans une sorte d'éditeur audio (j'ai utilisé Adobe Audition );
  • répétez les trois premières étapes jusqu'à ce qu'au moins 3 secondes soient accumulées.

J'ai donc obtenu le début de la mélodie dans le menu principal, mais en jouant 10 fois plus lentement que nécessaire. Ensuite, j'ai réduit la durée de l '«échantillon» de 55 ms à 5 ms - c'est devenu beaucoup mieux, mais toujours pas ça. Le problème de synchronisation est resté ouvert jusqu'à ce que je trouve cet article . Il s'est avéré que la huitième interruption est générée en alimentant le même 8253 , dont le canal zéro est connecté au contrôleur d'interruption ( PIC ). Lorsque la machine démarre, le BIOS définit le canal zéro pour générer des impulsions avec une fréquence de ~ 18,2 Hz (c'est-à-dire qu'une interruption est générée toutes les ~ 54,9 ms). Cependant, le canal zéro peut être reprogrammé afin qu'il génère des impulsions avec une fréquence plus élevée, pour cela, par analogie avec le deuxième canal, vous devez écrire une valeur égale à 1193180 / freq sur le 40e port, où freq est la valeur de fréquence requise en Hertz. Cela se produit dans la fonction init_8253 , juste au début, je n'y ai pas prêté attention:


 init_8253: ... mov al, 0B6h ; 10110110b out 43h, al ; Timer 8253-5 (AT: 8254.2). mov ax, 13B1h out 40h, al ; Timer 8253-5 (AT: 8254.2). mov al, ah out 40h, al ; Timer 8253-5 (AT: 8254.2). 

La valeur 13B1h traduite en fréquence: 1193180 / 13B1h ~ 236.7 , puis nous obtenons environ (1 / 236.7) * 1000 ~ 4.2 par "échantillon". Le puzzle s'est développé.


Ensuite, c'est une question de technologie - pour implémenter une fonction qui extrait les bandes sonores du jeu. Mais voici le problème, les valeurs des diviseurs de fréquence enregistrées dans le 42e port ne sont pas stockées explicitement. Ils sont calculés par un algorithme délicat, dont les données d'entrée et la zone de travail se trouvent directement dans le fichier exécutable du jeu (dans le septième segment, selon Ida ). De plus, parmi les fonctionnalités, il n'y a aucun signe de fin de piste, quand il n'y a plus rien à jouer, l'algorithme produit infiniment un état zéro du haut-parleur. Mais je n'ai pas pris la peine et, comme dans le cas de l'algorithme de décompression (la première partie ), j'ai simplement porté à l'assembleur 64 bits la fonction de réglage de la piste pour la lecture et l'algorithme pour obtenir la fréquence suivante (et j'ai pris le septième segment entièrement).


Et ça a marché. Après cela, j'ai implémenté les fonctions de génération de bande sonore pour la piste sélectionnée ( PCM, 44100 Hz, 8 bits, mono ; j'ai fait quelque chose comme un générateur utilisé dans l'émulateur de haut-parleur de DosBox ). J'ai résolu le problème du signe de la fin avec un simple compteur de silence: compté une seconde - nous complétons l'algorithme. Enveloppant la piste résultante dans l'en-tête WAV et en enregistrant le résultat dans un fichier, j'ai obtenu exactement la piste dans le menu principal. Et 13 autres pistes que vous pouvez écouter ci-dessous [ou dans la visionneuse de ressources, qui a maintenant un lecteur intégré et la possibilité d'enregistrer n'importe quelle piste dans .WAV] :



[En étudiant le problème, j'ai découvert des techniques plus avancées de «bip», comme l'utilisation de la modulation de largeur d'impulsion pour une reproduction sonore PCM de mauvaise qualité. À la fin de cet article, je fournirai une liste de documents à partir desquels vous pourrez en savoir plus.]




Dans la deuxième partie , lorsque différents formats de ressources ont été pris en compte, j'ai suggéré que les fichiers .ANH contiennent des animations pour les arrière-plans d'emplacement (c'est-à-dire pour les images stockées dans .PIC ). [C'est vrai.] Ayant fini avec le son, j'ai décidé de le vérifier. En partant du principe que l'animation est appliquée directement à l'image de fond stockée en mémoire (pas dans la mémoire vidéo , mais dans la chaîne de sprites ), j'ai décidé de vider cette mémoire, respectivement, avant et après l'application de l'animation (regardez où le curseur pointe vers le haut chaîne de lettres «S»):



 3DE6:0E26 03 B4 44 B3 ... ;   3DE6:0E26 03 BC CC B3 ... ;   

Exactement ce à quoi je m'attendais - la couleur rouge foncé (0x4) est devenue rouge vif (0xC). Vous pouvez maintenant essayer de définir le point d'arrêt pour modifier la valeur à l'adresse, par exemple, 3DE6:0E28 et, si vous êtes chanceux, effectuer une trace inverse. [J'ai eu de la chance.] Le point d'arrêt m'a conduit à une ligne qui modifie directement la valeur à l'adresse donnée: xor es:[bx], al . Après avoir examiné les environs, j'ai facilement construit une chaîne d'appels de la boucle du niveau principal jusqu'à ce point: sub_1231E (xor es:[bx], al) <- sub_12222 <- sub_105F6 <- sub_1038F ( ) .


Je n'entrerai pas dans les détails sur la façon exacte dont j'ai inversé le processus d'animation. C'est un travail assez routinier et méthodique, mais pas très difficile si les frontières sont clairement délimitées (le retour reçu est ces frontières). Mais je ne peux m'empêcher de parler de ce qui s'est finalement passé.


Tout d'abord sur le format .ANH . En fait, il s'agit d'un ensemble de conteneurs, et le premier mot du fichier .ANH est le nombre de conteneurs à l'intérieur:


 typedef struct anh_hdr_t { uint16_t anh_entries; /* first entry hdr */ } anh_hdr_t; 

Le conteneur lui-même est une animation prise séparément de l'élément d'arrière-plan. Vous pouvez sélectionner un en-tête pour le conteneur contenant sa taille en octets et le nombre d'images dans l'animation qu'il représente. À côté de l'en-tête se trouvent les valeurs de la durée (délai) de la trame suivante et du décalage des octets de la trame elle-même, par rapport aux octets de la première trame. Le nombre de ces paires est évidemment égal au nombre de trames:


 typedef struct anh_entry_hdr_t { uint16_t entry_size; uint16_t total_frames; /* anh_frame_data_t first_frame_data */ /* another frames data */ /* anh_frame_hdr first_frame_hdr */ /* another frames */ } anh_entry_hdr_t; typedef struct anh_frame_data_t { uint16_t frame_sleep; uint16_t frame_offset; } anh_frame_data_t; ... extern uint8_t *anh; anh_hdr_t *hdr = (anh_hdr_t*)anh; anh_entry_hdr_t *entry = (anh_entry_hdr_t*)(anh + sizeof(anh_hdr_t)); for (uint16_t u = 0; u < anh->anh_entries; u++) { uint8_t *p = (uint8_t*)entry; anh_frame_data_t *first_frame_data = (anh_frame_data_t*)(p + sizeof(anh_entry_hdr_t)); uint8_t *first_frame_bytes = p + (entry->total_frames * sizeof(anh_frame_data_t)); for (uint16_t k = 0; k < entry->total_frames; k++) { anh_frame_data_t *frame_data = first_frame_data + k; uint8_t *frame_bytes = first_frame_bytes + frame_data->frame_offset; ... } /* plus 2 bytes of padding */ p += (entry->entry_size + 2); entry = (anh_entry_hdr_t*)p; } 

Un cadre séparé est constitué d'un en-tête de quatre octets contenant ses dimensions linéaires et ses déplacements par rapport à l'image d'arrière-plan, et les pixels du cadre codés par l'algorithme que je connais déjà par Run Length :


 typedef struct anh_frame_hdr { uint8_t bg_x_offt; uint8_t bg_y_offt; uint8_t frame_width; uint8_t frame_height; /* rle encoded frame bytes */ }; 

La «superposition» du cadre sur le fond peut ressembler à ceci:


 extern uint8_t *level_bg; uint8_t frame_pix[8192]; anh_frame_hdr *hdr = (anh_frame_hdr*)frame_bytes; uint16_t frame_len = hdr->frame_width * hdr->frame_height; decode_rle(frame + sizeof(anh_frame_hdr), frame_len, frame_pix); /* 0xFB4E - some magic value, have no idea what is it */ uint16_t bg_offt = (hdr->bg_y_offt * 152) + hdr->bg_x_offt + 0xFB4E; uint16_t bg_skip = 152 - hdr->frame_width; uint8_t *p1 = frame_pix, *p2 = level_bg; for (uint16_t i = hdr->frame_height; i != 0; i--) { for (uint16_t j = hdr->frame_width; j != 0; j--) { *p2++ ^= *p1++; } p2 += bg_skip; } 

Il s'agit du format .ANH , mais il existe une autre structure qui fait que tout fonctionne:


 typedef struct bg_animation_control_table_t { uint16_t total_frames; uint8_t *first_frame_data; uint8_t *first_frame_bytes; uint16_t sleep; uint16_t curr_frame; } bg_animation_control_table_t; 

Dans le jeu lui-même, un tableau d'au moins quatre structures de ce type est globalement déclaré. Après le chargement du fichier .ANH suivant, le nombre d'animations à l'intérieur est également stocké dans une variable globale et les éléments du tableau sont initialisés comme suit:


 extern uint8_t *anh; uint16_t g_anim_amount = 0; bg_animation_control_table_t g_anim_ctl[4]; ... anh_hdr_t *hdr = (anh_hdr_t*)anh; anh_entry_hdr_t *entry = (anh_entry_hdr_t*)(anh + sizeof(anh_hdr_t)); g_anim_amount = hdr->anh_entries; for (uint16_t u = 0; u < g_anim_amount; u++) { uint8_t *p = (uint8_t*)entry; g_anim_ctl[u].total_frames = entry->total_frames; g_anim_ctl[u].first_frame_data = p + sizeof(anh_entry_hdr_t); g_anim_ctl[u].first_frame_bytes = g_anim_ctl[u].first_frame_data + (entry->total_frames * sizeof(anh_frame_data_t)); g_anim_ctl[u].sleep = *(uint16_t*)(g_animation_control[u].first_frame_data); g_anim_ctl[u].curr_frame = 0; /* plus 2 bytes of padding */ p += (entry->entry_size + 2); entry = (anh_entry_hdr_t*)p; } 

Enfin, appliquez les animations:


 for (uint16_t u = 0; u < g_anim_amount; u++) { bg_animation_control_table_t *anim = &g_anim_ctl[u]; if (anim->sleep-- == 0) { anh_frame_data_t *data = (anh_frame_data_t*)anim->first_frame_data + anim->curr_frame; /*    */ ... if (++anim->curr_frame == anim->total_frames) { anim->curr_frame = 0; data = (anh_frame_data_t*)anim->first_frame_data; } else { data++; } anim->sleep = data->frame_sleep; } } 

Et nous obtenons ce qui suit [vous pouvez voir beaucoup plus dans la visionneuse de ressources] :


R2.ANH.GIF

R6.ANH.GIF

Il y a quelques problèmes avec l'animation en ce moment. Le premier est que dans mon code, je joue toutes les animations disponibles, mais l'original vérifie certains drapeaux globaux indiquant s'il faut faire défiler la suivante. Et le second, du fait que certaines animations ajoutent à l'écran des objets qui ne s'y trouvent pas à l'origine. Et comme les cadres se «querellent» en arrière-plan, puis avec un défilement cyclique, à chaque deuxième cercle, l'objet disparaît tout simplement. Voici, par exemple, à quoi cela pourrait ressembler:


R53.ANH.GIF

Mais pour l'instant, laissons les choses telles quelles.




Rappelez-vous l'algorithme de décompression inconnu de la première partie ? A peine connecté au développement, viiri a non seulement déterminé de quel type d'algorithme il s'agissait, mais a également écrit sa propre version du décodeur, remplaçant le terrible morceau de trois lignes d'Assembler dans la base de code. À cet égard, j'ai demandé à viiri d'écrire un court essai sur le travail accompli. Ce qui a été fait, mais avant de le donner, il faut dire quelques mots sur la théorie.


Pour compresser les ressources, les développeurs de Neuromancer ont utilisé le code Huffman . Il s'agit de l'une des premières méthodes efficaces de codage des informations à l'aide de codes préfixes . Dans la théorie du codage, les codes avec un mot de longueur variable et ceux où aucun mot de code n'est le préfixe d'un autre sont appelés codes de préfixe . Autrement dit, si le mot «a» est inclus dans le code de préfixe, alors le mot «ab» n'existe pas dans le code. Cette propriété vous permet de décomposer de manière unique en mots le message codé par un tel code.


L'idée de l'algorithme de Huffman est que, connaissant la probabilité d'apparition de caractères d'un certain alphabet dans le message, nous pouvons décrire la procédure de construction de codes de longueur variable, consistant en un nombre entier de bits. Les symboles avec une probabilité d'occurrence plus élevée se voient attribuer des codes plus courts, et les symboles avec une probabilité plus faible, au contraire, plus longs. En général, la procédure de codage est réduite à la construction de l'arbre de code optimal et, sur sa base, à la mise en correspondance du symbole de message avec le code correspondant. La propriété de préfixe du code reçu vous permet de décoder de manière unique le message compressé.


Huffman.GIF

L'algorithme a un inconvénient important (en fait, pas un, mais maintenant seul celui-ci est important). Le fait est que pour récupérer le contenu d'un message compressé, le décodeur doit connaître la table des fréquences d'occurrence des caractères utilisée par le codeur. À cet égard, avec le message codé, une table de probabilité ou l'arbre de code lui-même (une option utilisée dans le jeu) doit être transmis. La taille des données supplémentaires peut être relativement importante, ce qui affecte considérablement l'efficacité de la compression.


Quelque chose sur la façon dont vous pouvez gérer cela, ainsi que sur votre décodeur et celui qui est implémenté dans le jeu, dit à viiri :


Il convient de mentionner que tout le jeu a été entièrement écrit à la main dans Assembler, de sorte que le code contient des solutions, des astuces et des optimisations intéressantes.

Selon les procédures. La fonction sub_1ff94 ( build_code_table ) est nécessaire pour charger une arborescence Huffman compressée à partir d'un fichier. Pour décoder un Huffman statique (parfois dynamique , et cette exigence ne s'applique pas à lui), avec le message, un arbre de code doit être transmis, qui est un mappage des codes Huffman en codes de caractères réels. Cet arbre est assez grand et il serait donc intéressant de le stocker efficacement. La manière la plus correcte consiste à utiliser des codes canoniques ( MOAR ). En raison de leurs propriétés, il existe un moyen très intéressant et efficace de stocker l'arborescence (utilisé dans la mise en œuvre de la méthode de compression Deflate de l'archiveur PKZip ). Mais les codes canoniques ne sont pas utilisés dans le jeu, à la place, une traversée d'arbre directe est effectuée et pour chaque sommet, le bit 0 est écrit dans le flux de sortie si le nœud n'est pas une feuille, ou le bit 1 si le nœud est une feuille, puis les 8 bits suivants sont le code de caractère dans ce nœud. Lors du décodage, une traversée d'arbre similaire est effectuée, ce que nous voyons dans le jeu. Il y a un exemple et une explication.

build_code_table
 build_code_table proc near call getbit ;     jb short loc_1FFA9 ;   ... shl dx, 1 inc bx call build_code_table ;  build_code_table    or dl, 1 call build_code_table ;  build_code_table    shr dx, 1 dec bx ret loc_1FFA9: call sub_1FFC2 ;      (8 ) ... ;     ret sub_1FF94 endp sub_1FFC2 proc near sub di, di mov ch, 8 loc_1FFC6: call getbit rcl di, 1 dec ch jnz short loc_1FFC6 retn sub_1FFC2 endp 

getbit ( sub_1ffd0 ) lit un peu du flux d'entrée. Son analyse nous permet de conclure que des bits individuels sont extraits du registre ax 16 bits, dont la valeur est chargée de la mémoire par l'instruction lodsw , qui charge deux octets du flux, mais comme le processeur Intel a un ordre d'octets peu fin, xchg réorganise la moitié du registre. De plus, l'ordre des bits dans le flux est quelque peu illogique - le premier n'est pas le moins, mais le bit le plus significatif. , shl , jb .

getbit
 getbit proc near or cl, cl jz short loc_1FFD9 dec cl shl ax, 1 retn loc_1FFD9: cmp si, 27B6h jz short loc_1FFE7 ;   lodsw xchg al, ah mov cl, 0Fh shl ax, 1 retn loc_1FFE7: call sub_202FC ;      lodsw xchg al, ah mov cl, 0Fh shl ax, 1 retn getbit endp 

viiri , :


huffman_decompress
 typedef struct node_t { uint8_t value; struct node_t *left, *right; } node_t; static uint8_t *g_src = NULL; static int getbits(int numbits) { ... } static uint32_t getl_le() { /*       4-    */ } static node_t* build_tree(void) { node_t *node = (node_t*)calloc(1, sizeof(node_t)); if (getbits(1)) { node->right = NULL; node->left = NULL; node->value = getbits(8); } else { node->right = build_tree(); node->left = build_tree(); node->value = 0; } return node; } int huffman_decompress(uint8_t *src, uint8_t *dst) { int length, i = 0; node_t *root, *node; g_src = src; length = getl_le(); node = root = build_tree(); while (i < length) { node = getbits(1) ? node->left : node->right; if (!node->left) { dst[i++] = node->value; node = root; } } ... } 

:


build_code_table . , , . , . , , , — ( huffman_decompress ).

? ! ! , . ( ): . , 3- , N , (3 — N) .

ABCD : A - 0b, B - 10b, C - 110b, D - 111b . ( ), , :


Code
0 00b1Un
10 0b2B
110 b3C
111 b3D

, . , , , 010b — . . , 'A' 0b , , . :


IndexCode
00 00b1Un
10 01b1Un
20 10b1Un
30 11b1Un
410 0b2B
510 1b2B
6110 b3C
7111 b3D

, 011010111b :


  • : 011b ;
  • , 011b (3) , A , ;
  • 011b 1, , : 110b ;
  • , 110b (6) , , ;
  • , .

«» 8- . 256 . 8 . , , :


: — , . , . 4 — 32- .

, . .




, github . , , , - [ , README.md] .


, , 2015- :


  • LibNeuroRoutines (, MASM) — , , . ( neuro_routines.h ) , . , :
    • ( huffman_decompression.c , decompression.c );
    • ( cp437.c );
    • ( dialog.c );
    • ( audio.c ).
  • NeuromancerWin64 () — . . , «» , , . CSFML ( SFML C ).
  • ResourceBrowser (C++) — . MFC -, .DAT -. :
    • BMP (8bpp) ( IMH , PIC );
    • ( ANH );
    • WAV (PCM, 44100Hz, 8bps, mono) ( SOUND ).

LibNeuroRoutines . LibNeuroRoutines CSFML ( ResourceBrowser SFML ).


64- Windows . , LibNeuroRoutines 64- MASM (Microsoft Macro Assembler) . — , 64- . , NASM FASM , , . VS 2015MASM .


, . C . , ( , MFC ).


, . - , .




. ? , . , . - , . , , , . ().


  1. Make sound from the speaker using assembly
  2. Programming the PC Speaker
  3. PC Speaker
  4. Programmable Interval Timer
  5. Making C Sing
  6. Beyond Beep-Boop: Mastering the PC Speaker

Source: https://habr.com/ru/post/fr417639/


All Articles