├── .gitignore ├── CMakeLists.txt ├── README.md └── Source ├── AudioPlayer ├── AudioPlayer.h ├── Data │ ├── AudioPlayerProcessor.cpp │ └── AudioPlayerProcessor.h ├── State │ └── AudioPlayerState.h └── View │ ├── AudioPlayerView.cpp │ ├── AudioPlayerView.h │ ├── AudioWaveformView.cpp │ ├── AudioWaveformView.h │ ├── CustomAudioThumbnail.cpp │ └── CustomAudioThumbnail.h ├── LookAndFeel └── StyleSheet.h ├── Main.cpp ├── MainComponent.cpp ├── MainComponent.h ├── Metadata ├── Metadata.h └── TagReader.h ├── MixerDevice └── Data │ ├── MixerDevice.h │ ├── MixerDeviceList.h │ └── MixerDeviceScanner.h ├── Playlist ├── Data │ ├── XmlPlaylist.cpp │ └── XmlPlaylist.h └── View │ ├── PlaylistView.cpp │ └── PlaylistView.h ├── Resources ├── Assets │ ├── WorkSans-Regular.ttf │ └── WorkSans-SemiBold.ttf ├── Resources.cpp └── Resources.h └── Toolbar ├── Components ├── Settings │ └── View │ │ ├── SettingsView.cpp │ │ └── SettingsView.h └── TrackAdd │ ├── State │ └── TrackAddState.h │ └── View │ ├── TrackAddView.cpp │ └── TrackAddView.h └── View ├── Toolbar.cpp └── Toolbar.h /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /JuceLibraryCode 3 | /Builds 4 | 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The first line of any CMake project should be a call to `cmake_minimum_required`, which checks 2 | # that the installed CMake will be able to understand the following CMakeLists, and ensures that 3 | # CMake's behaviour is compatible with the named version. This is a standard CMake command, so more 4 | # information can be found in the CMake docs. 5 | 6 | cmake_minimum_required(VERSION 3.25) 7 | 8 | # The top-level CMakeLists.txt file for a project must contain a literal, direct call to the 9 | # `project()` command. `project()` sets up some helpful variables that describe source/binary 10 | # directories, and the current project version. This is a standard CMake command. 11 | 12 | project(juceDjApp VERSION 0.0.1) 13 | 14 | # Set our C++ version to 17 15 | set(CMAKE_CXX_STANDARD 17) 16 | 17 | # By default we don't want Xcode schemes to be made for modules, etc 18 | set(CMAKE_XCODE_GENERATE_SCHEME OFF) 19 | 20 | # Go get JUCE using CMake's "FetchContent" 21 | set(FETCHCONTENT_QUIET OFF) 22 | 23 | include (FetchContent) 24 | 25 | option(JUCE_ENABLE_MODULE_SOURCE_GROUPS "Enable Module Source Groups" ON) 26 | set_property(GLOBAL PROPERTY USE_FOLDERS YES) 27 | 28 | FetchContent_Declare ( 29 | JUCE 30 | GIT_REPOSITORY https://github.com/juce-framework/JUCE.git 31 | GIT_TAG origin/master 32 | GIT_SHALLOW ON 33 | FIND_PACKAGE_ARGS 7.0.3 GLOBAL) 34 | 35 | FetchContent_Declare ( 36 | taglib 37 | GIT_REPOSITORY https://github.com/taglib/taglib.git 38 | GIT_TAG origin/master 39 | GIT_SHALLOW ON) 40 | 41 | FetchContent_Declare ( 42 | xwax 43 | GIT_REPOSITORY https://github.com/xwax/xwax.git 44 | GIT_TAG origin/master 45 | GIT_SHALLOW ON) 46 | 47 | FetchContent_MakeAvailable (JUCE) 48 | FetchContent_MakeAvailable (taglib) 49 | FetchContent_MakeAvailable (xwax) 50 | 51 | if(taglib_POPULATED) 52 | get_property(dirs DIRECTORY "${taglib_SOURCE_DIR}/taglib" PROPERTY INCLUDE_DIRECTORIES) 53 | target_include_directories(tag PUBLIC ${dirs} "${taglib_SOURCE_DIR}/taglib") 54 | endif() 55 | 56 | if(xwax_POPULATED) 57 | target_include_directories(tag PUBLIC ${xwax_SOURCE_DIR}) 58 | endif() 59 | 60 | juce_add_gui_app(juceDjApp) 61 | 62 | # We want all source code in the source directory 63 | file(GLOB_RECURSE SourceFiles CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Source/*.h") 64 | file(GLOB_RECURSE XWaxFiles CONFIGURE_DEPENDS "${xwax_SOURCE_DIR}/*.c" "${xwax_SOURCE_DIR}/*.h") 65 | 66 | # No, we don't want our source buried in extra nested folders 67 | set_target_properties("${PROJECT_NAME}" PROPERTIES FOLDER "") 68 | 69 | # The Xcode source tree should look like the source tree 70 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/Source PREFIX "Source" FILES ${SourceFiles}) 71 | source_group(TREE ${xwax_SOURCE_DIR} PREFIX "XWax" FILES ${XWaxFiles}) 72 | 73 | 74 | # `target_sources` adds source files to a target. We pass the target that needs the sources as the 75 | # first argument, then a visibility parameter for the sources which should normally be PRIVATE. 76 | # Finally, we supply a list of source files that will be built into the target. This is a standard 77 | # CMake command. 78 | 79 | target_sources("${PROJECT_NAME}" PRIVATE ${SourceFiles} ${XWaxFiles}) 80 | 81 | target_link_libraries(juceDjApp PRIVATE 82 | juce::juce_analytics 83 | juce::juce_audio_basics 84 | juce::juce_audio_devices 85 | juce::juce_core 86 | juce::juce_data_structures 87 | juce::juce_graphics 88 | juce::juce_gui_basics 89 | juce::juce_gui_extra 90 | juce::juce_audio_utils 91 | juce::juce_dsp 92 | juce::juce_recommended_config_flags 93 | juce::juce_recommended_lto_flags 94 | juce::juce_recommended_warning_flags 95 | tag) 96 | 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JUCE DJ App 2 | 3 | ## Overview 4 | This is the code for a new weekly livestream that I've been working on to show progress building a DJ app using C++ and JUCE. I thought this would be a great way for us to learn and grow together. Follow along on the [YouTube Channel](https://youtube.com/theaudioprogrammer)! 5 | 6 | [Here's a rough roadmap](https://quartz-asparagus-70f.notion.site/DJ-App-in-C-JUCE-404e1459800f4b268ee70df3ee56cb40) of what I'm looking to achieve on this project. Please feel free to make comments or suggestions! 7 | 8 | ## Compiling 9 | I've finally taken the plunge and started using CMake. Don't be scared! If I can do it, you can too. You're going to need some basic command line skills, and of course...CMake! Download it [here](https://cmake.org/download/) and install it. 10 | 11 | From the main repository folder, you will see `CMakeLists.txt`. In the command line, change the directory (`cd`) to this folder. Now you need to invoke CMake from the command line to create a Builds folder where a project in the IDE of your choice will reside. For me, this is XCode, so I can do this with: 12 | 13 | `cmake -B Builds -G Xcode` 14 | 15 | Check out this [awesome tutorial](https://melatonin.dev/blog/how-to-use-cmake-with-juce/) from Sudara on further info on CMake and JUCE. It takes a second to learn but it will greatly enhance your capabilities as a developer! 16 | 17 | ## Dependencies 18 | **Note:** These are already pulled in using CMake's "Fetch Content" so you don't need to clone these manually yourself. I've already taken care of it for you! 19 | 20 | [Taglib](https://github.com/taglib/taglib) - Reading and writing audio file metadata 21 | 22 | [XWax](https://github.com/xwax/xwax) - Digital Vinyl System (DVS) control 23 | 24 | ## License 25 | This source code is licensed under the GNU General Public License (GPLv3) agreement. 26 | -------------------------------------------------------------------------------- /Source/AudioPlayer/AudioPlayer.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioPlayer.h 5 | Created: 9 Feb 2023 4:01:30pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | #include "Data/AudioPlayerProcessor.h" 17 | #include "View/AudioPlayerView.h" 18 | #include "View/AudioWaveformView.h" 19 | 20 | 21 | /* Encapsulates audio processing, UI (view), and state into one object that can be easily controlled -- mutual area to pass data around between processor & view */ 22 | struct AudioPlayer : public juce::Timer 23 | { 24 | AudioPlayer() 25 | { 26 | playerView.onPlay = [this]() 27 | { 28 | processor.play(); 29 | }; 30 | 31 | playerView.onStop = [this]() 32 | { 33 | processor.stop(); 34 | }; 35 | 36 | playerView.onLoad = [this]() 37 | { 38 | loadFile(); 39 | }; 40 | 41 | playerView.onGainChange = [this]() 42 | { 43 | processor.setDecibelValue (static_cast(playerView.getGainSliderValue())); 44 | }; 45 | 46 | playerView.onTrackDroppedFromPlayList = [this]() 47 | { 48 | processor.loadTrack (playerView.pathToDroppedTrack); 49 | }; 50 | 51 | startTimerHz (30.0f); 52 | } 53 | 54 | ~AudioPlayer() 55 | { 56 | stopTimer(); 57 | } 58 | 59 | void timerCallback() 60 | { 61 | processor.convertSamplesToTime(); 62 | playerView.percentageInTrackPlayed = processor.getPercentagePlayedInTrack(); 63 | playerView.repaint(); 64 | waveformView.repaint(); 65 | } 66 | 67 | void loadFile() 68 | { 69 | processor.getState().setLoaded (false); 70 | 71 | songSelector = std::make_unique("Select a track to play", juce::File::getSpecialLocation (juce::File::SpecialLocationType::userDesktopDirectory), processor.getAudioFormatManager().getWildcardForAllFormats()); 72 | 73 | auto songSelectorFlags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles; 74 | 75 | songSelector->launchAsync (songSelectorFlags, [&] (const juce::FileChooser& chooser) 76 | { 77 | processor.loadTrack (chooser.getResult()); 78 | }); 79 | } 80 | 81 | AudioPlayerProcessor processor; 82 | AudioPlayerView playerView { processor.getState(), processor.getMetadata() }; 83 | AudioWaveformView waveformView { processor.getState(), processor.getTrackBuffer(), processor.getAudioFormatManager() }; 84 | 85 | private: 86 | /* Window to select a track */ 87 | std::unique_ptr songSelector; 88 | }; 89 | -------------------------------------------------------------------------------- /Source/AudioPlayer/Data/AudioPlayerProcessor.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioPlayerProcessor.cpp 5 | Created: 25 Jan 2023 4:11:19pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "AudioPlayerProcessor.h" 12 | 13 | AudioPlayerProcessor::AudioPlayerProcessor() 14 | { 15 | audioFormatManager.registerBasicFormats(); 16 | } 17 | 18 | void AudioPlayerProcessor::loadTrack (const juce::File& musicFile) 19 | { 20 | readPosition = 0; 21 | 22 | auto* r = audioFormatManager.createReaderFor (musicFile); 23 | std::unique_ptr reader (r); 24 | 25 | if (reader) 26 | { 27 | auto numSamples = static_cast(reader->lengthInSamples); 28 | 29 | audioSourceBuffer.setSize ((int)reader->numChannels, numSamples); 30 | jassert (numSamples > 0 && reader->numChannels > 0); 31 | 32 | loadMetadata (musicFile); 33 | 34 | //metadata.trackLength = juce::String { reader->lengthInSamples / reader->sampleRate }; 35 | trackNumSamples = reader->lengthInSamples; 36 | 37 | bool wasLoadSuccessful = reader->read (&audioSourceBuffer, 0, numSamples, 0, true, true); 38 | state.setLoaded (wasLoadSuccessful); 39 | } 40 | } 41 | 42 | /* It'd be much more convenient if we could call this directly in the audio thread, however this could create problems when the message thread wants to access as well, so I'm calling this from "Audioplayer" with a timer */ 43 | void AudioPlayerProcessor::convertSamplesToTime() 44 | { 45 | auto millis = readPosition / currentSampleRate * 1000.0; 46 | auto minutes = millis / 60000.0; 47 | auto seconds = (minutes - std::floor (minutes)) * 60.0; 48 | auto hundreths = (seconds - std::floor (seconds)) * 100.0; 49 | 50 | metadata.currentTime = convertTimeToString (minutes) + ":" + convertTimeToString (seconds) + ":" + convertTimeToString (hundreths); 51 | } 52 | 53 | juce::String AudioPlayerProcessor::convertTimeToString (double time) 54 | { 55 | return time < 10.0 ? "0" + juce::String (std::floor (time)) : juce::String (std::floor (time)); 56 | } 57 | 58 | float AudioPlayerProcessor::getPercentagePlayedInTrack() 59 | { 60 | return readPosition > 0.0f ? juce::jmap(float(readPosition), 1.0f, float(trackNumSamples), 0.0f, 100.0f) : 0.0f; 61 | } 62 | 63 | void AudioPlayerProcessor::loadMetadata (const juce::File& musicFile) 64 | { 65 | metadata = TagReader::getMetadataFromFile (musicFile); 66 | } 67 | 68 | void AudioPlayerProcessor::prepareToPlay (int numChannels, int samplesPerBlock, double sampleRate) 69 | { 70 | playerBuffer.setSize (numChannels, samplesPerBlock); 71 | currentSampleRate = sampleRate; 72 | } 73 | 74 | void AudioPlayerProcessor::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) 75 | { 76 | if (state.hasLoadedFile && state.isPlaying) 77 | processAudio (bufferToFill); 78 | } 79 | 80 | void AudioPlayerProcessor::processAudio (const juce::AudioSourceChannelInfo &bufferToFill) 81 | { 82 | auto* mainBuffer = bufferToFill.buffer; 83 | auto samplesRemaining = audioSourceBuffer.getNumSamples() - readPosition; 84 | 85 | /* We want to ensure the amount of samples we're processing is never greater 86 | than the amount of samples remaining in the track (that would be bad!). For 87 | This reason we can't just blindly copy "buffer->getNumSamples() every time. */ 88 | auto samplesToProcess = samplesRemaining > mainBuffer->getNumSamples() ? mainBuffer->getNumSamples() : samplesRemaining; 89 | 90 | playerBuffer.clear(); 91 | 92 | // You haven't called prepareToPlay()! 93 | jassert (playerBuffer.getNumChannels() == mainBuffer->getNumChannels()); 94 | jassert (playerBuffer.getNumSamples() > 0 && playerBuffer.getNumSamples() == mainBuffer->getNumSamples()); 95 | 96 | for (int ch = 0; ch < mainBuffer->getNumChannels(); ch++) 97 | { 98 | playerBuffer.copyFrom (ch, 0, audioSourceBuffer, ch, readPosition, samplesToProcess); 99 | playerBuffer.applyGain (ch, 0, playerBuffer.getNumSamples(), rawGain); 100 | 101 | // Add samples to main buffer (Note: May want to change this later) 102 | mainBuffer->addFrom (ch, 0, playerBuffer, ch, 0, samplesToProcess); 103 | } 104 | 105 | // Move read position along... 106 | readPosition+=samplesToProcess; 107 | } 108 | 109 | void AudioPlayerProcessor::play() 110 | { 111 | state.setPlaying (true); 112 | } 113 | 114 | void AudioPlayerProcessor::stop() 115 | { 116 | state.setPlaying (false); 117 | } 118 | 119 | void AudioPlayerProcessor::setDecibelValue (float value) 120 | { 121 | rawGain = juce::Decibels::decibelsToGain (value); 122 | } 123 | 124 | -------------------------------------------------------------------------------- /Source/AudioPlayer/Data/AudioPlayerProcessor.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioPlayerProcessor.h 5 | Created: 25 Jan 2023 4:11:19pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #include "../../Metadata/Metadata.h" 18 | #include "../../Metadata/TagReader.h" 19 | #include "../State/AudioPlayerState.h" 20 | 21 | 22 | 23 | 24 | //================================================================================ 25 | 26 | class AudioPlayerProcessor 27 | { 28 | public: 29 | AudioPlayerProcessor(); 30 | void loadTrack (const juce::File& musicFile); 31 | void prepareToPlay (int numChannels, int samplesPerBlock, double sampleRate); 32 | void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill); 33 | void play(); 34 | void stop(); 35 | void setDecibelValue (float value); 36 | void convertSamplesToTime(); 37 | float getPercentagePlayedInTrack(); 38 | AudioPlayerState& getState() { return state; } 39 | Metadata& getMetadata() { return metadata; } 40 | juce::AudioFormatManager& getAudioFormatManager() { return audioFormatManager; } 41 | juce::AudioBuffer& getTrackBuffer() { return audioSourceBuffer; } 42 | 43 | private: 44 | void loadMetadata (const juce::File& musicFile); 45 | 46 | /* Helper to ensure time is always "00" rather than "0" until it gets to "10" */ 47 | juce::String convertTimeToString (double time); 48 | 49 | /* Copies player buffer data to main audio buffer */ 50 | void processAudio (const juce::AudioSourceChannelInfo& bufferToFill); 51 | 52 | /* Necessary to register and stream audio formats */ 53 | juce::AudioFormatManager audioFormatManager; 54 | 55 | /* Holds an entire track in memory */ 56 | juce::AudioBuffer audioSourceBuffer; 57 | 58 | /* Holds mutable "temp data" where we can change gain / processing etc before being passed to the main output buffer */ 59 | juce::AudioBuffer playerBuffer; 60 | 61 | /* How far we are in the track */ 62 | int readPosition { 0 }; 63 | 64 | /* How many samples are in the track */ 65 | juce::int64 trackNumSamples { 0 }; 66 | 67 | /* Current sample rate of the soundcard */ 68 | double currentSampleRate { 0.0 }; 69 | 70 | /* Ensure our file is loaded before trying to access it */ 71 | //bool fileIsLoaded { false }; 72 | 73 | /* Class member that will hold our conversion from dBFS */ 74 | float rawGain { 1.0f }; 75 | 76 | /* Holds the state & metadata of our player */ 77 | AudioPlayerState state; 78 | Metadata metadata; 79 | }; 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Source/AudioPlayer/State/AudioPlayerState.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioPlayerState.h 5 | Created: 25 Jan 2023 4:27:08pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | // Set by the processor, observed by the view 16 | struct AudioPlayerState : public juce::ChangeBroadcaster 17 | { 18 | bool isPlaying = false; 19 | bool hasLoadedFile = false; 20 | 21 | void setPlaying (bool playing) 22 | { 23 | isPlaying = playing; 24 | sendChangeMessage(); 25 | } 26 | 27 | void setLoaded (bool loaded) 28 | { 29 | hasLoadedFile = loaded; 30 | sendChangeMessage(); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /Source/AudioPlayer/View/AudioPlayerView.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioPlayerView.cpp 5 | Created: 25 Jan 2023 4:12:07pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "AudioPlayerView.h" 12 | 13 | //============================================================================== 14 | AudioPlayerView::AudioPlayerView (AudioPlayerState& s, Metadata& m) 15 | : state (s) 16 | , metadata (m) 17 | { 18 | loadAudioButton.setButtonText ("Load"); 19 | playAudioButton.setButtonText ("Play"); 20 | stopAudioButton.setButtonText ("Stop"); 21 | addAndMakeVisible (loadAudioButton); 22 | addAndMakeVisible (playAudioButton); 23 | addAndMakeVisible (stopAudioButton); 24 | 25 | loadAudioButton.onClick = [&]() 26 | { 27 | onLoad(); 28 | }; 29 | 30 | playAudioButton.onClick = [&]() 31 | { 32 | onPlay(); 33 | }; 34 | 35 | stopAudioButton.onClick = [&]() 36 | { 37 | onStop(); 38 | }; 39 | 40 | // Values set in dBFS!! 41 | gainSlider.setRange (-60.0f, 0.0f, 0.01f); 42 | gainSlider.setValue (0.0f); 43 | 44 | gainSlider.onValueChange = [&]() 45 | { 46 | onGainChange(); 47 | }; 48 | 49 | addAndMakeVisible (gainSlider); 50 | 51 | juce::Font f { StyleSheet::boldFont }; 52 | 53 | trackNameLabel.setFont (f.withHeight (24.0f)); 54 | trackNameLabel.setText ("No Track Loaded", juce::NotificationType::dontSendNotification); 55 | trackNameLabel.setColour (juce::Label::ColourIds::outlineColourId, juce::Colours::white); 56 | addAndMakeVisible (trackNameLabel); 57 | 58 | artistNameLabel.setFont (f.withHeight (24.0f)); 59 | artistNameLabel.setText ("No Artist", juce::NotificationType::dontSendNotification); 60 | artistNameLabel.setColour (juce::Label::ColourIds::outlineColourId, juce::Colours::white); 61 | addAndMakeVisible (artistNameLabel); 62 | 63 | trackLengthLabel.setFont (f.withHeight (24.0f)); 64 | trackLengthLabel.setText ("00:00.0", juce::NotificationType::dontSendNotification); 65 | //trackLengthLabel.setColour (juce::Label::ColourIds::outlineColourId, juce::Colours::white); 66 | addAndMakeVisible (trackLengthLabel); 67 | 68 | loadAudioButton.getLookAndFeel().setDefaultSansSerifTypeface (StyleSheet::boldFont); 69 | playAudioButton.getLookAndFeel().setDefaultSansSerifTypeface (StyleSheet::boldFont); 70 | stopAudioButton.getLookAndFeel().setDefaultSansSerifTypeface (StyleSheet::boldFont); 71 | 72 | state.addChangeListener (this); 73 | } 74 | 75 | AudioPlayerView::~AudioPlayerView() 76 | { 77 | state.removeChangeListener (this); 78 | } 79 | 80 | void AudioPlayerView::paint (juce::Graphics& g) 81 | { 82 | g.fillAll (juce::Colours::black); 83 | drawDisc (g); 84 | trackLengthLabel.setText (metadata.currentTime, juce::dontSendNotification); 85 | 86 | if (somethingIsBeingDraggedOver) 87 | { 88 | g.setColour (juce::Colours::red); 89 | g.drawRect (getLocalBounds(), 3); 90 | } 91 | } 92 | 93 | void AudioPlayerView::drawDisc (juce::Graphics& g) 94 | { 95 | g.setColour (juce::Colours::white); 96 | 97 | juce::Rectangle discArea { 375.0f, static_cast(artistNameLabel.getBottom()) + 25.0f, 180.0f, 180.0f }; 98 | juce::Point discCenter = discArea.getCentre(); 99 | float radius { discArea.getWidth() / 2.0f }; 100 | 101 | // Main Disc 102 | g.drawEllipse (discArea, 10.0f); 103 | g.fillEllipse (discCenter.getX() - 10.0f, discCenter.getY() - 10.0f, 20.0f, 20.0f); 104 | 105 | g.drawLine (discCenter.getX(), 106 | discCenter.getY(), 107 | discCenter.getX() + radius * (float)std::sin (2.0 * juce::MathConstants::pi * percentageInTrackPlayed), 108 | discCenter.getY() - radius * (float)std::cos (2.0f * juce::MathConstants::pi * percentageInTrackPlayed), 109 | 10.0f); 110 | 111 | // Completion disc 112 | g.setColour (juce::Colours::orange.withAlpha (0.7f)); 113 | juce::Path path; 114 | path.startNewSubPath (discCenter.getX(), discCenter.getY() - radius); 115 | path.addCentredArc (discCenter.getX(), discCenter.getY(), radius, radius, 0.0f, 0.0f, (percentageInTrackPlayed * 0.01f) * 2.0f * juce::MathConstants::pi); 116 | g.strokePath (path, juce::PathStrokeType (10.0f)); 117 | } 118 | 119 | void AudioPlayerView::resized() 120 | { 121 | // Assuming component is 600 x 300 122 | auto w = 100; 123 | auto h = 50; 124 | auto pad = 10; 125 | auto x = 5; 126 | auto y = getHeight() - (h + pad); 127 | auto labelWidth = 500; 128 | auto labelHeight = 30; 129 | 130 | loadAudioButton.setBounds (x, y, w, h); 131 | playAudioButton.setBounds (loadAudioButton.getRight() + pad, y, w, h); 132 | stopAudioButton.setBounds (playAudioButton.getRight() + pad, y, w, h); 133 | gainSlider.setBounds (x, 70, 50, 150); 134 | trackNameLabel.setBounds (x, 5, labelWidth, labelHeight); 135 | artistNameLabel.setBounds (x, trackNameLabel.getBottom(), labelWidth, labelHeight); 136 | trackLengthLabel.setBounds (275, artistNameLabel.getBottom() + 50, labelWidth / 5, labelHeight); 137 | } 138 | 139 | void AudioPlayerView::changeListenerCallback (juce::ChangeBroadcaster* source) 140 | { 141 | if (source == &state) 142 | update(); 143 | } 144 | 145 | void AudioPlayerView::itemDragEnter (const juce::DragAndDropTarget::SourceDetails& /* dragSourceDetails */) 146 | { 147 | somethingIsBeingDraggedOver = true; 148 | } 149 | 150 | bool AudioPlayerView::isInterestedInDragSource (const juce::DragAndDropTarget::SourceDetails& /* dragSourceDetails */) 151 | { 152 | return true; 153 | } 154 | 155 | void AudioPlayerView::itemDropped (const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) 156 | { 157 | somethingIsBeingDraggedOver = false; 158 | pathToDroppedTrack = dragSourceDetails.description; 159 | onTrackDroppedFromPlayList(); 160 | } 161 | 162 | void AudioPlayerView::update() 163 | { 164 | trackNameLabel.setText (metadata.title, juce::dontSendNotification); 165 | artistNameLabel.setText (metadata.artist, juce::dontSendNotification); 166 | trackLengthLabel.setText (metadata.currentTime, juce::dontSendNotification); 167 | repaint(); 168 | } 169 | -------------------------------------------------------------------------------- /Source/AudioPlayer/View/AudioPlayerView.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioPlayerView.h 5 | Created: 25 Jan 2023 4:12:07pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include "../../LookAndFeel/StyleSheet.h" 14 | #include "../State/AudioPlayerState.h" 15 | #include "../../Metadata/Metadata.h" 16 | 17 | 18 | 19 | //============================================================================== 20 | /* 21 | */ 22 | class AudioPlayerView : public juce::Component, 23 | public juce::DragAndDropTarget, 24 | public juce::ChangeListener 25 | { 26 | public: 27 | AudioPlayerView (AudioPlayerState& s, Metadata& m); 28 | ~AudioPlayerView() override; 29 | 30 | void paint (juce::Graphics&) override; 31 | void resized() override; 32 | 33 | // From ChangeListener 34 | void changeListenerCallback (juce::ChangeBroadcaster* source) override; 35 | 36 | // From Drag and drop target 37 | void itemDragEnter (const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) override; 38 | bool isInterestedInDragSource (const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) override; 39 | void itemDropped (const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) override; 40 | 41 | double getGainSliderValue() const { return gainSlider.getValue(); } 42 | 43 | // Lambdas to control -- we aren't giving the view direct access to the processor. 44 | std::function onPlay = [](){}; 45 | std::function onStop = [](){}; 46 | std::function onLoad = [](){}; 47 | std::function onGainChange = [](){}; 48 | std::function onTrackDroppedFromPlayList = [](){}; 49 | 50 | float percentageInTrackPlayed { 0.0f }; 51 | juce::String pathToDroppedTrack { "" }; 52 | 53 | private: 54 | void drawDisc (juce::Graphics& g); 55 | void update(); 56 | 57 | AudioPlayerState& state; 58 | Metadata& metadata; 59 | juce::TextButton loadAudioButton { "Load" }; 60 | juce::TextButton playAudioButton { "Play" }; 61 | juce::TextButton stopAudioButton { "Stop" }; 62 | 63 | juce::Slider gainSlider { juce::Slider::SliderStyle::LinearVertical, juce::Slider::TextBoxBelow }; 64 | 65 | juce::Label trackNameLabel { "Song Name" }; 66 | juce::Label artistNameLabel { "Artist Name" }; 67 | juce::Label trackLengthLabel { "Song Length" }; 68 | 69 | bool somethingIsBeingDraggedOver = false; 70 | 71 | 72 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPlayerView) 73 | }; 74 | -------------------------------------------------------------------------------- /Source/AudioPlayer/View/AudioWaveformView.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioWaveFormView.cpp 5 | Created: 10 Feb 2023 8:20:27pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "AudioWaveformView.h" 12 | 13 | AudioWaveformView::AudioWaveformView (AudioPlayerState& s, juce::AudioBuffer& b, juce::AudioFormatManager& manager) 14 | : state(s) 15 | , audioBuffer (b) 16 | , thumbnail (1024, manager, thumbnailCache) 17 | { 18 | state.addChangeListener (this); 19 | thumbnail.addChangeListener (this); 20 | } 21 | 22 | AudioWaveformView::~AudioWaveformView() 23 | { 24 | state.removeAllChangeListeners(); 25 | thumbnail.removeAllChangeListeners(); 26 | } 27 | 28 | void AudioWaveformView::prepare (double rate) 29 | { 30 | sampleRate = rate; 31 | } 32 | 33 | void AudioWaveformView::paint (juce::Graphics& g) 34 | { 35 | g.fillAll (juce::Colours::black); 36 | g.setColour (juce::Colours::white); 37 | g.drawRect (getLocalBounds()); 38 | 39 | if (state.hasLoadedFile) 40 | drawWaveform (g); 41 | } 42 | 43 | void AudioWaveformView::drawWaveform (juce::Graphics& g) 44 | { 45 | if (audioBuffer.getNumSamples() > 0) 46 | { 47 | g.setColour (juce::Colours::skyblue); 48 | thumbnail.drawChannel (g, getLocalBounds(), 0.0f, thumbnail.getTotalLength(), 0, 1.0f); 49 | } 50 | } 51 | 52 | void AudioWaveformView::resized() 53 | { 54 | 55 | } 56 | 57 | void AudioWaveformView::changeListenerCallback (juce::ChangeBroadcaster* source) 58 | { 59 | if (source == &state) 60 | { 61 | update(); 62 | } 63 | } 64 | 65 | void AudioWaveformView::update() 66 | { 67 | if (state.hasLoadedFile) 68 | { 69 | // You haven't called prepare! 70 | jassert (sampleRate > 0.0); 71 | thumbnail.clear(); 72 | thumbnailCache.clear(); 73 | thumbnail.setSource (&audioBuffer, sampleRate, 0); 74 | repaint(); 75 | } 76 | 77 | else if (!state.hasLoadedFile) 78 | { 79 | thumbnail.clear(); 80 | thumbnailCache.clear(); 81 | repaint(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Source/AudioPlayer/View/AudioWaveformView.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | AudioWaveFormView.h 5 | Created: 10 Feb 2023 8:20:27pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | 20 | #include "../../AudioPlayer/State/AudioPlayerState.h" 21 | 22 | 23 | class AudioWaveformView : public juce::Component, public juce::ChangeListener 24 | { 25 | public: 26 | AudioWaveformView (AudioPlayerState& s, juce::AudioBuffer& b, juce::AudioFormatManager& manager); 27 | ~AudioWaveformView() override; 28 | void prepare (double rate); 29 | void paint (juce::Graphics& g) override; 30 | void resized() override; 31 | 32 | // Change Listener 33 | void changeListenerCallback (juce::ChangeBroadcaster* source) override; 34 | 35 | private: 36 | void drawWaveform (juce::Graphics& g); 37 | void update(); 38 | 39 | AudioPlayerState& state; 40 | juce::AudioBuffer& audioBuffer; 41 | double sampleRate { 0.0 }; 42 | juce::AudioThumbnailCache thumbnailCache { 1 }; 43 | juce::AudioThumbnail thumbnail; 44 | }; 45 | -------------------------------------------------------------------------------- /Source/AudioPlayer/View/CustomAudioThumbnail.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | CustomAudioThumbnail.cpp 5 | Created: 20 Feb 2023 8:32:03am 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "CustomAudioThumbnail.h" 12 | -------------------------------------------------------------------------------- /Source/AudioPlayer/View/CustomAudioThumbnail.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | CustomAudioThumbnail.h 5 | Created: 20 Feb 2023 8:32:03am 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | -------------------------------------------------------------------------------- /Source/LookAndFeel/StyleSheet.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | StyleSheet.h 5 | Created: 26 Feb 2023 1:35:30pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | #include "../Resources/Resources.h" 16 | 17 | namespace StyleSheet 18 | { 19 | 20 | static juce::Typeface::Ptr boldFont { juce::Typeface::createSystemTypefaceFor (Resources::WorkSansSemiBold_ttf, Resources::WorkSansSemiBold_ttfSize) }; 21 | static juce::Typeface::Ptr plainFont { juce::Typeface::createSystemTypefaceFor (Resources::WorkSansRegular_ttf, Resources::WorkSansRegular_ttfSize) }; 22 | 23 | } 24 | 25 | class customLookandFeel : public juce::LookAndFeel_V4 26 | { 27 | 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /Source/Main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | This file contains the basic startup code for a JUCE application. 5 | 6 | ============================================================================== 7 | */ 8 | 9 | 10 | #include 11 | #include "MainComponent.h" 12 | 13 | struct ProjectInfo 14 | { 15 | static const char* const projectName; 16 | static const char* const companyName; 17 | static const char* const versionString; 18 | static const int versionNumber; 19 | }; 20 | 21 | inline const char* const ProjectInfo::projectName = "juceDjApp"; 22 | inline const char* const ProjectInfo::companyName = "The Audio Programmer"; 23 | inline const char* const ProjectInfo::versionString = "0.0.1"; 24 | inline const int ProjectInfo::versionNumber = 0; 25 | 26 | 27 | 28 | //============================================================================== 29 | class audioDeviceManagerApplication : public juce::JUCEApplication 30 | { 31 | public: 32 | //============================================================================== 33 | audioDeviceManagerApplication() {} 34 | 35 | const juce::String getApplicationName() override { return ProjectInfo::projectName; } 36 | const juce::String getApplicationVersion() override { return ProjectInfo::versionString; } 37 | bool moreThanOneInstanceAllowed() override { return true; } 38 | 39 | //============================================================================== 40 | void initialise (const juce::String& commandLine) override 41 | { 42 | // This method is where you should put your application's initialisation code.. 43 | juce::ignoreUnused (commandLine); 44 | 45 | mainWindow.reset (new MainWindow (getApplicationName())); 46 | } 47 | 48 | void shutdown() override 49 | { 50 | // Add your application's shutdown code here.. 51 | 52 | mainWindow = nullptr; // (deletes our window) 53 | } 54 | 55 | //============================================================================== 56 | void systemRequestedQuit() override 57 | { 58 | // This is called when the app is being asked to quit: you can ignore this 59 | // request and let the app carry on running, or call quit() to allow the app to close. 60 | quit(); 61 | } 62 | 63 | void anotherInstanceStarted (const juce::String& commandLine) override 64 | { 65 | juce::ignoreUnused (commandLine); 66 | 67 | // When another instance of the app is launched while this one is running, 68 | // this method is invoked, and the commandLine parameter tells you what 69 | // the other instance's command-line arguments were. 70 | } 71 | 72 | //============================================================================== 73 | /* 74 | This class implements the desktop window that contains an instance of 75 | our MainComponent class. 76 | */ 77 | class MainWindow : public juce::DocumentWindow 78 | { 79 | public: 80 | MainWindow (juce::String name) 81 | : DocumentWindow (name, 82 | juce::Desktop::getInstance().getDefaultLookAndFeel() 83 | .findColour (juce::ResizableWindow::backgroundColourId), 84 | DocumentWindow::allButtons) 85 | { 86 | setUsingNativeTitleBar (true); 87 | setContentOwned (new MainComponent(), true); 88 | 89 | #if JUCE_IOS || JUCE_ANDROID 90 | setFullScreen (true); 91 | #else 92 | setResizable (true, true); 93 | centreWithSize (getWidth(), getHeight()); 94 | #endif 95 | 96 | setVisible (true); 97 | } 98 | 99 | void closeButtonPressed() override 100 | { 101 | // This is called when the user tries to close this window. Here, we'll just 102 | // ask the app to quit when this happens, but you can change this to do 103 | // whatever you need. 104 | JUCEApplication::getInstance()->systemRequestedQuit(); 105 | } 106 | 107 | /* Note: Be careful if you override any DocumentWindow methods - the base 108 | class uses a lot of them, so by overriding you might break its functionality. 109 | It's best to do all your work in your content component instead, but if 110 | you really have to override any DocumentWindow methods, make sure your 111 | subclass also calls the superclass's method. 112 | */ 113 | 114 | private: 115 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow) 116 | }; 117 | 118 | private: 119 | std::unique_ptr mainWindow; 120 | }; 121 | 122 | //============================================================================== 123 | // This macro generates the main() routine that launches the app. 124 | START_JUCE_APPLICATION (audioDeviceManagerApplication) 125 | -------------------------------------------------------------------------------- /Source/MainComponent.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponent.h" 2 | 3 | //============================================================================== 4 | MainComponent::MainComponent() : deviceScanner (deviceManager), toolbar (deviceManager, xmlPlaylist), playlist (toolbar.getTrackAddState()) 5 | { 6 | setSize (1400, 600); 7 | 8 | if (juce::RuntimePermissions::isRequired (juce::RuntimePermissions::recordAudio) 9 | && ! juce::RuntimePermissions::isGranted (juce::RuntimePermissions::recordAudio)) 10 | { 11 | juce::RuntimePermissions::request (juce::RuntimePermissions::recordAudio, 12 | [&] (bool granted) { setAudioChannels (granted ? 2 : 0, 2); }); 13 | } 14 | else 15 | { 16 | setAudioChannels (2, 2); 17 | } 18 | 19 | setLookAndFeel (&customLookAndFeel); 20 | 21 | // Device manager broadcasts when a new device is connected 22 | deviceManager.addChangeListener (&deviceScanner); 23 | 24 | addAndMakeVisible (audioPlayer1.playerView); 25 | addAndMakeVisible (audioPlayer1.waveformView); 26 | addAndMakeVisible (playlist.getComponent()); 27 | addAndMakeVisible (toolbar); 28 | } 29 | 30 | MainComponent::~MainComponent() 31 | { 32 | setLookAndFeel (nullptr); 33 | deviceManager.removeAllChangeListeners(); 34 | shutdownAudio(); 35 | } 36 | 37 | //============================================================================== 38 | void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sampleRate) 39 | { 40 | // Assuming this is a stereo setup for each track 41 | audioPlayer1.processor.prepareToPlay (2 , samplesPerBlockExpected, sampleRate); 42 | audioPlayer1.waveformView.prepare (sampleRate); 43 | } 44 | 45 | void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) 46 | { 47 | bufferToFill.clearActiveBufferRegion(); 48 | audioPlayer1.processor.getNextAudioBlock (bufferToFill); 49 | 50 | } 51 | 52 | void MainComponent::releaseResources() 53 | { 54 | 55 | } 56 | 57 | //============================================================================== 58 | void MainComponent::paint (juce::Graphics& g) 59 | { 60 | g.fillAll (juce::Colours::dimgrey); 61 | } 62 | 63 | void MainComponent::resized() 64 | { 65 | auto pad = 10; 66 | 67 | toolbar.setBounds (10, 10, getWidth() - 20, 70); 68 | 69 | audioPlayer1.playerView.setBounds (10, toolbar.getBottom() + pad, 600, 300); 70 | audioPlayer1.waveformView.setBounds (audioPlayer1.playerView.getRight() + pad, toolbar.getBottom() + pad, 600, 100); 71 | playlist.getComponent().setBounds (10,audioPlayer1.playerView.getBottom() + pad, 1200, 400); 72 | } 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Source/MainComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Assets 6 | #include "LookAndFeel/StyleSheet.h" 7 | 8 | // Data 9 | #include "MixerDevice/Data/MixerDeviceScanner.h" 10 | 11 | // Encapsulated objects (UI, Data & State) 12 | #include "AudioPlayer/AudioPlayer.h" 13 | 14 | // Playlist 15 | #include "Playlist/View/PlaylistView.h" 16 | 17 | // Toolbar with settings, adding tracks etc 18 | #include "Toolbar/View/Toolbar.h" 19 | 20 | 21 | //============================================================================== 22 | /* 23 | This component lives inside our window, and this is where you should put all 24 | your controls and content. 25 | */ 26 | class MainComponent : public juce::AudioAppComponent, 27 | public juce::DragAndDropContainer 28 | { 29 | public: 30 | //============================================================================== 31 | MainComponent(); 32 | ~MainComponent() override; 33 | 34 | //============================================================================== 35 | void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override; 36 | void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override; 37 | void releaseResources() override; 38 | 39 | //============================================================================== 40 | void paint (juce::Graphics& g) override; 41 | void resized() override; 42 | 43 | //============================================================================== 44 | 45 | private: 46 | // Checks our list of USB devices when a new device is connected 47 | MixerDeviceScanner deviceScanner; 48 | 49 | AudioPlayer audioPlayer1; 50 | 51 | Toolbar toolbar; 52 | 53 | Playlist playlist; 54 | 55 | bool fileIsLoaded { false }; 56 | XmlPlaylist xmlPlaylist; 57 | 58 | juce::LookAndFeel_V4 customLookAndFeel; 59 | 60 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) 61 | }; 62 | -------------------------------------------------------------------------------- /Source/Metadata/Metadata.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | 6 | /* Describe all the data we want for our tracks */ 7 | struct Metadata 8 | { 9 | juce::String title { "" }; 10 | juce::String artist { "" }; 11 | juce::String album { "" }; 12 | juce::String year { 0 }; 13 | juce::String genre { "" }; 14 | juce::String comment { "" }; 15 | juce::String currentTime { "" }; 16 | 17 | // Audio Properties 18 | juce::String bitrate { "" }; 19 | juce::String sampleRate { "" }; 20 | juce::String channels { "" }; 21 | juce::String length { "" }; 22 | juce::String path { "" }; 23 | }; 24 | -------------------------------------------------------------------------------- /Source/Metadata/TagReader.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include "Metadata.h" 6 | #include 7 | #include 8 | 9 | 10 | /* Read the metadata of an audio file and store it in a Metadata struct */ 11 | class TagReader 12 | { 13 | public: 14 | static Metadata getMetadataFromFile (const juce::File& musicFile) 15 | { 16 | Metadata metadata; 17 | TagLib::FileRef tagReader (musicFile.getFullPathName().toUTF8()); 18 | 19 | if (! tagReader.isNull() && tagReader.tag()) 20 | { 21 | TagLib::Tag* tag = tagReader.tag(); 22 | 23 | metadata.title = juce::String (tag->title().toCString()); 24 | metadata.artist = juce::String (tag->artist().toCString()); 25 | metadata.album = juce::String (tag->album().toCString()); 26 | metadata.year = juce::String (tag->year()); 27 | metadata.genre = juce::String (tag->genre().toCString()); 28 | metadata.comment = juce::String (tag->comment().toCString()); 29 | 30 | if(tagReader.audioProperties()) 31 | { 32 | TagLib::AudioProperties *properties = tagReader.audioProperties(); 33 | 34 | metadata.bitrate = juce::String (properties->bitrate()); 35 | metadata.sampleRate = juce::String (properties->sampleRate()); 36 | metadata.channels = juce::String (properties->channels()); 37 | 38 | auto seconds = juce::String (properties->length() % 60); 39 | auto minutes = juce::String ((properties->length() - seconds.getIntValue()) / 60); 40 | 41 | metadata.length = minutes + ":" + seconds; 42 | 43 | metadata.path = musicFile.getFullPathName(); 44 | } 45 | } 46 | else 47 | { 48 | metadata.title = musicFile.getFileNameWithoutExtension(); 49 | metadata.artist = "Unknown"; 50 | } 51 | 52 | return metadata; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /Source/MixerDevice/Data/MixerDevice.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | MixerDevice.h 5 | Created: 22 Jan 2023 3:23:50pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | 16 | 17 | /* This is where we describe data for each mixer that we may connect to the app and bundle this data to pass to our device manager and change settings, such as: 18 | - audio device name 19 | - midi device name (which may differ from the audio device name (or be the same!) 20 | - Samplerate the mixer may want to function at 21 | - Buffersize the mixer wants to operate at 22 | */ 23 | 24 | class MixerDevice 25 | { 26 | public: 27 | enum class ProcessingType 28 | { 29 | Internal, 30 | External 31 | }; 32 | 33 | MixerDevice (const juce::String& mixerAudioName, const juce::String& mixerMidiName, const double rate, const int buffer, const int channel, const int cc, const int channelIns, const int channelOuts) 34 | : audioDeviceName (mixerAudioName), midiDeviceName (mixerMidiName), sampleRate (rate), bufferSize (buffer), midiChannel (channel), midiCC (cc), inputChannels (channelIns), outputChannels (channelOuts) {} 35 | 36 | // How we update the device manager when we connect the mixer 37 | juce::AudioDeviceManager::AudioDeviceSetup createAudioDeviceSetup() 38 | { 39 | juce::AudioDeviceManager::AudioDeviceSetup setup; 40 | 41 | setup.inputDeviceName = audioDeviceName; 42 | setup.outputDeviceName = audioDeviceName; 43 | setup.sampleRate = sampleRate; 44 | setup.bufferSize = bufferSize; 45 | setup.inputChannels.setRange (0, inputChannels, true); 46 | setup.outputChannels.setRange (0, outputChannels, true); 47 | 48 | return setup; 49 | } 50 | 51 | // Getters & Setters 52 | juce::String getAudioDeviceName() const { return audioDeviceName; } 53 | juce::String getMidiDeviceName() const { return midiDeviceName; } 54 | double getSampleRate() const { return sampleRate; } 55 | int getBufferSize() const { return bufferSize; } 56 | int getMidiChannel() const { return midiChannel; } 57 | int getMidiCC() const { return midiCC; } 58 | int getNumInputChannels() const { return inputChannels; } 59 | int getNumOutputChannels() const { return outputChannels; } 60 | 61 | private: 62 | juce::String audioDeviceName { "" }; 63 | juce::String midiDeviceName { "" }; 64 | double sampleRate { 0.0 }; 65 | int bufferSize { 0 }; 66 | int midiChannel { 0 }; 67 | int midiCC { 0 }; 68 | int inputChannels { 0 }; 69 | int outputChannels { 0 }; 70 | }; 71 | -------------------------------------------------------------------------------- /Source/MixerDevice/Data/MixerDeviceList.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | MixerDeviceList.h 5 | Created: 22 Jan 2023 3:24:30pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include "MixerDevice.h" 14 | 15 | 16 | /* Holds our list of mixers */ 17 | struct MixerDeviceList 18 | { 19 | MixerDeviceList() 20 | { 21 | deviceSetups.push_back (pioneerDJMS9); 22 | } 23 | 24 | std::vector deviceSetups; 25 | MixerDevice pioneerDJMS9 { "DJM-S9", "Pioneer DJ DJM-S9", 48000.0, 64, 7, 31, 4, 8 }; 26 | }; 27 | -------------------------------------------------------------------------------- /Source/MixerDevice/Data/MixerDeviceScanner.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | MixerDeviceScanner.h 5 | Created: 22 Jan 2023 3:24:41pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include "MixerDeviceList.h" 14 | 15 | /* When a user plugs in a USB device, scans through our device list to see if it's listed. If yes, then updates the settings */ 16 | class MixerDeviceScanner : public juce::ChangeListener 17 | { 18 | public: 19 | MixerDeviceScanner (juce::AudioDeviceManager& m) : deviceManager (m) {} 20 | 21 | void changeListenerCallback (juce::ChangeBroadcaster *source) override 22 | { 23 | if (source == dynamic_cast(&deviceManager)) 24 | { 25 | const auto& deviceTypes = deviceManager.getAvailableDeviceTypes(); 26 | 27 | for (auto& type : deviceTypes) 28 | { 29 | // According to docs, this must be called at least once before calling getDeviceNames() 30 | type->scanForDevices(); 31 | 32 | for (const auto& deviceName : type->getDeviceNames()) 33 | { 34 | for (auto mixer : mixerDeviceList.deviceSetups) 35 | 36 | if (deviceName == mixer.getAudioDeviceName()) 37 | { 38 | deviceManager.initialise (mixer.getNumInputChannels(), mixer.getNumOutputChannels(), nullptr, true); 39 | deviceManager.setAudioDeviceSetup (mixer.createAudioDeviceSetup(), true); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | private: 47 | MixerDeviceList mixerDeviceList; 48 | juce::AudioDeviceManager& deviceManager; 49 | }; 50 | -------------------------------------------------------------------------------- /Source/Playlist/Data/XmlPlaylist.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "XmlPlaylist.h" 3 | 4 | 5 | XmlPlaylist::XmlPlaylist() 6 | { 7 | auto userFolderAndPlaylistExists = checkForUserFolderAndPlaylist(); 8 | jassert (userFolderAndPlaylistExists); 9 | } 10 | 11 | bool XmlPlaylist::checkForUserFolderAndPlaylist() 12 | { 13 | auto userFolderExists = checkForUserFolder(); 14 | 15 | jassert (userFolderExists); 16 | 17 | if (! playlistFile.existsAsFile()) 18 | return createNewXml(); 19 | 20 | return playlistFile.existsAsFile(); 21 | } 22 | 23 | bool XmlPlaylist::createNewXml() 24 | { 25 | addHeaderData(); 26 | return playlistFile.existsAsFile(); 27 | } 28 | 29 | bool XmlPlaylist::checkForUserFolder() const 30 | { 31 | if (! userFolder.exists()) 32 | return userFolder.createDirectory(); 33 | 34 | return userFolder.isDirectory(); 35 | } 36 | 37 | const juce::File& XmlPlaylist::getPlaylistFile() 38 | { 39 | jassert (playlistFile.existsAsFile()); 40 | return playlistFile; 41 | } 42 | 43 | void XmlPlaylist::addHeaderData() 44 | { 45 | juce::XmlElement xmlData ("TABLE_DATA"); 46 | 47 | auto* headerData = new juce::XmlElement ("HEADERS"); 48 | 49 | std::vector> columns = { { "Title", "400" }, 50 | { "Artist", "400" }, 51 | { "Path" , "400" }}; 52 | 53 | for (size_t i = 0; i < columns.size(); i++) 54 | { 55 | auto* columnData = new juce::XmlElement ("COLUMN"); 56 | columnData->setAttribute ("columnId", juce::String (i + 1)); 57 | columnData->setAttribute ("name", columns[i][0]); 58 | columnData->setAttribute ("width", columns[i][1]); 59 | 60 | headerData->addChildElement (columnData); 61 | } 62 | 63 | xmlData.addChildElement (headerData); 64 | xmlData.writeTo (playlistFile); 65 | } 66 | 67 | void XmlPlaylist::addTrackData (const juce::File& directoryToSearch) 68 | { 69 | if (! playlistFile.existsAsFile()) 70 | checkForUserFolderAndPlaylist(); 71 | 72 | jassert (checkForUserFolderAndPlaylist()); 73 | 74 | const juce::String library { "LIBRARY" }; 75 | bool hasLibraryElement { false }; 76 | 77 | xml = juce::XmlDocument::parse (playlistFile); 78 | 79 | for (auto* element : xml->getChildIterator()) 80 | { 81 | // We already have a library element, so we just need to add tracks to it 82 | if (element->hasTagName (library)) 83 | { 84 | addTracks (element, directoryToSearch); 85 | xml->writeTo (playlistFile); 86 | hasLibraryElement = true; 87 | } 88 | } 89 | 90 | // This means we've checked and don't have any of the element we're looking for... 91 | if (! hasLibraryElement) 92 | { 93 | auto* element = new juce::XmlElement (library); 94 | addTracks (element, directoryToSearch); 95 | xml->addChildElement (element); 96 | xml->writeTo (playlistFile); 97 | } 98 | } 99 | 100 | void XmlPlaylist::addTracks (juce::XmlElement* xmlElement, const juce::File& directoryToSearchForTracks) 101 | { 102 | // Look for all audio files in the directory we give it 103 | auto tracksInDirectory = directoryToSearchForTracks.findChildFiles (juce::File::TypesOfFileToFind::findFiles, true, "*.mp3;*.wav" ); 104 | 105 | // Create the xml from the metadata in each audio file 106 | for (auto& track : tracksInDirectory) 107 | { 108 | auto* fileData = new juce::XmlElement ("AUDIOFILE"); 109 | 110 | auto metadata = TagReader::getMetadataFromFile (track); 111 | 112 | fileData->setAttribute ("Title", metadata.title); 113 | fileData->setAttribute ("Artist", metadata.artist); 114 | fileData->setAttribute ("Path", metadata.path); 115 | 116 | xmlElement->addChildElement (fileData); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Source/Playlist/Data/XmlPlaylist.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../../Metadata/TagReader.h" 5 | 6 | 7 | /* Defines header and tracklist information for the app */ 8 | class XmlPlaylist 9 | { 10 | public: 11 | XmlPlaylist(); 12 | bool checkForUserFolderAndPlaylist(); 13 | 14 | const juce::File& getPlaylistFile(); 15 | 16 | void addHeaderData(); 17 | void addTrackData (const juce::File& directoryToSearch); 18 | 19 | private: 20 | bool checkForUserFolder() const; 21 | bool createNewXml(); 22 | void addTracks (juce::XmlElement* xmlElement, const juce::File& directoryToSearchForTracks); 23 | 24 | juce::File userFolder { juce::File::getSpecialLocation (juce::File::SpecialLocationType::userMusicDirectory).getChildFile ("TAP DJ App") }; 25 | juce::File playlistFile { userFolder.getChildFile ("Playlist.xml") }; 26 | std::unique_ptr xml; 27 | }; 28 | -------------------------------------------------------------------------------- /Source/Playlist/View/PlaylistView.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "PlaylistView.h" 3 | 4 | Playlist::Playlist (TrackAddState& t) : trackState (t) 5 | { 6 | trackState.addChangeListener (this); 7 | 8 | if (playlistFile.existsAsFile()) 9 | loadData (playlistFile); 10 | else 11 | jassertfalse; 12 | } 13 | 14 | Playlist::~Playlist() 15 | { 16 | trackState.removeAllChangeListeners(); 17 | listBox.setModel (nullptr); 18 | } 19 | 20 | void Playlist::loadData (const juce::File& xmlDir) 21 | { 22 | xmlData = juce::XmlDocument::parse (xmlDir); 23 | 24 | jassert (xmlDir.exists()); 25 | 26 | xmlData = juce::XmlDocument::parse (xmlDir); 27 | 28 | juce::String headerElement { "HEADERS" }; 29 | juce::String libElement { "LIBRARY" }; 30 | 31 | for (auto* element : xmlData->getChildIterator()) 32 | { 33 | if (element->hasTagName (headerElement)); 34 | { 35 | headerList = xmlData->getChildByName (headerElement); 36 | 37 | for (auto* column : headerList->getChildIterator()) 38 | { 39 | std::unordered_set::const_iterator it = headerAttributeList.find (column->getStringAttribute ("name")); 40 | 41 | // If we didn't find this column name in our list of columns, we need to add it to the table header 42 | if (it == headerAttributeList.end()) 43 | { 44 | listBox.getHeader().addColumn (column->getStringAttribute ("name"), 45 | column->getIntAttribute ("columnId"), 46 | column->getIntAttribute ("width")); 47 | 48 | // Add column name to the list of columns already added so we don't try to add a duplicate column 49 | headerAttributeList.insert (column->getStringAttribute ("name")); 50 | } 51 | } 52 | } 53 | 54 | if (element->hasTagName (libElement)) 55 | { 56 | trackList = xmlData->getChildByName (libElement); 57 | numRows = trackList->getNumChildElements(); 58 | listBox.updateContent(); 59 | } 60 | } 61 | 62 | jassert (headerList); 63 | } 64 | 65 | int Playlist::getNumRows() 66 | { 67 | return numRows; 68 | } 69 | 70 | int Playlist::getColumnAutoSizeWidth (int columnId) 71 | { 72 | if (columnId == 9) 73 | return 50; 74 | 75 | int widest = 32; 76 | 77 | for (auto i = getNumRows(); --i >= 0;) 78 | { 79 | if (auto* rowElement = trackList->getChildElement (i)) 80 | { 81 | auto text = rowElement->getStringAttribute (getAttributeNameForColumnId (columnId)); 82 | 83 | widest = juce::jmax (widest, font.getStringWidth (text)); 84 | } 85 | } 86 | 87 | return widest + 8; 88 | } 89 | 90 | void Playlist::paintRowBackground (juce::Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) 91 | { 92 | juce::ignoreUnused (width, height); 93 | 94 | if (rowIsSelected) 95 | g.fillAll (juce::Colours::dimgrey); 96 | else if (rowNumber % 2) 97 | g.fillAll (juce::Colours::black); 98 | 99 | g.setColour (juce::Colours::white.withAlpha (0.2f)); 100 | g.fillRect (width - 1, 0, 1, height); 101 | } 102 | 103 | void Playlist::paintCell (juce::Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) 104 | { 105 | g.setColour (rowIsSelected ? juce::Colours::darkblue : juce::Colours::white); 106 | g.setFont (font); 107 | 108 | if (auto* rowElement = trackList->getChildElement (rowNumber)) 109 | { 110 | auto text = rowElement->getStringAttribute (getAttributeNameForColumnId (columnId)); 111 | g.drawText (text, 2, 0, width - 4, height, juce::Justification::centredLeft, true); 112 | } 113 | 114 | g.setColour (juce::Colours::black); 115 | g.fillRect (width - 1, 0, 1, height); 116 | } 117 | 118 | juce::Component* Playlist::refreshComponentForCell (int rowNumber, int columnId, bool isRowSelected, juce::Component* existingComponentToUpdate) 119 | { 120 | if (isRowSelected == true) 121 | currentRow = rowNumber; 122 | 123 | if (columnId == 1) 124 | { 125 | auto* selectionBox = static_cast (existingComponentToUpdate); 126 | 127 | if (selectionBox == nullptr) 128 | selectionBox = new TrackListCell (*this); 129 | 130 | selectionBox->setColumnAndRow (columnId, rowNumber); 131 | return selectionBox; 132 | } 133 | 134 | if (columnId == 2) 135 | { 136 | auto* textLabel = static_cast (existingComponentToUpdate); 137 | 138 | if (textLabel == nullptr) 139 | textLabel = new TrackListCell (*this); 140 | 141 | textLabel->setColumnAndRow (columnId, rowNumber); 142 | return textLabel; 143 | } 144 | 145 | if (columnId == 3) 146 | { 147 | auto* textLabel = static_cast (existingComponentToUpdate); 148 | 149 | if (textLabel == nullptr) 150 | textLabel = new TrackListCell (*this); 151 | 152 | textLabel->setColumnAndRow (columnId, rowNumber); 153 | return textLabel; 154 | } 155 | 156 | jassert (existingComponentToUpdate == nullptr); 157 | return nullptr; 158 | } 159 | 160 | juce::String Playlist::getAttributeNameForColumnId (const int columnId) const 161 | { 162 | for (auto* column : headerList->getChildIterator()) 163 | { 164 | if (column->getIntAttribute ("columnId") == columnId) 165 | return column->getStringAttribute ("name"); 166 | } 167 | 168 | return {}; 169 | } 170 | 171 | void Playlist::setText (const int columnNumber, const int rowNumber, const juce::String& newText) 172 | { 173 | const auto& columnName = listBox.getHeader().getColumnName (columnNumber); 174 | trackList->getChildElement (rowNumber)->setAttribute (columnName, newText); 175 | } 176 | 177 | juce::String Playlist::getText (const int columnNumber, const int rowNumber) 178 | { 179 | return trackList->getChildElement (rowNumber)->getStringAttribute (getAttributeNameForColumnId (columnNumber)); 180 | } 181 | 182 | void Playlist::changeListenerCallback (juce::ChangeBroadcaster* broadcaster) 183 | { 184 | if (broadcaster == &trackState) 185 | { 186 | loadData (playlistFile); 187 | } 188 | } 189 | 190 | 191 | -------------------------------------------------------------------------------- /Source/Playlist/View/PlaylistView.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include "../../Metadata/Metadata.h" 6 | #include "../Data/XmlPlaylist.h" 7 | #include "../../Toolbar/Components/TrackAdd/State/TrackAddState.h" 8 | 9 | 10 | class Playlist : public juce::TableListBoxModel, 11 | public juce::ChangeListener 12 | { 13 | public: 14 | Playlist (TrackAddState& t); 15 | ~Playlist() override; 16 | 17 | void loadData (const juce::File& xmlDir); 18 | 19 | // From TableListBoxModel 20 | int getNumRows() override; 21 | int getColumnAutoSizeWidth (int columnId) override; 22 | void paintRowBackground (juce::Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) override; 23 | void paintCell (juce::Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) override; 24 | juce::Component* refreshComponentForCell (int rowNumber, int columnId, bool isRowSelected, juce::Component* existingComponentToUpdate) override; 25 | 26 | juce::String getAttributeNameForColumnId (const int columnId) const; 27 | 28 | // Getting & updating data 29 | void setText (const int columnNumber, const int rowNumber, const juce::String& newText); 30 | juce::String getText (const int columnNumber, const int rowNumber); 31 | 32 | juce::var getDragSourceDescription (const juce::SparseSet& selectedRows) override 33 | { 34 | juce::ignoreUnused (selectedRows); 35 | return getText (3, currentRow); 36 | } 37 | 38 | juce::Component& getComponent() { return listBox; } 39 | 40 | // From Change Listener 41 | void changeListenerCallback (juce::ChangeBroadcaster* b) override; 42 | 43 | private: 44 | juce::TableListBox listBox { "Playlist", this }; 45 | std::unique_ptr xmlData; 46 | juce::XmlElement* headerList { nullptr }; 47 | juce::XmlElement* trackList { nullptr }; 48 | XmlPlaylist xmlPlaylist; 49 | int numRows = 0; 50 | int currentRow = 0; 51 | juce::Font font { 14.0f }; 52 | TrackAddState& trackState; 53 | 54 | // TODO: This should be global, static, or otherwise 55 | juce::File userFolder { juce::File::getSpecialLocation (juce::File::SpecialLocationType::userMusicDirectory).getChildFile ("TAP DJ App") }; 56 | juce::File playlistFile { userFolder.getChildFile ("Playlist.xml") }; 57 | std::unordered_set headerAttributeList; 58 | 59 | 60 | 61 | class TrackListCell : public juce::Label 62 | { 63 | public: 64 | TrackListCell (Playlist& tl) : owner (tl) 65 | { 66 | setEditable (false, true, false); 67 | setInterceptsMouseClicks (false, false); 68 | } 69 | 70 | void mouseDown (const juce::MouseEvent& event) override 71 | { 72 | owner.listBox.selectRowsBasedOnModifierKeys (row, event.mods, false); 73 | juce::Label::mouseDown (event); 74 | //path = owner.getText (3, row); 75 | } 76 | 77 | void mouseDrag (const juce::MouseEvent& event) override 78 | { 79 | //owner.startDraggingComponent (row, columnId, event); 80 | juce::Label::mouseDrag (event); 81 | } 82 | 83 | void textWasEdited() override 84 | { 85 | owner.setText (columnId, row, getText()); 86 | } 87 | 88 | void setColumnAndRow (const int newColumn, const int newRow) 89 | { 90 | columnId = newColumn; 91 | row = newRow; 92 | setText (owner.getText (columnId, row) , juce::dontSendNotification); 93 | } 94 | 95 | juce::String path { "" }; 96 | private: 97 | int row = 0; 98 | int columnId = 0; 99 | Playlist& owner; 100 | }; 101 | }; 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Source/Resources/Assets/WorkSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAudioProgrammer/juceDjApp/61876c71b81c71b38147772fd5edaac4993f7c09/Source/Resources/Assets/WorkSans-Regular.ttf -------------------------------------------------------------------------------- /Source/Resources/Assets/WorkSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAudioProgrammer/juceDjApp/61876c71b81c71b38147772fd5edaac4993f7c09/Source/Resources/Assets/WorkSans-SemiBold.ttf -------------------------------------------------------------------------------- /Source/Resources/Resources.h: -------------------------------------------------------------------------------- 1 | /* (Auto-generated binary data file). */ 2 | 3 | #pragma once 4 | 5 | namespace Resources 6 | { 7 | extern const char* WorkSansRegular_ttf; 8 | const int WorkSansRegular_ttfSize = 191916; 9 | 10 | extern const char* WorkSansSemiBold_ttf; 11 | const int WorkSansSemiBold_ttfSize = 192372; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Source/Toolbar/Components/Settings/View/SettingsView.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | SettingsView.cpp 5 | Created: 30 Jan 2023 2:38:24pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #include "SettingsView.h" 12 | 13 | SettingsView::SettingsView (juce::AudioDeviceManager& deviceManager) : settingsWindow ("Settings", juce::Colours::black, true) 14 | { 15 | settingsButton.setButtonText ("Settings"); 16 | settingsButton.setToggleable (false); 17 | 18 | // Creates a new window with our audio device selector component 19 | settingsButton.onClick = [&]() 20 | { 21 | juce::DialogWindow::LaunchOptions windowOptions; 22 | windowOptions.dialogTitle = "Settings"; 23 | windowOptions.useNativeTitleBar = true; 24 | windowOptions.content.setOwned (new juce::AudioDeviceSelectorComponent (deviceManager, 2, 8, 2, 8, true, true, false, false)); 25 | windowOptions.content->setSize (800, 600); 26 | windowOptions.launchAsync(); 27 | }; 28 | 29 | addAndMakeVisible (settingsButton); 30 | } 31 | 32 | void SettingsView::paint (juce::Graphics& g) 33 | { 34 | g.fillAll (juce::Colours::black); 35 | } 36 | 37 | void SettingsView::resized() 38 | { 39 | settingsButton.setBounds (0, 0, 100, 50); 40 | } 41 | -------------------------------------------------------------------------------- /Source/Toolbar/Components/Settings/View/SettingsView.h: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================== 3 | 4 | SettingsUI.h 5 | Created: 30 Jan 2023 2:38:24pm 6 | Author: Joshua Hodge 7 | 8 | ============================================================================== 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | 19 | class SettingsView : public juce::Component 20 | { 21 | public: 22 | SettingsView (juce::AudioDeviceManager& deviceManager); 23 | void paint (juce::Graphics& g) override; 24 | void resized() override; 25 | 26 | private: 27 | // UI 28 | juce::TextButton settingsButton { "Settings "}; 29 | juce::DialogWindow settingsWindow; 30 | }; 31 | -------------------------------------------------------------------------------- /Source/Toolbar/Components/TrackAdd/State/TrackAddState.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | 6 | struct TrackAddState : public juce::ChangeBroadcaster 7 | { 8 | bool hasAddedTracks = false; 9 | 10 | void justAddedTracks() 11 | { 12 | hasAddedTracks = true; 13 | sendChangeMessage(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /Source/Toolbar/Components/TrackAdd/View/TrackAddView.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "TrackAddView.h" 3 | 4 | 5 | TrackAddView::TrackAddView (XmlPlaylist& xml) : xmlPlaylist (xml) 6 | { 7 | audioFormatManager.registerBasicFormats(); 8 | 9 | trackAddButton.onClick = [this]() 10 | { 11 | songSelector = std::make_unique("Select a Directory to Add Tracks", juce::File::getSpecialLocation (juce::File::SpecialLocationType::userDesktopDirectory), audioFormatManager.getWildcardForAllFormats()); 12 | 13 | auto songSelectorFlags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectDirectories; 14 | 15 | songSelector->launchAsync (songSelectorFlags, [&] (const juce::FileChooser& chooser) 16 | { 17 | xmlPlaylist.addTrackData (chooser.getResult()); 18 | trackAddState.justAddedTracks(); 19 | }); 20 | }; 21 | 22 | addAndMakeVisible (trackAddButton); 23 | } 24 | 25 | void TrackAddView::resized() 26 | { 27 | trackAddButton.setBounds (getLocalBounds()); 28 | } 29 | -------------------------------------------------------------------------------- /Source/Toolbar/Components/TrackAdd/View/TrackAddView.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include "../../../../Playlist/Data/XmlPlaylist.h" 7 | #include "../State/TrackAddState.h" 8 | 9 | 10 | class TrackAddView : public juce::Component 11 | { 12 | public: 13 | TrackAddView (XmlPlaylist& xml); 14 | void resized() override; 15 | 16 | // TODO: State should not reside in a view 17 | TrackAddState& getState() { return trackAddState; } 18 | 19 | private: 20 | juce::TextButton trackAddButton { "Add Tracks "}; 21 | std::unique_ptr songSelector; 22 | XmlPlaylist& xmlPlaylist; 23 | 24 | /* Necessary to register and stream audio formats */ 25 | juce::AudioFormatManager audioFormatManager; 26 | 27 | TrackAddState trackAddState; 28 | }; 29 | -------------------------------------------------------------------------------- /Source/Toolbar/View/Toolbar.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "Toolbar.h" 4 | 5 | Toolbar::Toolbar (juce::AudioDeviceManager& m, XmlPlaylist& xml) : settingsView (m), trackAddView (xml) 6 | { 7 | addAndMakeVisible (settingsView); 8 | addAndMakeVisible (trackAddView); 9 | } 10 | 11 | void Toolbar::paint (juce::Graphics& g) 12 | { 13 | g.setColour (juce::Colours::white); 14 | g.drawRect (getLocalBounds()); 15 | g.fillAll (juce::Colours::black); 16 | } 17 | 18 | void Toolbar::resized() 19 | { 20 | auto pad = 10; 21 | settingsView.setBounds (10, 10, 100, 50); 22 | trackAddView.setBounds (settingsView.getRight() + pad, 10, 100, 50); 23 | } 24 | -------------------------------------------------------------------------------- /Source/Toolbar/View/Toolbar.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include 6 | #include "../Components/Settings/View/SettingsView.h" 7 | #include "../Components/TrackAdd/View/TrackAddView.h" 8 | #include "../../Playlist/Data/XmlPlaylist.h" 9 | 10 | class Toolbar : public juce::Component 11 | { 12 | public: 13 | Toolbar (juce::AudioDeviceManager& m, XmlPlaylist& xml); 14 | void paint (juce::Graphics& g) override; 15 | void resized() override; 16 | 17 | // TODO: This should probably be cleaned up 18 | TrackAddState& getTrackAddState() { return trackAddView.getState(); } 19 | 20 | private: 21 | SettingsView settingsView; 22 | TrackAddView trackAddView; 23 | }; 24 | --------------------------------------------------------------------------------