Sound Forge Scripting - Efficient Cassette Tape Archiving

Dan-Hicks wrote on 6/20/2024, 1:40 PM

Hi, I have zero experience with scripting. I'm wondering if this is possible with Sound Forge scripting, and if so, is anyone willing to create such a script for me?

Here's what I need. I'm working on a large cassette tape archiving project, and for the sake of efficiency, I'm using a 4-track cassette deck in double speed mode. By recording all four tracks at double speed, I can record each tape in 1/4 the amount of time it'd normally take. I know this isn't best for quality purposes, but for this project, it's sufficient.

Once each tape is recorded into a quad wav file, I need to:

1. Halve the sampling rate

2. Reverse channels 3&4 (ie side B of the tape)

3. Swap channels 3&4 (ie left-right stereo swap)

4. Convert the quad wav to a stereo wav, with channels 1&2 first (side A), followed by channels 3&4 (side B)

 

It'd speed up my workflow if I could batch process my quad wav files with a script rather than do all these steps manually for each file. Is it possible?

Thanks!

Comments

SP. wrote on 6/20/2024, 5:21 PM

@Dan-Hicks Yes, for example, these are the methods to do this. Open one of your quad files in Sound Forge, then simply copy and paste the code into the script editor, compile and run it. If this works as you intend it, then we can modify it to batch load files from a directory.

using System;
using System.Windows.Forms;
using SoundForge;

