Add audio support to visualiser

pull/261/head
James H Ball 2024-12-14 14:21:26 +00:00 zatwierdzone przez James H Ball
rodzic 977e9d72a8
commit 1a1229fcba
6 zmienionych plików z 128 dodań i 202 usunięć

Wyświetl plik

@ -0,0 +1,105 @@
#pragma once
#include <JuceHeader.h>
//==============================================================================
class AudioRecorder final {
public:
AudioRecorder() {
backgroundThread.startThread();
}
~AudioRecorder() {
stop();
}
void setSampleRate(double sampleRate) {
stop();
this->sampleRate = sampleRate;
}
//==============================================================================
void startRecording(const juce::File& file) {
stop();
if (sampleRate > 0) {
// Create an OutputStream to write to our destination file...
file.deleteFile();
if (auto fileStream = std::unique_ptr<juce::FileOutputStream>(file.createOutputStream())) {
// Now create a WAV writer object that writes to our output stream...
juce::WavAudioFormat wavFormat;
if (auto writer = wavFormat.createWriterFor(fileStream.get(), sampleRate, 2, 32, {}, 0)) {
fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
// Now we'll create one of these helper objects which will act as a FIFO buffer, and will
// write the data to disk on our background thread.
threadedWriter.reset(new juce::AudioFormatWriter::ThreadedWriter(writer, backgroundThread, 32768));
nextSampleNum = 0;
// And now, swap over our active writer pointer so that the audio callback will start using it..
const juce::ScopedLock sl(writerLock);
activeWriter = threadedWriter.get();
}
}
}
}
void stop() {
// First, clear this pointer to stop the audio callback from using our writer object..
{
const juce::ScopedLock sl(writerLock);
activeWriter = nullptr;
}
// Now we can delete the writer object. It's done in this order because the deletion could
// take a little time while remaining data gets flushed to disk, so it's best to avoid blocking
// the audio callback while this happens.
threadedWriter.reset();
}
bool isRecording() const {
return activeWriter.load() != nullptr;
}
void audioThreadCallback(const juce::AudioBuffer<float>& buffer) {
if (nextSampleNum >= recordingLength * sampleRate) {
stop();
stopCallback();
return;
}
const juce::ScopedLock sl(writerLock);
int numSamples = buffer.getNumSamples();
if (activeWriter.load() != nullptr) {
activeWriter.load()->write(buffer.getArrayOfReadPointers(), numSamples);
nextSampleNum += numSamples;
}
}
void audioThreadCallback(const std::vector<float>& left, const std::vector<float>& right) {
juce::AudioBuffer<float> buffer(2, left.size());
buffer.copyFrom(0, 0, left.data(), left.size());
buffer.copyFrom(1, 0, right.data(), right.size());
audioThreadCallback(buffer);
}
void setRecordLength(double recordLength) {
recordingLength = recordLength;
}
std::function<void()> stopCallback;
private:
juce::TimeSliceThread backgroundThread { "Audio Recorder Thread" }; // the thread that will write our audio data to disk
std::unique_ptr<juce::AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data
juce::int64 nextSampleNum = 0;
double recordingLength = 99999999999.0;
double sampleRate = 192000;
juce::CriticalSection writerLock;
std::atomic<juce::AudioFormatWriter::ThreadedWriter*> activeWriter { nullptr };
};

Wyświetl plik

