kopia lustrzana https://github.com/jameshball/osci-render
Add audio support to visualiser
rodzic
977e9d72a8
commit
1a1229fcba
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Ładowanie…
Reference in New Issue