O reverso do Neuromancer. Parte 4: Som, Animação, Huffman, Github

Olá, como você já entendeu, esta é uma continuação da minha história de engenharia reversa e portabilidade do Neuromant.



O reverso do Neuromancer. Parte 1: Sprites
O reverso do Neuromancer. Parte 2: Renderizar fonte
O reverso do Neuromancer. Parte 3: Renderização finalizada, faça o jogo

Hoje, vamos começar com duas boas notícias:


  • primeiro, não estou mais sozinho - o usuário do viiri já entrou no projeto e já fez uma contribuição significativa;
  • segundo, agora temos um repositório aberto no github .

Em geral, as coisas estão indo muito bem, e talvez em breve teremos pelo menos alguma versão jogável. E, como de costume, vamos falar sobre o que e como foram alcançados no momento.




Ele começou a lidar com o som. Infelizmente, entre os recursos do jogo, não havia nada parecido com o áudio, e como eu não tinha idéia de como a música funciona no MS-DOS , não era muito claro por onde começar. Depois de ler um pouco sobre todos os tipos de SoundBlaster , o melhor que descobri é rolar o código desmontado na esperança de ver algumas assinaturas familiares. E quem procura, ele geralmente encontra, mesmo que não seja exatamente o que estava procurando (comentários de 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 

Tendo passado por esse Timer 8253-5, me deparei com um artigo que se tornou a primeira chave para entender o que estava acontecendo. Abaixo vou tentar explicar o que é o quê.


Portanto, na era do IBM-PC , antes do advento de placas de som acessíveis, o dispositivo de reprodução de som mais comum era o PC Speaker , também conhecido como "bip". Este dispositivo nada mais é do que um alto-falante comum conectado à placa-mãe, na maioria dos casos, através de um conector de quatro pinos. O sinal sonoro, de acordo com a idéia, possibilitou a reprodução de um pulso retangular de dois níveis (correspondente a dois níveis de tensão, geralmente 0V e + 5V) e foi controlado através da 61ª porta do controlador PPI (Programmable Peripheral Interface) . Especificamente, os dois primeiros bits do valor enviado para a porta são responsáveis ​​por controlar o "alto-falante" (ver comentários nas linhas in al, 61h e out 61h, al ).


Como eu disse (em poucas palavras diferentes), nosso falante pode estar em dois estados - "dentro" e "fora" ( "baixo" - "alto" , "desligado" - "ligado" , "desligado" - "ligado" , o que for). Para criar um impulso, é necessário mudar o estado atual para o oposto e, depois de algum tempo, voltar. Isso pode ser feito diretamente, manipulando o primeiro bit (contagem a partir do zero) da porta 61, por exemplo, assim:


 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-  

O resultado da execução deste código será semelhante a este:


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

Repetindo PULSO com um atraso, obtemos um sinal retangular:


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

Se no primeiro caso dificilmente teríamos ouvido algo, no segundo obteremos um tom de frequência, dependendo da velocidade da máquina na qual esse código é executado. Isso é ótimo, mas associado a certas dificuldades. De qualquer forma, existe uma maneira mais conveniente de controlar o alto-falante.


Aí vem o cronômetro programável de três canais para jogos - Intel 8253 , cujo segundo canal (começando do zero) está conectado ao sinal sonoro. Este timer recebe um sinal do relógio Intel 8254 , enviando 1193180 pulsos por segundo (~ 1.193 MHz) e pode ser programado para uma reação específica após um número especificado de pulsos. Uma dessas reações é enviar um pulso quadrado para o alto-falante. Em outras palavras, o 8253 pode funcionar na forma de um gerador de um sinal retangular de frequência ajustável, o que torna relativamente fácil sintetizar vários efeitos sonoros no alto-falante. E aqui está o que você precisa para isso:


  1. Defina o segundo canal do temporizador para o modo de geração de sinal retangular. Para fazer isso, escreva um valor especial de byte único na porta 43 ( registrador de modo / comando 8253 ). No meu caso, este é 10110110B (mais detalhes aqui ):

 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. Defina a frequência desejada no segundo canal. Para fazer isso, byte por bit, do mais novo ao mais antigo, enviamos para a 42ª porta ( porta de dados 8253 Canal 2 ) um valor igual a 1193180 / freq , em que freq é o valor de frequência necessário em Hertz.


  2. Permita que o alto-falante receba pulsos do timer. Para fazer isso, defina os dois primeiros bits do valor na porta 61 ( PPI ) como unidade. O fato é que, se o bit zero for definido como 1, o primeiro será interpretado como um "comutador":



 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. 

Como resultado, temos a seguinte imagem:


  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 ;   

E é exatamente isso que o código que eu citei no início (exceto para a inicialização, mas eu o encontrei em outra função): no si + 8 há um divisor de frequência enviado para a porta 42 e no si + 0Ah - status do alto-falante ( “ligado” - “desligado” ) gravado na porta 61.


O mecanismo de reprodução é simples e direto, mas você teve que lidar com horários. Tendo examinado o código próximo, vi que na mesma função em que o timer é inicializado ( sub_2037A , daqui em diante - init_8253 ), o oitavo manipulador de interrupções é substituído pela função sub_20416 (a seguir - play_sample ). Logo ficou claro que essa interrupção é gerada aproximadamente 18,2 vezes por segundo e serve para atualizar a hora do sistema. Substituir o manipulador dessa interrupção é uma prática comum se você precisar executar alguma ação 18 vezes por segundo (na verdade, o manipulador original também deve ser chamado dentro do gancho, caso contrário, a hora do sistema será interrompida). Com base nisso, verifica-se que a próxima frequência é carregada no gerador a cada (1 / 18.2) * 1000 ~ 55 .


O plano era este:


  • coloque um ponto de interrupção na função play_sample , na linha em que o próximo divisor de frequência é extraído;
  • calcular a frequência de acordo com a fórmula freq = 1193180 / divisor ;
  • gerar 55ms de sinal de frequência quadrada em algum tipo de editor de áudio (usei o Adobe Audition );
  • repita os três primeiros passos até acumular pelo menos 3 segundos.

Então, eu peguei o começo da melodia no menu principal, mas toquei 10 vezes mais devagar do que o necessário. Depois reduzi a duração da "amostra" de 55 ms para 5 ms - ela ficou muito melhor, mas ainda não é isso. O problema de tempo permaneceu em aberto até eu encontrar este artigo . Descobriu-se que a oitava interrupção é gerada pela alimentação do mesmo 8253 , cujo canal zero está conectado ao controlador de interrupção ( PIC ). Quando a máquina é inicializada, o BIOS define o canal zero para gerar pulsos com uma frequência de ~ 18,2 Hz (ou seja, uma interrupção é gerada a cada ~ 54,9 ms). No entanto, o canal zero pode ser reprogramado para gerar pulsos com uma frequência mais alta; para isso, por analogia com o segundo canal, é necessário escrever um valor igual a 1193180 / freq na 40ª porta, em que freq é o valor de frequência necessário em Hertz. Isso acontece na função init_8253 , mas inicialmente não prestei a devida atenção:


 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). 

