Russian Qt Forum

Qt => Вопросы новичков => Тема начата: Miralissa от Январь 04, 2020, 13:12



Название: Проблема применения кольцевого буфера
Отправлено: Miralissa от Январь 04, 2020, 13:12
Ребята, help! Хочу лёгкий аудио-рекордер на Qt + PortAudio, застряла на тестах передачи данных через кольцевой буфер. Цепочка представляет собой следующий маршрут: PortAudio -> Callback-функция(принимающая буферы с сэмплами) -> Кольцевой буфер -> обработчик (анализ и преобразование сэмплов/запись в файл) -> функция-индикатор (передаёт пиковые значения на QProgressBar). Загвоздка происходит при попытке записать данные в кольцевой буфер, выдаёт Exception at 0x7ff69bf44d73, code: 0xc0000005: read access violation at: 0xa, flags=0x0 (first chance). Не могу понять где я накосячила ^_^, вроде по указателям всё сходится... Чувствую себя жутко тупой... ниже привожу код:

Это мой кольцевой буффер:

Код:
#include <QObject>

template<class T>

class QRingBuffer
{

public:
    QRingBuffer(quint16 size) : buffer(new T[size]),
        head(0), tail(0), bufferSize(size){}

    ~QRingBuffer(){delete[] buffer;}

    void addSample(T sample) //ЗДЕСЬ ВОЗНИКАЕТ ОШИБКА ЧТЕНИЯ ПАМЯТИ
    {
        if (++head >= bufferSize)
            head -= bufferSize;
        buffer[head] = sample;
        head++;
    }

    T getSample()
    {
        if (++tail >= bufferSize)
            tail -= bufferSize;
        return buffer[tail++];
    }

private:
    T *buffer;
    quint16 bufferSize, head, tail;

};

Это обработчик, который должен запускаться в отдельном потоке:

DSPEngine.h

Код:
#include <QObject>
#include <QThread>
#include "qringbuffer.h"

extern "C"
{
#include <portaudio.h>
}

#define SAMPLE_RATE (44100)
#define PA_SAMPLE_SIZE (paInt16)
#define NUM_CHANNELS (1)
#define FRAMES_PER_BUFFER (512)
#define RINGBUFFER_SIZE (4)
#define SILENCE (0)
typedef short SAMPLE;

struct DataShuttle
{
    char syncFlag;
    QRingBuffer<SAMPLE> *ringBuffer;
};

class DSPEngine : public QObject
{
    Q_OBJECT
public slots:
    //бесконечный цикл обработки сигнала запускается в отдельном потоке
    void startEngine(void *userData);

private:
    DataShuttle *data;
    void process();

signals:
    //Сигнал с пиковым значением для индикатора
    void levelChanged(quint16);
};

DSPEngine.cpp

Код:
#include "dspengine.h"

void DSPEngine::startEngine(void *userData)
{
    //получаем указатель на структуру данных
    data = (DataShuttle*)userData;
    //В бесконечном цикле ждём флага синхронизации с callback
    //(это означает, что кольцевой буфер готов и мы можем читать из него)
    while (true)
    {
        if (data->syncFlag == 'r')
        {
            data->syncFlag = 'w';
            process();
        }
    }
}

void DSPEngine::process()
{
    //Считываем сэмплы из кольцевого буфера
    SAMPLE *samples = nullptr;
    quint16 bufferSize = FRAMES_PER_BUFFER * NUM_CHANNELS;
    for (int i = 0; i < bufferSize; i++)
        samples[i] = data->ringBuffer->getSample();

    //Получаем пиковое значение для индикации сигнала
    quint16 max = 0;
    for (int x = 0; x < bufferSize; x++)
        if(samples[x] > max)
            max = samples[x];
    emit levelChanged(max);
}

И основной модуль:

BlackBox.h

Код:
#include <QMainWindow>
#include "dspengine.h"

QT_BEGIN_NAMESPACE
namespace Ui { class BlackBox; }
QT_END_NAMESPACE

class BlackBox : public QMainWindow
{
    Q_OBJECT
    QThread dspThread;
public:
    BlackBox(QWidget *parent = nullptr);
    ~BlackBox();

private slots:
    void listen(int indx);
    void indicate(quint16 val);

private:
    Ui::BlackBox *ui;
    bool initialize();
    void errorHandler(PaError error);
    bool queryAudioInputs();
    PaStreamParameters inputParameters;
    void ctrlsDisable();
    PaStream *inStream;

signals:
    //Сигнал для запуска обработчика в отдельном потоке
    void startDSP(void *data);
};

BlackBox.cpp

Код:
#include "blackbox.h"
#include "ui_blackbox.h"