@ -1,201 +1,7 @@
/*
==============================================================================
This file is part of the JUCE examples.
Copyright (c) 2022 - Raw Material Software Limited
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
/*******************************************************************************
The block below describes the properties of this PIP. A PIP is a short snippet
of code that can be read by the Projucer and used to generate a JUCE project.
BEGIN_JUCE_PIP_METADATA
name: AudioRecordingComponent
version: 1.0.0
vendor: JUCE
website: http://juce.com
description: Records audio to a file.
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
juce_audio_processors, juce_audio_utils, juce_core,
juce_data_structures, juce_events, juce_graphics,
juce_gui_basics, juce_gui_extra
exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
type: Component
mainClass: AudioRecordingComponent
useLocalCopy: 1
END_JUCE_PIP_METADATA
*******************************************************************************/
#pragma once
#include "DoubleTextBox.h"
#include "../audio/AudioRecorder.h"
//==============================================================================
class AudioRecorder final {
public:
AudioRecorder(OscirenderAudioProcessor& p, juce::AudioThumbnail& thumbnailToUpdate)
: audioProcessor(p), thumbnail(thumbnailToUpdate) {
backgroundThread.startThread();
audioProcessor.setAudioThreadCallback([this](const juce::AudioBuffer<float>& buffer) { audioThreadCallback(buffer); });
}
~AudioRecorder() {
audioProcessor.setAudioThreadCallback(nullptr);
stop();
}
//==============================================================================
void startRecording(const juce::File& file) {
stop();
if (audioProcessor.currentSampleRate > 0) {
// Create an OutputStream to write to our destination file...
file.deleteFile();
if (auto fileStream = std::unique_ptr<juce::FileOutputStream>(file.createOutputStream())) {
// Now create a WAV writer object that writes to our output stream...
juce::WavAudioFormat wavFormat;
if (auto writer = wavFormat.createWriterFor(fileStream.get(), audioProcessor.currentSampleRate, 2, 32, {}, 0)) {
fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
// Now we'll create one of these helper objects which will act as a FIFO buffer, and will
// write the data to disk on our background thread.
threadedWriter.reset(new juce::AudioFormatWriter::ThreadedWriter(writer, backgroundThread, 32768));
// Reset our recording thumbnail
thumbnail.reset(writer->getNumChannels(), writer->getSampleRate());
nextSampleNum = 0;
// And now, swap over our active writer pointer so that the audio callback will start using it..
const juce::ScopedLock sl(writerLock);
activeWriter = threadedWriter.get();
}
}
}
}
void stop() {
// First, clear this pointer to stop the audio callback from using our writer object..
{
const juce::ScopedLock sl(writerLock);
activeWriter = nullptr;
}
// Now we can delete the writer object. It's done in this order because the deletion could
// take a little time while remaining data gets flushed to disk, so it's best to avoid blocking
// the audio callback while this happens.
threadedWriter.reset();
}
bool isRecording() const {
return activeWriter.load() != nullptr;
}
void audioThreadCallback(const juce::AudioBuffer<float>& buffer) {
if (nextSampleNum >= recordingLength * audioProcessor.currentSampleRate) {
stop();
stopCallback();
return;
}
const juce::ScopedLock sl(writerLock);
int numSamples = buffer.getNumSamples();
if (activeWriter.load() != nullptr) {
activeWriter.load()->write(buffer.getArrayOfReadPointers(), numSamples);
thumbnail.addBlock(nextSampleNum, buffer, 0, numSamples);
nextSampleNum += numSamples;
}
}
void setRecordLength(double recordLength) {
recordingLength = recordLength;
}
std::function<void()> stopCallback;
private:
OscirenderAudioProcessor& audioProcessor;
juce::AudioThumbnail& thumbnail;
juce::TimeSliceThread backgroundThread { "Audio Recorder Thread" }; // the thread that will write our audio data to disk
std::unique_ptr<juce::AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data
juce::int64 nextSampleNum = 0;
double recordingLength = 99999999999.0;
juce::CriticalSection writerLock;
std::atomic<juce::AudioFormatWriter::ThreadedWriter*> activeWriter { nullptr };
};
//==============================================================================
class RecordingThumbnail final : public juce::Component,
private juce::ChangeListener {
public:
RecordingThumbnail() {
formatManager.registerBasicFormats();
thumbnail.addChangeListener(this);
}
~RecordingThumbnail() override {
thumbnail.removeChangeListener(this);
}
juce::AudioThumbnail& getAudioThumbnail() { return thumbnail; }
void setDisplayFullThumbnail(bool displayFull) {
displayFullThumb = displayFull;
repaint();
}
void paint(juce::Graphics& g) override {
g.setColour(juce::Colours::white);
if (thumbnail.getTotalLength() > 0.0) {
auto endTime = displayFullThumb ? thumbnail.getTotalLength()
: juce::jmax(30.0, thumbnail.getTotalLength());
auto thumbArea = getLocalBounds();
thumbnail.drawChannels(g, thumbArea.reduced(2), 0.0, endTime, 1.0f);
}
}
private:
juce::AudioFormatManager formatManager;
juce::AudioThumbnailCache thumbnailCache { 10 };
juce::AudioThumbnail thumbnail { 128, formatManager, thumbnailCache };
bool displayFullThumb = false;
void changeListenerCallback(juce::ChangeBroadcaster* source) override {
if (source == &thumbnail)
repaint();
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RecordingThumbnail)
};
//==============================================================================
class AudioRecordingComponent final : public juce::Component {
public:
AudioRecordingComponent(OscirenderAudioProcessor& p) : audioProcessor(p) {
@ -232,8 +38,11 @@ public:
});
};
addAndMakeVisible(recordingThumbnail);
recordingThumbnail.setDisplayFullThumbnail(true);
audioProcessor.setAudioThreadCallback([this](const juce::AudioBuffer<float>& buffer) { recorder.audioThreadCallback(buffer); });
}
~AudioRecordingComponent() {
audioProcessor.setAudioThreadCallback(nullptr);
}
void resized() override {
@ -247,14 +56,12 @@ public:
recordLength.setBounds(area.removeFromLeft(80).withSizeKeepingCentre(60, 25));
}
area.removeFromLeft(5);
recordingThumbnail.setBounds(area);
}
private:
OscirenderAudioProcessor& audioProcessor;
RecordingThumbnail recordingThumbnail;
AudioRecorder recorder{ audioProcessor, recordingThumbnail.getAudioThumbnail() };
AudioRecorder recorder;
SvgButton recordButton{ "record", BinaryData::record_svg, juce::Colours::red, juce::Colours::red.withAlpha(0.01f) };
juce::File lastRecording;

