O &arts; em Detalhe
Arquitectura
A estrutura do &arts;.
Módulos & Portos
A ideia do &arts; é que a síntese pode ser feita com módulos pequenos que só fazem uma coisa, voltando a combiná-los depois em estruturas complexas. Os pequenos módulos normalmente têm entradas, onde poderão obter alguns sinais ou parâmetros, e saídas, onde produzirão alguns sinais.
Um módulo (o Synth_ADD), por exemplo, apanha simplesmente os dois sinais à entrada e adiciona-os em conjunto. O resultado fica disponível como um sinal de saída. Os locais onde os módulos oferecem os seus sinais de entrada e saída chamam-se portos.
Estruturas
Uma estrutura é uma combinação de módulos ligados, alguns dos quais têm parâmetros codificados directamente nos seus portos de entrada, outros que poderão estar ligados e outros ainda que não estão ligados de todo.
O que você pode fazer com o &arts-builder; é descrever as estruturas. Você descreve quais os módulos que quer que estejam ligados com outros módulos. Quando terminar, você poderá gravar a descrição dessa estrutura num ficheiro, ou dizer ao &arts; para criar essa estrutura que descreveu (Executar).
Aí você irá provavelmente ouvir algum som, se fez tudo correctamente.
Latência
O Que é a Latência?
Suponha que tem uma aplicação chamada pling_rato
(que fará um som de um pling
se carregar num botão). A latência é o tempo que passa entre você pressionar o botão do rato com o seu dedo e você ouvir o som. A latência nesta configuração compõe-se por si só em várias latências, que poderão ter causas diferentes.
Latência em Aplicações Simples
Nesta aplicação simples, a latência ocorre nestes sítios:
O tempo até o 'kernel' notificar o servidor do X11 que foi carregado um botão do rato.
O tempo até o servidor do X11 notificar a sua aplicação que um botão do rato foi pressionado.
O tempo até à aplicação 'pling_rato' decidir que este botão merece tocar um 'pling'.
O tempo que leva à aplicação 'pling_rato' dizer ao servidor de som que deverá tocar um 'pling'.
O tempo que leva para o 'pling' (que o servidor de som começa a misturar com a outra saída ao mesmo tempo) vá para os dados dos 'buffers', até que atinge a posição onde a placa de som reproduz o toque.
O tempo que leva o som do 'pling' dos altifalantes a atingir o seu ouvido.
Os primeiros três itens são latências externas ao &arts;. Elas são interessantes, mas saem do âmbito deste documento. Todavia, tenha em atenção que elas existem, por isso, mesmo que você tenha optimizado tudo o resto para valores muito baixos, você poderá não obter exactamente o resultado que calculou.
Indicar ao servidor para tocar qualquer coisa envolve normalmente uma única chamada de &MCOP;. Existem medidas que confirmam isso, na mesma máquina e usando 'sockets' do domínio Unix, que dizem que, para dizer ao servidor para tocar algo, poderão ser feitas cerca de 9000 invocações por segundo na implementação actual. Espera-se que a maioria disto seja devido à sobrecarga no 'kernel', na mudança de uma aplicação para outra. Claro que este valor altera com o tipo exacto dos parâmetros. Se você transferir uma imagem inteira numa chamada, a chamada será mais lenta do que se transferir um valor inteiro. Aplica-se o mesmo para o valor devolvido. Contudo, para as cadeias de caracteres (como o nome do ficheiro WAV a tocar), isto não deverá ser nenhum problema.
Isto significa que podemos aproximar este tempo a 1/9000 sec, o que fica abaixo de 0.15 ms. Concluir-se-á que isto não é relevante.
A seguir vem o tempo entre o servidor começar a tocar e a placa de som a obter algo. O servidor precisa de armazenar os dados temporariamente em tampões ('buffers'), por isso quando as outras aplicações começarem a executar, como o seu servidor de X11 ou a aplicação pling_rato
não se poderão ouvir quebras. A forma como isso é feito no &Linux; é recorrendo a um conjunto de fragmentos de determinado tamanho. O servidor voltará a preencher os fragmentos, e a placa de som irá reproduzi-los.
Por isso, suponha que existem três fragmentos. O servidor preenche o primeiro, e a placa de som começa a tocá-lo. O servidor preenche o segundo e o terceiro, terminando assim a sua parte. As outras aplicações podem agora fazer algo.
Dado que a placa de som acabou de tocar o primeiro fragmento, começa a tocar o segundo e o servidor volta a preencher o primeiro, repetindo este processo indefinidamente.
A maior latência que você obtém com tudo isto é igual a (número de fragmentos)*(tamanho de cada fragmento)/(taxa amostragem * (tamanho de cada amostra)). Suponha que tem estéreo a 44kHz, com 7 fragmentos de 1024 bytes (o valor por omissão actual do &arts;): isso irá corresponder a 40 ms.
Estes valores poderão ser ajustados de acordo com as suas necessidades. Todavia, a utilização do CPU aumenta com latências menores, dado que o servidor de som terá de preencher os tampões com maior frequência e em menores partes. É também quase impossível atingir valores melhores sem dar ao servidor de som a prioridade de tempo-real, porque caso contrário irá ter frequentes quebras.
Contudo, é realista ter algo como 3 fragmentos de 256 bytes cada, o que iria fazer com que este valor fosse igual a 4,4 ms. Com um atraso de 4,4ms a utilização inactiva do CPU seria de aproximadamente 7,5% por parte do &arts;. Com um atraso de 40ms, seria de aproximadamente 3% (num PII-350, e este valor poderá depender da sua placa de som, versão do 'kernel', entre outros).
Agora, finalmente, tem o tempo que leva o som do 'pling' a sair dos altifalantes e a chegar ao seu ouvido. Suponha que a sua distância até aos altifalantes é de 2 metros. O som viaja à velocidade de 330 metros por segundo. Por isso, esse tempo poder-se-á aproximar a 6 ms.
Latência em Aplicações de Transmissão
As aplicações de transmissão ou difusão são aquelas que produzem elas próprias o som, e que origina uma sequência constanted de amostras, e que será agora adapto para reproduzir as coisas através do &arts;. Por exemplo: quando se pressiona uma tecla, a figura que está a tocar salta, aparecendo um som de 'boing'.
Primeiro que tudo, você precisa de saber como é que o &arts; faz a transmissão. É bastante semelhante às E/S com a placa de som. O jogo envia alguns pacotes com amostras para o servidor de som. Imagine-se que são três pacotes. Assim que o servidor estiver pronto com o primeiro pacote, envia uma confirmação de volta para o jogo a dizer que este pacote está pronto.
O jogo cria outro pacote de som e envia-o para o servidor. Entretanto o servidor começa a consumir o segundo pacote de som, e assim por diante. A latência aqui é semelhante à do caso simples:
O tempo até que o 'kernel' notifique o servidor de X11 que uma tecla foi carregada.
O tempo até que o servidor de X11 notifique o jogo de que uma tecla foi carregada.
O tempo até que o jogo se decida que esta tecla merece tocar um 'boing'.
O tempo até o pacote de som onde o jogo começou a colocar o som do 'boing' leva a chegar ao servidor de som.
O tempo que leva ao 'boing' (que o servidor de som começa a misturar para de uma vez) passe para os dados nos tampões ('buffers'), até que atinja a posição em que a placa de som começa a tocar.
O tempo que o 'boing' leva a sair dos altifalantes até atingir o seu ouvido.
As latências externas, tal como acima, estão fora do âmbito deste documento.
Obviamente, a latência da transmissão depende do tempo que leva a todos os pacotes que são usados na transmissão a serem tocados uma vez. Deste modo, é igual a (número de pacotes)*(taxa de amostragem * (tamanho de cada amostra))
Como você vê, é a mesma fórmula que se aplica para os fragmentos. Contudo, para os jogos, não faz sentido fazer demoras tão pequenas. Pode-se dizer que uma configuração realista para os jogos seria de 2048 bytes por pacote, usando 3 pacotes. A latência resultante seria de 35 ms.
Isto baseia-se no seguinte: assuma que o jogo desenha 25 imagens por segundo. É provavelmente seguro assumir que você não notará nenhuma diferença na saída de som para uma imagem. Por isso, 1/25 segundos para a transmissão é aceitável, o que por sua vez significa que 40 ms seria ok.
A maioria das pessoas também não irão executar os seus jogos, com prioridade de tempo-real, onde o perigo de quebras no som não pode ser negligenciado. A transmissão com 3 pacotes de 256 bytes cada é possível (tentou-se isso) - mas provoca uma carga grande de CPU para a transmissão.
para as latências do lado do servidor, você pode calculá-las exactamente como está dito em cima.
Algumas considerações de utilização do CPU
Existem vários factores que influenciam a utilização do CPU num cenário complexo, com algumas aplicações de transmissão entre outras, alguns 'plugins' no servidor, &etc;. Só para indicar algumas:
Utilização em bruto de CPU pelos cálculos que são necessários.
A sobrecarga do escalonamento interno do &arts; - como é que o &arts; decide qual o módulo que deve calcular o quê.
A sobrecarga da conversão de inteiros para números de vírgula flutuante.
A sobrecarga do protocolo &MCOP;.
'Kernel': mudança de contexto/processo.
'Kernel': sobrecarga nas comunicações
Para a carga em bruto do CPU usada nos cálculos, se você tocar duas sequências em simultâneo, você terá de efectuar somas. Se você aplicar um filtro, estão envolvidos alguns cálculos. Para dar um exemplo simplificado, a adição de duas sequências envolve talvez quatro ciclos de CPU por soma, o que num processador a 350MHz corresponde a 44100*2*4/350000000 = 0,1% utilização do CPU.
Escalonamento interno do &arts;: o &arts; precisa de decidir qual o 'plugin' que irá calcular um dado conjunto de dados; isto leva tempo. Faça uma análise da performance se você estiver interessado nisso. Geralmente o que se pode dizer é: quanto menos de tempo-real fizer (&ie;. quanto maiores os blocos que poderão ser calculados numa dada altura), a menor sobrecarga de escalonamento você obterá. Acima do cálculo de blocos de 128 amostras de cada vez (usando deste modo tamanhos de fragmentos de 512 bytes), a sobrecarga no escalonamento não será grave.
Conversão de inteiros para números de vírgula flutuante: o &arts; usa números de vírgula flutuante internamente como formato de dados. Este são simples de usr e nos processadores mais recentes não são mais lentos do que as operações com inteiros. Contudo, se existirem clientes que lidem com dados que não estejam em vírgula flutuante (como um jogo que deverá fazer a sua saída de som através do &arts;), estes precisam de ser convertidos. O mesmo aplica-se que você quiser reproduzir os sons na sua placa de som. A placa de som está à espera de inteiros, por isso você terá de converter.
Aqui estão números para um Celeron, da quantidade aproximada de 'ticks' por amostra, com o egcs 2.91.66 com a opção -O2 (dados de Eugene Smith hamster@null.ru). Isto é altamente dependente do processador, como é óbvio:
convert_mono_8_float: 14
convert_stereo_i8_2float: 28
convert_mono_16le_float: 40
interpolate_mono_16le_float: 200
convert_stereo_i16le_2float: 80
convert_mono_float_16le: 80
Por isso significa 1% de utilização do CPU para a conversão e 5% para a interpolação para este processador de 350 MHz.
A sobrecarga que o protocolo &MCOP; provoca; este protocolo origina, como regra de algibeira, 9000 invocações por segundo. Muitas destas não são culpa do protocolo &MCOP; em si, mas relaciona-se com as duas causas do 'kernel' indicadas em baixo. Contudo, isto fornece uma base para cálculo do quanto custa a transmissão.
Cada pacote de dados que é transmitido poderá ser considerado uma invocação do &MCOP;. Claro que os pacotes grandes são mais lentos do que 9000 pacotes/s, mas isto é a ideia básica.
Suponha que você usa tamanhos de pacotes de 1024 bytes. Deste modo, para transferir uma sequência estéreo de 44kHz, você precisa de transferir 44100*4/1024 = 172 pacotes por segundo. Suponha que po100% de utilização de CPU, 9000 pacotes, então iria obter (172*100)/9000 = 2% de utilização de CPU devido à transmissão de pacotes de 1024 bytes.
Existem aproximações. Contudo, estas mostram que você poderia estar muito melhor (se o poder fazer para o bem da latência), se usasse por exemplo pacotes de 4096 bytes. Pode-se fazer aqui uma fórmula compacta, calculando o tamanho do pacote que provoca uma utilização de 100% do CPU como sendo igual a 44100*4/9000 = 19,6 amostras, obtendo assim a fórmula rápida:
utilização de CPU na transmissão em percentagem = 1960/(tamanho do seu pacote)
o que dará 0,5% de utilização do CPU ao transmitir com pacotes de 4096 bytes.
Mudança de contextos/processos do 'kernel': isto faz parte da sobrecarga do protocolo &MCOP;. A mudança entre dois processos leva tempo. Existe um novo mapeamento de memória, as 'caches' são invalidadas, entre outras coisas (se existir alguém experiente no 'kernel' a ler isto - que diga quais são as causas exactas). Tudo isto para dizer: leva tempo.
Não é certo quantas mudanças de contexto o I &Linux; consegue fazer por segundo, mas esse número não é infinito. Por isso, muita parte da sobrecarga do protocolo &MCOP; deve-se, supostamente, em grande medida à mudança de contextos. No início do &MCOP;, foram feitos testes para usar a mesma comunicação dentro de um processo e isso era muito mais rápido (quatro vezes mais rápido, aproximadamente).
'Kernel': sobrecarga na comunicação: Isto faz parte da sobrecarga do protocolo &MCOP;. A transferência de dados entre processos é feita de momento, recorrendo a 'sockets'. Isto é conveniente, dado que os métodos normais do select() podem ser usados para determinar quando chegou uma mensagem. Também pode ser combinado com ou E/S de áudio, o servidor do X11 ou outras fontes, com relativa facilidade.
Contudo, estas chamadas de leitura e escrita custam certamente ciclos processador. Para as invocações pequenas (como a transferência de um evento MIDI), isso não é provavelmente assim tão mau, mas para as chamadas pesadas (como a transferência de uma imagem de vídeo com vários megabytes), isto é claramente um problema.
A utilização de memória partilhada no &MCOP;, sempre que apropriado, é provavelmente a melhor solução. Isto deverá ser feito de forma transparente para o programador da aplicação.
Obtenha um analisador ('profiler') ou faça outros testes para descobrir exactamente como é que a transmissão de áudio tem impacto se usar ou não memória partilhada. Contudo, não é mau, dado que a transmissão de áudio (reproduzir MP3s, por exemplo) poderá ser feita com utilização total de 6% de carga do CPU pelo &artsd; e pelo artscat (e 5% pelo descodificador de MP3). Contudo, isto inclui todas as coisas, desde os cálculos necessários até à sobrecarga nos 'sockets', por isso poder-se-á dizer que, nesta configuração, você poderá talvez poupar 1% se usar memória partilhada.
Alguns Números em Bruto
Estes são retirados a partir da versão actual em desenvolvimento. Tentou-se obter também casos reais, porque isso não é o que as aplicações do dia-a-dia deverão usar.
Foi criada uma aplicação chamada 'som_sequencia' que transmite dados para o &arts;. Aqui está a correr com prioridade de tempo-real (sem problemas) e com um 'plugin' pequeno por parte do servidor (ajuste e recorte do volume):
4974 stefan 20 0 2360 2360 1784 S 0 17.7 1.8 0:21 artsd
5016 stefan 20 0 2208 2208 1684 S 0 7.2 1.7 0:02 som_sequencia
5002 stefan 20 0 2208 2208 1684 S 0 6.8 1.7 0:07 som_sequencia
4997 stefan 20 0 2208 2208 1684 S 0 6.6 1.7 0:07 som_sequencia
Cada um deles está a transmitir com 3 fragmentos de 1024 bytes (18 ms). Existem três clientes do mesmo tipo a correr em simultâneo. É certo que isto parece demasiado, mas como foi dito: pegue num analisador ('profiler') e procure o que é que leva tempo e, se o desejar, tente melhorá-lo.
Contudo, não se deve pensar que a utilização da transmissão desta forma é realista ou faz sentido. Para levar isto ainda mais ao extremo, tentou-se a menor latência possível. Resultado: você poderá fazer transmissões sem interrupções com uma aplicação-cliente, se tiver 2 fragmentos de 128 bytes entre o &arts; e a placa de som e entre a aplicação-cliente e o &arts;. Isto significa que você tem uma latência total máxima de 128*4/44100*4 = 3 ms, onde 1,5 ms são gerados devido à E/S da placa de som e os outros 1,5 devem-se à comunicação com o &arts;. Ambas as aplicações precisam de correr em tempo-real.
Mas isto tem um custo enorme do CPU. Este exemplo custa-lhe cerca de 45% num P-II/350. Ele também começa a fazer 'clicks' se você iniciar o 'top', se mover as janelas no seu ecrã do X11 ou se fizer E/S de disco. Todas estas questões são respeitantes ao 'kernel'. O problema é que o escalonamento de duas ou mais aplicações em tempo-real custam-lhe uma quantidade enorme de esforço também, e ainda mais se elas comunicarem, notificarem-se uma à outra, &etc;.
Finalmente, um exemplo mais real. Isto é o &arts; com o &artsd; e um 'artscat' (um cliente de transmissão) que estão a correr 16 fragmentos de 4096 bytes:
5548 stefan 12 0 2364 2364 1752 R 0 4.9 1.8 0:03 artsd
5554 stefan 3 0 752 752 572 R 0 0.7 0.5 0:00 top
5550 stefan 2 0 2280 2280 1696 S 0 0.5 1.7 0:00 artscat
Barramentos
Os barramentos são ligações criadas dinamicamente que transferem o áudio. Basicamente, existem alguns canais de envio e de recepção e de envio. Todos os sinais dos canais de envio são adicionados e enviados para os canais de recepção.
Os barramentos, tal como são implementados actualmente, operam em estéreo, por isso você só poderá transferir dados em estéreo nos barramentos. Se você quiser dados mono, bem, transfira apenas por um canal e coloque o outro a zeros ou com outro valor qualquer. Tudo o que precisa de fazer é criar um ou mais objectos Synth_BUS_UPLINK e dar-lhes o nome de um barramento, com o qual eles deverão falar (⪚ áudio
ou bateria
). Basta largar os dados aí.
Aí, você terá de criar um ou mais objectos Synth_BUS_DOWNLINK, indicando-lhe o nome do barramento (áudio
ou bateria
... se corresponde, os dados serão transferidos para aí), e os dados misturados irão sair de novo.
Os canais de envio e de recepção poderão residir em estruturas diferentes, e você até poderá ter vários &arts-builder;s diferentes a correr e iniciar um canal de envio num e receber os dados noutro, através do canal de recepção respectivo.
O que é interessante acerca dos barramentos é que eles são completamente dinâmicos. Os clientes poder-se-ão ligar instantaneamente. Não deverá haver nenhum 'click' ou ruído à medida que isto acontece.
Claro que você não deverá desligar um cliente que toca um sinal, dado que poderá não estar a um nível nulo quando for desligado do barramento, ao que se ouvirá então um 'click'.
Mediador
O &arts;/&MCOP; baseia-se em grande medida na divisão das coisas em pequenos componentes. Isto torna as coisas muito flexíveis, à medida que vai extendendo o sistema facilmente com a adição de componentes novos que implementam efeitos novos, formatos de ficheiros, osciladores, elementos gráficos.... Dado que quase tudo é um componente, quase tudo poderá ser extendido facilmente, sem alterar o código existente. Os componentes novos poderão ser simplesmente carregados dinamicamente para melhorar as aplicações já existentes.
Contudo, para isto funcionar, são necessárias duas coisas:
Os componentes têm de se publicitar a eles próprios - eles precisam de descrever quais as coisas que eles oferecem, para que as aplicações sejam capazes de as usar.
As aplicações precisam de procurar activamente os componentes que elas poderão usar, em vez de usar sempre a mesma coisa para uma dada tarefa.
A combinação disto - os componentes que dizem aqui estou eu, sou bom, usem-me
, e as aplicações (ou, se preferir, outros componentes) que vão e procuram qual o componente que eles poderão usar para ter uma coisa feita - é o que é chamado de 'mediação' ou 'negociação'.
No &arts;, os componentes descrevem-se a si próprios, indicando valores que suportam
para as propriedades.. Uma propriedade típica para um componente de leitura de ficheiros poderá ser a extensão dos ficheiros que pode processar. Os valores típicos poderão ser o wav, o aiff ou o mp3.
De facto, todos os componentes poderão optar por oferecer vários valores diferentes para uma dada propriedade. Por isso, um único componente poder-se-á oferecer para ler tanto os ficheiros wav como os aiff, indicando que suporta estes valores para a propriedade Extension
(Extensão).
Para o fazer, um componente terá de colocar um ficheiro .mcopclass num local apropriado, contendo as propriedades que suporta; no caso do exemplo actual, isto poderá assemelhar-se ao seguinte (e estará instalado como dir_componente/Arts/WavPlayObject.mcopclass):
Interface=Arts::WavPlayObject,Arts::PlayObject,Arts::SynthModule,Arts::Object
Author="Stefan Westerfeld <stefan@space.twc.de>"
URL="http://www.arts-project.org"
Extension=wav,aiff
MimeType=audio/x-wav,audio/x-aiff
É importante que o nome do ficheiro .mcopclass também diga como é que se chama a interface do componente. O mediador não olha para o conteúdo de todo, se o ficheiro (tal como está aqui) se chamar Arts/WavPlayObject.mcopclass, a interface do componente é chamada de Arts::WavPlayObject (os módulos mapeiam-se nas pastas).
Para ver os componentes, existem duas interfaces (que estão definidas em core.idl, por isso você irá tê-las em todas as aplicações), chamadas de Arts::TraderQuery e Arts::TraderOffer. Você poderá fazer uma ida às compras
nos componentes deste tipo:
Crie um objecto de pesquisa:
Arts::TraderQuery pesquisa;
Indique o que pretende. Como viu em cima, os componentes descrevem-se a si próprios recorrendo a propriedades, para os quais eles oferecem determinados valores. Por isso, poderá indicar o que quiser através da selecção de componentes que suportem um dado valor para uma dada propriedade. Isto é feito se usar o método 'supports' de uma TraderQuery:
pesquisa.supports("Interface","Arts::PlayObject");
pesquisa.supports("Extension","wav");
Finalmente, efectue a pesquisa usando o método 'query'. Aí, você irá obter (ou assim se espera) algumas ofertas:
vector<Arts::TraderOffer> *ofertas = pesquisa.query();
Agora você poderá examinar o que encontrou. O que é importante é o método 'interfaceName' do TraderOffer, o qual lhe dirá o nome do componente que correspondeu à pesquisa. Você poderá também encontrar mais propriedades com o método 'getProperty'. O código seguinte irá simplesmente iterar por todos os componentes, imprimir o nome das suas interfaces (que poderão ser usados na criação), e limpar os resultados da pesquisa de novo:
vector<Arts::TraderOffer>::iterator i;
for(i = ofertas->begin(); i != ofertas->end(); i++)
cout << i->interfaceName() << endl;
delete ofertas;
Para este tipo de serviço de mediação ser útil, é importante concordar de alguma forma nos tipos de propriedades que os componentes deverão definir normalmente. É essencial que mais ou menos todos os componentes de uma determinada área usem o mesmo conjunto de propriedades para se descreverem a si próprios (e o mesmo conjunto de valores, sempre que se aplicar), de modo a que as aplicações (ou as outras componentes) sejam capazes de os encontrar.
Author (tipo 'texto', opcional): Isto poderá ser usado para mostrar em última instância ao mundo que você fez algo. Aqui você poderá escrever tudo o que quiser, se bem que um endereço de e-mail é obviamente útil.
Buildable (tipo booleano, recomendado): Isto indica se o componente pode sre usado com ferramentas de RAD (como o &arts-builder;) que usam os componentes, atribuindo-lhes propriedades e ligando os seus portos. Recomenda-se ter este valor a 'true' (verdadeiro) para quase todos os componentes de processamento de sinal (como os filtros, efeitos, osciladores, ...) e para todas as outras coisas que podem usadas numa abordagem RAD, mas não para coisas internas como, por exemplo, o Arts::InterfaceRepo.
Extension (tipo texto, onde for relevante): Tudo o que lide com ficheiros deverá optar por usar isto. Você deverá colocar aqui a versão em minúsculas da extensão do ficheiro sem o .
, como por exemplo wav.
Interface (tipo texto, obrigatório): Isto deverá incluir a lista completa de interfaces (úteis) que os seus componentes suportam, incluindo provavelmente o Arts::Object e, se se aplicar, o Arts::SynthModule.
Language (tipo texto, recomendado): Se você quiser que o seu componente seja carregado dinamicamente, você precisa de indicar aqui a linguagem. De momento, o único valor permitido é o C++, o que significa que o componente foi criado com a API normal de C++. Se o fizer, você também terá de definir a propriedade Library
em baixo.
Library (tipo texto, usado quando relevante): Os componentes feitos em C++ podem ser carregados dinamicamente. Para o fazer, você terá de os compilar num módulo de biblioteca carregada dinamicamente (.la). Aqui você poderá indicar o nome do ficheiro .la. Lembre-se de usar o REGISTER_IMPLEMENTATION (como sempre).
MimeType (tipo texto, usado quando for relevante): Tudo o que lide com ficheiros deverá optar por usar isto. Você deverá colocar aqui a versão em minúsculas do tipo MIME normal, como por exemplo audio/x-wav.
&URL; (tipo texto, opcional): Se quiser que as pessoas saibam onde poderão obter uma nova versão do componente (ou uma página pessoal, ou algo do género), você podê-lo-á fazer aqui. Isto deverá ser um &URL; normal de &HTTP; ou de &FTP;.
Espaços de nomes no &arts;
Introdução
Cada declaração de espaço de nomes corresponde à declaração de um módulo
na &IDL; do &MCOP;.
// idl de mcop
module M {
interface A
{
}
};
interface B;
Neste caso, o código de C++ gerado para o excerto de &IDL; deverá ser algo semelhante a isto:
// código de C++
namespace M {
/* declaração de A_base/A_skel/A_stub e itens semelhantes */
class A { // Classe de interface de referência
/* [...] */
};
}
/* declaração de B_base/B_skel/B_stub e itens semelhantes */
class B {
/* [...] */
};
Por isso, quando se referir às classes do seu código em C++, você terá de escrever M::A, mas só B. Todavia, você poderá indicar using M
algures - como em qualquer 'namespace' do C++.
Como o &arts; usa os espaços de nomes
Existe um espaço de nomes global chamado Arts
, o qual todos os programas e bibliotecas usam para colocar lá as suas declarações. Isto significa que, ao criar código em C++ que dependa do &arts;, você terá normalmente de anteceder cada classe que usar com o Arts::, tal como se segue:
int main(int argc, char **argv)
{
Arts::Dispatcher mediador;
" Arts::SimpleSoundServer servidor(Arts::Reference("global:Arts_SimpleSoundServer"));
servidor.play("/var/xpto/um_ficheiro.wav");
A outra alternativa é usar um 'using', tal como se segue:
using namespace Arts;
int main(int argc, char **argv)
{
Dispatcher mediador;
SimpleSoundServer servidor(Reference("global:Arts_SimpleSoundServer"));
servidor.play("/var/xpto/um_ficheiro.wav");
[...]
Nos ficheiros &IDL;, você não tem de facto escolha alguma. Se estiver a fazer código que pertença ao &arts; em si, você terá de o pôr no módulo do &arts;.
// Ficheiro IDL para código do aRts:
#include <artsflow.idl>
module Arts { // colocar no espaço de nomes Arts
interface Synth_AJUSTE : SynthModule
{
in audio stream entrada;
out audio stream saida;
attribute float factorAjuste;
};
};
Se você fizer código que não pertença ao &arts; em si, você não o deverá colocar no espaço de nomes Arts
. Contudo, você poderá criar um espaço de nomes próprio se quiser. Em qualquer dos casos, você terá de anteceder as classes que usar do &arts;.
// Ficheiro IDL para código que não pertence ao aRts:
#include <artsflow.idl>
// pode criar sem declaração do módulo, onde as classes geradas não irão
// usar nenhum 'namespace' (espaço de nomes):
interface Synth_AJUSTE2 : Arts::SynthModule
{
in audio stream entrada;
out audio stream saida;
attribute float factorAjuste;
};
// contudo, você também poderá escolher o seu espaço de nomes, se preferir, por
// isso se criar uma aplicação "Radio", você poderá fazê-lo da seguinte forma:
module Radio {
struct Estacao {
string nome;
float frequencia;
};
interface Sintonizador : Arts::SynthModule {
attribute Estacao estacao; // não é necessário anteceder o Estacao, por ser do mesmo módulo
out audio stream esquerda, direita;
};
};
Detalhes Internos: Como Funciona a Implementação
Normalmente, nas interfaces, conversões ('casts'), assinaturas dos métodos e noutras situações semelhantes, o &MCOP; precisa de se referir aos nomes dos tipos ou das interfaces. Estes são representados como texto nas estruturas de dados comuns do &MCOP;, enquanto que o espaço de nomes é sempre representado por completo no estilo do C++. Isto significa que os textos iriam conter M::A
e B
, seguindo o exemplo acima.
Repare que isto se aplica mesmo se, dentro do texto do &IDL;, os qualificadores de espaços de nomes não foram indicados, dado que o contexto tornou claro qual a o espaço de nomes em que a interface A pretendia ser usada.
Tarefas no &arts;
Básicos
A utilização de tarefas ('threads') em todas as plataformas não é possível. Foi por isso que o &arts; originalmente foi feito sem qualquer suporte multitarefa. Para quase todos os problemas, para cada solução multitarefa para o problema, existe uma solução monotarefa que faz o mesmo.
Por exemplo, em vez de colocar a saída de áudio numa tarefa em separado, tornando-a bloqueante, o &arts; usa a saída de áudio não-bloqueante, e tenta descobrir quando deve escrever os próximos blocos de dados com o select().
Contudo, o &arts; (em versões muito recentes) oferece pelo menos o suporte para as pessoas que queiram implementar os seus objectos com tarefas separadas. Por exemplo, se você já tiver código para um leitor de mp3 e o código do descodificador de mp3 está à espera de correr numa tarefa separada, é normalmente o acto mais fácil manter este desenho.
A implementação do &arts;/&MCOP; é desenhada tendo como ideia de base a partilha do estado entre os objectos separados, implementada de formas óbvias ou menos óbvias. Uma pequena lista do estado partilhado inclui:
O objecto Dispatcher (mediador) que faz a comunicação do &MCOP;.
A contagem de referências (interfaces inteligentes).
O IOManager (gestor de E/S) que vigia os temporizadores e descritores de ficheiros.
O ObjectManager (gestor de objectos), que cria os objectos e carrega automaticamente os 'plugins'.
O FlowSystem (sistema de fluxo) que invoca o 'calculateBlock' nas situações apropriadas.
Todos os objectos acima não estão à espera de ser usados concorrentemente (&ie; chamados em tarefas separadas ao mesmo tempo). Normalmente, existem duas formas de resolver isto:
Obrigar ao invocador de todas as funções nestes objectos a adquirir um bloqueio antes de as usar.
Tornar estes objectos realmente seguros em multitarefa e/ou criar instâncias por cada tarefa das mesmas.
O &arts; segue a primeira aproximação: você terá de bloquear os objectos sempre que precisar de comunicar com qualquer um deles. A segunda aproximação é mais difícil de conseguir. Um truque que tenta obter isto está disponível em http://space.twc.de/~stefan/kde/download/arts-mt.tar.gz, mas nesta altura do campeonato, funcionará melhor uma aproximação minimalista, e causará menos problemas com as aplicações existentes.
Quando/como efectuar o bloqueio?
Você poderá efectuar/libertar o bloqueio com as duas funções:
Arts::Dispatcher::lock()
Arts::Dispatcher::unlock()
Geralmente, você não terá de efectuar o bloqueio (e não deverá ter de tentar fazê-lo), se já foi efectuado anteriormente. Segue-se uma lista com as condições em que este é o caso:
Você recebe uma chamada de resposta do IOManager (um temporizador ou um descritor).
Você é invocado devido a algum pedido do &MCOP;.
Você é chamado a partir do NotificationManager (gestor de notificações).
Você é invocado a partir do FlowSystem (pelo 'calculateBlock')
Existem também algumas excepções de funções que você só poderá invocar na tarefa principal e que, por essa razão, nunca irá necessitar de bloquear para as chamar:
O construtor/destrutor do Dispatcher/IOManager.
Dispatcher::run() / IOManager::run()
IOManager::processOneEvent()
Mas é tudo. Para tudo o resto que esteja relacionado de qualquer forma com o &arts;, você irá necessitar de obter o bloqueio e libertá-lo quando terminar - sempre. Aqui está um exemplo simples:
class TarefaTempoSuspensao : Arts::Thread {
public:
void run() {
/*
* você precisa deste bloqueio porque:
* - a criação de uma referência necessita de um bloqueio (porque o
* 'global:' vai para o gestor de objectos, o qual poderá necessitar
* por seu turno do objecto GlobalComm para procurar onde se ligar)
* - a atribuição de uma interface inteligente necessita de um
* bloqueio
* - a construção de um objecto a partir de uma referência necessita * de um bloqueio (porque poderá necessitar de se ligar a um
* servidor)
*/
Arts::Dispatcher::lock();
Arts::SoundServer servidor = Arts::Reference("global:Arts_SoundServer");
Arts::Dispatcher::unlock();
for(;;) { /*
* você precisa de bloquear aqui, por que
* - libertar a referência a uma interface inteligente necessita
* de um bloqueio (porque poderá fazer uma criação tardia)
* - fazer uma invocação do MCOP necessita de efectuar um bloqueio
*/
Arts::Dispatcher::lock();
long segundos = servidor.secondsUntilSuspend();
Arts::Dispatcher::unlock();
printf("segundos até à suspensão = %d",segundos);
sleep(1);
}
}
}
Classes relacionadas com tarefas
As seguintes classes relacionadas com tarefas estão disponíveis de momento:
O Arts::Thread - que encapsula uma tarefa.
O Arts::Mutex - que encapsula um 'mutex' - uma exclusão mútua.
O Arts::ThreadCondition - que oferece o suporte para acordar as tarefas que estão à espera que uma dada condição se torne verdadeira.
O Arts::SystemThreads - que encapsula a camada do suporte multitarefa do sistema operativo (e que oferece algumas funções úteis para os programadores das aplicações).
Veja os 'links' para obter mais documentação.
Referências e Tratamento de Erros
As referências no &MCOP; são um dos conceitos mais centrais na programação com o &MCOP;. Esta secção irá tentar descrever como é que as referências são usadas ao certo, e irá também tentar especialmente cobrir os casos de falha ).
Propriedades básicas das referências
Uma referência de &MCOP; não é um objecto, mas sim a referência a um objecto: Ainda que a seguinte declaração
Arts::Synth_PLAY p;
se pareça com a definição de um objecto, apenas declara a referência a um objecto. Como programador de C++, você poderá pensar nisto como um Synth_PLAY *, um tipo de ponteiro para um objecto Synth_PLAY. Isto significa especialmente que o 'p' poderá ser a mesma coisas que um ponteiro nulo (NULL).
Você poderá criar uma referência a NULL (valor nulo), atribuindo este valor explicitamente
Arts::Synth_PLAY p = Arts::Synth_PLAY::null();
Invocar objectos numa referência NULL irá conduzir a um estoiro
Arts::Synth_PLAY p = Arts::Synth_PLAY::null();
string s = p.toString();
irá levar a um estoiro. Se comparar isto com um ponteiro, é exactamente o mesmo que
QWindow* janela = 0;
janela->show();
, o qual todos os programadores de C++ sabem que deverão evitar.
Os objectos não-inicializados tentar-se-ão criar 'a posteriori' quando forem usados pela primeira vez
Arts::Synth_PLAY p;
string s = p.toString();
é algo diferente de fazer uma referência a um ponteiro NULL. Você não indicou ao objecto de todo o que ele é, e agora irá tentar usá-lo. A questão aqui é que você deseja ter uma instância local de um objecto Arts::Synth_PLAY. Claro que você poderá querer ter algo diferente (como criar o objecto noutro local qualquer, ou usar um objecto remoto existente). Contudo, é um atalho conveniente para criar objectos. A criação tardia não irá funcionar logo que tenha atribuído outra coisa qualquer (como por exemplo uma referência nula).
Os termos equivalentes em C++ seriam
QWidget* janela;
janela->show();
o que obviamente, em C++, iria dar um estoiro garantido. Por isso, isto é diferente aqui. Esta criação tardia é enganadora, porque não quer dizer que exista necessariamente uma implementação para a sua interface.
Por exemplo, considere uma coisa abstracta como um Arts::PlayObject. Existem decerto PlayObjects concretos como os que existem para tocar MP3s ou WAVs, mas o
Arts::PlayObject objecto;
objecto.play();
irá falhar de certeza. O problema é que, ainda que a criação tardia funcione e tente criar um PlayObject, irá falhar, dado que existem coisas do tipo Arts::WavPlayObject e outros do género. Daí, use a criação tardia apenas se tiver a certeza que existe uma implementação.
As referência podem apontar para o mesmo objecto
Arts::SimpleSoundServer s = Arts::Reference("global:Arts_SimpleSoundServer");
Arts::SimpleSoundServer s2 = s;
cria duas referências para o mesmo objecto. Isto não copia nenhum valor e não cria dois objectos.
Todos objectos fazem contagem das referências. Por isso, logo que um objecto já não seja mais referenciado por nenhum outro objecto, é removido. Não existe nenhuma forma explícita de remover um objecto, contudo poderá usar algo do género
Arts::Synth_PLAY p;
p.start();
[...]
p = Arts::Synth_PLAY::null();
para fazer com que o objecto Synth_PLAY se vá embora no fim. Especialmente não deverá ser necessário usar o 'new' e o 'delete' em conjunto com as referências.
O caso de falha
Dado que as referências poderão apontar para objectos remotos, os servidores que contenham esses objectos poderão estoirar. O que acontece então?
Um estoiro não muda se uma referência é nula. Isto significa que, se o xpto.isNull() foi true antes de um estoiro do servidor, então também será true depois do estoiro (o que parece claro). Significa também que, se o xpto.isNull() foi false antes de um estoiro do servidor (o 'xpto' fazia referência a um objecto), então também será false depois do estoiro do servidor.
A invocação dos métodos numa referência válida mantém-se segura. Suponha que o servidor que contém o objecto 'calc' estoirou. Se continuar a invocar coisas do tipo
int k = calc.subtrair(i,j)
estas serão seguras. Obviamente, o 'subtrair' terá de devolver algo aqui, o que não consegue porque o objecto remoto já não existe mais. Nesse caso, o (k == 0) será verdadeiro. Geralmente, as operações tentam devolver algo neutro
como resultado, como por exemplo 0,0, uma referência nula para os objectos ou textos em branco, sempre que o objecto não existir mais.
Se invocar o error() verá se algo correu bem ou mal.
No caso de cima, o
int k = calc.subtrair(i,j)
if(k.error()) {
printf("O k não é igual a i-j!\n");
}
iria imprimir O k não é igual a i-j sempre que a invocação remota não funcionasse. Caso contrário, o k é de facto o resultado da operação 'subtrair' efectuada pelo objecto remoto (sem estoiro do servidor). Contudo, para os métodos que fazem coisas como remover ficheiros, você não poderá saber de certeza se isso de facto aconteceu. Claro que aconteceu se o .error() devolveu false. Contudo, se o .error() devolveu true, existem duas possibilidades:
O ficheiro foi removido, e o servidor estoirou logo depois de o remover, mas antes de transferir o resultado.
O servidor estoirou antes de ser capaz de remover o ficheiro.
O uso de invocações aninhadas é perigoso em programas resistentes a estoiros
Se usar algo do tipo
janela.titlebar().setTitle("xpto");
não é uma boa ideia. Suponha que você sabia que essa janela era uma referência de Window válida. Agora suponha que sabe que o janela.titlebar() iria devolver uma referência a Titlebar porque o objecto Window foi criado correctamente. Todavia, ainda a frase acima não é segura.
O que poderia acontecer é que o servidor que continha o objecto Window tinha estoirado. Aí, independentemente de quão válida fosse a implementação de Window, você iria obter uma referência nula como resultado da operação 'janela.titlebar()'. E aí, obviamente, a invocação de 'setTitle' nessa referência nula iria provocar um estoiro à mesma.
Por isso, uma variante segura disto seria
Titlebar titulo = janela.titlebar();
if(!janela.error())
titulo.setTitle("xpto");
, adicionando o tratamento de erros apropriado, se o desejar. Se você não confiar na implementação de Window, você poderá também usar
Titlebar titulo = janela.titlebar();
if(!titulo.isNull())
titulo.setTitle("xpto");
em que ambas são seguras.
Existem outras condições de falha, como a quebra de rede (suponha que você retira o cabo entre o seu servidor e o cliente enquanto a sua aplicação corre). Contudo, o efeito é o mesmo que um estoiro do servidor.
De um modo geral, é claro uma questão de política a forma como você tenta eliminar os erros de comunicação na sua aplicação. Você poderá seguir o método de se o servidor estoirar, é necessário depurar o servidor até que nunca mais estoire de novo
, o que significaria que você não se precisa de se incomodar com todos esses problemas.
Detalhes Internos: Contagem de Referências Distribuída
Um objecto, para existir, precisa de pertencer a alguém. Se não pertencer, deixará de existir (mais ou menos) imediatamente. Internamente, a pertença é indicada ao chamar o _copy(), o qual incrementa uma contagem de referências e é devolvida ao invocar o _release(). Logo que a contagem de referências chegue a zero, será feito um 'delete'.
Como variação do tema, a utilização remota é indicada pelo _useRemote() e desfeita pelo _releaseRemote(). Estas funções mantêm uma lista dos servidores que invocaram o objecto (e que, por esse motivo, o possuem). Isto é usado no caso deste servidor se desligar (&ie; estoiro, falha de rede), para remover as referência que ainda existem nos objectos. Isto é feito com o _disconnectRemote().
Agora existe um problema. Considere um valor devolvido. Normalmente, o objecto do valor devolvido não pertencerá mais à função que foi chamada. Também não pertencerá à função que chamou, até que a mensagem que mantém o objecto seja recebida. Deste modo, existe um tempo para os objectos sem dono
.
Agora, ao enviar um objecto, poder-se-á assumir que, assim que seja recebido, passará a ter um dono de novo, a menos que, mais uma vez, o receptor morra. Contudo, isto significa que é preciso ter um cuidado especial com os objectos, pelo menos ao enviá-los e provavelmente ao recebê-los, de modo a que não morra de uma vez.
A forma como o &MCOP; faz isto é marcando
os objectos que estão em vias de ser copiados para a rede. Antes de se dar início a uma cópia, o _copyRemote é invocado. Isto evita que o objecto seja libertado durante algum tempo (5 segundos). Logo que o receptor invoque o _useRemote(), a marca é removida de novo. Deste modo, todos os objectos que são enviado para a rede são marcados antes da transferência.
Se o receptor obtiver um objecto que está no seu servidor, é óbvio que ele não irá invocar o _useRemote(). Para esse caso especial, o _cancelCopyRemote() existe para remover a marca manualmente. Para além disso, existe também a remoção de marcas temporizada, se tiver sido feita a marcação mas o destinatário não recebeu de facto o objecto (devido a um estoiro ou falha de rede). Isto é feito com a classe ReferenceClean.
Elementos &GUI;
Os elementos &GUI; estão neste momento num estado experimental. Contudo, esta secção irá descrever o que é suposto acontecer aqui por isso, se você for um programador, você será capaz de perceber como é que o &arts; irá lidar com as &GUI;s no futuro. Existe já algum código, também.
Os elementos &GUI; deverão ser usados para permitir às estruturas de síntese interagirem com o utilizador. No caso mais simples, o utilizador deverá ser capaz de modificar alguns parâmetros de uma estrutura directamente (como um factor de ganho que é usado antes do módulo final de reprodução).
Nas opções mais complexas, pode-se imaginar que o utilizador deseja modificar os parâmetros de grupos de estruturas e/ou ainda não tem as estruturas a correr, como a modificação do envelope ADSR do instrumento &MIDI; activo no momento. Outra coisa seria mudar o nome do ficheiro de um instrumento baseado em amostras.
Por outro lado, o utilizador poderá querer monitorizar o que o sintetizador está a fazer. Poderão existir osciloscópios, analisadores de espectro, medidores de volume e outras experiências
que mostram a curva de transferência na frequência de um dado módulo de filtragem.
Finalmente, os elementos &GUI; deverão ser capazes de controlar a estrutura completa de o que é que está a correr dentro do &arts; e como. O utilizador deverá ser capaz de associar instrumentos a canais &MIDI;, iniciar novos processadores de efeitos, configurar a sua mesa de mistura principal (a qual é ela própria baseada em estruturas do &arts;) para ter mais um canal e usar outra estratégia para os seus equalizadores.
Você pode ver - os elementos GUI deverão trazer todas as possibilidades do estúdio virtual que o &arts; deverá simular para o utilizador. Claro, eles deverão interagir de forma ordeira com as entradas &MIDI; (assim como as barras se deverão mexer se elas tiverem entradas &MIDI; que mudem também esse parâmetro), e provavelmente até elas próprias gerarem eventos, para permitir a interacção com o utilizador ser registada com o sequenciador.
Tecnicamente, a ideia é ter uma classe de base de &IDL; para todos os elementos (a Arts::Widget), e derivar um conjunto de elementos comuns desta (como o Arts::Poti, o Arts::Panel, o Arts::Window, ...).
Aí, poder-se-á implementar estes elementos com uma biblioteca, como por exemplo o &Qt; ou o Gtk. Finalmente, os efeitos deverão criar as suas &GUI;s a partir dos elementos existentes. Por exemplo, um ' poderia criar a sua interface a partir de cinco objectos Arts::Poti e de uma Arts::Window. Por isso, SE existir uma implementação do &Qt; para esses elementos de base, o efeito deverá ser capaz de se apresentar, usando o &Qt;. Se existir uma implementação para Gtk, então deverá também funcionar para o Gtk (e assemelhar-se/funcionar mais ou menos da mesma forma).
Finalmente, dado que tem sido utilizada aqui a &IDL;, o &arts-builder; (ou outras ferramentas), serão capazes de ligar as interfaces visualmente, ou gerar automaticamente as interfaces com base nos parâmetros definidos, baseando-se apenas nas interfaces. Deverá ser relativamente simples criar uma classe para criar uma &GUI; a partir da descrição
, que obtém uma descrição da &GUI; (contendo os vários parâmetros e elementos) e criar um objecto gráfico vivo a partir dele.
Baseando-se na &IDL; e no modelo de componentes do &arts;/&MCOP;, deverá ser simples extender os objectos possíveis que podem ser usados na &GUI; de forma tão simples como para adicionar um 'plugin' que implementa um novo filtro para o &arts;.