O valor 13B1h traduzido para a frequência: 1193180 / 13B1h ~ 236.7 , então obtemos aproximadamente (1 / 236.7) * 1000 ~ 4.2 por "amostra". O quebra-cabeça se desenvolveu.


Então é uma questão de tecnologia - implementar uma função que extrai as trilhas sonoras do jogo. Mas eis que os valores dos divisores de frequência registrados na 42ª porta não são armazenados explicitamente. Eles são calculados por algum algoritmo complicado, cujos dados de entrada e a área de trabalho estão diretamente no arquivo executável do jogo (no sétimo segmento, de acordo com Ida ). Além disso, dos recursos, não há sinal do fim da faixa, quando não há mais nada para reproduzir, o algoritmo produz infinitamente um estado zero do alto-falante. Mas eu não me incomodei e, como no caso do algoritmo de descompressão (a primeira parte ), acabei de portar para o montador de 64 bits a função de definir a faixa para reprodução e o algoritmo para obter a próxima frequência (e eu peguei o sétimo segmento inteiramente).


E funcionou. Depois disso, implementei as funções de geração da trilha sonora para a faixa selecionada ( PCM, 44100 Hz, 8 bits, mono ; fiz algo como um gerador usado no emulador de alto-falante do DosBox ). Resolvi o problema com o sinal do fim com um simples contador de silêncio: contei um segundo - concluímos o algoritmo. Envolvendo a faixa resultante no cabeçalho WAV e salvando o resultado em um arquivo, obtive exatamente a faixa no menu principal. E mais 13 faixas que você pode ouvir abaixo [ou no visualizador de recursos, que agora possui um player embutido e a capacidade de salvar qualquer faixa em .WAV] :