BlackBox::BlackBox(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::BlackBox)
{
    ui->setupUi(this);
    //инициализируем PortAudio
    if (!initialize())
        ctrlsDisable();
    else
    {
        //подключаем выбор устройства записи
        connect(ui->devBox,SIGNAL(currentIndexChanged(int)), this, SLOT(listen(int)));
        //создаём поток обработчика сигнала
        DSPEngine *dspEngine = new DSPEngine;
        dspEngine->moveToThread(&dspThread);
        //подключаем сигналы-слоты для общения между потоками
        connect(&dspThread, &QThread::finished, dspEngine, &QObject::deleteLater);
        connect(this, &BlackBox::startDSP, dspEngine, &DSPEngine::startEngine);
        connect(dspEngine, &DSPEngine::levelChanged, this, &BlackBox::indicate);
        //стартуем отдельный поток
        dspThread.start();
    }
}

BlackBox::~BlackBox()
{
    dspThread.quit();
    dspThread.wait();
    delete ui;
}

//это наш callback, который получает данные с устройства записи и заполняет кольцевой буффер
static int recordCallBack(const void *inBuffer, void *outBuffer,
                      unsigned long framesPerBuffer,
                      const PaStreamCallbackTimeInfo *timeInfo,
                      PaStreamCallbackFlags statusFlags,
                          void *userData)
{
    DataShuttle *data = (DataShuttle*)userData;
    const SAMPLE *rptr = (const SAMPLE*)inBuffer;
    quint16 bufferLength = framesPerBuffer * NUM_CHANNELS;
    (void)outBuffer;
    (void)timeInfo;
    (void)statusFlags;
    for (int i = 0; i < bufferLength; i++)
        data->ringBuffer->addSample(rptr[i]);

    //с помощью флагов заботимся о том,
    //чтобы буффер успел заполниться дважды,
    //прежде чем его считает обработчик
    if (data->syncFlag == 'w')
        data->syncFlag = 'r';
    else
        data->syncFlag = 'w';
    return paContinue;
}

void BlackBox::errorHandler(PaError error)
{
    ui->statusbar->showMessage(QString(Pa_GetErrorText(error)), 0);
}

void BlackBox::ctrlsDisable()
{
    ui->devBox->setDisabled(true);
    ui->startButton->setDisabled(true);
    ui->pathEdit->setDisabled(true);
    ui->pathButton->setDisabled(true);
}

bool BlackBox::initialize()
{
    int err = Pa_Initialize();
    if(err != paNoError)
    {
        errorHandler(err);
        return false;
    }
    else
    {
        return queryAudioInputs();
    }
}

bool BlackBox::queryAudioInputs()
{
    int numDevices = Pa_GetDeviceCount();
    if (numDevices < 1)
    {
        ui->statusbar->showMessage(numDevices < 0 ? QString(Pa_GetErrorText(numDevices)) : "No input device found!", 0);
        return false;
    }
    else
    {
        for (int i = 0; i < numDevices; i++)
        {
            const PaDeviceInfo *devInfo;
            devInfo = Pa_GetDeviceInfo(i);
            if(devInfo->maxInputChannels > 0)
                ui->devBox->addItem(QString(devInfo->name), qVariantFromValue(i));
        }
        return true;
    }
}

//функция запускает обработку данных с устройства записи по схеме
// PortAudio -> Callback -> RingBuffer -> DSP -> indicator
void BlackBox::listen(int indx)
{
    //создаём параметры входящего потокаы
    inputParameters.device = ui->devBox->itemData(indx).value<int>();
    inputParameters.channelCount = NUM_CHANNELS;
    inputParameters.sampleFormat = PA_SAMPLE_SIZE;
    inputParameters.suggestedLatency = Pa_GetDeviceInfo(inputParameters.device)->defaultLowInputLatency;
    inputParameters.hostApiSpecificStreamInfo = nullptr;
    //Создаём структуру данных для обмена между потоками обработчика и Callback-функции
    DataShuttle sampleData;
    //Создаём кольцевой буффер
    sampleData.ringBuffer = new QRingBuffer<SAMPLE>(FRAMES_PER_BUFFER * RINGBUFFER_SIZE);
    //Устанавливаем флаг для пропуска 1 цикла записи в буффер без чтения
    sampleData.syncFlag = 's';
    //запускаем поток PortAudio
    Pa_OpenStream(&inStream, &inputParameters, NULL, SAMPLE_RATE, FRAMES_PER_BUFFER, paClipOff, &recordCallBack, &sampleData);
    Pa_StartStream(inStream);
    //Запускаем обработчик данных
    emit startDSP(&sampleData);
}

void BlackBox::indicate(quint16 val)
{
    ui->progressBar->setValue(val);
}



