Verzögerungsfreier Soundserver



  • Hallo,
    für mein Program benötige ich eine verzögerungsfreie Soundausgabe, d.h. wenn ich meine „play“ funktion aufrufe muss sofort der Sound ertönen. Ich habe schon OpenAl und Alsa ausprobiert, aber ich habe jeweils eine kurze Verzögerung. Deswegen habe ich versucht, das ganze direkt auf der Soundkarte mit „/dev/dsp“auszugeben. Da ich auch mehrere Sounds gleichzeitig abspielen möchte, habe ich mir einen kleinen Soundserver geschrieben, allerdings habe ich wieder eine kleine Verzögerung. Hier mein Code:

    class PlaybackDsp : public Playback {
    
    …
    
    int playIdBuf;
    std::vector<int> playing;
    int currPlaying;
    std::map<int, std::string> id_file;
    std::map<int, unsigned int> id_pos;
    
    ...
    
    void play(std::string filename);
    static void* sound_server( void* ptr );
    void mix(unsigned char* dest, unsigned int dest_size, unsigned char* src, unsigned int src_size, unsigned int src_pos, unsigned int src_length);
    
    }
    
    PlaybackDsp::play(std::string filename) {
    
        //überprüfung ob sound vorhanden ist...
        ...
    
        //sound zur playlist hinzufügen
    
        playing.push_back(playIdBuf);
        id_file[playIdBuf] = filename;
        id_pos[playIdBuf] = 0;
        playIdBuf++;
    
    }
    
    void* PlaybackDsp::sound_server( void* ptr ) {
        PlaybackDsp* _this = (PlaybackDsp*) ptr;
    
        unsigned int bufferSize = 100;
        unsigned char* buffer;
        buffer = new unsigned char[bufferSize];
        std::vector<int> toDelete;
    
        //sound server main loop
        while(true) {
    
            //clear buffer
            for(unsigned int i = 0; i < bufferSize; i++) {
                buffer[i] = 0;
            }
    
            //mix current playing files into buffer
            std::vector<int>::iterator iter = _this->playing.begin();
            _this->currPlaying = _this->playing.size();
            while( iter != _this->playing.end() ) {
    
                _this->mix(buffer, bufferSize, _this->sounds[_this->id_file[*iter]]->data(), _this->sounds[_this->id_file[*iter]]->size(), _this->id_pos[*iter], bufferSize);
    
                if( _this->id_pos[*iter] + bufferSize < _this->sounds[_this->id_file[*iter]]->size() ) {
                    _this->id_pos[*iter] += bufferSize;
                } else {
                    toDelete.push_back(*iter);
                }
    
                iter++;
            }
    
            //write data in sound device
            if (write(_this->fd, buffer, bufferSize) == -1) {
                std::cerr << "Error writing to \"/dev/dsp\" !";
                continue;
            }
    
            //remove played sounds from playlist
            std::vector<int>::iterator itd = toDelete.begin();
            for(; itd < toDelete.end(); itd++) {
                int id = *itd;
    
                _this->id_file.erase(id);
                _this->id_pos.erase(id);
    
                std::vector<int>::iterator it = _this->playing.begin();
                while( it != _this->playing.end() ) {
                    if(*it == id) {
                        it = _this->playing.erase(it);
                    } else {
                        it++;
                    }
                }
    
            }
            toDelete.clear();
        }
    
        delete[] buffer;
    
        return 0;
    }
    
    void PlaybackDsp::mix(unsigned char* dest, unsigned int dest_size, unsigned char* src, unsigned int src_size, unsigned int src_pos, unsigned int src_length) {
    
        switch(sampleSize) {
            case 8:
                //...
                break;
    
            case 16: {
                short* dest_s = reinterpret_cast<short*>(dest);
                short* src_s = reinterpret_cast<short*>(src);
    
                unsigned int length;
                if(src_pos+src_length > src_size) {
                    length = src_size-src_pos;
                } else {
                    length = src_length;
                }
                if(length > dest_size) {
                    length = dest_size;
                }
    
                for(unsigned int i = 0; i < length/2; i++) {
                    dest_s[i] += src_s[src_pos/2+i]/2;
                }
    
                dest = reinterpret_cast<unsigned char*>(src_s);
    
                break;
            }
        }
    }
    

    Wie bekomme ich eine verzögerungsfreie Soundausgabe, oder mit welcher Bibliothek ist das möglich?

    2. Frage:
    Beim zusammenmixen von Sound halbiere ich zuerst die Amplitude um kein übersteuern der Sounds zu bekommen. Wenn ich jedoch mehrere habe, könnte es immer noch übersteuern, aber die Amplitude immer kleiner zu machen ist ja keine Lösung. Wie füge ich am besten die Sounds zusammen?

    Mit freundlichen Grüßen
    Lukas



  • Zur 1. Frage:
    Jack ist der übliche Kandidat.



  • Wie hier schon erwähnt wurde, kannst du jackd nutzen. Das wäre wohl am einfachsten.

    Je nach dem, was du vorhast, könntest du aber auch mit ALSA weiter kommen. Einfach mal den Puffer kleiner machen. Je kleiner der Puffer, um so geringer ist die Latenzzeit. Nichts anderes macht auch jackd . Wenn das dann noch nicht reicht, kannst du die Real-Time-Erweiterung der Kernels installieren. Die hilft dir aber nur etwas, wenn dein Puffer so klein ist, dass es zu underruns kommt.



  • Hallo,
    danke für die bisherigen Antworten. Mit Jack bekomme ich nun eine verzögerungsfreie Ausgabe, allerdings bleibt Frage 2, wie ich das parallele Abspielen mehrerer Sound realisiere. Mein bisheriges Vorgehen:

    1. Laden der Sounds mit libsnd
    2. Ins richtige Format mit libsamplerate konvertieren
    3. In meiner "play" Funktion füge ich den abzuspielenden Sound in meine play liste hinzu

    4. Im Process Callback von Jack addiere ich nun die einzelnen Samples der Sounds

    int PlaybackJack::process_cb(jack_nframes_t nframes, void *arg) {
    
        PlaybackJack* _this = (PlaybackJack*) arg;
    
        //grab output buffer
        sample_t *out_l = (sample_t *) jack_port_get_buffer( _this->output_port_l, nframes );
        sample_t *out_r = (sample_t *) jack_port_get_buffer( _this->output_port_r, nframes );
    
        //for each required sample
        for(jack_nframes_t i = 0; i < nframes; i++) {
    
            sample_t tmp_l = 0;
            sample_t tmp_r = 0;
            std::map<int, sample_t*>::iterator iter = _this->playing.begin();
            while( iter != _this->playing.end() ) {
                int id = iter->first;
    
                if(_this->playing_offset[id]+1 < _this->playing_samples[id]) {
                    tmp_l += _this->playing[id][_this->playing_offset[id]]/2;
                    tmp_r += _this->playing[id][_this->playing_offset[id]+1]/2;
                    _this->playing_offset[id]++;
                } else {
                    _this->playing.erase(id);
                    _this->playing_samples.erase(id);
                    _this->playing_offset.erase(id);
                }
    
                iter++;
            }
    
            out_l[i] = tmp_l;
            out_r[i] = tmp_r;
        }
    
        return 0;      
    }
    

    Ist dieses Vorgehen sinnvoll? Und wie addiere ich die Samples richtig, damit es zu keiner Übersteuerung und Lautstärkenänderung der Sounds kommt?

    Mit freundlichen Grüßen
    Lukas



    Als naiven Ansatz würde ich die Streams addieren und dann abschneiden.

    also

    o = i0 + i1;
    if(o > max) o = max;
    

    Wobei du natürlich einen möglichen Integer-Overflow durch die Addition auch abfangen solltest.



  • was denkt ihr von fließkommazahlen fürs mixen von sounddateien hab mal gehört das neuere bild/ton/video programme langsam umsteigen bzw. schon umgestiegen sind 😕

    lg lolo



  • Hallo,
    Jack arbeitet mit floats. Wie überprüfe ich da auf ein Overflow und was wäre dann das sinnvollste? Einfach auf den maximal Wert setzen hört sich dann ja nicht sonderlich gut an.

    Jetzt ist aber noch ein weiteres Problem aufgetreten. Wenn ich mehr als 7 sounds gleichzeitig abspiele benötigt mein Callback zu lange und Jack verwirft mein callback, sodass nichts mehr abgespielt wird. Wie kann ich mein callback noch optimieren?

    int PlaybackJack::process_cb(jack_nframes_t nframes, void *arg) {
    
        PlaybackJack* _this = (PlaybackJack*) arg;
    
        pthread_mutex_lock(&_this->mutex);
    
            //grab output buffer
            sample_t *out_l = (sample_t *) jack_port_get_buffer( _this->output_port_l, nframes );
            sample_t *out_r = (sample_t *) jack_port_get_buffer( _this->output_port_r, nframes );
    
            //for each required sample
            for(jack_nframes_t i = 0; i < nframes; i++) {
    
                sample_t tmp_l = 0;
                sample_t tmp_r = 0;
    
                std::map<int, std::string>::iterator iter = _this->playing.begin();
                while( iter != _this->playing.end()) {
    
                    if(_this->playing_offset[iter->first]+1 < _this->length[iter->second]) {
                        tmp_l += _this->sounds[iter->second][_this->playing_offset[iter->first]];
                        tmp_r += _this->sounds[iter->second][_this->playing_offset[iter->first]+1];
                        _this->playing_offset[iter->first]+=2;
                    } else {
                        _this->playing.erase(iter->first);
                        _this->playing_offset.erase(iter->first);
                    }
    
                    iter++;
                }
                out_l[i] = tmp_l;
                out_r[i] = tmp_r;
            }
    
        pthread_mutex_unlock(&_this->mutex);
    
        return 0;
    }
    

    Sollte ich das ganze mit normalen arrays realisieren und nicht std::map benutzen, oder gibt es da keinen großen Geschwindigkeitsunterschied?
    Ich habe auch schon versucht die Buffer Größe von Jack zu verkleinern (momentan 1024) aber dann brauch mein callback immer zu lang.

    Mit freundlichen Grüßen
    Lukas



  • lakiday schrieb:

    Hallo,
    Jack arbeitet mit floats. Wie überprüfe ich da auf ein Overflow und was wäre dann das sinnvollste? Einfach auf den maximal Wert setzen hört sich dann ja nicht sonderlich gut an.

    Bei Floats limitiert man sich ja eh auf den Bereich -1.0f bis 1.0f. Du hast also keine Probleme mit potentiellen Overflows.

    Sollte ich das ganze mit normalen arrays realisieren und nicht std::map benutzen, oder gibt es da keinen großen Geschwindigkeitsunterschied

    Eine Map ist ziemlich aufwendig! Du solltest lieber std::unordered_map oder einen std::vector / std::array benutzen, wenn es geht.



  • Hallo,
    ich hab jetzt das ganze auf normale arrays umgestellt. Jetzt kann ich bei gleicher Buffer Größe ca. 350 sounds gleichzeitig abspielen.

    Vielen Dank für die Antworten!

    Mit freundlichen Grüßen
    Lukas



  • Noch eine Anmerkung hierzu: Im JACK Process Callback ist genaugenommen alles tabu, was den Thread auf unbestimmte Zeit blockieren könnte. In dem von dir geposteten Code verstößt du an zwei Stellen gegen diese Regel:

    pthread_mutex_lock(&_this->mutex);
    
    _this->playing_offset.erase(iter->first);
    

    Klar, der Code läuft trotzdem irgendwie, aber wenn er auch bei niedriger Latenz wirklich zuverlässig und ohne Aussetzer funktionieren soll, solltest du auf solche Dinge achten und die Laufzeit so deterministisch wie möglich halten. Wenn dein Callback zu lange braucht, wartet die Soundkarte schließlich vergeblich auf neue Daten. Und das betrifft ggf. nicht nur dein eigenes Programm, sondern auch andere, "unschuldige" JACK-Clients die gleichzeitig laufen und durch die Verzögerung nicht mehr rechtzeitig zum Zuge kommen.


Anmelden zum Antworten