Back-End

16 fev, 2017

Processamento de áudio em tempo real com Ruby

Publicidade

Quando eu li pela primeira vez sobre MRuby, eu só quis brincar com ele. A primeira coisa que veio à minha mente foi um plugin VST. Eu poderia construir um pequeno wraper e passar todas as funções para o interpretador Ruby. Vou descrever como eu fiz isso.

Preparando

Antes de tudo, precisamos configurar algumas coisas. Vamos começar com MRuby.

MRuby

Precisamos clonar o respositório MRuby:

$ git clone https://github.com/mruby/mruby
$ cd mruby

Depois disso, precisamos configurar nossa compilação MRuby. Precisamos modificar build_config.rb. No final do arquivo, vamos adicionar nossa especificação de compilação.

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 toolchain gcc. Além disso, incluiremos todas as gems padrão e algumas outras. Em MRuby, gems são compiladas dentro. Então, se você quiser mudar um conjunto de gems, você tem que recompilar a compilação mruby. Vamos precisar do 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.

Estamos prontos, devemos compilá-lo.

$ rake

No final do processo de build, nós 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 MRuby. Vamos continuar.

SDK VST

Queremos construir o plugin VST. Baixe o SDK e o descompacte em algum lugar em seu computador.

DAW

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

Construindo VST

A próxima coisa é 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 em Rakefile, há constantes no topo.

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 de mudar MRUBY_DIR e VST_SDK_DIR para corrigir o local destas bibliotecas no seu computador. SCRIPT_PATH aponta para um arquivo que será carregado em Ruby VM na inicialização e VST_CLASS é o nome da classe que o MRuby instanciará. Agora podemos construí-lo!

$ rake

Nós acabamos de fazer o build. Funciona? Não. Precisamos copiar ou linkar a um diretório especial, para que o DAW possa encontrá-lo. No Mac, plugins VST estão em ~/Library/Audio/Plug-Ins/VST/. Mas espere: existe um script que vai fazer isso por você.

$ rake link

Ele fará o link da sua compilação VST a esse diretório. Você também pode desfazer o link.

$ rake unlink

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

Isso é como o VST deve se parecer em Ableton Live (em outro DAW, parecerá diferente)

Ótimo! O plugin funciona, agora vamos olhar para detalhes de implementação.

Implementação

Dê uma olhada no arquivo mrubyvst.h.

#include "public.sdk/source/vst2.x/audioeffectx.h"

#include <mutex>

#include "mruby.h"
#include "mruby/compile.h"
#include "mruby/string.h"
#include "mruby/array.h"
#include "mruby/class.h"
#include "mruby/variable.h"

//-------------------------------------------------------------------------------------------------------
class MRubyVst : public AudioEffectX {

public:
  MRubyVst(audioMasterCallback audioMaster);
  ~MRubyVst();

  // Processing
  virtual void processReplacing(float** inputs, float** outputs, VstInt32 sampleFrames);

  // Program
  virtual void setProgram(VstInt32 index);
  virtual bool getProgramNameIndexed(VstInt32 category, VstInt32 index, char* text);

  // Parameters
  virtual void setParameter(VstInt32 index, float value);
  virtual float getParameter(VstInt32 index);
  virtual void getParameterLabel(VstInt32 index, char* label);
  virtual void getParameterDisplay(VstInt32 index, char* text);
  virtual void getParameterName(VstInt32 index, char* text);

  // Info
  virtual bool getEffectName(char* name);
  virtual bool getVendorString(char* text);
  virtual bool getProductString(char* text);
  virtual VstInt32 getVendorVersion();

  // Helpers
  void log(const char* txt);

protected:
  std::mutex m;
  mrb_state *mrb;
  mrb_value vst_instance;
};

Em VST, há quatro grupos de coisas que precisamos implementar.

O mais importante é o processamento. Neste método, vamos alterar uma entrada de dados de áudio. Mas vamos abordar este método mais tarde. O segundo grupo são os programas. O programa é um estado de todos os parâmetros. E os parâmetros são variáveis únicas de que depende o processamento de som. O último é info. VST tem que retornar algumas peças de informação 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 4 parâmetros com nomes e valores. Precisamos decidir quantos programas e parâmetros precisaremos antes da compilação. Em nosso exemplo, existem 10 programas e 4 parâmetros.

Mas espere. E o Ruby? Este é o arquivo de cabeçalho C++! Isso está correto, precisamos criar um pequeno invólucro que transmita informações de e para o Ruby VM. Fique calmo.

Inicialização

O constructor é um ótimo lugar para inicializar nosso Ruby. Depois disso, precisamos carregar o script Ruby e instanciar nossa classe MRubyVST. Além disso, precisamos definir poucas 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 que começamos nossa Ruby VM embutida. Em seguida, estamos abrindo um arquivo e, se ele existir, carrega em nossa VM.

Agora o nosso script Ruby está carregado. A linha 14 vai ter a nossa classe Ruby e nos permite definir algumas constants. 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

Este é o script Ruby. Todas as informações estão aqui. Vendedor, produto, versão, etc. Nosso plugin usará programas para carregar vários módulos Ruby. Quando o usuário for mudar um programa, vamos instanciar a nova classe e passar a maioria dos métodos para este módulo. A lista de programas é apenas um arquivo dos programas directory. O submódulo simples se parece com isso:

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 mudará 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 do parâmetro com determinado índice

parameter_value: retorna um valor de parâmetro com determinado índice

parameter_display_value: retorna um valor do parâmetro com determinado índice como uma string, você pode formatá-lo como quiser

parameter_label: retorna etiqueta de nosso parâmetro

set_parameter: define um valor de parâmetro com determinado índice

process: processa dados de entrada e retorna de saída. Neste caso, irá multiplicar todos os valores de entrada com um valor de ganho.

MRuby Wrapper

Agora precisamos voltar para o nosso wraper. E implementar métodos que passarão dados para ruby e de volta. A maioria dos métodos parecem muito semelhantes, 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. Fazer um lock quando começarmos a usar o MRuby e desbloqueá-lo no final. Verifique se a nossa instância VST não é nula e se ela responde ao método program_name. Agora podemos invocá-lo se o resultado não é nulo, podemos decifrá-lo e copiá-lo para que o VST possa lê-lo, retornando significados verdadeiros que copiamos algo. Não é muito complicado descobrir quando e como os dados são convertidos. Agora, dê uma olhada na coisa 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();
}

Existem algumas coisas acontecendo aqui. Precisamos instruir o Ruby Garbage Collector a remover dados não utilizados. Isto é o que as linhas 4 e 36 fazem. Todos os objetos criados entre essas linhas podem ser coleta de lixo. Portanto, não haverá vazamentos de memória. Em seguida, precisamos converter uma array de floats em uma array de floats Ruby. Criar uma array Ruby para cada entrada e adicionar a ele um float convertido. Agora podemos invocar um método de processo com parâmetros. O resultado desse método é convertido de volta para array de float em C e, em seguida, passada para DAW.

Palavras finais

Sim, funciona. Claro, o plugin de ganho simples exige muito mais CPU do que o escrito em C. Mas, como você sabe, precisamos fazer um pacote em um objeto Ruby e extrair no final. Agora você pode fazer o que quiser com o stream 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, é mais simples e mais divertido.

MRubyByExample

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

Espero que tenha sido útil para você. Obrigado por ler!

 

***

Artigo publicado originalmente em https://blog.fazibear.me/processing-audio-with-ruby-330796afd06#.p216mhk82