Back-End

17 nov, 2017

Processamento de áudio em tempo real com Ruby

100 visualizações
Publicidade

Quando eu li pela primeira vez sobre o MRuby, eu só queria brincar com ele. A primeira coisa que veio à minha mente foi um plugin VST. Eu poderia construir um pequeno wrapper e passar toda a função para o interpretador Ruby. Vou descrever como fiz isso.

Preparação

Antes de tudo, precisamos configurar algumas coisas. Comecemos com o MRuby.

MRuby

Precisamos clonar o repo do MRuby:

$ git clone <a href="https://github.com/mruby/mruby">https://github.com/mruby/mruby</a>
$ cd mruby

Depois disso, precisamos configurar nossa build MRuby. Devemos modificar build_config.rb. No final do arquivo, vamos adicionar nossas especificações de build.

MRuby::Build.new('mrubyvst') do |conf|
  toolchain :gcc
  conf.gembox 'default'
  conf.gem :core => 'mruby-eval'
  conf.gem :github => 'iij/mruby-dir'
  conf.gem :github => 'iij/mruby-io'
end

Neste caso, especificamos que usaremos uma cadeia de ferramentas gcc. Além disso, incluiremos todas as gems padrão e algumas outras. Em Mruby, gems são compiladas. Então, se você quiser mudar um conjunto de gems, deve recompilar o mruby build. Nós precisaremos de mruby-eval para permitir que o script Ruby carregue outros scripts ruby, mruby-dir para listar o conteúdo do diretório e mruby-io para acessar arquivos, etc.

Terminamos, devemos compilá-lo.

$ rake

No final do processo de build, veremos:

================================================
      Config Name: mrubyvst
 Output Directory: build/mrubyvst
    Included Gems:
             mruby-sprintf - standard Kernel#sprintf method
             mruby-print - standard print/puts/p
             mruby-math - standard Math module
             mruby-time - standard Time class
             mruby-struct - standard Struct class
             mruby-enum-ext - Enumerable module extension
             mruby-string-ext - String class extension
             mruby-numeric-ext - Numeric class extension
             mruby-array-ext - Array class extension
             mruby-hash-ext - Hash class extension
             mruby-range-ext - Range class extension
             mruby-proc-ext - Proc class extension
             mruby-symbol-ext - Symbol class extension
             mruby-random - Random class
             mruby-object-ext - Object class extension
             mruby-objectspace - ObjectSpace class
             mruby-fiber - Fiber class
             mruby-enumerator - Enumerator class
             mruby-enum-lazy - Enumerator::Lazy class
             mruby-toplevel-ext - toplevel object (main) ...
             mruby-compiler - mruby compiler library
             mruby-bin-mirb - mirb command
               - Binaries: mirb
             mruby-bin-mruby - mruby command
               - Binaries: mruby
             mruby-bin-strip - irep dump debug section ...
               - Binaries: mruby-strip
             mruby-kernel-ext - Kernel module extension
             mruby-class-ext - class/module extension
             mruby-eval - standard Kernel#eval method
             mruby-dir
             mruby-io

Ótimo, acabamos de construir o MRuby. Vamos continuar.

SDK VST

Queremos criar o plugin VST. Nós temos que baixar o SDK do site Steinberg e descompactá-lo em algum lugar do computador.

DAW

Também precisamos de um DAW, que carregará nosso plugin. Eu o testei com Ableton Live 9. Existe uma versão de teste, mas também uma demo. No modo demo, você não pode salvar seu projeto, mas não precisamos desse recurso. Existem builds de 32 e 64 bits – vamos construir uma versão de 64 bits, então precisaremos de um daw de 64 bits.

Construindo o VST

A próxima coisa a ser feita, é um plugin VST em si. Vamos clonar o repositório, configurá-lo e iniciá-lo dentro do DAW.

$ git clone https://github.com/fazibear/mrubyvst
$ cd mrubyvst
$ rake init

Dê uma olhada no Rakefile. No topo há constantes.

