How to not append to file (create new/truncate) with audio writer?


#1

I tried to experiment with recording .wavs, so I modified https://www.juce.com/doc/tutorial_simple_synth_noise - the code is this:

MainComponent.cpp

#ifndef MAINCOMPONENT_H_INCLUDED 
#define MAINCOMPONENT_H_INCLUDED 
 
#include "../JuceLibraryCode/JuceHeader.h" 
 
 
class MainContentComponent   : public AudioAppComponent, 
                               public Slider::Listener 
{ 
public: 
    MainContentComponent(): 
      outwavFile(File::getCurrentWorkingDirectory().getChildFile("out.wav").getFullPathName()), 
      outStream(outwavFile.createOutputStream()) 
    { 
        targetLevel = 0.125; 
 
        levelSlider.setRange (0.0, 0.25); 
        levelSlider.setValue (targetLevel, dontSendNotification); 
        levelSlider.setTextBoxStyle (Slider::TextBoxRight, false, 100, 20); 
        levelSlider.addListener (this); 
 
        levelLabel.setText ("Noise Level", dontSendNotification); 
 
        addAndMakeVisible (&levelSlider); 
        addAndMakeVisible (&levelLabel); 
 
        setSize (800, 100); 
 
        setAudioChannels (0, 2); 
    } 
 
    ~MainContentComponent() 
    { 
        shutdownAudio(); 
    } 
 
    void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override 
    { 
        resetParameters(); 
        aFwriter = ScopedPointer<AudioFormatWriter>(wav.createWriterFor (outStream, sampleRate, 
                                                                          1, 32, 
                                                                          StringPairArray(), 0)); 
        if (aFwriter != nullptr) 
        { 
          outStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it); 
        } 
    } 
 
    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override 
    { 
        int numSamplesRemaining = bufferToFill.numSamples; 
        int offset = 0; 
 
        if (samplesToTarget > 0) 
        { 
            const float levelIncrement = (targetLevel - currentLevel) / samplesToTarget; 
            const int numSamplesThisTime = jmin (numSamplesRemaining, samplesToTarget); 
 
            for (int sample = 0; sample < numSamplesThisTime; ++sample) 
            { 
                for (int channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel) 
                    bufferToFill.buffer->setSample (channel, sample, random.nextFloat() * currentLevel); 
 
                currentLevel += levelIncrement; 
                --samplesToTarget; 
            } 
 
            offset = numSamplesThisTime; 
            numSamplesRemaining -= numSamplesThisTime; 
 
            if (samplesToTarget == 0) 
                currentLevel = targetLevel; 
        } 
 
        if (numSamplesRemaining > 0) 
        { 
            for (int channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel) 
            { 
                float* buffer = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample + offset); 
 
                for (int sample = 0; sample < numSamplesRemaining; ++sample) 
                    *buffer++ = random.nextFloat() * currentLevel; 
            } 
        } 
 
        aFwriter->writeFromAudioSampleBuffer(*bufferToFill.buffer, 0, bufferToFill.numSamples); //ok
    } 
 
    void releaseResources() override 
    { 
    } 
 
    void resized() override 
    { 
        levelLabel.setBounds (10, 10, 90, 20); 
        levelSlider.setBounds (100, 10, getWidth() - 110, 20); 
    } 
 
    void sliderValueChanged (Slider* slider) override 
    { 
        if (&levelSlider == slider) 
        { 
            targetLevel = levelSlider.getValue(); 
            samplesToTarget = rampLengthSamples; 
        } 
    } 
 
    void resetParameters() 
    { 
        currentLevel = targetLevel; 
        samplesToTarget = 0; 
    } 
 
 
private: 
    Random random; 
    Slider levelSlider; 
    Label levelLabel; 
    float currentLevel; 
    float targetLevel; 
    int samplesToTarget; 
    static const int rampLengthSamples; 
    //# added: 
    File outwavFile; 
    WavAudioFormat wav; 
    ScopedPointer<OutputStream> outStream; 
    ScopedPointer<AudioFormatWriter> aFwriter; 
 
 
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) 
}; 
 
const int MainContentComponent::rampLengthSamples = 128; 
 
Component* createMainContentComponent()     { return new MainContentComponent(); } 
 
#endif  // MAINCOMPONENT_H_INCLUDED 

So, first of all, I took the code for recording from the AudioRecordingDemo.cpp, so I’m aware I should use a background thread etc, but just for a quick tester, this generally works.

My problem is with the file operations: with this code, if a file out.wav does not exist, it gets created, and the audio data stream gets written into it - so far so good. If a file out.wav exists, however, then the audio data stream gets appended to the already existing file; and since the already existing content already has a RIFF header that specifies the length of the already existing original, opening the out.wav after the second running of the program/recording will still give the contents of the original recording only (kinda hard to see here as its noise, but I’ve tried this with different audio contents, and that is how it seems to behave).

Now, I’d like to initialize outwavFile and outStream in the constructor, so I let C++ worry about their destruction/lifecycle:

        MainContentComponent(): 
          outwavFile(File::getCurrentWorkingDirectory().getChildFile("out.wav").getFullPathName()), 
          outStream(outwavFile.createOutputStream()) 

If inside the constructor, I try to intervene and delete the file (with the intent of it being recreated anew when the stream starts writing to it):

        if(outwavFile.existsAsFile()) 
          outwavFile.deleteFile(); 

… the file gets deleted permanently. I’ve also tried outwavFile.create(); and outwavFile.replaceWithData("", 0); and none of them quite work.

so, what is the proper way to initialize a file for audio writing in this case, so that each time the program is ran (recording happens throughout the duration of the program), out.wav gets truncated to zero bytes if it already exists, and then is filled with new data from the latest program run?


#2

ok, I got to this, which should be placed inside the constructor, which seems to work (overwrites the file):

    outwavFile.replaceWithText(""); // file gets truncated to 0 and never gets written to 
    outStream = outwavFile.createOutputStream(); // ... unless this command is repeated 

… but then initialization of outStream happens twice: once in the constructor initializer, once in here. So, the question still stands - what would be the right way to do this? Is it possible to somehow achieve the same, and still just keep the initializers only?:

    MainContentComponent(): 
      outwavFile(File::getCurrentWorkingDirectory().getChildFile("out.wav").getFullPathName()), 
      outStream(outwavFile.createOutputStream())

#3
file.deleteFile();

FileOutputStream out (file);

if (out.openedOK())
{
    ..etc..

#4

Thanks @jules - much appreciated!


#5

Or alternatively, your File::createOutputStream() creates a FileOutputStream, where you can do:

ScopedPointer<FileOutputStream> outStream = outwavFile.createOutputStream();
if (outStream->openedOk()) {
    outStream->setPosition (0);
    outStream->truncate();
    // ...
}

Pro: the file is not deleted, thus if you have write permissions on the file but not on the directory, you are still able to write there / ownership is not changed etc.

Con: more to write