Não recomendo: Usando funções da libc em códigos puramente em assembly

Sempre topo com essa questão em foruns e mídias como o Discord: Como usar funções da biblioteca padrão de C em códigos puramente escritos em assembly? É “fácil”, mas eu não recomendo. Primeiro mostrarei como, usando o NASM e o GCC e depois digo o por quê da não recomendação. Eis um “hello, world” simples:

; test.asm
; Compile com:
;   nasm -felf64 -o test.o test.asm

bits 64

; A glibc exige "RIP relative addressing". 
; De qualquer maneira, isso é sempre uma boa 
; prática no modo x86-64.
default rel

section .rodata

msg:
  db    `hello, world`,0

section .text

  ; Este símbolo está na glibc!
  extern puts

  global main
main:
  ; Chama puts(), passando o ponteiro da string.
  lea   rdi,[msg]
  call  puts wrt ..plt

  ; return 0;
  xor   eax,eax
  ret

O código acima é diretamente equivalente a:

#include <stdio.h>

int main( void )
{
  puts( "hello, world" );
  return 0;
}

Para compilar, linkar e testar:

$ nasm -felf64 -o test.o test.asm
$ gcc -o test test.o
$ ./test
hello, world

Simples assim. O gcc deve ser usado como linker porque ele sabe onde a libc está.

O wrt ..plt é um operador do NASM chamado With Referece To. Ele diz que o símbolo está localizado em uma section específica. No caso a section ..plt, que contém os saltos para as funções da libc (late binding). Isso é necessário, senão obterá um erro de linkagem porque o linker não encontrará o símbolo… PLT é a sigla de “Procedure Linkage Table“. É uma tabela com ponteiros para as funções carregadas dinamicamente da libc.

Eis o porque de minha não recomendação: O compilador nem sempre usa o nome “oficial” da função, que pode ser um #define num header e, ás vezes, existem argumentos adicionais “invisíveis” para certas funções “intrínsecas”, mas o principal argumento contra essa prática é que seu código não ficará menor ou mais rápido só porque você está usando assembly. Por exemplo, ambos os códigos em C e Assembly, acima, fazem exatamente a mesma coisa e têm o mesmíssimo tamanho final (6 KiB, extirpando os códigos de debbugging e stack protection, no caso de C).

De fato, é provável que você crie um código em assembly pior do que o compilador C criaria…

Addedum: Não recomendo, particularmente, que se tente escrever códigos em assembly que usem a libc no ambiente Windows. O problema é que Windows coloca algum código adicional nas funções. Por exemplo, A pilha deve ser alterada para acomodar SEH (Structured Exception Handling), no caso de main, é necessário chamar uma função chamada _main… Coisas assim são problemáticas para o novato.

Por que dimensionar vídeos com número par de linhas e colunas?

Já reparou nisso? Se você pede para dimensionar seus vídeos com número ímpar de linhas ou colunas o ffmpeg (e outros encoders) reclamam. Ainda, às vezes, quando você edita seu vídeo dimensionado dessa forma, ou observa colunas ou linhas pretas adicionadas no lado direito ou abaixo, ou linhas com “lixo” (um padrão de pixels maluco)… Isso acontece porque vídeos codificados com o padrão YUV 4:2:0 (ou NV12, por exemplo) ou qualquer um dos padrões YUV exigem que 4 pixels sejam agrupados juntos (2 pixels horizontais versus 2 pixels verticais) e, portanto, sua resolução tem que ter componentes pares.

Outro detalhe é relativo aos codecs. Se você usa um dos dois mais populares: h264 ou h265, cada quadro é dividido em blocos de pixels de tamanho (horizontal e vertical) múltiplos de 2^n (4×4, 8×8, 16×16 ou, no caso do h265, outros tamanhos múltiplos de 2^n). Isso significa, de novo, que a imagem precisa ter dimensionamento par nos dois sentidos.

Fica a dica…

O pesadelo dos “padrões” de tamanhos de vídeos

De novo esse troço? Well… Info para os aficionados por vídeos: Existem vários padrões de tamanho!

Os padrões mais simples de lembrar são os formatos widescreen, de aspecto \frac{16}{9}. Aqui temos o formato HD (1280×720), Full HD (1920×1080) e 4K (3840×2160). Vale notar que o formato 4K tem o dobro do tamanho (vertical e horizontal) do formato Full HD. Outra nota interessante é que ele não é, necessariamente “4K” (4 mil), mas tem 3840 pixels horizontais! Se bem que esses formatos geralmente são medidos pela quantidade de linhas, não de colunas. Seria esperado que o formato 4K fosse referenciado como 2.16K! :)

