Skip to content

Commit

Permalink
Connect bpm track script to UI
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiashienzsch committed Jun 12, 2022
1 parent 4fc5277 commit 6ba2e9e
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 47 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ target_sources(StiggiDJ

"src/Core/Array.hpp"

"src/DSP/BeatTrack.cpp"
"src/DSP/BeatTrack.hpp"
"src/DSP/DJPlayer.cpp"
"src/DSP/DJPlayer.hpp"

Expand Down
32 changes: 32 additions & 0 deletions src/DSP/BeatTrack.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "BeatTrack.hpp"

namespace ta
{
auto beatTrack(juce::File const& audioFile) -> BeatTrackResult
{
auto const* pythonPath = "/home/tobante/Developer/tobanteAudio/StiggiDJ/venv/bin/python3";
auto const scriptDir = juce::File{"/home/tobante/Developer/tobanteAudio/StiggiDJ/src/Scripts"};
auto const scriptPath = scriptDir.getChildFile("bpm_track.py");

juce::ChildProcess process{};
auto processArgs = juce::StringArray{pythonPath, scriptPath.getFullPathName(), audioFile.getFullPathName()};
if (!process.start(processArgs)) { return BeatTrackResult{"Failed to start"}; }
if (!process.waitForProcessToFinish(10'000)) { return BeatTrackResult{"Timeout"}; }

auto exitCode = process.getExitCode();
auto output = process.readAllProcessOutput();
if (exitCode != 0) { return BeatTrackResult{output}; }

auto outputLines = juce::StringArray::fromLines(output);
if (outputLines.size() < 2) { return BeatTrackResult{"Invalid output"}; }

auto bpm = outputLines[0].getDoubleValue();
auto beatPositions = std::vector<double>{};
beatPositions.reserve(outputLines.size() - 1);

auto toDouble = [](auto const& line) { return line.getDoubleValue(); };
std::transform(outputLines.begin() + 1, outputLines.end(), std::back_inserter(beatPositions), toDouble);

return BeatTrackResult{bpm, std::move(beatPositions)};
}
} // namespace ta
32 changes: 32 additions & 0 deletions src/DSP/BeatTrack.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#include <juce_core/juce_core.h>

namespace ta
{

struct BeatTrackResult
{
explicit BeatTrackResult(juce::String error) : _errorMessage{std::move(error)} {}

BeatTrackResult(double estimatedBPM, std::vector<double>&& onsetPositions)
: _bpm{estimatedBPM}, onsets{std::move(onsetPositions)}
{
}

auto ok() const noexcept -> bool { return _errorMessage.isEmpty(); }
auto error() const noexcept -> bool { return !ok(); }
auto errorMessage() const -> juce::String const& { return _errorMessage; }

auto estimatedBPM() const -> double { return _bpm; }
auto beatPositions() const -> std::vector<double> const& { return onsets; }

private:
double _bpm{};
std::vector<double> onsets{};
juce::String _errorMessage{};
};

auto beatTrack(juce::File const& audioFile) -> BeatTrackResult;

} // namespace ta
20 changes: 16 additions & 4 deletions src/DSP/DJPlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
namespace ta
{

DJPlayer::DJPlayer(juce::AudioFormatManager& formatManager) : _formatManager(formatManager) {}
DJPlayer::DJPlayer(juce::ThreadPool& threadPool, juce::AudioFormatManager& formatManager)
: _threadPool{threadPool}, _formatManager(formatManager)
{
}

DJPlayer::~DJPlayer() = default;

Expand Down Expand Up @@ -47,11 +50,11 @@ auto DJPlayer::releaseResources() -> void
_resampleSource.releaseResources();
}

auto DJPlayer::loadFile(juce::File audioFile) -> LengthAndSamplerate
auto DJPlayer::loadFile(juce::File file) -> LengthAndSamplerate
{
auto sr = 0.0;
auto length = std::int64_t{};
auto* reader = _formatManager.createReaderFor(audioFile);
auto* reader = _formatManager.createReaderFor(file);

if (reader != nullptr)
{
Expand All @@ -64,7 +67,16 @@ auto DJPlayer::loadFile(juce::File audioFile) -> LengthAndSamplerate

gain(1.0);
positionRelative(0.0);
_listeners.call(&Listener::djPlayerFileChanged, audioFile);
_listeners.call(&Listener::djPlayerFileChanged, file);

auto runBeatTrack = [this, file]
{
auto result = beatTrack(file);
auto callListeners = [this, result] { _listeners.call(&Listener::djPlayerFileAnalysisFinished, result); };
juce::MessageManager::callAsync(callListeners);
};

_threadPool.addJob(runBeatTrack);

return LengthAndSamplerate{length, sr};
}
Expand Down
9 changes: 7 additions & 2 deletions src/DSP/DJPlayer.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "DSP/BeatTrack.hpp"

#include <juce_audio_basics/juce_audio_basics.h>
#include <juce_audio_devices/juce_audio_devices.h>
#include <juce_audio_formats/juce_audio_formats.h>
Expand All @@ -18,14 +20,15 @@ struct DJPlayerListener
{
virtual ~DJPlayerListener() = default;

virtual auto djPlayerFileChanged(juce::File const& file) -> void = 0;
virtual auto djPlayerFileChanged(juce::File const& file) -> void { (void)file; }
virtual auto djPlayerFileAnalysisFinished(BeatTrackResult const& result) -> void { (void)result; }
};

struct DJPlayer final : juce::AudioSource
{
using Listener = DJPlayerListener;

explicit DJPlayer(juce::AudioFormatManager& formatManager);
DJPlayer(juce::ThreadPool& threadPool, juce::AudioFormatManager& formatManager);
~DJPlayer() override;

auto loadFile(juce::File audioFile) -> LengthAndSamplerate;
Expand Down Expand Up @@ -59,6 +62,8 @@ struct DJPlayer final : juce::AudioSource
auto releaseResources() -> void override;

private:
juce::ThreadPool& _threadPool;

juce::AudioFormatManager& _formatManager;
std::unique_ptr<juce::AudioFormatReaderSource> _readerSource;
juce::AudioTransportSource _transportSource;
Expand Down
3 changes: 2 additions & 1 deletion src/MainComponent.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#include "MainComponent.hpp"

#include "Core/Array.hpp"
#include "DSP/BeatTrack.hpp"

MainComponent::MainComponent() : _djPlayer{_formatManager}
MainComponent::MainComponent() : _djPlayer{_threadPool, _formatManager}
{
setAudioDevices();

Expand Down
2 changes: 2 additions & 0 deletions src/MainComponent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ struct MainComponent final : juce::Component
private:
auto setAudioDevices() -> void;

juce::ThreadPool _threadPool{juce::SystemStats::getNumCpus()};

juce::AudioDeviceManager _deviceManager;
juce::AudioFormatManager _formatManager;
juce::AudioSourcePlayer _audioPlayer;
Expand Down
28 changes: 0 additions & 28 deletions src/Scripts/bpm_detect.py

This file was deleted.

23 changes: 23 additions & 0 deletions src/Scripts/bpm_track.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sys

import numpy as np
import librosa


def main():
try:
file = sys.argv[1]
y, sr = librosa.load(file, sr=None, mono=True)
tempo, beats = librosa.beat.beat_track(y=y, sr=sr)
beats = librosa.frames_to_time(beats, sr=sr)
print(f'{tempo}')
for b in beats:
print(f'{b}')
sys.exit(0)
except Exception as e:
print(e)
sys.exit(1)


if __name__ == '__main__':
main()
34 changes: 25 additions & 9 deletions src/UI/Component/WaveformDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@ void WaveformDisplay::paint(juce::Graphics& g)
auto area = getLocalBounds().reduced(5);

g.setColour(juce::Colours::white);
if (_fileLoaded)
{
_audioThumb.drawChannel(g, area, 0, _audioThumb.getTotalLength(), 0, 1.0f);
if (!_fileLoaded) { return; }

auto const length = _audioThumb.getTotalLength();
_audioThumb.drawChannel(g, area, 0, length, 0, 1.0f);

auto const x = area.getX();
auto const top = area.getY();
auto const bottom = area.getBottom();
auto const width = area.getWidth();

auto x = area.getX() + (area.getWidth() * _position);
auto top = area.getY();
auto bottom = area.getBottom();
g.fillRect(juce::Rectangle<double>(x, top, 3.0, bottom - top).toFloat());
g.fillRect(juce::Rectangle<double>(x + (width * _playHeadPosition), top, 2.0, bottom - top).toFloat());

g.setColour(juce::Colours::white.withAlpha(0.5f));
for (auto beat : _beatPositions)
{
auto normalized = beat / length;
g.fillRect(juce::Rectangle<double>(x + (width * normalized), top, 2.0, bottom - top).toFloat());
}
}

Expand All @@ -35,10 +44,17 @@ void WaveformDisplay::changeListenerCallback(juce::ChangeBroadcaster* /*source*/

void WaveformDisplay::setPositionRelative(double pos)
{
if (pos != _position && !std::isnan(pos))
if (pos != _playHeadPosition && !std::isnan(pos))
{
_position = pos;
_playHeadPosition = pos;
repaint();
}
}

auto WaveformDisplay::beatPositions(std::vector<double> positionsInSeconds) -> void
{
_beatPositions = std::move(positionsInSeconds);
repaint();
}

} // namespace ta
5 changes: 4 additions & 1 deletion src/UI/Component/WaveformDisplay.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ struct WaveformDisplay final
/// \brief Set the relative position of the playhead
void setPositionRelative(double pos);

auto beatPositions(std::vector<double> positionsInSeconds) -> void;

void paint(juce::Graphics& g) override;
void changeListenerCallback(juce::ChangeBroadcaster* source) override;

private:
juce::AudioThumbnail _audioThumb;
bool _fileLoaded{false};
double _position{0};
double _playHeadPosition{0};
std::vector<double> _beatPositions{};

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WaveformDisplay) // NOLINT
};
Expand Down
44 changes: 42 additions & 2 deletions src/UI/Section/Display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,59 @@ namespace ta
Display::Display(juce::AudioFormatManager& formatManager, DJPlayer& djPlayer)
: _djPlayer{djPlayer}, _thumbnailCache{1}, _waveformDisplay{formatManager, _thumbnailCache}
{
addAndMakeVisible(_filename);
addAndMakeVisible(_bpm);
addAndMakeVisible(_waveformDisplay);

_bpm.setJustificationType(juce::Justification::centred);

_djPlayer.addListener(this);

startTimerHz(30);
}

Display::~Display() { _djPlayer.removeListener(this); }

auto Display::resized() -> void { _waveformDisplay.setBounds(getLocalBounds()); }
auto Display::resized() -> void
{
using namespace juce;

Grid grid{};
grid.rowGap = 4_px;
grid.columnGap = 4_px;
grid.autoColumns = Grid::TrackInfo(1_fr);
grid.autoRows = Grid::TrackInfo(1_fr);
grid.autoFlow = Grid::AutoFlow::row;
grid.templateRows = fillArray(Grid::TrackInfo(1_fr), 4);
grid.templateColumns = fillArray(Grid::TrackInfo(1_fr), 4);
grid.items.addArray({
GridItem(_filename).withArea({}, GridItem::Span(3)),
GridItem(_bpm).withArea({}, {}),
GridItem(_waveformDisplay).withArea(GridItem::Span(3), GridItem::Span(4)),
});

grid.performLayout(getLocalBounds());
}

auto Display::timerCallback() -> void { _waveformDisplay.setPositionRelative(_djPlayer.positionRelative()); }

auto Display::loadURL(juce::URL const& url) -> void { _waveformDisplay.loadURL(url); }

auto Display::djPlayerFileChanged(juce::File const& file) -> void { _waveformDisplay.loadURL(juce::URL{file}); }
auto Display::djPlayerFileChanged(juce::File const& file) -> void
{
_waveformDisplay.loadURL(juce::URL{file});
_filename.setText(file.getFileName(), juce::sendNotification);
}

auto Display::djPlayerFileAnalysisFinished(BeatTrackResult const& result) -> void
{
if (result.ok())
{
_bpm.setText(juce::String{result.estimatedBPM()} + " bpm", juce::sendNotification);
_waveformDisplay.beatPositions(result.beatPositions());
return;
}

_bpm.setText("Error:" + result.errorMessage(), juce::sendNotification);
}
} // namespace ta
3 changes: 3 additions & 0 deletions src/UI/Section/Display.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ struct Display final
auto resized() -> void override;
auto timerCallback() -> void override;
auto djPlayerFileChanged(juce::File const& file) -> void override;
auto djPlayerFileAnalysisFinished(BeatTrackResult const& result) -> void override;

private:
DJPlayer& _djPlayer;
juce::AudioThumbnailCache _thumbnailCache;
WaveformDisplay _waveformDisplay;
juce::Label _filename;
juce::Label _bpm;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Display) // NOLINT
};
Expand Down

0 comments on commit 6ba2e9e

Please sign in to comment.