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 "fonts.hpp"
#include "nodeui.hpp"
#include "task.hpp"
#include "ui.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() {
    qb::initfonts();

    _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 {
    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++;
    _width = width;
    _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);

    qb::draw_ruler(*this);
    qb::draw_playhead(*this);

    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 (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);
    }

    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;
    }
    _audiosource = audio;

    saudio_desc d;
    d.sample_rate = audio->data()->getSampleRate();
    d.num_channels = audio->data()->getNumChannels();
    d.buffer_frames = 0;
    d.packet_frames = 0;
    d.num_packets = 0;
    d.stream_cb = nullptr;
    d.stream_userdata_cb = nullptr;
    d.user_data = nullptr;

    saudio_setup(&d);
    _audioinit = saudio_isvalid();
    if (!_audioinit) {
      ImGui::Text(
        "could not initialize audio engine (rate=%d, channels=%d)",
        d.sample_rate, d.num_channels);
    }
  }

  void context::pushaudio() {
    if (!_audioinit or !_audiosource) {
      if (_playing) {
        const double elapsed = ImGui::GetIO().DeltaTime;
        _playhead += elapsed * _framerate;
      }
      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) {}
}