Название: Re: Проблема применения кольцевого буфера
Отправлено: Авварон от Январь 04, 2020, 15:14
Точно падает тут? Может, падает
Код:
rptr[i]
?

Код:
 void addSample(T sample) //ЗДЕСЬ ВОЗНИКАЕТ ОШИБКА ЧТЕНИЯ ПАМЯТИ
    {
        if (++head >= bufferSize)
            head -= bufferSize;
        buffer[head] = sample;
        head++;
    }

Вообще это странный код, head увеличивается дважды. Можно же проще:
Код:
 void addSample(T sample) //ЗДЕСЬ ВОЗНИКАЕТ ОШИБКА ЧТЕНИЯ ПАМЯТИ
    {
        buffer[head] = sample;
        head = (head + 1) % bufferSize;
    }


Название: Re: Проблема применения кольцевого буфера
Отправлено: Miralissa от Январь 04, 2020, 16:00
Точно падает тут? Может, падает
Код:
rptr[i]
?

Попробовала поменять на *rptr++, ошибка сместилась сюда (но ведь это не должно влиять - к членам массива в цикле вроде бы можно обращаться и так ptr и так *ptr++)  ???:

Код:
 void addSample(T sample)
    {
        if (++head >= bufferSize)
            head -= bufferSize;
        buffer[head] = sample; //ТЕПЕРЬ ЗДЕСЬ ВОЗНИКАЕТ ОШИБКА ЧТЕНИЯ ПАМЯТИ
        head++;
    }


Вообще это странный код, head увеличивается дважды. Можно же проще:

Код:
 void addSample(T sample)
    {
        buffer[head] = sample;
        head = (head + 1) % bufferSize;
    }


Посчитала, что % будет дороже условия, возможно я ошибалась  :)


Название: Re: Проблема применения кольцевого буфера
Отправлено: Miralissa от Январь 05, 2020, 01:01
Так, кольцевой буфер я проверила в VS - работает как часы  8), внесла пару поправок. Возможно, стоит попробовать запустить цепочку не применяя потоки QThread. У меня почему-то он всегда капризничает.  ::)


Название: Re: Проблема применения кольцевого буфера
Отправлено: Авварон от Январь 05, 2020, 12:32
Откуда вообще информация, что в rptr есть bufferLength элементов?


Название: Re: Проблема применения кольцевого буфера
Отправлено: Miralissa от Январь 05, 2020, 13:46
Откуда вообще информация, что в rptr есть bufferLength элементов?

Callback-функция периодически вызывается библиотекой PortAudio для передачи заполненного на величину bufferLength буфера после вызова метода Pa_StartStream() в файле BlackBox.cpp, и когда я включала qDebug в цикл
Код:
for (int i = 0; i < bufferLength; i++)
        qDebug() << rptr[i];
        //data->ringBuffer->addSample(rptr[i]);
то вывод приложения показывает вполне правдоподобные данные с подключенного микрофона. Я подумала, что проблема кроется в указателе на кольцевой буфер и тем, что PortAudio вызывает callback в отдельном потоке, сейчас пробую передать в callback в качестве пользовательских данных не структуру с кольцевым буфером и флагами, а объект класса с самим callback-ом, как рекомендуют здесь: https://app.assembla.com/wiki/show/portaudio/Tips_CPlusPlus, посмотрим что получится  ;)


Название: Re: Проблема применения кольцевого буфера
Отправлено: Igors от Январь 05, 2020, 15:54
Так, кольцевой буфер я проверила в VS - работает как часы  8), внесла пару поправок.
Ну VS не такой уж авторитет :) И "парой поправок" там не отделаться, я бы сделал так

Код
C++ (Qt)
template<class T>
class CRingBuffer {
public:
   CRingBuffer( size_t  size ) :
     buf(size + 1),
     first(0),
     next(0)
  {
     assert(size > 0);
  }
 
  void push_back( const T & val )
  {
    buf[next] = val;
    next = (next + 1) % buf.size();
    if (next == first)
      first = (first + 1) % buf.size();
   }
 
  T pop_front( void )
  {
    assert(first != next);
    T val = buf[first];
    first = (first + 1) % buf.size();
    return val;
  }
 
  size_t size( void )  const
  {
    return (next >= first) ? (next- first) : (buf.size() - (first - next));
  }
 
private:
// data
  std::vector<T> buf;  
  size_t first;    // first written element index
  size_t next;   // index of next element to write
};
Писал здесь, возможны ошибки

Посчитала, что % будет дороже условия, возможно я ошибалась  :)
Оптимизировать можно/нужно когда достигнут ф-ционвл

Edit: чуть подправил


Название: Re: Проблема применения кольцевого буфера
Отправлено: qate от Январь 06, 2020, 00:28
Есть же сигналы и слоты с безопасной передачей данных между потоками, но нет - будем городить кольцевые буфера !