MRUBY_DIR = File.expand_path('../mruby')
VST_SDK_DIR = File.expand_path('../vst-sdk')
SCRIPT_PATH = File.expand_path('./mrubyvst.rb')
VST_CLASS = 'MRubyVST'
PROGRAMS_COUNT = 10
PARAMETERS_COUNT = 4

Temos que mudar MRUBY_DIR e VST_SDK_DIR para corrigir o local dessas bibliotecas no seu computador. SCRIPT_PATH aponta para um arquivo que será carregado na VM Ruby na inicialização, e VST_CLASS é o nome da classe que o MRuby instanciará. Agora podemos construí-lo!

$ rake

Nós apenas o construímos. Funciona? Não. Precisamos copiá-lo ou vinculá-lo a um diretório especial, então o DAW pode encontrá-lo. Nos plugins Mac VST, estão em ~/Library/Audio/Plug-Ins/VST/. Mas espere. Existe um script que fará isso por você.

$ rake link

Vamos linkar sua build VST a esse diretório. Você também pode desfazer o link.

$ rake unlink

Começando um DAW

Estamos prontos para lançar o DAW. Se você não sabe como usar o plugin VST, aqui está um pequeno tutorial. Boa sorte.

É assim que o VST se parece no Ableton Live (em outro DAW, parecerá diferente).

Ótimo! O plugin funciona. Agora vamos analisar os detalhes da implementação.

Implementação

Dê uma olhada no arquivo mrubyvst.h.

No VST, existem quatro grupos de coisas que precisamos implementar.

O mais importante é o processamento. Nesse método, mudaremos os dados de áudio de entrada. Mas vamos abordar esse método mais tarde. O segundo grupo é “programas”. O programa é um estado de todos os parâmetros. E os parâmetros são variáveis únicas das quais o processamento de som depende. O último é informação. VST tem que devolver algumas informações para se identificar.

Vamos visualizá-los. No topo, vemos mrubyvst, é um nome do nosso plugin. Dropdown com empty.rb é um campo de programas. Existe uma lista de programas. E, à direita, existem quatro parâmetros com nomes e valores. Precisamos decidir quantos programas e parâmetros precisamos antes da compilação. No nosso exemplo, existem 10 programas e quatro parâmetros.

Mas espere. E quanto ao Ruby? Esse é o arquivo de cabeçalho C++! Isso está correto, precisamos criar um pequeno wrapper que transmita informações para e a partir da VM Ruby. Fique calmo.

Inicialização

O construtor é um ótimo lugar para inicializar nossa terra do Ruby. Depois disso, precisamos carregar o script Ruby e instanciar nossa classe MRubyVST. Além disso, precisamos definir algumas constantes nessa classe.

MRubyVst::MRubyVst(audioMasterCallback audioMaster): AudioEffectX(audioMaster, PROGRAMS_COUNT, PARAMETERS_COUNT) {
  setUniqueID(666);
  canProcessReplacing();

  setNumInputs(2);
  setNumOutputs(2);

  mrb = mrb_open();

  FILE *file = fopen(SCRIPT_PATH, "r");
  if (file != NULL) {
    mrb_load_file(mrb, file);

    mrb_value vst_class = mrb_vm_const_get(mrb, mrb_intern_lit(mrb, VST_CLASS));
    mrb_const_set(mrb, vst_class, mrb_intern_lit(mrb, "PROGRAMS_COUNT"), mrb_fixnum_value(PROGRAMS_COUNT));
    mrb_const_set(mrb, vst_class, mrb_intern_lit(mrb, "PARAMETERS_COUNT"), mrb_fixnum_value(PARAMETERS_COUNT));
    mrb_const_set(mrb, vst_class, mrb_intern_lit(mrb, "SAMPLE_RATE"), mrb_float_value(mrb, getSampleRate()));
    mrb_const_set(mrb, vst_class, mrb_intern_lit(mrb, "SCRIPT_PATH"), mrb_str_new_cstr(mrb, (SCRIPT_PATH)));

    vst_instance = mrb_instance_new(mrb, vst_class);

    fclose(file);
  }
}