[Estudando a questão, aprendi sobre técnicas mais avançadas de "bip", como o uso da modulação por largura de pulso para uma reprodução de som PCM de baixa qualidade. No final deste artigo, fornecerei uma lista de materiais com os quais você pode aprender mais.]




Na segunda parte , quando vários formatos de recursos foram considerados, sugeri que os arquivos .ANH contenham animações para planos de fundo do local (ou seja, para imagens armazenadas em .PIC ). [É assim.] Depois de terminar o som, decidi dar uma olhada. Apenas no pressuposto de que a animação é aplicada diretamente à imagem de fundo armazenada na memória (não na memória de vídeo , mas na cadeia de sprites ), decidi despejar essa memória, respectivamente, antes e depois de aplicar a animação (veja onde o cursor aponta para o topo letra da letra 'S'):



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

Exatamente o que eu esperava - a cor vermelha escura (0x4) mudou para vermelho brilhante (0xC). Agora você pode tentar definir o ponto de interrupção para alterar o valor no endereço, por exemplo, 3DE6:0E28 e, se tiver sorte, realizar um rastreamento reverso. [Eu tive sorte.] O ponto de interrupção me levou a uma linha que altera diretamente o valor no endereço fornecido: xor es:[bx], al . Tendo examinado o ambiente, sub_1231E (xor es:[bx], al) <- sub_12222 <- sub_105F6 <- sub_1038F ( ) facilmente uma cadeia de chamadas do loop de nível principal até este ponto: sub_1231E (xor es:[bx], al) <- sub_12222 <- sub_105F6 <- sub_1038F ( ) .


Não vou entrar em detalhes sobre exatamente como invertei o processo de animação. Este é um trabalho bastante rotineiro e metódico, mas não muito difícil se os limites forem claramente delineados (a volta recebida é esses limites). Mas não posso deixar de falar sobre o que aconteceu no final.


Primeiro sobre o formato .ANH . Na verdade, é um conjunto de contêineres e a primeira palavra no arquivo .ANH é o número de contêineres dentro:


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

O contêiner em si é uma animação separada do elemento background. Você pode selecionar um cabeçalho para o contêiner que contém seu tamanho de byte e o número de quadros na animação que ele representa. Ao lado do cabeçalho estão os valores da duração (atraso) do próximo quadro e o deslocamento de bytes do próprio quadro, em relação aos bytes do primeiro quadro. O número de tais pares é obviamente igual ao número de quadros:


 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; } 

Um quadro separado é composto por um cabeçalho de quatro bytes que contém suas dimensões e deslocamentos lineares em relação à imagem de plano de fundo e os pixels do quadro codificados pelo algoritmo que já me é familiar pelo Comprimento da Execução :


 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 */ }; 

A "sobreposição" do quadro no pano de fundo pode ser assim:


 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; } 

Este é o formato .ANH , mas há outra estrutura que faz tudo funcionar:


 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; 

No próprio jogo, uma matriz de pelo menos quatro estruturas desse tipo é declarada globalmente. Depois de carregar o próximo arquivo .ANH , o número de animações dentro também é armazenado em uma variável global e os elementos da matriz são inicializados da seguinte maneira:


 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; } 

Por fim, aplique as animações:


 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; } } 

E temos o seguinte [você pode ver muito mais no visualizador de recursos] :


R2.ANH.GIF

R6.ANH.GIF

Há alguns problemas com a animação no momento. A primeira é que, no meu código, reproduzo todas as animações disponíveis, mas o original verifica se há alguns sinalizadores globais indicando se é necessário rolar o próximo. E a segunda, devido ao fato de que algumas animações adicionam objetos à tela que não estão originalmente lá. E como os quadros “brigam” no fundo, depois com uma rolagem cíclica, a cada segundo círculo o objeto simplesmente desaparece. Aqui, por exemplo, como pode parecer:


R53.ANH.GIF

Mas, por enquanto, vamos deixar como está.




