Russian Qt Forum
Ноябрь 23, 2024, 10:16 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: [1]   Вниз
  Печать  
Автор Тема: Проблема применения кольцевого буфера  (Прочитано 5973 раз)
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);
}

Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3260


Просмотр профиля
« Ответ #1 : Январь 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;
    }
Записан
Miralissa
Гость
« Ответ #2 : Январь 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;
    }


Посчитала, что % будет дороже условия, возможно я ошибалась  Улыбающийся
« Последнее редактирование: Январь 04, 2020, 16:02 от Miralissa » Записан
Miralissa
Гость
« Ответ #3 : Январь 05, 2020, 01:01 »

Так, кольцевой буфер я проверила в VS - работает как часы  Крутой, внесла пару поправок. Возможно, стоит попробовать запустить цепочку не применяя потоки QThread. У меня почему-то он всегда капризничает.  Строит глазки
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3260


Просмотр профиля
« Ответ #4 : Январь 05, 2020, 12:32 »

Откуда вообще информация, что в rptr есть bufferLength элементов?
Записан
Miralissa
Гость
« Ответ #5 : Январь 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, посмотрим что получится  Подмигивающий
Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #6 : Январь 05, 2020, 15:54 »

Так, кольцевой буфер я проверила в VS - работает как часы  Крутой, внесла пару поправок.
Ну 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: чуть подправил
« Последнее редактирование: Январь 06, 2020, 14:06 от Igors » Записан
qate
Супер
******
Offline Offline

Сообщений: 1177


Просмотр профиля
« Ответ #7 : Январь 06, 2020, 00:28 »

Есть же сигналы и слоты с безопасной передачей данных между потоками, но нет - будем городить кольцевые буфера !
Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #8 : Январь 06, 2020, 14:19 »

Есть же сигналы и слоты с безопасной передачей данных между потоками, но нет - будем городить кольцевые буфера !
Идет (бесконечный) поток данных, требуется хранить N последних. Как это сделать сигналами/слотами?
Записан
qate
Супер
******
Offline Offline

Сообщений: 1177


Просмотр профиля
« Ответ #9 : Январь 06, 2020, 17:35 »

Идет (бесконечный) поток данных, требуется хранить N последних. Как это сделать сигналами/слотами?

1. сигналом выдать данные из callback от устройства в поток обработки, тем самым обеспечить потокобезопасность
2. в потоке обработке уже можно что угодно делать, возможно у ТС проблема с потоками, весь код тайна и не показан
« Последнее редактирование: Январь 06, 2020, 17:40 от qate » Записан
Miralissa
Гость
« Ответ #10 : Январь 06, 2020, 21:51 »

Есть же сигналы и слоты с безопасной передачей данных между потоками, но нет - будем городить кольцевые буфера !


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

Сообщений: 11445


Просмотр профиля
« Ответ #11 : Январь 07, 2020, 14:49 »

1. сигналом выдать данные из callback от устройства в поток обработки, тем самым обеспечить потокобезопасность
2. в потоке обработке уже можно что угодно делать,
Ну обработчик будет получать фрагмент за фрагментом, все равно их придется как-то склеивать, отсекать старые и.т.д. Все равно маячит тот же кольцевой буфер, пусть на стороне обработчика

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

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

Сообщений: 1177


Просмотр профиля
« Ответ #12 : Январь 08, 2020, 00:02 »

Ну обработчик будет получать фрагмент за фрагментом, все равно их придется как-то склеивать, отсекать старые и.т.д. Все равно маячит тот же кольцевой буфер, пусть на стороне обработчика

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

писать буфера на стороне обработчика лучше тем, что отвязываемся от потока записи, он не должен занять про буфера обработки
если сигнал на каждый байт посылать не желательно - можно и накопить немного в обычном массиве, хотя не думаю, что такое будет при записи 44 кГц / 16 бит
Записан
Miralissa
Гость
« Ответ #13 : Январь 10, 2020, 01:38 »

Спасибо всем за помощь, основной механизм программы работает!!  Подмигивающий  В очередной раз убедилась, что между потоками QThread не должно быть ничего кроме сигналов-слотов, ошибка была в том что обращения к кольцевому буферу происходили в трёх разных потоках через указатели. Теперь кольцевой буфер я перенесла в поток обработчика, он действительно нужен, так как данные могут не успеть сконвертироваться и записаться в файл до получения новой порции сэмплов. Т.е., callback получает данные от драйвера и передаёт их по указателю сигналом в поток обработки, где они записываются в кольцевой буфер и если есть флаг готовности обработчика, считываются уже из буфера.
Записан
Страниц: [1]   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.082 секунд. Запросов: 23.