Название: Re: Проблема применения кольцевого буфера
Отправлено: Igors от Январь 06, 2020, 14:19
Есть же сигналы и слоты с безопасной передачей данных между потоками, но нет - будем городить кольцевые буфера !
Идет (бесконечный) поток данных, требуется хранить N последних. Как это сделать сигналами/слотами?


Название: Re: Проблема применения кольцевого буфера
Отправлено: qate от Январь 06, 2020, 17:35
Идет (бесконечный) поток данных, требуется хранить N последних. Как это сделать сигналами/слотами?

1. сигналом выдать данные из callback от устройства в поток обработки, тем самым обеспечить потокобезопасность
2. в потоке обработке уже можно что угодно делать, возможно у ТС проблема с потоками, весь код тайна и не показан


Название: Re: Проблема применения кольцевого буфера
Отправлено: Miralissa от Январь 06, 2020, 21:51
Есть же сигналы и слоты с безопасной передачей данных между потоками, но нет - будем городить кольцевые буфера !


Вот приходила же в голову такая идея, сигналом передавать ссылку на массив полученный в callback-функции и спокойно работать в другом потоке!! Надо попробовать! ;) А кольцевые буфера  ::) городила, потому что воспринимала это как единственную возможность подружить асинхронные процессы. Драйвер аудиокарты может наполнять буфер с разной скоростью (хотя в "вакууме" скорость потока данных, создаваемого АЦП должна быть однородна и соответствовать частоте дискретизации). Далее я планировала анализ значений сэмплов (задача - писать только когда есть звук выше определённого порога), конверсию в формат аас со сжатием и запись в файл. То есть обработка данных может занять больше времени, чем их получение. Ну а в тройном кольцевом буфере места для записи достаточно, чтобы записать 2 буфера от драйвера, пока читается 1 уже записанный, конвертируется и пишется в файл. Спасибо за совет, я обязательно попробую собрать вариант на сигналах-слотах и напишу о результатах!  :-*


Название: Re: Проблема применения кольцевого буфера
Отправлено: Igors от Январь 07, 2020, 14:49
1. сигналом выдать данные из callback от устройства в поток обработки, тем самым обеспечить потокобезопасность
2. в потоке обработке уже можно что угодно делать,
Ну обработчик будет получать фрагмент за фрагментом, все равно их придется как-то склеивать, отсекать старые и.т.д. Все равно маячит тот же кольцевой буфер, пусть на стороне обработчика

А кольцевые буфера  ::) городила, потому что воспринимала это как единственную возможность подружить асинхронные процессы.
"В огороде бузина, в Киеве дядька". т.е. эти вещи никак не связаны.

Далее я планировала анализ значений сэмплов (задача - писать только когда есть звук выше определённого порога)
И что, появился адын сампл выше порога - погнали писать? Это может быть случайный выброс/выхлоп. Скорее всего (ну это я предполагаю) надо поймать момент "сигнал пошел", напр среднее N самплов превысило порог. Поэтому для начала пихать все в кольцевой буфер (в нитке АЦП) - вполне здравая мысль. А когда сигнал пойман - отсылать обработчику, как сказал товарищ.


Название: Re: Проблема применения кольцевого буфера
Отправлено: qate от Январь 08, 2020, 00:02
Ну обработчик будет получать фрагмент за фрагментом, все равно их придется как-то склеивать, отсекать старые и.т.д. Все равно маячит тот же кольцевой буфер, пусть на стороне обработчика

мне всеже не ясно - зачем отсекать старые ?
звуковые редакторы пишут звук весь - от нажатия на "rec" до нажатия на "stop"

писать буфера на стороне обработчика лучше тем, что отвязываемся от потока записи, он не должен занять про буфера обработки
если сигнал на каждый байт посылать не желательно - можно и накопить немного в обычном массиве, хотя не думаю, что такое будет при записи 44 кГц / 16 бит


Название: Re: Проблема применения кольцевого буфера
Отправлено: Miralissa от Январь 10, 2020, 01:38
Спасибо всем за помощь, основной механизм программы работает!!  ;)  В очередной раз убедилась, что между потоками QThread не должно быть ничего кроме сигналов-слотов, ошибка была в том что обращения к кольцевому буферу происходили в трёх разных потоках через указатели. Теперь кольцевой буфер я перенесла в поток обработчика, он действительно нужен, так как данные могут не успеть сконвертироваться и записаться в файл до получения новой порции сэмплов. Т.е., callback получает данные от драйвера и передаёт их по указателю сигналом в поток обработки, где они записываются в кольцевой буфер и если есть флаг готовности обработчика, считываются уже из буфера.