Existem padrões mais antigos também. O formato desses não seguem o aspecto \frac{16}{9}, mas são mais “quadrados”. Por exemplo, existe o padrão NTSC-M (720×486), resultando num formato de aspecto \frac{40}{27}, bem próximo do padrão letterbox de aspecto \frac{4}{3}. Por quê 486 linhas? No formato NTSC temos dois quadros entrelaçados de 262.5 linhas, ou 525 linhas por quadro completo, mas apenas 486 são visíveis, as 39 linhas “invisíveis” são usadas no pulso de retraço vertical… No entanto, existem formatos europeus (um deles usado no Brasil também): PAL-M e SECAM. O formato PAL-M usa as mesmas 525 linhas, mas tem 480 visíveis, resultando no formato 720×480 com um aspecto de \frac{3}{2}. É um aspecto um pouco mais “largo” (wide) que o letterbox. O padrão SECAM tem mais linhas que o NTSC-M e PAL-M (625 linhas), mas, até onde sei, não é mais usado.

Até aqui temos os formatos disponibilizados para TVs. No entanto, existem diversos formatos para vídeos em outras mídias, como cinema, por exemplo. Na telona o aspecto mais usado é de \frac{11}{8} e IMAX usa os aspectos de \frac{143}{10} ou \frac{19}{10}, dependendo do filme. Isso não significa que não existam outros aspectos em uso. Filmes chamados de “35 mm” costumam ter aspecto de \frac{3}{2}, ou seja, são duas vezes mais largos que a altura; “Super 16 mm”, por outro lado, usam \frac{5}{3}; Enquanto “70 mm” usam \frac{11}{5}; TVs antigas e alguns monitores tinham aspecto de \frac{5}{4}; Alguns distribuidores de filmes usam o aspecto de \frac{14}{9} para facilitar o escalonamento para os aspectos widescreen e letterbox; Alguns monitores atuais têm aspecto de \frac{8}{5}; O padrão norte-americano para a telona de cinema costuma seguir o aspecto de \frac{37}{2}… É um pesadelo, não? Não tenho como citar todas os “padrões” existentes porque simplesmente não tenho a acesso a todos eles. Não citei, por exemplo, o usado em “Super 8 mm” (popular nos primórdios, especialmente pela distribuição de filmes pornográficos nesse formato!).

Assim, recomendo que você se atenha aos padrões “de fato” mais comuns, como 540p (aspecto \frac{4}{3}); 720p, 1080p e 2160p (aspecto \frac{16}{9}) e use aquele esquema de colocar barras horizontais (ou verticais, se assim for necessário), centralizando o vídeo, se o mesmo não tiver o aspecto desejado.

Ahhh… ainda existe o pesadelo da velocidade de reprodução (quadros por segundo)… mas, deixarei isso pra depois…

Convertendo vídeos para 1280×720 (juntando tudo)

Eis o script final para converter vídeos “widescreen”, mesmo com resoluções malucas. Aqui, considero qualquer vídeo com aspecto maior ou igual a \frac{16}{9}-10\%, ou \frac{159}{90}, como “widescreen”. Note que a comparação (em is_wide()) é feita usando cálculo inteiro, não ponto flutuante:

#!/bin/bash

# v2wide <files>
if [ -z "$1" ]; then
  echo -e "\e[1;33mUsage\e[m: $0 <files>"
  exit 1
fi

# Qualquer vídeo com variação de aspect ratio de 16/9 - 10%
# ou superior, considero como widescreen.
is_wide() {
  if [[ $(($1 * 90)) -ge $(($2 * 159)) ]]; then
    return 0
  fi
  return 1
}

showvideoinfo() {
  FR="`mediainfo --inform='Video;%FrameRate%' "$1"`"
  DURATION="`mediainfo --inform='Video;%Duration/String3%' "$1"`"
  SIZE="`du -h "$1" | cut -f1`"

  echo -e "\e[1m'$1'\e[m (${2}x${3}, $FR fps, $DURATION, $SIZE)"
}