A parte mais importante é a linha 8. Aqui é onde iniciamos a VM Ruby embutida. Em seguida, abrimos um arquivo e, se ele existe, é carregado em nossa VM.

Agora o nosso script Ruby está carregado. A linha 14 receberá nossa classe Ruby e nos permitirá estabelecer algumas constantes. Depois disso, podemos instanciá-lo e fechar o arquivo.

O script Ruby

class MRubyVST
  attr_reader :vendor, :product, :effect_name, :version

  def initialize
    @vendor = 'Mruby'
    @product = 'MrubyVST'
    @effect_name = 'MRubyEffects'
    @version = 0
    @programs_dir = "#{File.dirname(SCRIPT_PATH)}/programs"
  end

  def programs
    Dir.entries(@programs_dir) - ['.', '..']
  end

  def load_program(path)
    Module.new.instance_eval(
      File.open(path).read
    )
  end

  def change_program(index)
    @program = load_program("#{@programs_dir}/#{programs[index]}") if programs[index]
  end

  def program_name(index)
    programs[index] || "-empty-"
  end

  def set_parameter(index, value)
    @program.set_parameter(index, value) if @program
  end

  def parameter_name(index)
    @program.parameter_name(index) if @program
  end

  def parameter_value(index)
    @program.parameter_value(index) if @program
  end

  def parameter_display_value(index)
    @program.parameter_display_value(index) if @program
  end

  def parameter_label(index)
    @program.parameter_label(index) if @program
  end

  def process(data)
    @program.process(data) if @program
  end

  def log(str)
    io = File.open('/tmp/mrubyvst.log', 'a')
    io.write(str + "\n")
    io.close
  end
end

Esse é o script Ruby. Todas as informações estão aqui. Fornecedor, produto, versão, etc. Nosso plugin usará programas para carregar vários módulos Ruby. Quando o usuário mudar um programa, instanciaremos a nova classe e passaremos a maioria dos métodos para esse módulo. A lista de programas é formada apenas por arquivos do diretório de programas. O submódulo fica assim:

class GainVST
  def initialize
    @gain = 1.0
  end

  def set_parameter(index, value)
    @gain = value if index = 0
  end

  def parameter_name(index)
    index == 0 ? 'Gain' : 'empty'
  end

  def parameter_value(index)
    @gain if index == 0
  end

  def parameter_display_value(index)
    @gain.to_s if index == 0
  end

  def parameter_label(index)
    'dB' if index == 0
  end

  def process(data)
    data[0].map!{ |left | left * @gain}
    data[1].map!{ |right| right * @gain }
    data
  end
end

GainVST.new

É um plugin de ganho simples. Isso alterará o volume de áudio de entrada, dependendo de como o ganho é definido. O padrão é 1. O que esses métodos fazem? Usaremos apenas um parâmetro com índice 0 (zero).

  • parameter_name: retorna um nome de parâmetro com um dado índice.
  • parameter_value: retorna um valor de parâmetro com dado índice.
  • parameter_display_value: retorna um valor de parâmetro com um dado índice como uma string, e você pode formatá-lo como quiser.
  • parameter_label: retorna uma etiqueta do nosso parâmetro.
  • set_parameter: estabelece um valor de parâmetro com um dado índice.
  • process: processa dados de entrada e retorna a saída. Neste caso, irá multiplicar todos os valores de entrada com um valor de ganho.

Wrapper MRuby

Agora precisamos voltar para o nosso wrapper e implementar métodos que passem dados para Ruby e de volta. A maioria dos métodos é muito semelhante. Veja um aqui:

bool MRubyVst::getProgramNameIndexed(VstInt32 category, VstInt32 index, char* text) {
  m.lock();
  if(!mrb_nil_p(vst_instance) && mrb_respond_to(mrb, vst_instance, mrb_intern_lit(mrb, "program_name"))){
    mrb_value mrb_name = mrb_funcall(mrb, vst_instance, "program_name", 1, mrb_fixnum_value(index));
    if (!mrb_nil_p(mrb_name)) {
      vst_strncpy(text, RSTRING_PTR(mrb_name), kVstMaxProgNameLen);
      m.unlock();
      return true;
    }
  }

  m.unlock();
  return false;
}

