Visualisator audio di C ++
Saya telah menyelesaikan upaya pertama saya dari aplikasi visualisator musik sederhana untuk windows di C ++ menggunakan SDL. Dibutuhkan audio sistem dan mengeluarkan gelombang suara secara real-time. Saya menghargai beberapa umpan balik tentang bagaimana saya dapat meningkatkan kode, terutama desain OOP dan di mana saya dapat menggunakan fitur C ++ modern.
main.cpp:
#include <chrono>
#include <thread>
#include "visualizer.h"
constexpr int sleep_time = 20;
int main(int argc, char* argv[])
{
Visualizer generator;
bool continue_app = generator.init_successful();
while (continue_app)
{
continue_app = generator.update();
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
}
return 0;
}
audio_sink.h:
#pragma once
// Abstract base class to copy data from an AudioRecorder
class AudioSink
{
public:
// Copy a packet of data from the audio recorder
// param: data - pointer to data values
// param: channels - the number of audio channels
// param: frames - the number of frames of data
virtual void copy_data(float * data, int channels, int frames) = 0;
};
audio_recorder.h:
#pragma once
#include <atomic>
#include <Audioclient.h>
#include <Audiopolicy.h>
#include <Mmdeviceapi.h>
#include "audio_sink.h"
// Class for recording system audio via WASAPI Loopback
//
class AudioRecorder {
public:
AudioRecorder();
~AudioRecorder();
// The status of the initialization process
// return: bool - whether the initialization was successful
bool init_successful() const;
// Record audio data from the system indefinitely
// param: audio_sink - class which copies the recorded packets
// param: exit_flag - flag to stop recording (passed by ref so it can be stopped externally)
void record(AudioSink * audio_sink, std::atomic_bool &exit_flag) const;
private:
mutable HRESULT m_hr;
IMMDeviceEnumerator * m_device_enumerator = nullptr;
IMMDevice * m_audio_device_endpoint = nullptr;
IAudioClient * m_audio_client = nullptr;
IAudioCaptureClient *m_capture_client = nullptr;
int m_num_channels;
static const int sleep_time = 10; // Time spent sleeping tp wait for new packets
};
audio_recorder.cpp:
#include "audio_recorder.h"
#include <comdef.h>
#include <Audioclient.h>
#include <Audiopolicy.h>
#include <Mmdeviceapi.h>
#include <chrono>
#include <iostream>
#include <thread>
// Define IIDs for initialization
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
AudioRecorder::AudioRecorder() {
m_hr = S_OK;
m_audio_client = nullptr;
// Initialize audio device endpoint
CoInitialize(nullptr);
m_hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, nullptr,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&m_device_enumerator);
if (m_hr == S_OK && m_device_enumerator) {
m_hr = m_device_enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_audio_device_endpoint);
}
// init audio client
WAVEFORMATEX *pwfx = nullptr;
REFERENCE_TIME hns_requested_duration = 100000000;
if (m_hr == S_OK && m_audio_device_endpoint) {
m_hr = m_audio_device_endpoint->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void**)&m_audio_client);
}
if (m_hr == S_OK && m_audio_client) {
m_hr = m_audio_client->GetMixFormat(&pwfx);
if (m_hr == S_OK && pwfx) {
if (pwfx->wFormatTag != WAVE_FORMAT_EXTENSIBLE
|| reinterpret_cast<WAVEFORMATEXTENSIBLE *>(pwfx)->SubFormat != KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
std::cout << "Error: the audio format is not supported" << std::endl;
m_hr = S_FALSE;
}
}
if (m_hr == S_OK) {
m_hr = m_audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, hns_requested_duration, 10, pwfx, nullptr);
}
if (m_hr == S_OK && pwfx) {
m_num_channels = pwfx->nChannels;
}
if (m_hr == S_OK) {
m_hr = m_audio_client->GetService(IID_IAudioCaptureClient, (void**)&m_capture_client);
}
if (m_hr == S_OK && m_capture_client) {
m_hr = m_audio_client->Start(); // Start recording.
}
}
if (m_hr != S_OK) {
std::cout << "Error: During AudioRecorder intialization - " << _com_error(m_hr).ErrorMessage() << std::endl;
}
};
// safely release and nullify pointers (in destructor)
template<class T> inline void safe_release(T* &p_object) {
if (p_object) {
p_object->Release();
p_object = nullptr;
}
}
AudioRecorder::~AudioRecorder() {
if (m_audio_client) {
m_audio_client->Stop(); // Stop recording.
}
safe_release(m_device_enumerator);
safe_release(m_audio_device_endpoint);
safe_release(m_audio_client);
safe_release(m_capture_client);
}
bool AudioRecorder::init_successful() const {
return (m_hr == S_OK) && m_audio_client && m_capture_client;
}
void AudioRecorder::record(AudioSink * audio_sink, std::atomic_bool &exit_flag) const {
m_hr = S_OK;
UINT32 packet_length = 0;
BYTE *data = nullptr;
UINT32 num_frames_available;
DWORD flags;
while (!exit_flag) {
m_hr = m_capture_client->GetNextPacketSize(&packet_length);
while (packet_length != 0 && !exit_flag) // while there are available packets
{
// Get the available data in the shared buffer.
data = nullptr;
m_hr = m_capture_client->GetBuffer(
&data,
&num_frames_available,
&flags, nullptr, nullptr);
if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
data = nullptr; // data pointer is null for silence
}
audio_sink->copy_data((float*)data, m_num_channels, num_frames_available);
m_hr = m_capture_client->ReleaseBuffer(num_frames_available);
m_hr = m_capture_client->GetNextPacketSize(&packet_length);
}
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
}
}
visualizer.h:
#pragma once
#include <atomic>
#include <mutex>
#include <vector>
#include <thread>
#include "audio_recorder.h"
#include "audio_sink.h"
#include "SDL.h"
// Visualizer class
// This class owns the SDL window and performs visual updates based on system audio
class Visualizer: public AudioSink {
public:
Visualizer();
~Visualizer();
// The status of the initialization process
// return: bool - whether the initialization was successful
bool init_successful() const;
// Update the visuals based on the most recent packet
// return: bool - whether the update was successful
bool update();
// Copy a packet of data from the audio recorder
// Must override the method from AudioSink
// param: data - pointer to data values
// param: channels - the number of audio channels
// param: frames - the number of frames of data
void copy_data(float * data, int channels, int frames) override;
private:
SDL_Window* window = nullptr;
SDL_Renderer* renderer = nullptr;
SDL_Event current_event;
int window_width = 1000;
int window_height = 600;
bool mFullScreen = false;
bool mMinimized = false;
AudioRecorder recorder;
std::thread recording_thread;
std::atomic_bool exit_recording_thread_flag = false;
std::mutex packet_buffer_mutex;
typedef std::vector<float> packet;
packet packet_buffer; // Must use mutex to access
// Handle SDL window events
// param: e - the SDL window event to handle
void handle_event(const SDL_Event & e);
// Draw a horizontal soundwave with the most recent packet data
// param: start - the leftmost starting pixel of the wave
// param: length - the length in pixels of the wave
// param: pixel_amplitude - the maximum amplitude of the wave
// param: color - the color of the wave
void draw_wave(const SDL_Point &start, int length, int pixel_amplitude, const SDL_Color & color);
};
visualizer.cpp:
#pragma once
#include "visualizer.h"
#include <iostream>
#include <thread>
#include <vector>
#include "SDL.h"
Visualizer::Visualizer(): recorder() {
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
std::cout << "Unable to initialize SDL: " << SDL_GetError() << std::endl;
return;
}
window = SDL_CreateWindow("Visualizer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
if (window) {
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
}
if (renderer) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
}
if (recorder.init_successful()) {
recording_thread = std::thread(&AudioRecorder::record, &recorder, (AudioSink *)this, std::ref(exit_recording_thread_flag));
}
}
Visualizer::~Visualizer()
{
// Stop recording thread before implicitly destroying AudioRecorder
if (recording_thread.joinable()) {
exit_recording_thread_flag = true;
recording_thread.join();
}
// Destroy SDL window
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
bool Visualizer::init_successful() const {
return (renderer && recording_thread.joinable());
}
void Visualizer::handle_event(const SDL_Event & e) {
//Window event occured
if (e.type == SDL_WINDOWEVENT) {
switch (e.window.event) {
//Get new dimensions and repaint on window size change
case SDL_WINDOWEVENT_SIZE_CHANGED:
window_width = e.window.data1;
window_height = e.window.data2;
SDL_RenderPresent(renderer);
break;
//Repaint on exposure
case SDL_WINDOWEVENT_EXPOSED:
SDL_RenderPresent(renderer);
break;
case SDL_WINDOWEVENT_MINIMIZED:
mMinimized = true;
break;
case SDL_WINDOWEVENT_MAXIMIZED:
mMinimized = false;
break;
case SDL_WINDOWEVENT_RESTORED:
mMinimized = false;
break;
}
}
//Enter exit full screen on return key
else if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_RETURN) {
if (mFullScreen) {
SDL_SetWindowFullscreen(window, SDL_FALSE);
mFullScreen = false;
}
else {
SDL_SetWindowFullscreen(window, SDL_TRUE);
mFullScreen = true;
mMinimized = false;
}
}
}
void Visualizer::copy_data(float * data, int channels, int frames) {
std::lock_guard<std::mutex>read_guard(packet_buffer_mutex);
if (data) {
packet_buffer = packet(data, data + channels * frames);
}
else {
// use an empty vector if there is no data (silence)
packet_buffer = packet();
}
}
bool Visualizer::update() {
//Handle events on queue
while (SDL_PollEvent(¤t_event) != 0) {
if (current_event.type == SDL_QUIT) {
return false;
}
handle_event(current_event);
}
// Do not render if minimized
if (mMinimized) {
return true;
}
// Render visualizer
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
draw_wave(SDL_Point{ 0, window_height / 2 }, window_width, window_height, SDL_Color{ 255,255,255,255 });
SDL_RenderPresent(renderer);
return true;
}
void Visualizer::draw_wave(const SDL_Point &start, int length, int pixel_amplitude, const SDL_Color & color) {
std::vector<SDL_Point> points;
std::unique_lock<std::mutex>read_guard(packet_buffer_mutex);
if (!packet_buffer.empty()) {
// use smallest possible step so soundwave fills window
int step = (int)ceil((double)length / (double)packet_buffer.size());
auto amplitude = packet_buffer.begin();
for (int i = 0; i < length; i+=step) {
points.push_back(SDL_Point{ start.x + i, (int)(start.y + (*amplitude) * pixel_amplitude) });
++amplitude;
}
}
else { // silence
points.push_back(SDL_Point{ start.x, start.y });
points.push_back(SDL_Point{ start.x + length - 1, start.y });
}
read_guard.unlock();
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_RenderDrawLines(renderer, &points[0], (int)points.size());
}
Jawaban
Satu-satunya hal yang menurut saya layak untuk disebutkan adalah init_successfulpola Anda . Ini agak berlawanan dengan cara bahasa berorientasi objek dengan penanganan pengecualian mendorong kita untuk berpikir tentang konstruksi. Cara untuk "membuat konstruktor gagal" adalah dengan melempar pengecualian, yang pada gilirannya secara efektif mencegah kita untuk mempertahankan objek yang dibangun sama sekali. Dengan metode Anda, ada kemungkinan keadaan perantara di mana sebuah objek ada dalam memori, anggotanya dapat dipanggil, tetapi tidak valid. Itu buruk.
Jika Anda memerlukan logika khusus untuk menangani konstruksi yang gagal, tangkap pengecualian yang Anda lempar dari konstruktor.