# Converte TODOS os vídeos passados na linha de comando.
# Aceita, inclusive "globs", como '*.mkv', por exemplo.
for i in "$@"; do
  W="`mediainfo --inform='Video;%Width%' "$i"`"
  H="`mediainfo --inform='Video;%Height%' "$i"`"

  if ! is_wide $W $H; then
    echo -e "'$i': \e[1;31mNot a widescreen video.\e[m"
    continue
  fi

  NW=1280
  NH="`bc <<< "scale=3; 1280 / ( $W / $H )" | sed -E 's/\..+$//'`"

  # Se o aspecto é inferior a 16/9, considera 16/9
  if [ $NH -gt 720 ]; then
    NH=720
  fi
  BARH=$(( ( 720 - $NH ) / 2 ))

  FR='ntsc-film'  # Video framerate
  VBR="`bc <<< 'scale=3;(1280*720*(24000/1001))/13.824' | sed -E 's/\..+$//'`" # Video bitrate
  ABR='128k'      # Audio bitrate
  ASR='44.1k'     # Audio sampling rate

  # Cria um arquivo temporário em /tmp/
  TMPFILE="`mktemp /tmp/XXXXXXXXX.mp4`"

  # Mostra infos do arquivo original (aproveira width
  # e height já obtidos).
  showvideoinfo "$i" $W $H

  # Usando NVIDIA NENC para h264.
  #
  # Mude o codec para h265_nvenc para h265 (duh!).
  #
  # Note que sua placa pode codificar, no máximo, dois
  # vídeos simultâneos usando NVENC (a maioria das
  # low-end NVIDIA têm esse limite a não ser a Quadro e
  # Titan - que podem codificar 30, acho).
  #
  # Aqui, uso a aceleração CUDA (cuvid) e o encoder da NVIDIA
  # (h264_nvenc);
  #
  # Uma sincronização adaptativa de frames baseada no "novo"
  # framerate (vsync 1);
  #
  # Formato de subsampling de chroma YUV 4:2:0 progressivo;
  #
  # Codec de áudio AC3 (mude para fdk AAC se quiser);
  #
  # Downsampling para stereo (ac 2);
  #
  # Sampling rate de 44.1 kHz;
  #
  # Resincronização de áudio com vídeo (porque o framerate
  # pode ter mudado e a sincronização (vsync) pode "comer"
  # frames, mudando as marcas de temporização.
  #
  # Eliminação de metadados e informação de "capítulos"
  # (se houverem).
  #
  # -y (assume yes), -v error (só mostra erros se houverem),
  # mostra apenas a estatística da codificação (-stats).
  #
  # Repare que o arquivo original é apagado e substituído por um
  # de mesmo nome, mas com extensão .mp4. Em caso de falha,
  # o arquivo temporário é apagado.
  ffmpeg -y \
         -v error \
         -stats \
         -hwaccel cuvid \
         -i "$i" \
         -c:v h264_nvenc \
         -r $FR \
         -bufsize $VBR \
         -minrate $VBR \
         -maxrate $VBR \
         -b:v $VBR \
         -vsync 1 \
         -pix_fmt yuv420p \
         -vf scale=$NW:$NH:lanczos,pad=1280:720:0:$BARH:black,setdar=16/9 \
         -c:a ac3 \
         -b:a $ABR \
         -ac 2 \
         -ar $ASR \
         -async 1 \
         -map_metadata -1 \
         -map_chapters -1 \
         "$TMPFILE" && \
  ( rm "$i"; mv "$TMPFILE" "${i%.*}.mp4" ) || \
  rm "$TMPFILE"
done

Concatenando vídeos com ffmpeg

Existem alguns meios de “juntar” dois ou mais vídeos num só. O ffmpeg possui um filtro para isso, um muxer e até um protocolo. São 3 métodos diferentes de fazer a mágica, mas a minha preferida é converter os vídeos em um formato headless, juntar os arquivos manualmente e depois recodificá-lo.

O jeito que consome menos espaço em disco, e é mais rápido, é converter os vídeos para o formato mpeg2 (container e codecs – de vídeo e áudio). Esse formato é headless, ao contrário do que temos no container mpeg4, como pode ser visto abaixo:

MPEG-4 vs MPEG-2

Repare que o container MPEG-4 possui um header cheio de metadados (que podem ser retirados), mas o MPEG-2, não! Embora o formato MPEG-2 não tenha um header, as infos dos streams estão disponíveis. Como o arquivo MPEG-2 é comprimido também, ele não consumirá muito disco, mas você terá uma pequeníssima degradação da imagem ao converter os vídeos originais para MPEG-2, juntá-los e depois reconverter tudo de volta para MPEG-4 ou usando outro par de container/codecs quaisquer.