Como o MRuby não é thread-safe, precisamos usar o Mutex. Bloqueie-o quando começar a usar o MRuby e o desbloqueie no final. Verifique se a instância VST não é nula e se ela responde ao método program_name. Agora podemos invocá-lo e se o resultado não for nulo, podemos decodificá-lo e copiá-lo para que o VST possa lê-lo. Retornar verdadeiro significa que copiamos algo. Não é muito complicado descobrir quando e como os dados são convertidos. Agora, dê uma olhada no mais importante – processamento de áudio.

void MRubyVst::processReplacing(float** inputs, float** outputs, VstInt32 sampleFrames) {
  m.lock();
  if(!mrb_nil_p(vst_instance) && mrb_respond_to(mrb, vst_instance, mrb_intern_lit(mrb, "process"))){
    int ai = mrb_gc_arena_save(mrb);

    float* in1  =  inputs[0];
    float* in2  =  inputs[1];
    float* out1 = outputs[0];
    float* out2 = outputs[1];

    mrb_value mrb_inputs = mrb_ary_new(mrb);
    mrb_value mrb_input_1 = mrb_ary_new(mrb);
    mrb_value mrb_input_2 = mrb_ary_new(mrb);

    for (int i=0;i<sampleFrames;i++) {
      mrb_ary_push(mrb, mrb_input_1, mrb_float_value(mrb, (*in1++)));
      mrb_ary_push(mrb, mrb_input_2, mrb_float_value(mrb, (*in2++)));
    }

    mrb_ary_push(mrb, mrb_inputs, mrb_input_1);
    mrb_ary_push(mrb, mrb_inputs, mrb_input_2);

    mrb_value mrb_outputs = mrb_funcall(mrb, vst_instance, "process", 1, mrb_inputs);

    //mrb_value mrb_outputs = mrb_inputs;

    if (!mrb_nil_p(mrb_outputs)) {
      mrb_value mrb_output_1 = mrb_ary_shift(mrb, mrb_outputs);
      mrb_value mrb_output_2 = mrb_ary_shift(mrb, mrb_outputs);

      for (int i=0;i<sampleFrames;i++) {
        (*out1++) = mrb_float(mrb_ary_shift(mrb, mrb_output_1));
        (*out2++) = mrb_float(mrb_ary_shift(mrb, mrb_output_2));
      }
    }
    mrb_gc_arena_restore(mrb, ai);
  }
  m.unlock();
}

Poucas coisas acontecem aqui. Precisamos instruir o Ruby Garbage Collector para remover os dados não utilizados. Isso é o que fazem as linhas 4 e 36. Todos os objetos criados entre essas linhas podem ser coletados pelo coletor de lixo, portanto não teremos vazamentos de memória. Em seguida, precisamos converter um array de flutuadores no array de flutuadores do Ruby. Crie arrays Ruby para cada entrada e adicione um flutuador convertido a ele. Agora, podemos invocar um método de processo com parâmetros. O resultado desse método é convertido de volta para o array C de flutuadores e depois passado para o DAW.

Palavras finais

Sim, funciona. Claro, o plugin de ganho simples leva muito mais CPU do que o escrito em C. Mas, como você sabe, precisamos empacotá-lo no objeto Ruby e extrair no final. Agora você pode fazer o que quiser com o fluxo de áudio em Ruby. Existem alguns exemplos de plugins como pan e stereo enhancer. Mas você também pode criar o seu próprio. Tente executar o servidor web dentro dele. Qualquer coisa. Em Ruby, isso é mais simples e mais divertido.

MRubyByExample

Foi difícil fazer o debug do comportamento do MRuby no VST. Então fiz um pequeno script para testá-lo. Se você quiser brincar com o MRuby, você pode verificar este repo e observar esta documentação. Talvez você encontre algo interessante lá.

Esperando que tenha sido útil. Obrigado por ler! Se você gostou, click em curtir.

Obrigado!

***

Michał Kalbarczyk faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://blog.fazibear.me/processing-audio-with-ruby-330796afd06