public class EntryPoint {
public void Begin(IScriptableApp app) {

   ISfFileHost quadaudio = app.CurrentFile;

   //set sample rate to 48000Hz without resampling
   quadaudio.DoResample(48000, -1, EffectOptions.EffectOnly);

   //reverse channels 3 and 4
   quadaudio.DoEffect("Process.Reverse", "", new SfAudioSelection(0, quadaudio.Length, 12), EffectOptions.EffectOnly);

   //call a channel converter preset that swaps channels 3 and 4
   quadaudio.DoEffect("Channel Converter", "Channel 3-4 Swap", new SfAudioSelection(0, quadaudio.Length), EffectOptions.EffectOnly);

   //create new, empty stereo file, 48000Hz, 16 Bit
   ISfFileHost stereoaudio = app.NewFile(new SfWaveFormat(48000, 16, 2, false), false);

   //copy and paste channels 1 and 2 at the beginning of the stereo file
   stereoaudio.OverwriteAudio(0, 0, quadaudio, new SfAudioSelection(0, quadaudio.Length, 3));

   //copy and paste channels 3 and 4 at the end of the stereo file
   stereoaudio.OverwriteAudio(stereoaudio.Length, 0, quadaudio, new SfAudioSelection(0, quadaudio.Length, 12));  

}

public void FromSoundForge(IScriptableApp app) {
   ForgeApp = app; //execution begins here
   app.SetStatusText(String.Format("Script '{0}' is running.", Script.Name));
   Begin(app);
   app.SetStatusText(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, params object [] args) { ForgeApp.OutputText(String.Format(fmt, args)); }
} //EntryPoint

 

SP. wrote on 6/20/2024, 5:24 PM

@Dan-Hicks Be aware, you need to create a Channel Converter preset first, that swaps channels 3 and 4. This is mine:

Dan-Hicks wrote on 6/20/2024, 5:41 PM

@SP. Thanks so much! Actually, after writing my post, I realized that, of course, I could simply swap the L/R leads on channels 3&4, thereby removing the need to swap the channels in sound forge. Can you modify your script to remove that step? Also, I've been recording these at 24bit, 96khz, so I'd need the final stereo file to end up at 24bit, 48khz. Or, is it possible to simply divide the sampling rate by two? That way I can record at any sampling rate and the script would still work as intended.

SP. wrote on 6/20/2024, 5:44 PM

@Dan-Hicks Simply delete the lines:

//call a channel converter preset that swaps channels 3 and 4
quadaudio.DoEffect("Channel Converter", "Channel 3-4 Swap", new SfAudioSelection(0, quadaudio.Length), EffectOptions.EffectOnly);

 

Dan-Hicks wrote on 6/20/2024, 5:52 PM

@SP. Thanks again! Are you able to address the other points in my previous comment?

Dan-Hicks wrote on 6/20/2024, 5:55 PM

@SP. What I've actually been doing manually is copying the processed "side b" audio, copying it, pasting it at the end of the quad file, and then converting the quad file to stereo. This avoids the need to make a new blank stereo file to paste everything into.

SP. wrote on 6/20/2024, 6:00 PM

@Dan-Hicks Yes, here:

using System;
using System.Windows.Forms;
using SoundForge;

public class EntryPoint {
public void Begin(IScriptableApp app) {

   ISfFileHost quadaudio = app.CurrentFile;

   //set sample rate to half without resampling
   quadaudio.DoResample(quadaudio.SampleRate/2, -1, EffectOptions.EffectOnly);

   //reverse channels 3 and 4
   quadaudio.DoEffect("Process.Reverse", "", new SfAudioSelection(0, quadaudio.Length, 12), EffectOptions.EffectOnly);

   //create new, empty stereo file, halfed sample rate, 24 Bit
   ISfFileHost stereoaudio = app.NewFile(new SfWaveFormat(quadaudio.SampleRate, 24, 2, false), false);

   //copy and paste channels 1 and 2 at the beginning of the stereo file
   stereoaudio.OverwriteAudio(0, 0, quadaudio, new SfAudioSelection(0, quadaudio.Length, 3));

   //copy and paste channels 3 and 4 at the end of the stereo file
   stereoaudio.OverwriteAudio(stereoaudio.Length, 0, quadaudio, new SfAudioSelection(0, quadaudio.Length, 12));  

}

public void FromSoundForge(IScriptableApp app) {
   ForgeApp = app; //execution begins here
   app.SetStatusText(String.Format("Script '{0}' is running.", Script.Name));
   Begin(app);
   app.SetStatusText(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, params object [] args) { ForgeApp.OutputText(String.Format(fmt, args)); }
} //EntryPoint

Be aware, that the sample rate is an integer value. If it gets halfed (divided by an integer value like 2) the result is also an integer value. So please make sure your original sample rate is always divisible by 2 otherwise the result is rounded to the next smallest whole number.

SP. wrote on 6/20/2024, 6:15 PM

@SP. What I've actually been doing manually is copying the processed "side b" audio, copying it, pasting it at the end of the quad file, and then converting the quad file to stereo. This avoids the need to make a new blank stereo file to paste everything into.

Okay, now it should work like you do it:

using System;
using System.Windows.Forms;
using SoundForge;

public class EntryPoint {
public void Begin(IScriptableApp app) {

   ISfFileHost quadaudio = app.CurrentFile;

   //set sample rate to half without resampling
   quadaudio.DoResample(quadaudio.SampleRate/2, -1, EffectOptions.EffectOnly);

   //reverse channels 3 and 4
   quadaudio.DoEffect("Process.Reverse", "", new SfAudioSelection(0, quadaudio.Length, 12), EffectOptions.EffectOnly);

   //copy and paste channels 3 and 4 at the end of the file into channels 1 and 2
   quadaudio.OverwriteAudio(quadaudio.Length, 3, quadaudio, new SfAudioSelection(0, quadaudio.Length, 12));

   //convert audio from 4 to 2 channels
   quadaudio.DoConvertChannels(2, 0, new double[2,4] {{1.0, 0.0 ,0.0 ,0.0}, {0.0, 1.0, 0.0, 0.0}}, EffectOptions.EffectOnly);

}

public void FromSoundForge(IScriptableApp app) {
   ForgeApp = app; //execution begins here
   app.SetStatusText(String.Format("Script '{0}' is running.", Script.Name));
   Begin(app);
   app.SetStatusText(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, params object [] args) { ForgeApp.OutputText(String.Format(fmt, args)); }
} //EntryPoint

 

Dan-Hicks wrote on 6/20/2024, 8:52 PM

@SP. Thank you so much! I'll try it out later tonite and let you know how it works!

 

Dan-Hicks wrote on 6/20/2024, 11:08 PM

@SP. OK, I tried it and it works great! One request: can we add a marker to the end of the quad file prior to the other processes, such that in the final stereo file there's a marker that shows the dilineation between "side a" and "side b"? Also, how do we make this into something I can run on a whole folder full of files as a batch process?

SP. wrote on 6/21/2024, 8:13 AM

@Dan-Hicks This script will allow you to select a directory and will then run over all files in this directory (all subfolders!). The default path is C:\Users\Public\Documents\ You can change the default path by editing the script.

Read the comments in the script so you know what it does. The comments begin with //.

👉Make sure you select the correct path and also have a backup of all files, just in case something wents wrong!!!👈

using System;
using System.IO;
using System.Windows.Forms;
using SoundForge;

public class EntryPoint {
public void Begin(IScriptableApp app) {

   //select the directory
   String folder = SfHelpers.ChooseDirectory("Choose a directory to process files from", @"C:\Users\Public\Documents\"); //The default folder. Change it to something different if you want

   if(null == folder) return;

   //read the warning message
   DialogResult dialogResult = MessageBox.Show("Are you sure to process " + folder + " and all its subfolders? This script will change ALL files in this directory. Be sure to have backup of your data in case something wents wrong!!!", "WARNING", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);

   //process the files
   if(dialogResult == DialogResult.Yes)
   {
      DirectoryInfo di = new DirectoryInfo(folder);
      
      foreach (FileInfo file in di.GetFiles("*", SearchOption.AllDirectories))
      {
         DPF("Trying to load file {0}", file.FullName);         

         //load file
         ISfFileHost audiofile = app.OpenFile(file.FullName, false, true);

         DPF("File {0} opened", file.FullName);

         //set sample rate to half without resampling
         audiofile.DoResample(audiofile.SampleRate/2, -1, EffectOptions.EffectOnly);

         //reverse channels 3 and 4
         audiofile.DoEffect("Process.Reverse", "", new SfAudioSelection(0, audiofile.Length, 12), EffectOptions.EffectOnly);

         //add "Side A" marker at the beginning of the file
         SfAudioMarker sideA = new SfAudioMarker(0);
         sideA.Name = "Side A";
         audiofile.Markers.Add(sideA);

         //add "Side B" marker at the end of the file
         SfAudioMarker sideB = new SfAudioMarker(audiofile.Length-1);
         sideB.Name = "Side B";
         audiofile.Markers.Add(sideB);   

         //copy and paste channels 3 and 4 at the end of the file into channels 1 and 2
         audiofile.OverwriteAudio(audiofile.Length, 3, audiofile, new SfAudioSelection(0, audiofile.Length, 12));

         //convert audio from 4 to 2 channels
         audiofile.DoConvertChannels(2, 0, new double[2,4] {{1.0, 0.0 ,0.0 ,0.0}, {0.0, 1.0, 0.0, 0.0}}, EffectOptions.EffectOnly);
                    
         //save file with changes
         audiofile.Close(CloseOptions.SaveChanges);

         //This command deletes the sfk-files created by Sound Forge
         File.Delete(Path.ChangeExtension(file.FullName, ".sfk"));

         DPF("File {0} sucessfully processed", file.FullName);
      }
   }
}

public void FromSoundForge(IScriptableApp app) {
   ForgeApp = app; //execution begins here
   app.SetStatusText(String.Format("Script '{0}' is running.", Script.Name));
   Begin(app);
   app.SetStatusText(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, params object [] args) { ForgeApp.OutputText(String.Format(fmt, args)); }
} //EntryPoint

 

Dan-Hicks wrote on 6/21/2024, 11:59 AM

@SP. THANK YOU SO MUCH! Is it possible to make the basic script a preset, for one-off processing of individual files?

SP. wrote on 6/21/2024, 1:44 PM

@Dan-Hicks This script only processes the current file in Sound Forge. The changes are not automatically saved.

Save this script into the Sound Forge script folder (look under Tools > Scripting):

using System;
using System.Windows.Forms;
using SoundForge;

public class EntryPoint {
public void Begin(IScriptableApp app) {

   ISfFileHost audiofile = app.CurrentFile;

   //set sample rate to half without resampling
   audiofile.DoResample(audiofile.SampleRate/2, -1, EffectOptions.EffectOnly);

   //reverse channels 3 and 4
   audiofile.DoEffect("Process.Reverse", "", new SfAudioSelection(0, audiofile.Length, 12), EffectOptions.EffectOnly);

   //add "Side A" marker at the beginning of the file
   SfAudioMarker sideA = new SfAudioMarker(0);
   sideA.Name = "Side A";
   audiofile.Markers.Add(sideA);

   //add "Side B" marker at the end of the file
   SfAudioMarker sideB = new SfAudioMarker(audiofile.Length-1);
   sideB.Name = "Side B";
   audiofile.Markers.Add(sideB);   

   //copy and paste channels 3 and 4 at the end of the file into channels 1 and 2
   audiofile.OverwriteAudio(audiofile.Length, 3, audiofile, new SfAudioSelection(0, audiofile.Length, 12));

   //convert audio from 4 to 2 channels
   audiofile.DoConvertChannels(2, 0, new double[2,4] {{1.0, 0.0 ,0.0 ,0.0}, {0.0, 1.0, 0.0, 0.0}}, EffectOptions.EffectOnly);

}

public void FromSoundForge(IScriptableApp app) {
   ForgeApp = app; //execution begins here
   app.SetStatusText(String.Format("Script '{0}' is running.", Script.Name));
   Begin(app);
   app.SetStatusText(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, params object [] args) { ForgeApp.OutputText(String.Format(fmt, args)); }
} //EntryPoint