Wyświetl plik

@ -130,6 +130,8 @@ int VisualiserComponent::prepareTask(double sampleRate, int bufferSize) {
xResampler.prepare(sampleRate, RESAMPLE_RATIO);
yResampler.prepare(sampleRate, RESAMPLE_RATIO);
zResampler.prepare(sampleRate, RESAMPLE_RATIO);
audioRecorder.setSampleRate(sampleRate);
int desiredBufferSize = sampleRate / FRAME_RATE;
@ -198,9 +200,14 @@ void VisualiserComponent::setRecording(bool recording) {
ffmpegProcess.start(cmd);
framePixels.resize(renderTexture.width * renderTexture.height * 4);
tempAudioFile = std::make_unique<juce::TemporaryFile>(".wav");
audioRecorder.startRecording(tempAudioFile->getFile());
setPaused(false);
stopwatch.start();
} else if (ffmpegProcess.isRunning()) {
audioRecorder.stop();
ffmpegProcess.close();
chooser = std::make_unique<juce::FileChooser>("Save recording", lastOpenedDirectory, "*.mp4");
auto flags = juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::warnAboutOverwriting;
@ -208,8 +215,8 @@ void VisualiserComponent::setRecording(bool recording) {
chooser->launchAsync(flags, [this](const juce::FileChooser& chooser) {
auto file = chooser.getResult();
if (file != juce::File()) {
// move the temporary file to the final location
tempVideoFile->getFile().moveFileTo(file);
ffmpegProcess.start("\"" + ffmpegFile.getFullPathName() + "\" -i \"" + tempVideoFile->getFile().getFullPathName() + "\" -i \"" + tempAudioFile->getFile().getFullPathName() + "\" -c:v copy -c:a aac -y \"" + file.getFullPathName() + "\"");
ffmpegProcess.close();
lastOpenedDirectory = file.getParentDirectory();
}
});
@ -374,6 +381,8 @@ void VisualiserComponent::renderOpenGL() {
glBindTexture(GL_TEXTURE_2D, renderTexture.id);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, framePixels.data());
ffmpegProcess.write(framePixels.data(), 4 * renderTexture.width * renderTexture.height);
audioRecorder.audioThreadCallback(xSamples, ySamples);
}
renderingSemaphore.release();

Wyświetl plik

@ -10,6 +10,7 @@
#include "../img/qoixx.hpp"
#include "../components/DownloaderComponent.h"
#include "../concurrency/WriteProcess.h"
#include "../audio/AudioRecorder.h"
#define FILE_RENDER_DUMMY 0
#define FILE_RENDER_PNG 1
@ -87,6 +88,8 @@ private:
juce::File& lastOpenedDirectory;
std::unique_ptr<juce::FileChooser> chooser;
std::unique_ptr<juce::TemporaryFile> tempVideoFile;
std::unique_ptr<juce::TemporaryFile> tempAudioFile;
AudioRecorder audioRecorder;
juce::String ffmpegURL = juce::String("https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/") +
#if JUCE_WINDOWS
#if JUCE_64BIT

Wyświetl plik

@ -63,6 +63,7 @@
</GROUP>
<GROUP id="{75439074-E50C-362F-1EDF-8B4BE9011259}" name="Source">
<GROUP id="{85A33213-D880-BD92-70D8-1901DA6D23F0}" name="audio">
<FILE id="HE3dFE" name="AudioRecorder.h" compile="0" resource="0" file="Source/audio/AudioRecorder.h"/>
<FILE id="NWuowi" name="BitCrushEffect.cpp" compile="1" resource="0"
file="Source/audio/BitCrushEffect.cpp"/>
<FILE id="Bc8UeW" name="BitCrushEffect.h" compile="0" resource="0"

Wyświetl plik

@ -51,6 +51,7 @@
<FILE id="Zum0qz" name="qoixx.hpp" compile="0" resource="0" file="Source/img/qoixx.hpp"/>
</GROUP>
<GROUP id="{85A33213-D880-BD92-70D8-1901DA6D23F0}" name="audio">
<FILE id="UVcqLN" name="AudioRecorder.h" compile="0" resource="0" file="Source/audio/AudioRecorder.h"/>
<FILE id="S5ChqG" name="BooleanParameter.h" compile="0" resource="0"
file="Source/audio/BooleanParameter.h"/>
<FILE id="mP5lpY" name="Effect.cpp" compile="1" resource="0" file="Source/audio/Effect.cpp"/>