git.haldean.org qb / main qb.cpp
main

Tree @main (Download .tar.gz)

qb.cpp @mainraw · history · blame

#include "qb.hpp"

#include "audionode.hpp"
#include "colors.hpp"
#include "cue.hpp"
#include "nodeui.hpp"
#include "task.hpp"

#include "sokol/sokol_app.h"
#include "sokol/sokol_audio.h"
#include "cimgui/imgui/imgui.h"

#include <cmath>

namespace qb {
  template <typename T> static T
  clamp(const T &a, const T &min, const T &max) {
    if (a < min) return min;
    if (a > max) return max;
    return a;
  }

  context::context() {
    _nodes.push_back(std::make_shared<qb::audio_node>(this, "test/coc-mono.wav"));

    auto cuenode = std::make_shared<qb::cue_node>(this, "test/cues.txt");

    qb::track t1("first");
    for (int i = 0; i < 200; i += 4) {
      t1.hits.push_back(i);
    }
    cuenode->tracks.push_back(t1);

    qb::track t2("second");
    for (int i = 400; i < 2000; i += 24) {
      t2.hits.push_back(i);
    }
    cuenode->tracks.push_back(t2);
    cuenode->tracks.push_back(qb::track("third"));
    _nodes.push_back(cuenode);
    qb::theme::setdefault();
    ImGui::GetIO().KeyRepeatRate = 1.0f / _framerate;
  }

  int context::frame2pixel(double frame) {
    const double off = frame - playhead;
    const double ndc = off / (double)visrad;
    const int px = (int) round(width * (ndc + 1) / 2.0);
    if (px < 0 || px >= width) {
      return -1;
    }
    return px;
  }

  void context::frame(int width, int height) {
    taskpool::get().frame();
    spinstep++;
    this->width = width;
    this->height = height;

    ImGui::SetNextWindowPos(ImVec2(0, 0));
    ImGui::SetNextWindowSize(ImVec2((float) width, (float) height));
    ImGui::PushStyleColor(ImGuiCol_WindowBg, qb::theme::colors[qb::theme::background]);
    ImGui::Begin("MainWindow", nullptr,
                 ImGuiWindowFlags_NoDecoration);

    double minframe = 0;
    double maxframe = 0;
    for (const auto &n : _nodes) {
      minframe = std::min(minframe, (double) n->minframe());
      maxframe = std::max(maxframe, (double) n->maxframe());
    }

    if (!audioinit) {
      initaudio();
    }
    if (audioinit) {
      if (ImGui::IsKeyPressed(SAPP_KEYCODE_SPACE, /* repeat */ false)) {
        playing = !playing;
      }
      if (!playing) {
        if (ImGui::IsKeyPressed(SAPP_KEYCODE_LEFT)) {
          playhead = floor(playhead) - 1.0;
        } else if (ImGui::IsKeyPressed(SAPP_KEYCODE_RIGHT)) {
          playhead = floor(playhead) + 1.0;
        } else if (ImGui::IsKeyPressed(SAPP_KEYCODE_UP)) {
          playhead = minframe;
        } else if (ImGui::IsKeyPressed(SAPP_KEYCODE_DOWN)) {
          playhead = maxframe;
        }
      } else {
        if (playhead > maxframe) {
          playing = false;
        }
      }
      pushaudio();
    }

    if (ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
      const ImVec2 delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle);
      const bool horiz = !playing && abs(delta.x) > abs(delta.y);
      const double pxperframe = width / (2.0 * std::max(visrad, 1.0));
      const double framedist = horiz ? 0.1 * delta.x / pxperframe : 0;
      const double zoomdist = !horiz ? 0.1 * delta.y / pxperframe : 0;
      playhead = clamp((double)playhead + framedist, minframe, maxframe);
      visrad = clamp((double)visrad + zoomdist, 3.0, maxframe - minframe);
    }

    // draw playhead
    {
      ImDrawList * const drawlist = ImGui::GetWindowDrawList();
      const float f0x = (float) frame2pixel(playhead);
      const float f1x = (float) frame2pixel(playhead + 1);
      if (f1x > 0 && f1x != f0x) {
        drawlist->AddRectFilled(ImVec2(f0x, 0.0), ImVec2(f1x, (float) height),
                                qb::theme::colors[qb::theme::color::playhead]);
      } else {
        drawlist->AddLine(ImVec2(f0x, 0), ImVec2(f0x, (float) height),
                          qb::theme::colors[qb::theme::color::playhead]);
      }
    }

    for (const auto &n : _nodes) {
      draw_node(n);
    }
    ImGui::End();
    ImGui::PopStyleColor();

    //qb::theme::showeditor();
  }

  void context::initaudio() {
    const audio_node *audio = nullptr;
    for (const auto &n : _nodes) {
      if (const auto an = dynamic_cast<const audio_node *>(&(*n))) {
        if (an->loadstate().stage == load_state::loading) {
          return;
        }
        if (an->loadstate().stage == load_state::failed) {
          continue;
        }
        audio = an;
        break;
      }
    }
    if (!audio) {
      return;
    }

    saudio_desc d = {0};
    d.sample_rate = audio->data()->getSampleRate();
    d.num_channels = audio->data()->getNumChannels();

    saudio_setup(&d);
    audioinit = saudio_isvalid();
    if (!audioinit) {
      ImGui::Text("could not initialize audio engine");
    } else {
      audiosource = audio;
    }
  }

  void context::pushaudio() {
    if (!audiosource) {
      return;
    }

    const audio_node *audio = static_cast<const audio_node*>(audiosource);
    const int samplerate = audio->data()->getSampleRate();
    const double sampleperframe = (double) samplerate / (double) _framerate;
    const std::vector<float> &s = audio->packed();

    if (!playing) {
      if ((int) playhead == playedframe) {
        return;
      }
      const double firstsample = floor(playhead) * sampleperframe;
      const size_t s0 = (size_t) floor(firstsample);
      if (s0 >= s.size()) {
        return;
      }
      const int topush = std::min((int) sampleperframe, (int) (s.size() - s0));
      const int pushed = saudio_push(&s[s0], topush);
      playedframe = (int) playhead;
      pushedsample = (int64_t) (s0 + sampleperframe - 1);

    } else {
      const double elapsed = ImGui::GetIO().DeltaTime;
      playhead += elapsed * _framerate;

      const int64_t nextsample = (int64_t) floor(playhead * sampleperframe);

      // buffer 125ms of audio
      const int64_t buffersamples = samplerate / 8;
      // add to buffer whenever we have less than 50ms left in the buffer
      const int64_t bufferrefill = samplerate / 20;

      int64_t fillfrom = -1;
      int64_t dist = pushedsample - nextsample;
      if (pushedsample < 0) {
        fillfrom = nextsample;
      } else if (dist < bufferrefill) {
        fillfrom = pushedsample + 1;
      }

      if (fillfrom >= 0 && (size_t) fillfrom < s.size()) {
        const int fillamount =
          (int) std::min(buffersamples, (int64_t)(s.size() - fillfrom - 1));
        const int pushed = saudio_push(&s[fillfrom], fillamount);
        pushedsample = fillfrom + pushed;
      }
    }
  }

  void context::shutdown() {
  }

  node::node(const context *c, const std::string &n, node_type t)
    : _c(c), _name(n), _type(t) {}
}