De qualquer maneira, para juntar dois vídeos basta ter certeza que estejam com a mesma resolução, mesmo frame rate (e o mesmo sampling rate do áudio também)… Como exemplo, suponha que tenhamos um video1.mkv e um video2.avi e eu queira juntá-los. Suponha que ambos tenham resolução wide screen, mas o primeiro seja 1920×1080 e o segundo 1280×720. Suponha, ainda, que eu queira um vídeo final em 1280×720:

$ ffmpeg -i video1.mkv \
         -s 1280x720 -r ntsc-film \
         -maxrate 2M -bufsize 2M -b:v 2M \
         -b:a 128k -ar 44.1k -ac 2 \
         video1.mpg
$ ffmpeg -i video2.avi \
         -s 1280x720 -r ntsc-film \
         -maxrate 2M -bufsize 2M -b:v 2M \
         -b:a 128k -ar 44.1k -ac 2 \
         video2.mpg
$ cat video{1,2}.mpg > video.mpg
$ ffmpeg -i video.mpg \
         -c:v libx264 \
         -maxrate 1.6M -bufsize 1.6M -b:v 1.6M \
         -c:a ac3 \
         -b:a 128k
         video.mp4
$ rm *.mpg

Os primeiros dois comandos ffmpeg convertem os vídeos para MPEG-2, garantindo a resolução de 1280×720 e o frame rate de 23.976 fps. Informei o bitrate do vídeo ligeiramente maior que o necessário para não perder qualidade (o ffmpeg usa bitrate de 200 kb/s de vídeo e 192 kb/s de áudio por default!). Repare, ainda, que não forneci os codecs de vídeo e áudio para os arquivos MPEG-2. O ffmpeg usa os defaults (mpeg2video e mp2).

Obtidos os vídeos video1.mpg e video2.mpg, é uma questão de concatená-los via comando cat e recodificar o resultado para o formato desejado. Neste caso, usando os codecs libx264 (h264) e AC-3, para um container MPEG-4.

Podemos evitar o problema de dupla degradação da imagem, por recompressões sucessivas, obtendo as características dos vídeos e convertendo-os para um formato não comprimido (RAW), mas, ainda assim, headless. Primeiro, converto os vídeos para um formato RAW com frame rate e formato de chroma subsampling conhecidos, respectivamente ntsc-film (23.976 fps) e YUV 4:2:0p, e também na mesma resolução (1280×720). Mais ou menos como fizemos antes:

$ ffmpeg -i video1.mkv 
         -r ntsc-film \
         -pix_fmt yuv420p \
         -s 1280x720 \
         -an \
         video1.yuv
$ ffmpeg -i video2.avi \
         -r ntsc-film \
         -pix_fmt yuv420p \
         -s 1280x720 \
         -an \
         video2.yuv

Note que o bitrate é supérfluo, já que estamos convertendo para um formato RAW. Um aviso: Prepare-se! Os vídeos *.yuv, por não serem comprimidos, ocuparão um espaço enorme em disco. Para dar uma ideia, um vídeo de uns 30 minutos, por exemplo, pode chegar a ocupar uns 100 GiB! (Yep! GibiBytes!) Esse é o preço que se paga para tentar manter a qualidade…

Os “containers” *.yuv não permitem múltiplos streams. Trata-se de um arquivo com um monte de bytes, sem nenhum metadado sobre o seu conteúdo. Assim, temos que extrair os áudios, com o mesmo sample rate e quantidade de canais, mas no formato PCM (aqui usei 32 bits, little endian, sinalizado). Os áudios também não são comprimidos e, portanto, temos menos perdas, mas bastante espaço em disco usado:

$ ffmpeg -i video1.mkv -vn \
         -f s32le -ar 44.1k -ac 2 \
         audio1.raw
$ ffmpeg -i video2.avi -vn \
         -f s32le -ar 44.1k -ac 2 \
         audio2.raw

De novo, o bitrate do áudio é supérfluo aqui…

Agora, junto tudo (vídeos e áudios, isoladamente) com o cat e depois converto pro formato desejado:

$ cat video{1,2}.yuv > video.yuv
$ cat audio{1,2}.raw > audio.raw
$ ffmpeg -f rawvideo \
         -pix_fmt yuv420p -s 1280x720 -r ntsc-film \
         -i video.yuv \
         -f s32le -ar 44.1k -ac 2 \
         -i audio.raw \
         -c:v libx264 -r ntsc-film \
         -maxrate 1.6M -bufsize 1.6M -b:v 1.6M \
         -c:a ac3 -b:a 128k \
         -map 0 -map 1 \
         -f mp4 videoout.mp4
$ rm *.yuv *.raw

O ffmpeg obedece a ordem das opções fornecidas. Antes da inclusão do arquivo de vídeo temos que dizer que ele é rawvideo e o seu formato (as opções -f, -pix_fmt, -s e -r, antes da opção -i). Depois, temos que fazer o mesmo para o áudio. Depois da inclusão dos arquivos as opções se aplicam à saída. Note que usei as opções -map para dizer que ambas as entradas têm que ser usadas na saída.

Por fim, matamos os arquivos temporários (que, provavelmente, estarão consumindo o seu disco todo, se houver espaço).

Recomendo usar alguma variação disso apenas se você quer manter a qualidade… Uma dica para a saída é não usar as opções -maxrate, -bufsize e -b:v, mas sim a opção -qp 1, que te dará a máxima qualidade possível (mas um arquivo grande), se qualidade é realmente importante no arquivo final. No que se refere ao áudio, aumente o bitrate, se necessário (192 kb/s é mais que suficiente!).

Usando um frame-rate e um bit-rate menores…

Ok… “comi mosca”, como dizia um professor meu.

Até o momento eu estava recodificando vídeos com um frame rate de \frac{30000}{1001} fps (ou 29.97 fps), ou seja, padrão NTSC. Com isso, segundo minha “formuletinha”, que mostrei aqui, para resolução HD, ou 720p, tería um bit rate de 1.998 Mb/s.

Mas… Que tal usar um frame rate e um bit rate menores? O menor frame rate padronizado é o NTSC-FILM (\frac{24000}{1001} fps, ou 23.976 fps. Isso nos dá um bit rate, para 1280×720, de 1.599 Mb/s… Esses cerca de 400 kb/s a menos nos dá uma redução de cerca de duas vezes o tamanho do stream de vídeo. Se, usando minha “formuletinha” eu conseguia uma redução de 4 a 6 vezes do tamanho do stream, agora tenho uma redução de 6 a 8 vezes!

E não… com a redução do frame rate você não tem uma degradação do vídeo. Pelo menos, não uma que você possa perceber (a não ser que você venha de Krypton, seja o Flash ou tenha os poderes de percepção de um pombo ou uma mosca!).

Considere que tenhamos um vídeo “videoin.mp4” com resolução de 1920×1080 @ 30 fps, 4.5 Mb/s… Querendo reduzir isso significativamente, eu faço:

$ ffmpeg -hwaccel cuvid \
         -i videoin.mp4 \
         -c:v h264_nvenc \
         -r ntsc-film \
         -maxrate 1.599M -bufsize 1.599M -b:v 1.599M \
         -vf scale=1280:720:flags=lanczos,setdar=16/9 \
         -c:a ac3 \
         -b:a 128k \
         -ac 2 \
         -ar 44.1k \
         -async 1 \
         -map_metadata -1 \
         videoout.mp4

Espere obter um arquivo umas 6 vezes menor (pelo menos!).

Portabilidade: O tamanho de um byte (ISO 9989)

Sempre achei que o símbolo CHAR_BIT, definido em limits.h, fosse apenas o tamanho, em bits, do tipo char. Well… estava enganado. A sessão 6.2.6.1.4 da ISO 9989:1999 (em diante) nos diz que:

“Values stored in non-bit-field objects of any other object type consist of n × CHAR_BIT bits, where n is the size of an object of that type, in bytes. The value may be copied into an object of type unsigned char [n] (e.g., by memcpy); the resulting set of bytes is called the object representation of the value. Values stored in bit-fields consist of m bits, where m is the size specified for the bit-field. The object representation is the set of m bits the bit-field comprises in the addressable storage unit holding it. Tw o values (other than NaNs) with the same object representation compare equal, but values that compare equal may have different object representations.”

Note a frase inicial: “Valores armazenados em objetos não bit-field de qualquer objeto consistem em n*CHAR_BIT bits”.

Isso é interessante, já que, em arquiteturas antigas, 1 “byte” podia ter tamanho diferente de 8 bits.

Fica a dica…