I want to share my first SF script! It fades out the selection at a constant dB rate. On long fades, e.g. a minute or more, you may feel that the various SF fade curves stall or speed up at points. In the past I've painstakingly crafted a constant-ish fade by piecing together line and curve segments in the graphic fade tool, but it's a lot of effort. This one feels pretty good to me, but I wouldn't be surprised if, on dynamic material, certain regions still need to be adjusted for subjective loudness. You can also make it fade in and change the dB range of the fade by editing the code in the function currentAttenuation().
If any of that was good news to you, then that was the good news. The bad news is that it doesn't run on SF Pro 13 because for some reason the readAudio and writeAudio methods don't work. I've tested this on SF Pro 10 builds 491 and 507. I also have some questions about the way certain methods work, which you can see in my capitalized comments. I've read in this forum that scripting expertise is "scarcer than hen's teeth" but it doesn't hurt to ask I guess. I'm also open to suggestions on how to improve things. Thanks for reading and I hope you find this useful.
/*
Constant dB Fade.cs
James Lo
Feb 2020
This file implements a Sound Forge 10.0 (build 507 or 491) constant dB-rate fade script. It may
work on other versions, but it has not been tested on them. If the normal SF fade tools make fades
that seem to stall or speed up at points, give this script a try.
Modify the code in the function "currentAttenuation" to set how much it fades and whether it fades up or down.
NB: since it is constant dB-rate, the audio won't reach (or start at) true silence unless
the currentAttenuation reduces the signal to a level that's below the precision of the output bit depth.
In practice, you'll probably want to settle for about 48dB to 60dB of fade, and then use normal SF fade tools
to get to (or start at) silence.
*/
using System;
using System.IO;
using System.Windows.Forms;
using SoundForge;
public class EntryPoint {
public string Begin(IScriptableApp app) {
ISfDataWnd wnd = app.ActiveWindow;
if (null == wnd)
return "No file open";
//get selection info
ISfFileHost file = app.CurrentFile;
SfAudioSelection asel = file.Window.EditRegion;
DPF(" length = " + asel.Length);
DPF(" start = " + asel.Start);
DPF(" channel mask = " + asel.ChanMask);
DPF(" nrChannels = " + file.Channels);
//read audio into buffer
float[,] selectionData = new float[asel.Length, file.Channels];
//WE HAVE TO READ ALL CHANNELS, NOT JUST THE SELECTED ONES, OR SF THROWS AN EXCEPTION
int nrSamples = file.ReadAudio(selectionData, asel.Start);
DPF(" nrSamples read = " + nrSamples);
double maxSampleIndex = nrSamples - 1;
//process the audio in the buffer
uint initialChannelMask = asel.ChanMask;
if (initialChannelMask == 0) initialChannelMask = 0xFF;
for (int sampleIndex = 0; sampleIndex < asel.Length; sampleIndex++) {
float attenuation = currentAttenuation(sampleIndex, maxSampleIndex);
//for each channel
uint channelMask = initialChannelMask;
for (int channelIndex = 0; channelIndex < file.Channels; channelIndex++) {
//if the channel is selected
if ((channelMask & 0x01) == 1) {
selectionData[sampleIndex, channelIndex] *= attenuation;
}
channelMask >>= 1;
}
}
//write it back to the file
ISfWriteAudioStream writeAudioStream = file.OpenWriteAudioStream(SfSampleType.Wav16, 0, 0);
writeAudioStream.Append(selectionData);
//FILE.WRITEAUDIO ASSUMES THAT THE INPUT STREAM ONLY CONTAINS THE SELECTED CHANNELS
//SO WE WRITE ALL CHANNELS OUT SINCE WE WERE FORCED TO READ THEM EARLIER ANYWAY
SfAudioSelection outpSel = new SfAudioSelection(asel);
outpSel.ChanMask = 0;
file.WriteAudio(outpSel, writeAudioStream);
writeAudioStream.Close(CloseOptions.DiscardChanges);
//WHY ARE "UNRECOGNIZED TEMP FILES" LEFT AFTER THIS SCRIPT FINISHES?
return null;
}
private float currentAttenuation(double sampleIndex, double maxSampleIndex) {
const double nrPowersOf2 = 10.0; //max volume change expressed in # of 6.02dB, may be fractional
//FADE OUT
return (float) Math.Pow(2.0, (sampleIndex/maxSampleIndex) * -nrPowersOf2);
//FADE IN
//return (float) Math.Pow(2.0, ((sampleIndex/maxSampleIndex) * nrPowersOf2) - nrPowersOf2);
}
public void FromSoundForge(IScriptableApp app) {
ForgeApp = app; //execution begins here
app.SetStatusText(String.Format("Script '{0}' is running.", Script.Name));
string msg = Begin(app);
app.SetStatusText(msg != null ? msg : String.Format("Script '{0}' is done.", Script.Name));
}
public static IScriptableApp ForgeApp = null;
public static void DPF(string sz) { ForgeApp.OutputText(sz); }
public static void DPF(string fmt, object o) { ForgeApp.OutputText(String.Format(fmt,o)); }
public static void DPF(string fmt, object o, object o2) { ForgeApp.OutputText(String.Format(fmt,o,o2)); }
public static void DPF(string fmt, object o, object o2, object o3) { ForgeApp.OutputText(String.Format(fmt,o,o2,o3)); }
public static string GETARG(string k, string d) { string val = Script.Args.ValueOf(k); if (val == null || val.Length == 0) val = d; return val; }
public static int GETARG(string k, int d) { string s = Script.Args.ValueOf(k); if (s == null || s.Length == 0) return d; else return Script.Args.AsInt(k); }
public static bool GETARG(string k, bool d) { string s = Script.Args.ValueOf(k); if (s == null || s.Length == 0) return d; else return Script.Args.AsBool(k); }
} //EntryPoint