&arts; i detalj
Arkitektur
&arts; strukturer.
Moduler & portar
Idén med &arts; är att syntes kan göras med små moduler, som bara gör en enda sak, och sedan kombinera dem i komplexa strukturer. De små modulerna har normalt ingångar, där de kan ta emot några signaler eller parametrar, och utgångar där de producerar några signaler.
En modul (Synth_ADD) tar till exempel bara de två signalerna på sina ingångar och adderar dem. Resultatet är tillgängligt som en utsignal. De ställen där moduler tillhandahåller sina in- eller utsignaler kallas portar.
Strukturer
En struktur är en kombination av ihopkopplade moduler, där några kan ha parametrar som är direktkodade på deras inportar, andra kan vara ihopkopplade, och en del kan vara helt oanslutna.
Vad du kan göra med aRts-byggaren är att beskriva strukturer. Du beskriver vilka moduler du vill ska kopplas ihop med vilka andra moduler. När du är klar, kan du spara strukturbeskrivningen i en fil, eller be &arts; att skapa (köra) den strukturen som du beskrivit.
Därefter hör du förmodligen något ljud, om du har gjort allt på rätt sätt.
Latenstid
Vad är latenstid?
Anta att du har ett program som heter muspling
(som ska avge ett pling
ljud om du klickar på en musknapp). Latenstiden är tiden mellan ditt finger trycker på musknappen och du hör plinget. Latenstiden för det här scenariot består av flera olika latenstider, som har olika orsaker.
Latenstid i enkla program
För det här enkla programmet uppstår latenstiden på följande ställen:
Tiden till kärnan har meddelat X11-servern att musknappen har tryckts ner.
Tiden till X11-servern har meddelat ditt program att musknappen har tryckts ner.
Tiden till muspling-programmet har bestämt att den här knappen var värd att få ett pling spelat.
Tiden det tar för muspling-programmet att tala om för ljudservern att den ska spela ett pling.
Tiden det tar för plinget (som ljudservern börjar mixa med övrig utmatning omedelbart) att ta sig igenom buffrad data, till det verkligen når stället där ljudkortet spelar.
Tiden det tar för pling-ljudet att gå från högtalarna till dina öron.
De första tre punkterna är latenstid utanför &arts;. De är intressanta, men utanför det här dokumentets omfattning. Hur som helst, var medveten om att de finns, så att även om du har optimerat allting annat till verkligt små värden, så kanske du inte nödvändigtvis får exakt det resultat du förväntar dig.
Att be servern att spela någonting innebär oftast bara ett enda &MCOP;-anrop. Det finns mätningar som bekräftar att det går att be servern att spela någonting 9000 gånger per sekund med den nuvarande implementeringen, på samma värddator med Unix domänuttag. Jag antar att det mesta av det här är kärnans omkostnad, för att byta från en process till en annan. Naturligtvis ändras det här värdet med de exakta typerna på parametrarna. Om man överför en hel bild med ett anrop, blir det långsammare än om man bara överför ett "long" värde. Detsamma är sant för returvärdet. För vanliga strängar (som filnamnet på wav-filen som ska spelas) ska inte det här vara ett problem.
Det här betyder att vi kan approximera den här tiden med 1/9000 sekund, det vill säga under 0,15 ms. Vi kommer att se att detta inte är relevant.
Därefter kommer tiden efter servern börjar spela och ljudkortet tar emot någonting. Servern måste buffra data, så att inga pauser hörs när andra program, som din X11-server eller muspling
-programmet, kör. Det sätt som detta hanteras på &Linux; är att det finns ett antal fragment av en viss storlek. Servern fyller på fragment, och ljudkortet spelar fragment.
Så antag att det finns tre fragment. Servern fyller det första och ljudkortet börjar spela det. Servern fyller det andra. Servern fyller det tredje. Servern är klar och andra program kan nu göra någonting.
När ljudkortet har spelat det första fragmentet, börjar det spela det andra och servern börjar fylla det första igen, och så vidare.
Den maximala latenstiden du får med allt detta är (antal fragment) * (storlek på varje fragment) / (samplingsfrekvens * (storlek på varje sampling)). Om vi antar 44 kHz stereo, och sju fragment på 1024 byte (de nuvarande förvalda inställningarna i aRts), så får vi 40 ms.
De här värdena kan anpassas enligt dina behov. CPU-användningen ökar dock med mindre latenstider, eftersom ljudservern måste fylla på buffrarna oftare, och med mindre delar. Det är också oftast omöjligt att nå bättre värden utan att ge ljudservern realtidsprioritet, eftersom man annars ofta får pauser.
Det är i alla fall realistiskt att göra någonting i stil med 3 fragment med 256 byte vardera, som skulle ändra det här värdet till 4,4 ms. Med 4,4 ms fördröjning skulle &arts; CPU-användning vara ungefär 7,5 %. Med en 40 ms fördröjning, skulle den vara ungefär 3 % (för en PII-350, och värdet kan bero på ditt ljudkort, version av kärnan och annat).
Så är det tiden som det tar för pling-ljudet att gå från högtalarna till dina öron. Antag att ditt avstånd från högtalarna är 2 meter. Ljud rör sig med hastigheten 330 meter per sekund. Så vi kan uppskatta den här tiden till 6 ms.
Latenstid i program med ljudflöden
Program med ljudflöden är de som skapar sitt ljud själva. Tänk dig ett spel som skickar ett konstant flöde med samplingar, och nu ska anpassas att spela upp ljud via &arts;. Som ett exempel: när jag trycker på en tangent så hoppar figuren som jag använder, och ett bång-ljud spelas upp.
Först så måste du veta hur &arts; hanterar strömmar. Det är mycket likt I/O med ljudkortet. Spelet skickar några paket med samplingar till ljudservern, låt oss anta tre stycken. Så fort som ljudservern är klar med det första paketet, skickar det en bekräftelse tillbaka till spelet att det paketet är klart.
Spelet skapar ytterligare ett ljudpaket och skickar det till servern. Under tiden börjar servern konsumera det andra ljudpaketet, och så vidare. Latenstiden här liknar den i det enklare fallet:
Tiden till kärnan har meddelat X11-servern att en knapp har tryckts ner.
Tiden till X11-servern har meddelat spelet att en knapp har tryckts ner.
Tiden till spelet har bestämt att den här knappen var värd att få ett bång spelat.
Tiden till ljudpaketet som spelet har börjat stoppa in bång-ljudet i når ljudservern.
Tiden det tar för bånget (som ljudservern börjar mixa med övrig utmatning omedelbart) att ta sig igenom buffrad data, till det verkligen når stället där ljudkortet spelar.
Tiden det tar för bång-ljudet från högtalarna att nå dina öron.
De externa latenstiderna, som ovan, är utanför det här dokumentets omfattning.
Det är uppenbart att latenstiden för strömmar beror på tiden det tar för alla paket som används att spelas en gång. Så den är (antal paket) * (storlek på varje paket) / (samplingsfrekvensen * (storlek på varje sampling)).
Som du ser är detta samma formel som gäller för fragmenten. För spel finns det dock ingen anledning att ha så korta fördröjningar som ovan. Jag skulle vilja säga att ett realistiskt exempel för ett spel skulle vara 2048 byte per paket, använd 3 paket. Latenstidsresultatet skulle då vara 35 ms.
Det här är baserat på följande: antag att ett spel renderar 25 bilder per sekund (för skärmen). Det är antagligen helt säkert att anta att en skillnad på en bild för ljudutmatningen inte skulle märkas. Därför är 1/25 sekunds fördröjning för ljudflöden acceptabelt, vilket i sin tur betyder att 40 ms skulle vara ok.
De flesta personer kör inte heller sina spel med realtidsprioritet, och faran med pauser i ljudet kan inte bortses ifrån. Strömmar med 3 paket på 256 byte är möjliga (jag provade det) - men orsakar mycket CPU-användning för strömhantering.
Latenstider på serversidan kan du beräkna precis som ovan.
Några CPU-användningshänsyn
Det finns många faktorer som påverkar CPU-användning i ett komplext scenario, med några program med ljudflöden och några andra program, några insticksprogram i servern, etc. För att ange några få:
Rå CPU-användning för de nödvändiga beräkningarna.
&arts; interna schemaläggningsomkostnad - hur &arts; bestämmer när vilken modul ska beräkna vad.
Omkostnad för konvertering av heltal till flyttal.
&MCOP; protokollomkostnad.
Kärnans process/sammanhangsbyte.
Kärnans kommunikationsomkostnad.
För beräkning av rå CPU-användning, om du spelar upp två strömmar samtidigt måste du göra additioner. Om du applicerar ett filter, är vissa beräkningar inblandade. För att ta ett förenklat exempel, att addera två strömmar kräver kanske fyra CPU-cykler per addition, på en 350 MHz processor är detta 44100 * 2 * 4 / 350000000 = 0,1 % CPU-användning.
&arts; interna schemaläggning: &arts; behöver bestämma vilken insticksmodul som ska beräkna vad när. Detta tar tid. Använd ett profileringsverktyg om du är intresserad av det. Vad som kan sägas i allmänhet är att ju mindre realtid som används (dvs. ju större block som kan beräknas åt gången) desto mindre schemaläggningsomkostnad fås. Över beräkning av block med 128 samplingar åt gången (alltså med användning av fragmentstorlekar på 512 byte) är schemaläggningsomkostnad förmodligen inte värt att bry sig om.
Konvertering från heltal till flyttal: &arts; använder flyttal som internt dataformat. De är enkla att hantera, och på moderna processorer inte mycket långsammare än heltalsoperationer. Om det i alla fall finns klienter som spelar data som inte är flyttal (som ett spel som ska göra sin ljudutmatning via &arts;), behövs konvertering. Detsamma gäller om du vill spela upp ljud på ditt ljudkort. Ljudkortet behöver heltal, så du måste konvertera.
Här är värden för en Celeron, ungefärliga klockcykler per sampling, med -O2 och egcs 2.91.66 (mätta av Eugene Smith hamster@null.ru). Det här är förstås ytterst processorberoende:
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
Så detta betyder 1 % CPU-användning för konvertering och 5 % för interpolation på den här 350 MHz processorn.
&MCOP; protokollomkostnad: &MCOP; klarar, som en tumregel, 9000 anrop per sekund. En stor del av detta är inte &MCOP;:s fel, utan hör ihop med de två orsakerna för kärnan som nämns nedan. Det här ger i alla fall en bas för att göra beräkningar av vad kostaden för strömhantering är.
Varje datapaket som skickas med en ström kan anses vara ett &MCOP;-anrop. Stora paket är förstås långsammare än 9000 paket/s, men det ger en god idé.
Antag att du använder paketstorlekar på 1024 byte. På så sätt, för att överföra en ström med 44 kHz stereo, behöver du överföra 44100 * 4 / 1024 = 172 paket per sekund. Antag att du kunde överföra 9000 paket med 100 % CPU-användning, då får du (172 *100) / 9000 = 2 % CPU-användning på grund av strömmen med 1024 byte paket.
Detta är en approximation. Det visar i alla fall att du skulle klara dig mycket bättre (om du har råd med latenstiden), med att till exempel använda paket på 4096 byte. Här kan vi skapa en kompakt formel, genom att beräkna paketstorleken som orsakar 100 % CPU-användning som 44100 * 4 / 9000 = 19,6 samplingar, och på så sätt få snabbformeln:
CPU-användning för en ström i procent = 1960 / (din paketstorlek)
som ger oss 0,5 % CPU-användning med en ström av 4096 byte paket.
Kärnans process/sammanhangsbyte: Det här är en del av &MCOP;-protokollets omkostnad. Att byta mellan två processer tar tid. Det blir en ny minnesmappning, cachar blir ogiltiga, och en del annat (om en expert på kärnan läser det här - tala om de exakta orsakerna för mig). Det här betyder: det tar tid.
Jag är inte säker på hur många processbyten &Linux; kan göra per sekund, men värdet är inte oändligt. Så av &MCOP;-protokollets omkostnad, antar jag att en hel del beror på processbyten. När &MCOP; först påbörjades provade jag samma kommunikation inne i en process, och det var mycket snabbare (ungefär fyra gånger snabbare).
Kärnans kommunikationsomkostnad: Det här är en del av &MCOP;-protokollets omkostnad. Att överföra data mellan processer görs för närvarande via ett uttag (socket). Det här är bekvämt, eftersom den vanliga select() metoden kan användas för att avgöra när ett meddelande har anlänt. Det kan också kombineras med andra I/O-källor som ljud-I/O, X11-server eller vad som helst annat.
De här läs- och skrivanropen kostar definitivt processorcykler. För små anrop (som att överföra en midi-händelse) är det förmodligen inte så farligt, men för stora anrop (som att överföra en videobild med flera Mibyte) är det helt klart ett problem.
Att lägga till användning av delat minne till &MCOP; där det är lämpligt är förmodligen den bästa lösningen. Det måste dock göras transparent för tillämpningsprogrammeraren.
Ta ett profileringsverktyg och gör andra prov för att exakt ta reda på hur nuvarande ljudströmmar påverkas av att inte använda delat minne. Det är dock inte så dåligt, eftersom ljudströmmar (spela upp mp3) kan göras med totalt 6 % CPU-användning för &artsd; och artscat (och 5 % för mp3-avkodaren). Det här innefattar dock allting från nödvändiga beräkningar till omkostnad för uttaget, så jag skulle uppskatta att man skulle vinna ungefär 1 % på att använda delat minne.
Några riktiga värden
De här är gjorda med den nuvarande utvecklingsversionen. Jag ville också försöka med riktigt svåra fall, så det här är inte vad program för daglig användning skulle göra.
Jag skrev ett program som heter streamsound som skickar dataflöden till &arts;. Här körs det med realtidsprioritet (utan problem), och en liten insticksmodul på serversidan (volymskalning och klippning):
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 streamsound
5002 stefan 20 0 2208 2208 1684 S 0 6.8 1.7 0:07 streamsound
4997 stefan 20 0 2208 2208 1684 S 0 6.6 1.7 0:07 streamsound
Var och en av programmen skickar en ström med 3 fragment på 1024 byte (18 ms). Det finns tre sådana klienter som kör samtidigt. Jag vet att det verkar vara lite väl mycket, men som jag sa: ta ett profileringsverktyg och ta reda på vad som kostar tid, och om du vill, förbättra det.
Jag tror i alla fall inte att använda strömmar så här är realistiskt eller vettigt. För att göra det hela ännu mer extremt, försökte jag med den minsta möjliga latenstiden. Resultat: man kan använda strömmar utan avbrott med ett klientprogram, om man tar 2 fragment med 128 byte mellan aRts och ljudkortet, och mellan klientprogrammet och aRts. Det här betyder att man har en total maximal latenstid på 128 * 4 / 44100 * 4 = 3 ms, där 1,5 ms skapas på grund av I/O till ljudkortet och 1,5 ms skapas av kommunikation med &arts;. Båda programmen måste köra med realtidsprioritet.
Men det här kostar en enorm mängd CPU. Det här exemplet kostar ungefär 45 % på min P-II/350. Det börjar också gå fel om man startar top, flyttar fönster på X11-skärmen eller gör disk-I/O. Allt det här har med kärnan att göra. Problemet är att schemalägga två eller flera processer med realtidsprioritet också kostar en enorm ansträngning, ännu mer än kommunikation och meddelande till varandra, etc.
Till sist, ett mer vardagligt exempel: Det här är &arts; med artsd och en artscat (en klient med dataflöde) som kör 16 fragment på 4096 byte:
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
Bussar
Bussar är förbindelser som byggs dynamiskt för att överföra ljud. Det finns ett antal upplänkar och nerlänkar. Alla signaler från upplänkarna adderas och skickas till nerlänkarna.
Bussar är för närvarande implementerade för att fungera med stereo, så du kan bara överföra stereodata via bussar. Om du vill ha monodata, nå, överför det bara på en kanal och sätt den andra till noll eller något godtyckligt. Vad du måste göra är att skapa en eller flera Synth_BUS_UPLINK-objekt och ge dem ett bussnamn, som de ska prata med (t.ex. ljud
eller trummor
). Skicka sedan helt enkelt in data dit.
Därefter måste du skapa en eller flera Synth_BUS_DOWNLINK-objekt, och tala om bussnamnet för dem (ljud
eller trummor
... om det passar ihop, kommer data igenom), och det blandade ljudet kommer ut igen.
Upplänkarna och nerlänkarna kan finnas i olika strukturer, du kan till och med ha olika aRts-byggare som kör och starta en upplänk i en och ta emot data i den andra med en nerlänk.
Vad som är trevligt med bussar är att de är fullständigt dynamiska. Klienter kan kopplas in eller ur i farten. Det ska inte höras några klick eller brus när detta sker.
Du ska förstås inte koppla in eller ur en klient medan den spelar en signal, eftersom den förmodligen inte är noll när den kopplas ur, och då uppstår ett klick.
Handlaren
&arts;/&MCOP; förlitar sig helt på att dela upp objekt i små komponenter. Det här gör allt mycket flexibelt, eftersom man lätt kan utöka systemet genom att lägga till nya komponenter, som implementerar nya effekter, filformat, oscillatorer, grafiska element, ... Eftersom nästan allt är komponenter, kan nästan allt lätt utökas utan att ändra befintlig källkod. Nya komponenter kan enkelt laddas dynamiskt för att förbättra program som redan finns.
För att detta ska fungera, behövs dock två saker:
Komponenter måste tala om att de finns - de måste beskriva vilka storartade saker de erbjuder, så att program kan använda dem.
Program måste aktivt leta efter komponenter som de skulle kunna använda, istället för att alltid använda samma komponent för en viss uppgift.
Kombinationen av det här: komponenter som säger här är jag, jag är tuff, använd mig
, och program (eller om man vill, andra komponenter) som går ut och letar efter vilken komponent som de kan använda för att få någonting gjort, kallas att handla.
I &arts; beskriver komponenter sig genom att ange värden som de stöder
som egenskaper. En typisk egenskap för en filladdningskomponent kan vara filändelsen för filerna som den kan behandla. Typiska värden kan vara wav, aiff eller mp3.
I själva verket kan varje komponent välja att erbjuda många olika värden för en egenskap. Så en enda komponent skulle kunna erbjuda att läsa både wav och aiff filer, genom att ange att den stöder de här värdena för egenskapen Extension
.
För att göra det här, måste en komponent lägga en .mcopclass-fil som innehåller egenskaperna den stöder på ett lämpligt ställe. För vårt exempel, kan den se ut så här (och skulle installeras i komponentkatalog/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
Det är viktigt att filnamnet på .mcopclass-filen också anger vad komponentens gränssnitt heter. Handlaren tittar inte på innehållet alls, om filen (som här) heter Arts/WavPlayObject.mcopclass, och komponentgränssnittet heter Arts::WavPlayObject (moduler hör ihop med kataloger).
För att leta efter komponenter finns det två gränssnitt (som är definierade i core.idl, så de är tillgängliga i varje program), som heter Arts::TraderQuery och Arts::TraderOffer. Du går på en shoppingrunda
efter komponenter så här:
Skapa ett frågeobjekt:
Arts::TraderQuery query;
Ange vad du vill ha. Som du såg ovan, beskriver komponenter sig själva med egenskaper, som de sätter till vissa värden. Så att specificera vad du vill ha görs genom att välja komponenter som stöder ett visst värde för en egenskap. Det här sker med metoden supports i TraderQuery:
query.supports("Interface","Arts::PlayObject");
query.supports("Extension","wav");
Till sist utförs förfrågan med metoden query. Sedan får du (förhoppningsvis) några erbjudanden:
vector<Arts::TraderOffer> *offers = query.query();
Nu kan du undersöka vad du hittade. Det viktiga är metoden interfaceName i TraderOffer, som ger dig namnen på komponenterna som svarade på frågan. Du kan också ta reda på ytterligare egenskaper med getProperty. Följande kod löper helt enkelt igenom alla komponenterna, skriver ut deras gränssnittsnamn (som skulle kunna användas för att skapa dem), och tar bort resultatet av frågan:
vector<Arts::TraderOffer>::iterator i;
for(i = offers->begin(); i != offers->end(); i++)
cout << i->interfaceName() << endl;
delete offers;
För att den här sortens handelsservice ska vara användbar, är det viktigt att på något sätt komma överens om vilka egenskaper som komponenter normalt ska definiera. Det är väsentligt att mer eller mindre alla komponenter inom ett visst område använder samma uppsättning egenskaper för att beskriva sig själva (och samma uppsättning värden när det behövs), så att program (eller andra komponenter) kan hitta dem.
Author (typ stäng, valfri): Upphovsman. Det här kan användas för att till sist låta världen få reda på att du skrivit någonting. Du kan skriva vad du vill här, en e-postadress är förstås en bra hjälp.
Buildable (typ boolean, rekommenderas): Byggbar. Det här anger om komponenten är användbar med RAD-verktyg (som aRts-byggaren) som använder komponenter genom att tilldela egenskaper och ansluta portar. Det rekommenderas att det här värdet sätts till true för nästan alla signalbehandlingskomponenter (som filer, ljudeffekter, oscillatorer, ...), och för alla andra objekt som kan användas på ett RAD-liknande sätt, men inte för interna objekt som till exempel Arts::InterfaceRepo.
Extension (typ sträng, använd där det passar): Filändelse. Alla moduler som hanterar filer bör fundera på att använda det här. Du anger filändelsen med små bokstäver utan .
här, så något som wav ska fungera utmärkt.
Interface (typ sträng, krävs): Gränssnitt. Det här ska omfatta hela listan på (användbara) gränssnitt som din komponent stöder, troligen inklusive Arts::Object och om tillämpligt Arts::SynthModule.
Language (typ sträng, rekommenderas): Språk. Om du vill att din komponent ska laddas dynamiskt, måste du ange språket här. För närvarande är det enda tillåtna värdet C++, som betyder att komponenten är skriven med det normala C++ programmeringsgränssnittet. Om du anger detta, måste du också ange egenskapen Library
nedan.
Library (typ sträng, använd där det passar): Bibliotek. Komponenter som är skrivna i C++ kan laddas dynamiskt. För att göra det måste du kompilera dem i en dynamiskt laddningsbar libtool (.la) modul. Här kan du ange namnet på .la-filen som innehåller din komponent. Kom ihåg att använda REGISTER_IMPLEMENTATION (som alltid).
MimeType (typ sträng, använd där det passar): Mimetyp. Alla som hanterar filer bör tänka sig att använda det här. Du ska ange standard-mimetypen med små bokstäver här, till exempel audio/x-wav.
&URL; (typ sträng, valfri): Om du vill tala om var man kan hitta en ny version av komponenten (eller en hemsida eller något annat), kan du göra det här. Det här ska vara en standard &HTTP;- eller &FTP;-webbadress.
Namnrymder i &arts;
Inledning
Varje namnrymdsdeklaration hör ihop med en deklaration av en modul
i &MCOP; &IDL;.
// mcop idl
module M {
interface A
{
}
};
interface B;
I det här fallet skulle den genererade C++ koden för &IDL;-fragmentet se ut så här:
// C++ deklaration
namespace M {
/* deklaration av A_base/A_skel/A_stub och liknande */
class A { // Smartwrap referensklass
/* [...] */
};
}
/* deklaration av B_base/B_skel/B_stub och liknande */
class B {
/* [...] */
};
Så när du hänvisar till klasserna från exemplet ovan i din C++ kod, måste du skriva M::A, men bara B. Du kan förstås använda using M
någonstans, som med alla namnrymnder i C++.
Hur &arts; använder namnrymder
Det finns en global namnrymd som kallas Arts
, som alla program och bibliotek som hör till &arts; själv använder för att lägga sina deklarationer i. Det här betyder att när du skriver C++ kod som beror på &arts;, måste du normalt använda prefixet Arts:: för varje klass som du använder, så här:
int main(int argc, char **argv)
{
Arts::Dispatcher dispatcher;
Arts::SimpleSoundServer server(Arts::Reference("global:Arts_SimpleSoundServer"));
server.play("/var/foo/någon_fil.wav");
Det andra alternativet är att skriva "using" en gång, så här:
using namespace Arts;
int main(int argc, char **argv)
{
Dispatcher dispatcher;
SimpleSoundServer server(Reference("global:Arts_SimpleSoundServer"));
server.play("/var/foo/någon_fil.wav");
[...]
I &IDL;-filer, har du egentligen inget val. Om du skriver kod som tillhör &arts; själv, måste du lägga den i modulen &arts;.
// IDL-fil för aRts-kod:
#include <artsflow.idl>
module Arts { // lägg den i Arts-namnrymd
interface Synth_TWEAK : SynthModule
{
in audio stream invalue;
out audio stream outvalue;
attribute float tweakFactor;
};
};
Om du skriver kod som inte hör till &arts; själv, ska du inte lägga den i namnrymden Arts
. Du kan dock skapa en egen namnrymd om du vill. Hur som helst, måste du använda prefix för klasser från &arts; som du använder.
// IDL-fil för kod som inte hör till aRts:
#include <artsflow.idl>
// skriv antingen med eller utan moduldeklaration, och de genererade klasserna
// kommer inte att använda en namnrymd:
interface Synth_TWEAK2 : Arts::SynthModule
{
in audio stream invalue;
out audio stream outvalue;
attribute float tweakFactor;
};
// du kan dock välja en egen namnrymd om du vill, så om du
// skriver programmet "PowerRadio", skulle du kunna göra så här:
module PowerRadio {
struct Station {
string name;
float frequency;
};
interface Tuner : Arts::SynthModule {
attribute Station station; // inget prefix för Station, samma modul
out audio stream left, right;
};
};
Interna funktioner: hur implementeringen fungerar
&MCOP; behöver ofta hänvisa till namn på typer och gränssnitt för typkonverteringar, gränssnitt och metodsignaturer. Dessa representeras av strängar i de vanliga &MCOP;-datastrukturerna, medan namnrymden alltid är fullständigt representerad i C++ stilen. Det här betyder att strängarna skulle innehålla M::A
och B
, enligt exemplen ovan.
Observera att detta gäller till och med om namnrymdskvalificeringen inte angavs inne i &IDL;-texten, eftersom sammanhanget klargör vilken namnrymd gränssnittet A var tänkt att användas.
Trådar i &arts;
Grundläggande information
Att använda trådar är inte möjligt på alla plattformar. Det här är orsaken att &arts; ursprungligen skrevs utan att använda trådar alls. För nästan alla problem, finns det en lösning utan trådar som gör samma sak som varje lösning med trådar.
Till exempel, istället för att placera ljudutmatning i en separat tråd, och göra den blockerande, använder &arts; ljudutmatning som inte blockerar, och räknar ut när nästa del av utdata ska skrivas med select().
&arts; stöder åtminstone (i de senaste versionerna) de som vill implementera sina objekt med trådar. Till exempel om du redan har kod för en mp3-spelare, och koden förväntar sig att mp3-avkodaren ska köras i en separat tråd, är det oftast lättast att behålla den konstruktionen.
Implementeringen av &arts;/&MCOP; är uppbyggd genom att dela tillståndet mellan olika objekt på tydliga och mindre tydliga sätt. En kort lista på delade tillstånd innefattar:
Avsändarobjektet som gör &MCOP;-kommunikation
Referensräkningen (Smartwrappers).
I/O-hanteraren som hanterar tidsgränser och fd-tidmätning.
Objekthanteraren som skapar objekt och laddar insticksmoduler dynamiskt.
Flödessystemet som anropar calculateBlock vid lämpliga tillfällen.
Inget av de ovanstående objekten förväntar sig att användas med samtidighet (dvs. anropas från olika trådar samtidigt). I allmänhet finns det två sätt att lösa detta:
Kräva att den som anropar vilken funktion som helst i objektet skaffar ett lås innan den används.
Göra objekten verkligt trådsäkra och/eller skapa instanser av dem för varje tråd.
&arts; använder det första sättet. Du behöver ett lås varje gång du ska komma åt några av de här objekten. Det andra sättet är svårare att göra. En snabbfix som försöker åstadkomma detta finns på http://space.twc.de/~stefan/kde/download/arts-mt.tar.gz, men för närvarande fungerar förmodligen ett minimalt sätt bättre, och orsakar mindre problem med befintliga program.
När/hur ska låsning ske?
Du kan skaffa/släppa låset med de två funktionerna:
Arts::Dispatcher::lock()
Arts::Dispatcher::unlock()
I allmänhet behöver du inte skaffa ett lås (och du ska inte försöka att göra det), om det redan hålls. En lista på villkor när detta är fallet är:
Du tar emot ett återanrop från I/O-hanteraren (tidsgräns eller fd).
Du anropas på grund av någon &MCOP;-begäran.
Du anropas från NotificationManager.
Du anropas från flödessystemet (calculateBlock)
Det finns också några undantag för funktioner som du bara kan anropa i huvudtråden, och av den anledningen aldrig behöver ett lås för att anropa dem:
Skapa och ta bort avsändaren eller I/O-hanteraren.
Dispatcher::run() / IOManager::run()
IOManager::processOneEvent()
Men det är allt. För allt annat som på något sätt hör ihop med &arts;, måste du skaffa låset, och släppa det igen när du är klar. Här är ett enkelt exempel:
class SuspendTimeThread : Arts::Thread {
public:
void run() {
/*
* du behöver det här låset därför att:
* - skapa en referens behöver ett lås (eftersom global: går till
* objekthanteraren, som i sin tur kan behöva GlobalComm
* objektet för att slå upp vart anslutningen ska göras)
* - tilldela en smartwrapper behöver ett lås
* - skapa ett objekt från en referens behöver ett lås (eftersom
* det kan behöva ansluta till en server)
*/
Arts::Dispatcher::lock();
Arts::SoundServer server = Arts::Reference("global:Arts_SoundServer");
Arts::Dispatcher::unlock();
for(;;) { /*
* du behöver ett lås här, eftersom
* - följa en referens för en smartwrapper behöver ett lås
* (eftersom det kan skapa objektet när det används)
* - att göra ett MCOP-anrop behöver ett lås
*/
Arts::Dispatcher::lock();
long seconds = server.secondsUntilSuspend();
Arts::Dispatcher::unlock();
printf("sekunder till vänteläge = %d",seconds);
sleep(1);
}
}
}
Trådrelaterade klasser
Följande trådrelaterade klasser finns tillgängliga för närvarande:
Arts::Thread - som kapslar in en tråd.
Arts::Mutex - som kapslar in en mutex.
Arts::ThreadCondition - som ger stöd för att väcka upp trådar som väntar på att ett visst villkor ska bli sant.
Arts::SystemThreads - som kapslar in operativsystemets trådningslager (och ger några hjälpfunktioner för tillämpningsprogrammerare).
Se länkarna för dokumentation.
Referenser och felhantering
&MCOP;-referenser är ett av de mest centrala koncepten i &MCOP; programmering. Det här avsnittet försöker beskriva exakt hur referenser används, och behandlar särskilt felfall (serverkrascher).
Grundläggande egenskaper för referenser
En &MCOP; referens är inte ett objekt, utan en referens till ett objekt: Även om följande deklaration
Arts::Synth_PLAY p;
ser ut som en definition av ett objekt, så deklarerar det bara en referens till ett objekt. Som C++ programmerare, kan du också se den som Synth_PLAY *, en sorts pekare till ett Synth_PLAY-objekt. Det betyder i synnerhet att p kan vara samma sak som en NULL-pekare.
Du kan skapa en NULL-referens genom att explicit tilldela den.
Arts::Synth_PLAY p = Arts::Synth_PLAY::null();
Att anropa objekt med en NULL-referens orsakar en minnesdump
Arts::Synth_PLAY p = Arts::Synth_PLAY::null();
string s = p.toString();
orsakar en minnesdump. Om man jämför detta med en pekare, är det i stor sett samma som
QWindow* w = 0;
w->show();
vilket varje C++ programmerare vet att man ska undvika.
Oinitierade objekt försöker att skapa sig själva när de först används
Arts::Synth_PLAY p;
string s = p.toString();
är något annorlunda än att följa en NULL-pekare. Du talade inte alls om för objektet vad det är, och nu försöker du använda det. Gissningen här är att du vill ha en ny lokal instans av ett Arts::Synth_PLAY-objekt. Du kan förstås ha velat göra något annat (som att skapa objektet någon annanstans, eller använda ett befintligt fjärrobjekt). Det är i alla fall en bekväm genväg för att skapa objekt. Att skapa ett objekt när det först används fungerar inte när du väl har tilldelat det något annat (som en null-referens).
Den motsvarande C++ terminologin skulle vara
QWidget* w;
w->show();
som naturligtvis helt enkelt ger ett segmenteringsfel i C++. Så detta är annorlunda här. Det här sättet att skapa objekt är knepigt, eftersom det inte är nödvändigt att det finns en implementering för ditt gränssnitt.
Betrakta till exempel ett abstrakt objekt som ett Arts::PlayObject. Det finns naturligtvis konkreta PlayObjects, som de för att spela mp3-filer eller wav-filer, men
Arts::PlayObject po;
po.play();
misslyckas helt säkert. Problemet är att fastän ett PlayObject försöker skapas, så misslyckas det eftersom det bara finns objekt som Arts::WavPlayObject och liknande. Använd därför bara det här sättet att skapa objekt om du är säker på att det finns en implementering.
Referenser kan peka på samma objekt
Arts::SimpleSoundServer s = Arts::Reference("global:Arts_SimpleSoundServer");
Arts::SimpleSoundServer s2 = s;
skapar två referenser som anger samma objekt. Det kopierar inte något värde, och skapar inte två objekt.
Alla objekt referensräknas. Så fort ett objekt inte har några referenser längre, tas det bort. Det finns inget sätt att uttryckligen ta bort ett objekt, men du kan dock använda något sådant här
Arts::Synth_PLAY p;
p.start();
[...]
p = Arts::Synth_PLAY::null();
för att få Synth_PLAY-objektet att försvinna till slut. I synnerhet ska det aldrig vara nödvändigt att använda new och delete i samband med referenser.
Fallet med misslyckanden
Eftersom referenser kan peka på fjärrobjekt, kan servrarna som innehåller de här objekten krascha. Vad händer då?
En krasch ändrar inte om en referens är en null-referens. Det här betyder att om foo.isNull() var true innan en serverkrasch är den också true efter en serverkrasch (vilket är självklart). Det betyder också att om foo.isNull() var false innan en serverkrasch (foo angav ett objekt) är den också false efter serverkraschen.
Att anropa metoder med en giltig referens förblir säkert. Antag att servern som innehåller objektet calc kraschade. Fortfarande är anrop till objekt som
int k = calc.subtract(i,j)
säkra. Det är uppenbart att subtract måste returnera något här, vilket den inte kan eftersom fjärrobjektet inte längre finns. I det här fallet skulle (k == 0) vara sant. I allmänhet försöker operationer returnera något neutralt
som resultat, som 0.0, en null-referens för objekt eller tomma strängar, när objektet inte längre finns.
Att kontrollera med error() avslöjar om något fungerade.
För ovanstående fall, skulle
int k = calc.subtract(i,j)
if(k.error()) {
printf("k är inte i-j!\n");
}
skriva ut k är inte i-j när fjärranropet inte fungerade. Annars är k verkligen resultatet av subtraktionsoperationen som utförs av fjärrobjektet (ingen serverkrasch). För metoder som gör saker som att ta bort en fil, kan du inte veta säkert om det verkligen skett. Naturligtvis skedde det om .error() är false. Men om .error() är true, finns det två möjligheter:
Filen togs bort, och servern kraschade precis efter den togs bort, men innan resultatet överfördes.
Servern kraschade innan den kunde ta bort filen.
Att använda nästlade anrop är farligt i ett program som ska vara kraschsäkert.
Att använda något som liknar
window.titlebar().setTitle("foo");
är ingen bra Idé. Antag att du vet att fönstret innehåller en giltig fönsterreferens. Antag att du vet att window.titlebar() returnerar en referens till namnlisten eftersom fönsterobjektet är riktigt implementerat. Men satsen ovan är ändå inte säker.
Vad kan hända om servern som innehåller fönsterobjektet har kraschat. Då, oberoende av hur bra implementeringen av Window är, så kommer du att få en null-referens som resultat av operationen window.titlebar(). Och sedan kommer förstås anropet till setTitle med den här null-referensen också leda till en krasch.
Så en säker variant av detta skulle vara
Titlebar titlebar = window.titlebar();
if(!window.error())
titlebar.setTitle("foo");
och lägg till den riktiga felhanteringen om du vill. Om du inte litar på implementeringen av Window, kan du lika väl använda
Titlebar titlebar = window.titlebar();
if(!titlebar.isNull())
titlebar.setTitle("foo");
som båda är säkra.
Det finns andra felorsaker, som nerkoppling av nätverket (antag att du tar bort kabeln mellan din server och klient medan ditt program kör). Deras effekt är ändå likadan som en serverkrasch.
Totalt sett är det förstås en policyfråga hur strikt du försöker hantera kommunikationsfel i hela ditt program. Du kan följa metoden om servern kraschar, måste vi avlusa den till den aldrig kraschar igen
, som skulle betyda att du inte behöver bry dig om alla de här problemen.
Interna funktioner: distribuerad referensräkning
Ett objekt måste ägas av någon för att finnas till. Om det inte gör det, kommer det upphöra att finnas till (mer eller mindre) omedelbart. Internt anges en ägare genom att anropa _copy(), som räknar upp en referensräknare, och en ägare tas bort genom att anropa _release(). Så fort referensräknaren når noll, så tas objektet bort.
Som en variation på temat, anges fjärranvändning med _useRemote(), och löses upp med _releaseRemote(). Dessa funktioner har en lista över vilken server som har anropat dem (och därför äger objektet). Det här används om servern kopplar ner (dvs. krasch, nätverksfel), för att ta bort referenser som fortfarande finns till objektet. Det här görs i _disconnectRemote().
Nu finns det ett problem. Betrakta ett returvärde. I vanliga fall ägs inte returvärdesobjektet av funktionen som anropas längre. Det ägs inte heller av den som anropar, förrän meddelandet som innehåller objektet har tagits emot. Så det finns en tid med objekt som saknar ägare
.
Nu när vi skickar ett objekt kan man vara rimligt säker på att så fort det tas emot, ägs det av någon igen, om inte, återigen, mottagaren kraschar. Det här betyder i alla fall att speciell hänsyn måste tas för objekt åtminstone vid sändning, och troligen också vid mottagning, så att de inte tas bort meddetsamma.
Sättet som &MCOP; gör det här är genom att tagga
objekt som håller på att kopieras över nätverket. Innan en sådan kopiering börjar, anropas _copyRemote. Det här förhindrar att objektet tas bort på ett tag (5 sekunder). Så fort mottagaren anropar _useRemote(), tas taggen bort igen. Så alla objekt som skickas över nätverket, taggas innan överföringen.
Om mottagaren tar emot ett objekt som finns på samma server, så används förstås inte _useRemote(). I det här specialfallet, finns funktionen _cancelCopyRemote() för att ta bort taggen manuellt. Förutom detta, finns det också en tidsbaserad borttagning av taggen, om taggning gjordes, men mottagaren verkligen inte fick objektet (på grund av krasch, nätverksfel). Det här görs med klassen ReferenceClean.
Grafiska gränssnittselement
Grafiska gränssnittselement är för närvarande på det experimentella stadiet. Det här avsnittet beskriver vad som är meningen ska hända med dem, så om du är en utvecklare, kan du förstå hur &arts; kommer att hantera grafiska gränssnitt i framtiden. Det finns också redan en del kod på plats.
Grafiska gränssnittselement ska användas för att låta syntesstrukturer växelverka med användaren. I det enklaste fallet ska användaren kunna ändra några parametrar för en struktur direkt (som en förstärkningsfaktor som används innan den slutliga uppspelningsmodulen).
För mer komplexa fall, kan man tänka sig att användaren ändrar parametrar för grupper av strukturer och/eller strukturer som inte kör än, som att ändra ADSR enveloppen för det aktiva &MIDI;-instrumentet. Något annat skulle kunna vara att ange filnamnet för ett samplingsbaserat instrument.
Å andra sidan, skulle användaren kunna vilja övervaka vad synthezisern gör. Det skulle kunna finnas oscilloskop, spektrumanalysatorer volymmätare och experiment
som till exempel räknar ut frekvensöverföringskurvan för en given filtermodul.
Till sist, ska grafiska gränssnittselement kunna kontrollera hela strukturen av allt som kör inne i &arts;, och på vilket sätt. Användaren ska kunna tilldela instrument till midi-kanaler, starta nya effektbehandlingssteg, anpassa sin huvudmixerpanel (som själv är byggd av &arts;-strukturer) för att få ytterligare en kanal eller använda en annan strategi för tonkontroll.
Som du ser, ska grafiska gränssnittselement ge alla möjligheterna i en virtuell studio som &arts; simulerar åt användaren. Naturligtvis ska de också växelverka med midi-ingångar (som skjutreglage som också flyttas om de får &MIDI;-indata som ändrar motsvarande parameter), och troligen till och med skapa händelser själva, för att växelverkan med användaren ska kunna spelas in via en sequencer.
Tekniskt sett är Idén att ha en &IDL;-basklass för alla grafiska komponenter (Arts::Widget), och härleda ett antal vanliga komponenter från den (som Arts::Poti, Arts::Panel, Arts::Window, ...).
Därefter kan man implementera de här grafiska komponenterna med en verktygslåda, till exempel &Qt; eller Gtk. Till slut, bör effekter bygga sina grafiska gränssnitt från existerande komponenter. En efterklangseffekt skulle till exempel kunna bygga sitt grafiska gränssnitt från fem Arts::Poti-mojänger och ett Arts::Window. Så OM det finns en &Qt; implementering för de här grundkomponenterna, kan effekten visas med &Qt;. Om det finns en Gtk implementering, fungerar det också med Gtk (och ser mer eller mindre likadant ut).
Till sist, eftersom vi använder &IDL; här, kan aRts-byggaren (eller andra verktyg) sätta ihop grafiska gränssnitt visuellt, eller automatgenerera grafiska gränssnitt, med tips för parametervärden, enbart baserat på gränssnitten. Det borde vara ganska rättframt att skriva en klass för att skapa grafiskt gränssnitt från en beskrivning
, som tar en beskrivning av ett grafiskt gränssnitt (som innehåller de olika parametrarna och grafiska komponenterna), och skapar ett levande objekt för ett grafiskt gränssnitt från den.
Baserat på &IDL; och &arts;/&MCOP;-komponentmodellen, bör det vara lätt att utöka de möjliga objekten som kan användas för det grafiska gränssnittet precis lika lätt som det är att lägga till en insticksmodul till &arts; som till exempel implementerar ett nytt filter.