From 6befaf5eeaf3d57e9784caa0828c62d85d19aa5a Mon Sep 17 00:00:00 2001 From: illyum <90023277+itzilly@users.noreply.github.com> Date: Fri, 9 Aug 2024 02:14:16 -0600 Subject: [PATCH] Add random colors to follow bpm --- main.cpp | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index 5fccf62..67289bd 100644 --- a/main.cpp +++ b/main.cpp @@ -13,12 +13,14 @@ #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" #include "tinyfiledialogs.h" +#include void generateRandomColors(unsigned char& r, unsigned char& g, unsigned char& b); void sendDMXData(HANDLE hSerial, unsigned char* data, int length); void setupImGui(GLFWwindow* window); void renderImGui(); +void renderImGuiBpm(); // ui timeline stuff @@ -37,6 +39,10 @@ ma_sound sound; ma_engine engine; bool audioLoaded = false; +std::vector audioSamples; +bool audioDataLoaded = false; + +std::vector downsampledAudioSamples; const int num_channels = 512; unsigned char buffer[num_channels] = { 0 }; @@ -115,8 +121,10 @@ int main() { ImGui::NewFrame(); renderImGui(); + renderImGuiBpm(); ImGui::Render(); + int display_w, display_h; glfwGetFramebufferSize(window, &display_w, &display_h); glViewport(0, 0, display_w, display_h); @@ -184,6 +192,32 @@ void exportMarkersToFile(const std::string& filename) { } } +void downsampleAudioData(const std::vector& inputSamples, int targetSampleCount) { + downsampledAudioSamples.clear(); + int inputSampleCount = static_cast(inputSamples.size()); + int samplesPerDownsample = inputSampleCount / targetSampleCount; + + for (int i = 0; i < targetSampleCount; ++i) { + float maxSample = -1.0f; + float minSample = 1.0f; + int startIdx = i * samplesPerDownsample; + int endIdx = (std::min)(startIdx + samplesPerDownsample, inputSampleCount);// line 201 + + for (int j = startIdx; j < endIdx; ++j) { + if (inputSamples[j] > maxSample) { + maxSample = inputSamples[j]; + } + if (inputSamples[j] < minSample) { + minSample = inputSamples[j]; + } + } + + // Store both max and min to preserve the waveform's shape + downsampledAudioSamples.push_back(maxSample); + downsampledAudioSamples.push_back(minSample); + } +} + // Function to load markers from a JSON file void loadMarkersFromFile(const std::string& filename) { nlohmann::json j; @@ -208,6 +242,20 @@ void loadMarkersFromFile(const std::string& filename) { } } +void normalizeAudioData(std::vector& data) { + float maxAmplitude = 0.0f; + for (float sample : data) { + if (fabs(sample) > maxAmplitude) { + maxAmplitude = fabs(sample); + } + } + if (maxAmplitude > 0.0f) { + for (float& sample : data) { + sample /= maxAmplitude; + } + } +} + void loadAndPlayAudio(const std::string& filename) { if (audioLoaded) { ma_sound_uninit(&sound); @@ -215,13 +263,41 @@ void loadAndPlayAudio(const std::string& filename) { if (ma_sound_init_from_file(&engine, filename.c_str(), MA_SOUND_FLAG_STREAM, NULL, NULL, &sound) == MA_SUCCESS) { audioLoaded = true; + + ma_uint64 frameCount; + ma_sound_get_length_in_pcm_frames(&sound, &frameCount); + + ma_format format; + ma_uint32 channels; + ma_uint32 sampleRate; + ma_channel channelMap[MA_MAX_CHANNELS]; + size_t channelMapCap = MA_MAX_CHANNELS; + + // Correct function call with all required arguments + ma_data_source_get_data_format(sound.pDataSource, &format, &channels, &sampleRate, channelMap, channelMapCap); + + float audioDuration = frameCount / static_cast(sampleRate); + timeline_duration = audioDuration; // Set the timeline duration to the audio length + + audioSamples.resize(frameCount); + ma_data_source_seek_to_pcm_frame(sound.pDataSource, 0); + ma_data_source_read_pcm_frames(sound.pDataSource, audioSamples.data(), frameCount, NULL); + + audioDataLoaded = true; + + // Normalize and downsample audio data for visualization + normalizeAudioData(audioSamples); + downsampleAudioData(audioSamples, 20000); // Target around 2000 points for better resolution + ma_sound_start(&sound); } else { std::cerr << "Failed to load audio file: " << filename << std::endl; + audioDataLoaded = false; } } + std::string OpenAudioFileDialog() { const char* filterPatterns[2] = { "*.mp3", "*.wav" }; const char* filename = tinyfd_openFileDialog("Select an audio file", "", 2, filterPatterns, NULL, 0); @@ -257,6 +333,7 @@ GLFWwindow* initOpenGL() { return window; } + void setupImGui(GLFWwindow* window) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -379,13 +456,38 @@ void renderImGui() { // Custom timeline rendering with grid lines ImGui::Text("Timeline"); ImGui::PushID("CustomTimeline"); - ImGui::BeginChild("##Timeline", ImVec2(ImGui::GetContentRegionAvail().x, 100), true); + ImGui::BeginChild("##Timeline", ImVec2(ImGui::GetContentRegionAvail().x, 150), true); // Draw grid lines for beats ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); float timeline_width = ImGui::GetContentRegionAvail().x; + // Draw the audio waveform if loaded + if (audioDataLoaded && !downsampledAudioSamples.empty()) { + float timelineHeight = 100.0f; // Total height of the timeline + float waveformHeight = timelineHeight / 2.0f; // Waveform takes half the height of the timeline + float yOffset = cursor_pos.y + timelineHeight / 2.0f; // Center the waveform vertically + + int sampleCount = static_cast(downsampledAudioSamples.size()) / 2; + + // Adjust the scaling to fit the timeline duration + float pixelsPerSample = (timeline_width * zoom_level) / timeline_duration; + + for (int i = 0; i < sampleCount - 1; ++i) { + float x1 = cursor_pos.x + i * pixelsPerSample; + float x2 = cursor_pos.x + (i + 1) * pixelsPerSample; + + float y1_max = yOffset - (downsampledAudioSamples[i * 2] * waveformHeight); + float y1_min = yOffset - (downsampledAudioSamples[i * 2 + 1] * waveformHeight); + float y2_max = yOffset - (downsampledAudioSamples[(i + 1) * 2] * waveformHeight); + float y2_min = yOffset - (downsampledAudioSamples[(i + 1) * 2 + 1] * waveformHeight); + + draw_list->AddLine(ImVec2(x1, y1_max), ImVec2(x2, y2_max), IM_COL32(0, 255, 0, 255), 1.0f); + draw_list->AddLine(ImVec2(x1, y1_min), ImVec2(x2, y2_min), IM_COL32(0, 255, 0, 255), 1.0f); + } + } + for (int i = 0; i <= static_cast(timeline_duration / beat_interval); ++i) { float beat_position = i * beat_interval * zoom_level - pan_offset; float x_position = cursor_pos.x + (beat_position / timeline_duration) * timeline_width; @@ -412,6 +514,20 @@ void renderImGui() { markers.emplace_back(snap_position / timeline_duration, buffer[0], buffer[1], buffer[2]); } + // Add Random Marker button + ImGui::SameLine(); + if (ImGui::Button("Add Random Marker")) { + float snap_position = std::round(timeline_position / beat_interval) * beat_interval; + + // Generate random colors + unsigned char r = rand() % 256; + unsigned char g = rand() % 256; + unsigned char b = rand() % 256; + + // Add the marker with random colors + markers.emplace_back(snap_position / timeline_duration, r, g, b); + } + // Draw markers on the timeline for (const auto& marker : markers) { float marker_position = std::get<0>(marker) * timeline_duration * zoom_level - pan_offset; @@ -464,4 +580,88 @@ void renderImGui() { ImGui::SliderFloat("Pan", &pan_offset, 0.0f, timeline_duration * zoom_level); ImGui::End(); -} \ No newline at end of file +} + +void renderImGuiBpm() { + + // New BPM Control Window + ImGui::Begin("BPM Controller"); + + // BPM Slider/Input + ImGui::SliderFloat("BPM", &bpm, 30.0f, 240.0f); + ImGui::SameLine(); + ImGui::SetNextItemWidth(50); + if (ImGui::InputFloat("##BPMInput", &bpm, 0.0f, 0.0f, "%.1f")) { + if (bpm < 30.0f) bpm = 30.0f; + if (bpm > 240.0f) bpm = 240.0f; + } + + // Offset Slider in milliseconds + static float offset_ms = 0.0f; + ImGui::SliderFloat("Offset (ms)", &offset_ms, -5000.0f, 5000.0f, "%.0f ms"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(70); + if (ImGui::InputFloat("##OffsetInput", &offset_ms, 0.0f, 0.0f, "%.0f")) { + if (offset_ms < -5000.0f) offset_ms = -5000.0f; + if (offset_ms > 5000.0f) offset_ms = 5000.0f; + } + + // Convert offset from milliseconds to seconds for internal use + float offset_seconds = offset_ms / 1000.0f; + + // Start/Stop button + static bool is_playing_bpm = false; + static float lastBeatTime = 0.0f; + if (ImGui::Button(is_playing_bpm ? "Stop" : "Start")) { + is_playing_bpm = !is_playing_bpm; + if (is_playing_bpm) { + // Start playing the audio if not already playing + if (!is_playing && audioLoaded) { + // Apply offset to playback start time + playback_start_time = static_cast(glfwGetTime()) - timeline_position - offset_seconds; + + ma_uint64 frameCount; + ma_sound_get_length_in_pcm_frames(&sound, &frameCount); + // Calculate the frame to seek to based on the timeline position and offset + float playback_time = timeline_position + offset_seconds; + ma_uint64 targetFrame = static_cast((playback_time / timeline_duration) * frameCount); + ma_sound_seek_to_pcm_frame(&sound, targetFrame); + ma_sound_start(&sound); + is_playing = true; + } + // Initialize the beat timer with offset + lastBeatTime = timeline_position; // Start the beat timer from the timeline position + } + else { + if (audioLoaded) { + ma_sound_stop(&sound); + is_playing = false; + } + } + } + + // Non-blocking BPM-based Random Color Changes + if (is_playing_bpm && audioLoaded) { + float currentTime = timeline_position + offset_seconds; // Apply offset to current time + float timePerBeat = 60.0f / bpm; + + // Only update the color when the current time exceeds the next beat time + if (currentTime - lastBeatTime >= timePerBeat) { + // Calculate how many beats we have skipped and adjust `lastBeatTime` + int beatsSkipped = static_cast((currentTime - lastBeatTime) / timePerBeat); + lastBeatTime += beatsSkipped * timePerBeat; + + // Generate new random colors every beat + unsigned char r = rand() % 256; + unsigned char g = rand() % 256; + unsigned char b = rand() % 256; + + // Set the DMX buffer with the random colors + buffer[0] = r; + buffer[1] = g; + buffer[2] = b; + } + } + + ImGui::End(); +}