diff --git a/CMakeLists.txt b/CMakeLists.txt index ece5e94..0e844eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,4 +15,27 @@ set(CMAKE_AUTOMOC ON) # Ищем нужные модули Qt find_package(Qt6 REQUIRED COMPONENTS Widgets) +#ffmpeg +#Самый просто способ через pkg-config +find_package(PkgConfig REQUIRED) + +pkg_check_modules(AVCODEC REQUIRED libavcodec) +pkg_check_modules(AVFORMAT REQUIRED libavformat) +pkg_check_modules(AVUTIL REQUIRED libavutil) +pkg_check_modules(SWRESAMPLE REQUIRED libswresample) + +include_directories( + ${AVCODEC_INCLUDE_DIRS} + ${AVFORMAT_INCLUDE_DIRS} + ${AVUTIL_INCLUDE_DIRS} + ${SWRESAMPLE_INCLUDE_DIRS} +) + +link_directories( + ${AVCODEC_LIBRARY_DIRS} + ${AVFORMAT_LIBRARY_DIRS} + ${AVUTIL_LIBRARY_DIRS} + ${SWRESAMPLE_LIBRARY_DIRS} +) + add_subdirectory(src) diff --git a/src/AudioDecoder.cpp b/src/AudioDecoder.cpp new file mode 100644 index 0000000..e0f4520 --- /dev/null +++ b/src/AudioDecoder.cpp @@ -0,0 +1,158 @@ +#include "AudioDecoder.hpp" +#include + +AudioDecoder::AudioDecoder() = default; + +AudioDecoder::~AudioDecoder() { + close(); +} + +bool AudioDecoder::open(const QString& inputPath) { + m_inputPath = inputPath; + + if (avformat_open_input(&m_formatContext, m_inputPath.toUtf8().constData(), nullptr, nullptr) < 0) { + qWarning() << "ERROR: Failed to open input file"; + return false; + } + if (avformat_find_stream_info(m_formatContext, nullptr) < 0) { + qWarning() << "ERROR: Failed to find stream info"; + return false; + } + + m_streamIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); + if (m_streamIndex < 0) { + qWarning() << "ERROR: No audio stream found"; + return false; + } + + AVStream* stream = m_formatContext->streams[m_streamIndex]; + const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id); + if (!codec) { + qWarning() << "ERROR: Decoder not found"; + return false; + } + + m_codecContext = avcodec_alloc_context3(codec); + if (!m_codecContext || avcodec_parameters_to_context(m_codecContext, stream->codecpar) < 0) { + qWarning() << "ERROR: Failed to copy codec parameters"; + return false; + } + if (avcodec_open2(m_codecContext, codec, nullptr) < 0) { + qWarning() << "ERROR: Failed to open codec"; + return false; + } + + // ensure input layout is set + if (m_codecContext->ch_layout.order == AV_CHANNEL_ORDER_UNSPEC) { + av_channel_layout_default(&m_codecContext->ch_layout, m_codecContext->ch_layout.nb_channels); + } + av_channel_layout_copy(&m_inputChannelLayout, &m_codecContext->ch_layout); + + // save output params and init default layout + m_outputSampleRate = m_codecContext->sample_rate; + m_outputChannels = m_codecContext->ch_layout.nb_channels; + av_channel_layout_default(&m_outputChannelLayout, m_outputChannels); + + if (!initResampler()) + return false; + + m_packet = av_packet_alloc(); + m_frame = av_frame_alloc(); + m_endOfFile = false; + + return true; +} + +bool AudioDecoder::initResampler() { + if (!m_codecContext) + return false; + + if (m_swrContext) + swr_free(&m_swrContext); + + if (swr_alloc_set_opts2(&m_swrContext, + &m_outputChannelLayout, + m_outputSampleFormat, + m_outputSampleRate, + &m_inputChannelLayout, + m_codecContext->sample_fmt, + m_codecContext->sample_rate, + 0, nullptr) < 0) { + qWarning() << "ERROR: Failed to allocate resampler"; + return false; + } + if (swr_init(m_swrContext) < 0) { + qWarning() << "ERROR: Failed to initialize resampler"; + return false; + } + return true; +} + +int AudioDecoder::decode(uint8_t* outputBuffer, int outputBufferSize) { + if (!m_formatContext || !m_codecContext || !m_swrContext) + return -1; + + while (true) { + if (!m_endOfFile) { + if (av_read_frame(m_formatContext, m_packet) < 0) { + m_endOfFile = true; + avcodec_send_packet(m_codecContext, nullptr); + } else if (m_packet->stream_index == m_streamIndex) { + avcodec_send_packet(m_codecContext, m_packet); + av_packet_unref(m_packet); + } else { + av_packet_unref(m_packet); + continue; + } + } + + int ret = avcodec_receive_frame(m_codecContext, m_frame); + if (ret == AVERROR(EAGAIN)) + continue; + if (ret == AVERROR_EOF) + return 0; + if (ret < 0) { + qWarning() << "ERROR: Failed to receive frame"; + return -1; + } + + // resample into outputBuffer + int maxSamples = outputBufferSize / av_get_bytes_per_sample(m_outputSampleFormat); + int samplesConverted = swr_convert(m_swrContext, + &outputBuffer, + maxSamples, + (const uint8_t**)m_frame->data, + m_frame->nb_samples); + av_frame_unref(m_frame); + if (samplesConverted < 0) { + qWarning() << "ERROR: Failed to resample"; + return -1; + } + return samplesConverted * m_outputChannels * av_get_bytes_per_sample(m_outputSampleFormat); + } +} + +void AudioDecoder::close() { + if (m_frame) av_frame_free(&m_frame); + if (m_packet) av_packet_free(&m_packet); + if (m_swrContext) swr_free(&m_swrContext); + if (m_codecContext) avcodec_free_context(&m_codecContext); + if (m_formatContext) avformat_close_input(&m_formatContext); + + av_channel_layout_uninit(&m_inputChannelLayout); + av_channel_layout_uninit(&m_outputChannelLayout); +} + +int AudioDecoder::bitrate() const { + return m_formatContext ? m_formatContext->bit_rate : 0; +} + +qint64 AudioDecoder::durationUs() const { + return (m_formatContext && m_formatContext->duration != AV_NOPTS_VALUE) + ? m_formatContext->duration + : 0; +} + +QString AudioDecoder::sampleFormatName() const { + return QString::fromUtf8(av_get_sample_fmt_name(m_outputSampleFormat)); +} \ No newline at end of file diff --git a/src/AudioDecoder.hpp b/src/AudioDecoder.hpp new file mode 100644 index 0000000..fd03075 --- /dev/null +++ b/src/AudioDecoder.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +class AudioDecoder { +public: + AudioDecoder(); + ~AudioDecoder(); + + bool open(const QString& inputPath); + int decode(uint8_t* outputBuffer, int outputBufferSize); + void close(); + + int sampleRate() const { return m_outputSampleRate; } + int channels() const { return m_outputChannels; } + int bytesPerSample() const { return av_get_bytes_per_sample(m_outputSampleFormat); } + + int bitrate() const; + qint64 durationUs() const; + QString sampleFormatName() const; + +private: + bool initResampler(); + + QString m_inputPath; + AVFormatContext* m_formatContext = nullptr; + AVCodecContext* m_codecContext = nullptr; + SwrContext* m_swrContext = nullptr; + AVPacket* m_packet = nullptr; + AVFrame* m_frame = nullptr; + + int m_streamIndex = -1; + AVChannelLayout m_inputChannelLayout; + AVChannelLayout m_outputChannelLayout; + AVSampleFormat m_outputSampleFormat = AV_SAMPLE_FMT_S16; + int m_outputSampleRate = 0; + int m_outputChannels = 0; + bool m_endOfFile = false; +}; \ No newline at end of file diff --git a/src/AudioStreamBuffer.cpp b/src/AudioStreamBuffer.cpp new file mode 100644 index 0000000..9a292dd --- /dev/null +++ b/src/AudioStreamBuffer.cpp @@ -0,0 +1,96 @@ +#include "AudioStreamBuffer.hpp" +#include // memcpy + +AudioStreamBuffer::AudioStreamBuffer(AudioDecoder& decoder, double bufferDurationSec) + : m_decoder(decoder) +{ + m_frameSize = m_decoder.channels() * m_decoder.bytesPerSample(); + m_bufferSize = static_cast(m_decoder.sampleRate() * bufferDurationSec * m_frameSize); + m_buffer.resize(m_bufferSize, 0); +} + +bool AudioStreamBuffer::fill() { + if (m_eof) return false; + + uint8_t temp[8192]; // временный маленький буфер для одного чтения + + while (availableSamples() < m_bufferSize / 2) { + int bytesRead = m_decoder.decode(temp, sizeof(temp)); + if (bytesRead > 0) { + writeToBuffer(temp, static_cast(bytesRead)); + } else { + m_eof = true; + break; + } + } + return true; +} + +bool AudioStreamBuffer::getSamples(uint8_t* outBuffer, size_t bytesToRead) { + if (isEmpty()) return false; + + size_t bytesAvailable = availableSamples(); + size_t bytesToCopy = (bytesToRead < bytesAvailable) ? bytesToRead : bytesAvailable; + + if (m_readPos + bytesToCopy <= m_bufferSize) { + std::memcpy(outBuffer, &m_buffer[m_readPos], bytesToCopy); + m_readPos += bytesToCopy; + } else { + size_t firstPart = m_bufferSize - m_readPos; + size_t secondPart = bytesToCopy - firstPart; + std::memcpy(outBuffer, &m_buffer[m_readPos], firstPart); + std::memcpy(outBuffer + firstPart, &m_buffer[0], secondPart); + m_readPos = secondPart; + } + + if (m_readPos >= m_bufferSize) m_readPos = 0; + return true; +} + +bool AudioStreamBuffer::isEmpty() const { + return m_readPos == m_writePos; +} + +bool AudioStreamBuffer::isEof() const { + return m_eof; +} + +int AudioStreamBuffer::sampleRate() const { + return m_decoder.sampleRate(); +} + +int AudioStreamBuffer::channels() const { + return m_decoder.channels(); +} + +int AudioStreamBuffer::bytesPerSample() const { + return m_decoder.bytesPerSample(); +} + +void AudioStreamBuffer::writeToBuffer(const uint8_t* data, size_t size) { + if (size > m_bufferSize) { + // слишком большой кусок, обрезаем + data += size - m_bufferSize; + size = m_bufferSize; + } + + if (m_writePos + size <= m_bufferSize) { + std::memcpy(&m_buffer[m_writePos], data, size); + m_writePos += size; + } else { + size_t firstPart = m_bufferSize - m_writePos; + size_t secondPart = size - firstPart; + std::memcpy(&m_buffer[m_writePos], data, firstPart); + std::memcpy(&m_buffer[0], data + firstPart, secondPart); + m_writePos = secondPart; + } + + if (m_writePos >= m_bufferSize) m_writePos = 0; +} + +size_t AudioStreamBuffer::availableSamples() const { + if (m_writePos >= m_readPos) + return m_writePos - m_readPos; + else + return m_bufferSize - (m_readPos - m_writePos); +} diff --git a/src/AudioStreamBuffer.hpp b/src/AudioStreamBuffer.hpp new file mode 100644 index 0000000..cc41fdf --- /dev/null +++ b/src/AudioStreamBuffer.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "AudioDecoder.hpp" +#include +#include + +class AudioStreamBuffer { +public: + explicit AudioStreamBuffer(AudioDecoder& decoder, double bufferDurationSec = 2.0); + + bool fill(); // наполняет буфер новыми данными + bool getSamples(uint8_t* outBuffer, size_t bytesToRead); // копирует samples в outBuffer + + bool isEmpty() const; + bool isEof() const; + + int sampleRate() const; + int channels() const; + int bytesPerSample() const; + +private: + AudioDecoder& m_decoder; + + std::vector m_buffer; + size_t m_bufferSize = 0; + size_t m_readPos = 0; + size_t m_writePos = 0; + bool m_eof = false; + + size_t m_frameSize = 0; // bytes per frame (channels × bytes_per_sample) + + void writeToBuffer(const uint8_t* data, size_t size); + size_t availableSamples() const; +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bbb7c4c..c65e3eb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,4 +8,5 @@ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX) endif() -target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets) +target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets + avformat avcodec avutil swresample) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 69f3222..e6fcea6 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,25 +1,56 @@ #include "MainWindow.hpp" +#include "AudioDecoder.hpp" + +#include +#include #include +#include +#include +#include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { - // Размерчик - resize(400, 300); - - // Кнопочка - m_button = new QPushButton("BABLO", this); - m_button->setGeometry(150, 120, 100, 30); + auto central = std::make_unique(this); + auto layout = std::make_unique(central.get()); - // Подключение сигнала к слоту - connect(m_button, &QPushButton::clicked, this, &MainWindow::onButtonClicked); + m_openButton = new QPushButton(tr("Open Audio File"), this); + layout->addWidget(m_openButton); + connect(m_openButton, &QPushButton::clicked, this, &MainWindow::onOpenFile); + + setCentralWidget(central.release()); + + resize(400, 200); } -MainWindow::~MainWindow() -{ +MainWindow::~MainWindow() {} + +// Затестим tr() - переводит строку под язык пользователя + +void MainWindow::onOpenFile() { + const QString filter = tr("Audio Files (*.mp3 *.wav *.flac);;All Files (*)"); + const QString filePath = QFileDialog::getOpenFileName(this, tr("Select Audio File"), QDir::homePath(), filter); + if (!filePath.isEmpty()) { + showAudioInfo(filePath); + } } -void MainWindow::onButtonClicked() -{ - QMessageBox::information(this, "Сообщение", "BABLO BABLO!"); -} \ No newline at end of file +void MainWindow::showAudioInfo(const QString &filePath) { + AudioDecoder decoder; + if (!decoder.open(filePath)) { + QMessageBox::critical(this, tr("Error"), tr("Failed to open audio file.")); + return; + } + + QString info; + info += tr("File: %1\n").arg(filePath); + info += tr("Sample Rate: %1 Hz\n").arg(decoder.sampleRate()); + info += tr("Channels: %1\n").arg(decoder.channels()); + info += tr("Bytes Per Sample: %1\n").arg(decoder.bytesPerSample()); + info += tr("Bitrate: %1 bps\n").arg(decoder.bitrate()); + info += tr("Duration: %1 s\n").arg(decoder.durationUs() / 1e6, 0, 'f', 2); + info += tr("Sample Format: %1").arg(decoder.sampleFormatName()); + + QMessageBox::information(this, tr("Audio File Info"), info); +} diff --git a/src/MainWindow.hpp b/src/MainWindow.hpp index c6b64cb..d3efe9d 100644 --- a/src/MainWindow.hpp +++ b/src/MainWindow.hpp @@ -1,22 +1,23 @@ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H +#pragma once #include #include +#include -class MainWindow : public QMainWindow -{ +class MainWindow : public QMainWindow { Q_OBJECT public: - MainWindow(QWidget *parent = nullptr); + explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: - void onButtonClicked(); + void onOpenFile(); + private: - QPushButton *m_button; -}; + void showAudioInfo(const QString &filePath); -#endif // MAINWINDOW_H \ No newline at end of file + QPushButton *m_openButton; + QPushButton *m_infoButton; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 87137a7..a14c8bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include "MainWindow.hpp" +#include "AudioDecoder.hpp" #include int main(int argc, char *argv[])