Lembre-se do algoritmo de descompressão desconhecido da primeira parte ? Tendo mal conectado ao desenvolvimento, o viiri não apenas determinou que tipo de algoritmo era, mas também escreveu sua própria versão do decodificador, substituindo a terrível peça de três linhas do Assembler na base de código. A esse respeito, pedi à viiri que escrevesse um pequeno ensaio sobre o trabalho realizado. O que foi feito, mas antes que eu o cite, algumas palavras precisam ser ditas sobre a teoria.


Para compactar recursos, os desenvolvedores do Neuromancer usaram o código Huffman . Este é um dos primeiros métodos eficazes de codificar informações usando códigos de prefixo . Na teoria da codificação, códigos com uma palavra de comprimento variável e aqueles nos quais nenhuma palavra de código é um prefixo de outra são chamados códigos de prefixo . Ou seja, se a palavra "a" estiver incluída no código do prefixo, a palavra "ab" não existe no código. Essa propriedade permite que você divida exclusivamente em palavras a mensagem codificada por esse código.


A idéia do algoritmo de Huffman é que, sabendo a probabilidade de ocorrência de caracteres de um determinado alfabeto na mensagem, podemos descrever o procedimento para construir códigos de comprimento variável, consistindo em um número inteiro de bits. Símbolos com maior probabilidade de ocorrência recebem códigos mais curtos e símbolos com menor probabilidade, pelo contrário, códigos mais longos. Em geral, o procedimento de codificação é reduzido para construir a árvore de código ideal e, com base em, mapear o símbolo da mensagem para o código correspondente. A propriedade prefixo do código recebido permite decodificar exclusivamente a mensagem compactada.


Huffman.GIF

O algoritmo tem uma desvantagem significativa (de fato, não uma, mas agora apenas essa é importante). O fato é que, para recuperar o conteúdo de uma mensagem compactada, o decodificador deve conhecer a tabela de frequências de ocorrência de caracteres usados ​​pelo codificador. Nesse sentido, junto com a mensagem codificada, uma tabela de probabilidade ou a própria árvore de códigos (uma opção usada no jogo) devem ser transmitidas. O tamanho dos dados adicionais pode ser relativamente grande e isso afeta significativamente a eficiência da compactação.


Algo sobre como você pode lidar com isso, bem como sobre seu decodificador e o que é implementado no jogo, diz ao viiri :


Vale ressaltar que todo o jogo foi completamente escrito em Assembler, à mão, para que o código contenha soluções, truques e otimizações interessantes.

De acordo com os procedimentos. A função sub_1ff94 ( build_code_table ) é necessária para carregar uma árvore Huffman compactada de um arquivo. Para decodificar um Huffman estático (às vezes dinâmico , e esse requisito não se aplica a ele), juntamente com a mensagem, uma árvore de códigos deve ser transmitida, que é um mapeamento de códigos de Huffman para códigos de caracteres reais. Esta árvore é grande o suficiente e, portanto, seria bom armazená-la de alguma forma eficiente. A maneira mais correta é usar códigos canônicos ( MOAR ). Devido às suas propriedades, existe uma maneira muito interessante e eficaz de armazenar a árvore (usada na implementação do método de compactação Deflate do arquivador PKZip ). Mas no jogo, os códigos canônicos não são usados; em vez disso, é realizado um deslocamento direto da árvore e, para cada vértice, o bit 0 é gravado no fluxo de saída se o nó não for uma folha ou bit 1 se o nó for uma folha e os próximos 8 bits são o código de caractere. nó Ao decodificar, uma travessia de árvore semelhante é executada, o que vemos no jogo. um exemplo e alguma explicação.

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 ) lê um pouco do fluxo de entrada. Sua análise nos permite concluir que os bits individuais são extraídos do registro ax 16 bits, cujo valor é carregado da memória pela instrução lodsw , que carrega dois bytes do fluxo, mas como o processador Intel possui ordem de bytes little-endian , o xchg reorganiza metade do registro. Além disso, a ordem dos bits no fluxo é um tanto ilógica - o primeiro não é o menos, mas o mais significativo. , 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 . ( ), , :


0 00b1A
10 0b2B
110 b3C
111 b3D

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


0 00 00b1A
10 01b1A
20 10b1A
30 11b1A
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/pt417639/


All Articles