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