From 9c5ef496366076984edd5f87b4a85faeabb68792 Mon Sep 17 00:00:00 2001 From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Mon, 2 Dec 2024 08:21:59 +0100 Subject: [PATCH] Audio Handling and simple GUI - Audio Playback crashes the App due to an access violation rn - GUI will get a overhaul later, might switch to QT - SDK later on --- ApfelDAW.sln | 31 +++++++ ApfelDAW/ApfelDAW.vcxproj | 144 +++++++++++++++++++++++++++++ ApfelDAW/ApfelDAW.vcxproj.filters | 42 +++++++++ ApfelDAW/daw.cpp | 148 ++++++++++++++++++++++++++++++ ApfelDAW/daw.h | 33 +++++++ ApfelDAW/gui.cpp | 85 +++++++++++++++++ ApfelDAW/gui.h | 27 ++++++ ApfelDAW/main.cpp | 13 +++ ApfelDAW/trackeditor.cpp | 50 ++++++++++ ApfelDAW/trackeditor.h | 30 ++++++ 10 files changed, 603 insertions(+) create mode 100644 ApfelDAW.sln create mode 100644 ApfelDAW/ApfelDAW.vcxproj create mode 100644 ApfelDAW/ApfelDAW.vcxproj.filters create mode 100644 ApfelDAW/daw.cpp create mode 100644 ApfelDAW/daw.h create mode 100644 ApfelDAW/gui.cpp create mode 100644 ApfelDAW/gui.h create mode 100644 ApfelDAW/main.cpp create mode 100644 ApfelDAW/trackeditor.cpp create mode 100644 ApfelDAW/trackeditor.h diff --git a/ApfelDAW.sln b/ApfelDAW.sln new file mode 100644 index 0000000..81300fc --- /dev/null +++ b/ApfelDAW.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35431.28 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ApfelDAW", "ApfelDAW\ApfelDAW.vcxproj", "{2ECCB40B-E250-4F78-87B2-2BEB3D59E571}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Debug|x64.ActiveCfg = Debug|x64 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Debug|x64.Build.0 = Debug|x64 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Debug|x86.ActiveCfg = Debug|Win32 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Debug|x86.Build.0 = Debug|Win32 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Release|x64.ActiveCfg = Release|x64 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Release|x64.Build.0 = Release|x64 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Release|x86.ActiveCfg = Release|Win32 + {2ECCB40B-E250-4F78-87B2-2BEB3D59E571}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3779F40C-D8D6-4877-8316-981E5772D63B} + EndGlobalSection +EndGlobal diff --git a/ApfelDAW/ApfelDAW.vcxproj b/ApfelDAW/ApfelDAW.vcxproj new file mode 100644 index 0000000..4ad4ec1 --- /dev/null +++ b/ApfelDAW/ApfelDAW.vcxproj @@ -0,0 +1,144 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {2eccb40b-e250-4f78-87b2-2beb3d59e571} + ApfelDAW + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + + + Windows + true + true + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ApfelDAW/ApfelDAW.vcxproj.filters b/ApfelDAW/ApfelDAW.vcxproj.filters new file mode 100644 index 0000000..f9748e2 --- /dev/null +++ b/ApfelDAW/ApfelDAW.vcxproj.filters @@ -0,0 +1,42 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/ApfelDAW/daw.cpp b/ApfelDAW/daw.cpp new file mode 100644 index 0000000..726dd3d --- /dev/null +++ b/ApfelDAW/daw.cpp @@ -0,0 +1,148 @@ +#include "daw.h" +#include +#include +#include +#include +#include +#include + +static int audioCallback(const void* inputBuffer, void* outputBuffer, + unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, void* userData) { + auto* track = reinterpret_cast(userData); + auto* out = static_cast(outputBuffer); + + static size_t position = 0; + size_t remaining = track->audioData.size() - position; + size_t toCopy = std::min(framesPerBuffer * track->channels, remaining); + + std::copy(track->audioData.begin() + position, track->audioData.begin() + position + toCopy, out); + position += toCopy; + + if (toCopy < framesPerBuffer * track->channels) { + std::fill(out + toCopy, out + framesPerBuffer * track->channels, 0.0f); + position = 0; + return paComplete; + } + + return paContinue; +} + +void DAW::addTrack(const std::string& filePath) { + Track track; + loadAudio(filePath, track); + tracks.push_back(track); +} + +void DAW::removeTrack(int index) { + if (index >= 0 && index < tracks.size()) { + tracks.erase(tracks.begin() + index); + } +} + +std::vector& DAW::getTracks() { + return tracks; +} + +void DAW::loadAudio(const std::string& filePath, Track& track) { + SF_INFO sfInfo; + SNDFILE* sndFile = sf_open(filePath.c_str(), SFM_READ, &sfInfo); + if (!sndFile) { + throw std::runtime_error("Failed to open audio file: " + filePath); + } + + track.fileName = filePath; + track.sampleRate = sfInfo.samplerate; + track.channels = sfInfo.channels; + track.duration = static_cast(sfInfo.frames) / sfInfo.samplerate; + + size_t totalFrames = sfInfo.frames * sfInfo.channels; + track.audioData.resize(totalFrames); + + sf_read_float(sndFile, track.audioData.data(), totalFrames); + sf_close(sndFile); +} + +void DAW::play() { + if (tracks.empty() || playing) return; + + playing = true; + Pa_Initialize(); + + auto& track = tracks[0]; + PaStream* stream; + Pa_OpenDefaultStream(&stream, 0, track.channels, paFloat32, track.sampleRate, + 256, audioCallback, &track); + Pa_StartStream(stream); + + std::thread playbackThread([stream]() { + while (Pa_IsStreamActive(stream) == 1) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + Pa_CloseStream(stream); + Pa_Terminate(); + }); + playbackThread.detach(); +} + +static int mixedAudioCallback(const void* inputBuffer, void* outputBuffer, + unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, void* userData) { + static size_t position = 0; + auto* mixedAudio = static_cast*>(userData); + auto* out = static_cast(outputBuffer); + + size_t toCopy = std::min(framesPerBuffer * 2, mixedAudio->size() - position); + std::copy(mixedAudio->begin() + position, mixedAudio->begin() + position + toCopy, out); + position += toCopy; + + if (position >= mixedAudio->size()) { + position = 0; + return paComplete; + } + return paContinue; +} + +void DAW::playAll() { + if (tracks.empty() || playing) return; + + playing = true; + Pa_Initialize(); + + size_t maxSamples = 0; + for (const auto& track : tracks) { + maxSamples = std::max(maxSamples, track.audioData.size()); + } + std::vector mixedAudio(maxSamples, 0.0f); + + for (const auto& track : tracks) { + for (size_t i = 0; i < track.audioData.size(); ++i) { + mixedAudio[i] += track.audioData[i] / tracks.size(); + } + } + + PaStream* stream; + Pa_OpenDefaultStream(&stream, 0, 2, paFloat32, tracks[0].sampleRate, 256, + mixedAudioCallback, &mixedAudio); + + Pa_StartStream(stream); + + std::thread playbackThread([stream]() { + while (Pa_IsStreamActive(stream) == 1) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + Pa_CloseStream(stream); + Pa_Terminate(); + }); + playbackThread.detach(); +} + + +void DAW::stop() { + playing = false; + Pa_Terminate(); +} + +bool DAW::isPlaying() const { + return playing; +} \ No newline at end of file diff --git a/ApfelDAW/daw.h b/ApfelDAW/daw.h new file mode 100644 index 0000000..d35a403 --- /dev/null +++ b/ApfelDAW/daw.h @@ -0,0 +1,33 @@ +#ifndef DAW_H +#define DAW_H + +#include +#include + +struct Track { + std::string fileName; + int sampleRate; + int channels; + double duration; + std::vector audioData; +}; + +class DAW { +public: + void addTrack(const std::string& filePath); + void removeTrack(int index); + std::vector& getTracks(); + + void play(); + void playAll(); + void stop(); + bool isPlaying() const; + +private: + std::vector tracks; + bool playing = false; + + void loadAudio(const std::string& filePath, Track& track); +}; + +#endif \ No newline at end of file diff --git a/ApfelDAW/gui.cpp b/ApfelDAW/gui.cpp new file mode 100644 index 0000000..592af64 --- /dev/null +++ b/ApfelDAW/gui.cpp @@ -0,0 +1,85 @@ +#include "gui.h" +#include "trackeditor.h" + +enum { + ID_PLAY_PAUSE = 1 +}; + +BEGIN_EVENT_TABLE(GUI, wxFrame) +EVT_BUTTON(ID_PLAY_PAUSE, GUI::OnPlayPause) +EVT_KEY_DOWN(GUI::OnKeyDown) +END_EVENT_TABLE() + +GUI::GUI(const wxString& title) + : wxFrame(nullptr, wxID_ANY, title, wxDefaultPosition, wxSize(600, 400)) { + auto* panel = new wxPanel(this, wxID_ANY); + auto* vbox = new wxBoxSizer(wxVERTICAL); + + trackList = new wxListBox(panel, wxID_ANY); + vbox->Add(trackList, 1, wxEXPAND | wxALL, 10); + + auto* hbox = new wxBoxSizer(wxHORIZONTAL); + auto* addBtn = new wxButton(panel, wxID_ANY, wxT("Add Track")); + auto* removeBtn = new wxButton(panel, wxID_ANY, wxT("Remove Track")); + auto* playPauseBtn = new wxButton(panel, ID_PLAY_PAUSE, wxT("Play/Pause")); + auto* trackEditorBtn = new wxButton(panel, wxID_ANY, "Open Track Editor"); + + hbox->Add(trackEditorBtn, 1, wxEXPAND | wxALL, 5); + hbox->Add(addBtn, 1, wxEXPAND | wxALL, 5); + hbox->Add(removeBtn, 1, wxEXPAND | wxALL, 5); + hbox->Add(playPauseBtn, 1, wxEXPAND | wxALL, 5); + + vbox->Add(hbox, 0, wxALIGN_CENTER); + panel->SetSizer(vbox); + + Connect(addBtn->GetId(), wxEVT_BUTTON, wxCommandEventHandler(GUI::OnAddTrack)); + Connect(removeBtn->GetId(), wxEVT_BUTTON, wxCommandEventHandler(GUI::OnRemoveTrack)); + trackEditorBtn->Bind(wxEVT_BUTTON, &GUI::OnOpenTrackEditor, this); +} + +void GUI::OnOpenTrackEditor(wxCommandEvent& event) { + auto* editor = new TrackEditor(&daw); + editor->Show(true); +} + +void GUI::OnAddTrack(wxCommandEvent& event) { + wxFileDialog openFileDialog(this, wxT("Select Audio File"), "", "", + "Audio Files (*.wav;*.mp3;*.flac)|*.wav;*.mp3;*.flac", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() == wxID_CANCEL) return; + + daw.addTrack(openFileDialog.GetPath().ToStdString()); + UpdateTrackList(); +} + +void GUI::OnRemoveTrack(wxCommandEvent& event) { + int selection = trackList->GetSelection(); + if (selection != wxNOT_FOUND) { + daw.removeTrack(selection); + UpdateTrackList(); + } +} + +void GUI::OnPlayPause(wxCommandEvent& event) { + if (daw.isPlaying()) { + daw.stop(); + } + else { + daw.play(); + } +} + +void GUI::OnKeyDown(wxKeyEvent& event) { + if (event.GetKeyCode() == WXK_SPACE) { + wxCommandEvent dummyEvent; + OnPlayPause(dummyEvent); + } + event.Skip(); +} + +void GUI::UpdateTrackList() { + trackList->Clear(); + for (const auto& track : daw.getTracks()) { + trackList->Append(wxString::FromUTF8(track.fileName)); + } +} \ No newline at end of file diff --git a/ApfelDAW/gui.h b/ApfelDAW/gui.h new file mode 100644 index 0000000..bda8b32 --- /dev/null +++ b/ApfelDAW/gui.h @@ -0,0 +1,27 @@ +#ifndef GUI_H +#define GUI_H + +#include +#include +#include "daw.h" + +class GUI : public wxFrame { +public: + GUI(const wxString& title); + +private: + DAW daw; + wxListBox* trackList; + + void OnAddTrack(wxCommandEvent& event); + void OnRemoveTrack(wxCommandEvent& event); + void OnPlayPause(wxCommandEvent& event); + void OnOpenTrackEditor(wxCommandEvent& event); + void OnKeyDown(wxKeyEvent& event); + + void UpdateTrackList(); + + DECLARE_EVENT_TABLE() +}; + +#endif \ No newline at end of file diff --git a/ApfelDAW/main.cpp b/ApfelDAW/main.cpp new file mode 100644 index 0000000..a1fc7b0 --- /dev/null +++ b/ApfelDAW/main.cpp @@ -0,0 +1,13 @@ +#include +#include "gui.h" + +class ApfelDAWApp : public wxApp { +public: + virtual bool OnInit() { + GUI* gui = new GUI("Apfel DAW"); + gui->Show(true); + return true; + } +}; + +wxIMPLEMENT_APP(ApfelDAWApp); \ No newline at end of file diff --git a/ApfelDAW/trackeditor.cpp b/ApfelDAW/trackeditor.cpp new file mode 100644 index 0000000..86db24f --- /dev/null +++ b/ApfelDAW/trackeditor.cpp @@ -0,0 +1,50 @@ +#include "trackeditor.h" + +FileDropTarget::FileDropTarget(wxListBox* listBox, DAW* daw) + : trackList(listBox), daw(daw) {} + +bool FileDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& files) { + for (const auto& file : files) { + try { + daw->addTrack(file.ToStdString()); + trackList->Append(file); + } + catch (const std::exception& e) { + wxMessageBox(e.what(), "Error", wxICON_ERROR); + } + } + return true; +} + +TrackEditor::TrackEditor(DAW* daw) + : wxFrame(nullptr, wxID_ANY, "Track Editor", wxDefaultPosition, wxSize(600, 400)), daw(daw) { + auto* panel = new wxPanel(this, wxID_ANY); + auto* vbox = new wxBoxSizer(wxVERTICAL); + + trackList = new wxListBox(panel, wxID_ANY); + auto* dropTarget = new FileDropTarget(trackList, daw); + trackList->SetDropTarget(dropTarget); + + vbox->Add(trackList, 1, wxEXPAND | wxALL, 10); + + auto* hbox = new wxBoxSizer(wxHORIZONTAL); + auto* playBtn = new wxButton(panel, wxID_ANY, "Play All"); + auto* stopBtn = new wxButton(panel, wxID_ANY, "Stop"); + + hbox->Add(playBtn, 1, wxEXPAND | wxALL, 5); + hbox->Add(stopBtn, 1, wxEXPAND | wxALL, 5); + + vbox->Add(hbox, 0, wxALIGN_CENTER); + panel->SetSizer(vbox); + + Connect(playBtn->GetId(), wxEVT_BUTTON, wxCommandEventHandler(TrackEditor::OnPlay)); + Connect(stopBtn->GetId(), wxEVT_BUTTON, wxCommandEventHandler(TrackEditor::OnStop)); +} + +void TrackEditor::OnPlay(wxCommandEvent& event) { + daw->playAll(); +} + +void TrackEditor::OnStop(wxCommandEvent& event) { + daw->stop(); +} \ No newline at end of file diff --git a/ApfelDAW/trackeditor.h b/ApfelDAW/trackeditor.h new file mode 100644 index 0000000..b092d0e --- /dev/null +++ b/ApfelDAW/trackeditor.h @@ -0,0 +1,30 @@ +#ifndef TRACKEDITOR_H +#define TRACKEDITOR_H + +#include +#include +#include "daw.h" + +class FileDropTarget : public wxFileDropTarget { +public: + FileDropTarget(wxListBox* listBox, DAW* daw); + bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& files) override; + +private: + wxListBox* trackList; + DAW* daw; +}; + +class TrackEditor : public wxFrame { +public: + TrackEditor(DAW* daw); + +private: + DAW* daw; + wxListBox* trackList; + + void OnPlay(wxCommandEvent& event); + void OnStop(wxCommandEvent& event); +}; + +#endif \ No newline at end of file