Subject:Need some help with Marker/Region scripting
Posted by: Sound Samurai
Date:5/25/2005 2:19:55 AM
Hello gurus, I am superhappy with 8.0a and scripting. Sony/the coders really came thru, its a surgeons dream tool. Snappy, highly customizable and powerful editing. I have hacked together a script to do a process I do a lot manually, it worked so-so in 8.0, but it works even worse in 8.0a. So i was thinking to really make it well, but need some help. Here is what i do; i do "manual" recycling/slicing of loops. That is, I work my way thru a file/loop, and place markers at transients or where i want a slice to appear. Then, in 7.0 I used "Convert Markers to Regions", and "Export Regions to Files", so I had one file for each slice. I in 8.0 I made myself a script to do this, but it now in 8.0a the "save" dialog appears always at the root of the computer instead of the file's last save location, so i have to browse my way to the location, which makes each export a ten-mouse-click operation. (Then i move the samples into a sampler. I dont like recycle or any other auto-processing they are not surgically enough, i am the Transientfinder Level Boss) OK sorry for the long introduction. What i need help with, i dont ask anyone to write it all out for me but to point me out directions so i learn something: I want to write a script that 1 takes the current active open file 2 learns the filename and path of this file 3 checks to see if there are any duplicate markers and delete them (to avoid exporting empty regions/files) 3 converts the markers to regions 4 export the regions with the first five letters of the filename found in (2), into the path found in (2) There are a lot of things I am unsure about, like, should I use the "Export regions" function, or maybe it would be better to traverse the markers list and just save each marker as its own file manually. This way could avoid the 8 letter limit of the Export region, and introduce my own numbering/naming. And when I feel comfortable I can start to put in things like normalizing, remove sub freq etc in the process I am not completely new to programming, but i do not have much experience with C++, i mostly done visual basic, html and javascript (web programming). What makes me uneasy is all the declarations and preparations that seems to be going on - i come from a forgiving background of runtime coding. Message last edited on5/25/2005 2:22:39 AM bySound Samurai. |
Subject:RE: Need some help with Marker/Region scripti
Reply by: _TJ
Date:5/25/2005 9:07:41 PM
Well first of all, If you roll your own script, there is no need to Convert Markers To Regions or Export Regions. It's just as easy to walk the markers and create files using the current marker as the starting point and the next marker as the ending point. Skipping duplicate markers then becomes just a rule to skip writing a file if the size would be 0. I want to write a script that 1 takes the current active open file In C# this would be ISfFileHost file = app.CurrentFile; In JScript var file : ISfFileHost = app.CurrentFile; or just var file = app.CurrentFile; However if you use the second form, the compiler will be unable to do syntax checking on your use of methods on the file object, so just about anything will compile, and then you will get runtime errors where the first form would give the compiler a much better chance of catching your errors. In VB you would use dim file as ISfFileHost = app.CurrentFile or just dim file = app.CurrentFile Where, like JScript the second form tends to hide errors until runtime (also it has more overhead, but in scripts this rarely matters). 2 learns the filename and path of this file string strFullFilePathname = file.Filename; string strDirectory = Path.GetDirectoryName(strFullFilePathName); string strFilename = Path.GetFileName(strFullFilePathName); The Path class is part of the .NET runtime, and provides methods for breaking a pathname up into it's parts and combining them back together. 3 checks to see if there are any duplicate markers and delete them (to avoid exporting empty regions/files) Easy enough, but also unnecessary, you can just have your script skip them when it is writing out the files. 4 converts the markers to regions Unnecessary, Incidently the way you walk through the marker list is foreach (SfAudioMarker mk in file.Markers) { if (mk.IsRegion) { // it's a region } else { // it's a marker } } or SfAudioMarkerList markers = file.Markers; for (int ii = 0; ii < markers.Count; ++ii) { SfAudioMarker mk = markers[ii]; } Incidently, Markers and Regions are both in the same list, and they are in order BY CREATION DATE. so the 'Next' marker or region isn't guranteed to be at a later time in the file. So you might be well advised to create your own list of markers, then sort them by start time, and then walk through THAT list to create the files. This is essentially what you would have to do to convert markers to regions anyway. 5 export the regions with the first five letters of the filename found in (2), into the path found in (2) Have a look at the .NET class System.String for methods you can use to slice and dice strings. In particular have a look at String.Format(). This is the easiest and most powerful way to construct strings from other strings. For instance for (int ii = 0; ii < 3; ++ii) { string str = String.Format("{0} {1:3d}", "Begin", ii); } will generate 3 strings that look like this Begin 000 Begin 001 Begin 002 When you want to start creating filenames, have a look at the Path.Combine(). And finally, when you want to start extracting, the basic function is the RenderAs() Method of ISfFileHost. But it has quite a few options, You will need to know The Format (i.e. wav,mp3,avi, etc) Settings for the Format (we often call this the Template) The render filename The selection from the file to save (use 0,-1 to render the whole file) options to control Rendering UI and how errors are handled. To save the new files using the file format of the current file, you use the ISfRenderer you get from the SaveFormat property of the ISfFileHost. and a special template called "Default Template". string strFilename = Path.Combine(strDir, strName); SfAudioSelection asel = new SfAudioSelection(ccStart, ccLength); ISfRenderer rend = file.SaveFormat; file.RenderAs(strFilename, rend.Name, "Default Template", asel, RenderOptions.RenderOnly); Note that when you use RenderAs with a selection that is NOT (0,-1), there is no need to use file.WaitForDoneOrCancel() in order for your script to wait for the file to be saved. (in fact, you CAN'T wait for the render in the script) I recommend you have a look at the sample script NewFilesFromRegions.cs Another way to do this would be to have your script create a bunch of new files in Sound Forge from the regions, and then turn around and save those files. SfAudioSelection asel = new SfAudioSelection(ccStart, ccLength); ISfFileHost file2 = file.NewFile(asel); file2.SaveAs(strFilename, file.SaveFormat.Name, "Default Template", RenderOptions.RenderOnly) file2.WaitForDoneOrCancel(); The advantage to this is that you will already have the new files open when your script is done. tj Message last edited on5/25/2005 9:16:03 PM by_TJ. |
Subject:RE: Need some help with Marker/Region scripti
Reply by: _TJ
Date:5/25/2005 9:22:54 PM
When you start coding, I think you will find that you can catch errors much more easily if you make a habit of printing stuff out as your script runs. This is what the DPF() functions that are part of the standard script template are for. for instance try running a script that contains just this line (make sure you run it in the Script Editor). /*Begin Here*/ DPF(app.CurrentFile.Filename); You should see the full filename of the current file show up in the Output window at the bottom of the Script Editor window. If you make a habit of printing out file names before you pass them to SaveAs or RenderAs, or marker names as you walk through the marker list logic errors will be quite a bit easier to find. tj Message last edited on5/26/2005 3:44:07 PM by_TJ. |
Subject:RE: Need some help with Marker/Region scripti
Reply by: Sound Samurai
Date:5/28/2005 7:23:47 AM
Wow thanks a million for your elaborate response. I learned a lot. But not enough apperantly :) Here is the script so far. What I now cant figure out, is why it doesnt work. It creates the files allright, but all files are empty, except one, which contains the whole original file.. If you notice I also had to disable the "if (mk.Length = 0)" statement, because it always kicked in.. and i didnt understand why. I have thousands of questions i dont even know where to begin. But where is a good place to start learning the syntax and inner workings of C# - not to become a superguru, but just enough to be able to read other scripts etc. I downloaded the Scripting SDK but it doesnt make me any smarter. (And how do i make this appear as code?? Can I use html code in here..? I realize this is hard to read) <code> using System; using System.IO; using System.Windows.Forms; using SoundForge; //Run with a file that contains regions //Iterates through the regions, renders to PCA and saves the rendered file to c:\media\rip //Scan the file for MODIFY HERE to see how to quickly customize for your own use public class EntryPoint { public string Begin(IScriptableApp app) { //start MODIFY HERE----------------------------------------------- string szType = GETARG("type", ".wav"); //choose any valid extension: .avi .wav .w64 .mpg .mp3 .wma .mov .rm .aif .ogg .raw .au .dig .ivc .vox .pca object vPreset = GETARG("preset", ""); //put the name of the template between the quotes, or leave blank to pop the Template chooser. string szDir = GETARG("dir", ""); //hardcode a target path here //end MODIFY HERE ----------------------------------- int i = 0; ISfFileHost file = app.CurrentFile; if (null == file) return "Open a file containing regions before running this script. Script stopped."; if (null == file.Markers || file.Markers.Count <= 0) return "The file does not have any markers."; string strFullFilePathname = file.Filename; string strDirectory = Path.GetDirectoryName(strFullFilePathname); string strFilename = Path.GetFileName(strFullFilePathname); szDir = strDirectory; foreach (SfAudioMarker mk in file.Markers) { DPF(String.Format("{0} ", mk.Length)); //if (mk.Length = 0) // continue; string szName = String.Format("{0} {1} {2}", "Begin", i, ".wav"); szName = CleanFilename(szName); DPF("Queueing: '{0}'", szName); string szFullName = Path.Combine(szDir, szName); if (File.Exists(szFullName)) File.Delete(szFullName); SfAudioSelection range = new SfAudioSelection(mk.Start, mk.Length); ISfRenderer render = file.SaveFormat; file.RenderAs(szFullName, render.Name, "Default Template", range, RenderOptions.RenderOnly); DPF("Path: '{0}'", szFullName); i = i + 1; } return null; } public string CleanFilename(string szName) { szName = szName.Replace(':', '_'); szName = szName.Replace('\\', '_'); szName = szName.Replace('/', '_'); foreach (char ch in Path.InvalidPathChars) { szName = szName.Replace(ch, '_'); } return szName; } public string PromptForPath(string szDir) { FolderBrowserDialog dlg = new FolderBrowserDialog(); dlg.Description = "Select the target folder for saved files:"; if (null != szDir && "" != szDir) dlg.SelectedPath = szDir; dlg.ShowNewFolderButton = true; DialogResult res = dlg.ShowDialog(); if (res == DialogResult.OK) return dlg.SelectedPath; return null; } 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 </code> Message last edited on5/28/2005 7:24:53 AM bySound Samurai. |
Subject:RE: Need some help with Marker/Region scripti
Reply by: _TJ
Date:5/28/2005 12:37:37 PM
You can use limited HTML in here. I typically use <b>, <i> and <pre> tags. The PRE tag is what prevents the board from re-formatting your code. It also seems to force fixed pitch font, which is good. tj Message last edited on5/28/2005 12:42:00 PM by_TJ. |
Subject:RE: Need some help with Marker/Region scripti
Reply by: _TJ
Date:5/28/2005 12:50:08 PM
Ok, no own to substantive issues. First of all mk.Length = 0 isn't comparing the length to zero, it's setting the length to 0. use mk.Length == 0 to compare (in C# and JScript) or mk.Length <> 0 if you are writing in VB. Some programmers think its a good idea to always write comparisons with the constant first: if (0 == mk.Length) because if you make a mistake and write if (0 = mk.Length) the code won't compile, since you can't assign to a constant. Next, if these are markers rather than regions, then their length is always 0. Try running a script with this code in the body. int ii = 0; foreach (SfAudioMarker mk in file.Markers) { if (mk.HasLength) DPF("rgn[" + ii + "] " + mk.Ident + " " + mk.Type + " '" + mk.Name + "' (" + mk.Start + "," + mk.Length + ")"); else DPF("mrk[" + ii + "] " + mk.Ident + " " + mk.Type + " '" + mk.Name + "' (" + mk.Start + /*"," + mk.Length +*/ ")"); ++ii; } tj |
Subject:RE: Need some help with Marker/Region scripti
Reply by: _TJ
Date:5/28/2005 1:11:35 PM
Ok. so assuming that you are working with markers here, what you need to do is to take the distance between two markers rather than the length of the current marker. This code assumes that the markers are in order by start time, which is NOT guranteed by Sound Forge, but will be true if you created them from left to right. ISfRenderer render = file.SaveFormat; string szDir = Path.GetDirectoryName(file.Filename); SfAudioMarkerList markers = file.Markers; for (int ii = 0; ii < markers.Count; ++ii) { SfAudioMarker mk = markers[ii]; Int64 ccNext = file.Length; if (ii+1 < markers.Count) ccNext = markers[ii+1].Start; Int64 ccLength = ccNext - mk.Start; if (ccLength > 0) { SfAudioSelection range = new SfAudioSelection(mk.Start, ccLength); string szName = String.Format("{0} {1}.{2}", "Begin", ii, render.Extension); szName = CleanFilename(szName); DPF("Queueing: '{0}'", szName); string szFullName = Path.Combine(szDir, szName); if (File.Exists(szFullName)) File.Delete(szFullName); file.RenderAs(szFullName, render.Name, "Default Template", range, RenderOptions.RenderOnly); DPF("Path: '{0}'", szFullName); } } |
Subject:RE: Need some help with Marker/Region scripti
Reply by: Sound Samurai
Date:6/3/2005 9:16:25 AM
Hello TJ thank you again for your patience. your example works perfect, if the markers are created from left to right. but they rarely are, mostly my files already contains markers from long ago when i sampled them, and usually new markers are created at half intervals and then inwards. now i have spent a week to read all i could find on C# on the internet, tried to learn some basics and understand most of your examples and tried to do the following -sort the markers list by marker start position -then export all markers with length > 2 attached is here my current endeavours. sometimes it works beautifully, but sometimes it seems to remove every marker but two, and place those two at the end. the export result is then just two files.. i cant seem to wrap my head around why or what could be wrong? i seems to happen when the first created marker is placed at the end of the file, or when the file contained markers created long ago. i also tried using the .sort method i read about in the C# array documentation, but there is no such method for the Markers, right? I also thought about copying the markers into my own array and then use the sort method on this array and then re-export from thsi array, but then my head imploded. i know programming music and programming mathemacis and logic is related but appearantly not THAT related :) if you could offer any insight as to what i am doing wrong i would be very happy using System; using System.IO; using System.Windows.Forms; using SoundForge; //Run with a file that contains regions public class EntryPoint { public string Begin(IScriptableApp app) { ISfFileHost file = app.CurrentFile; if (null == file) return "Open a file containing regions before running this script. Script stopped."; if (null == file.Markers || file.Markers.Count <= 0) return "The file does not have any markers."; ISfRenderer render = file.SaveFormat; string szDir = Path.GetDirectoryName(file.Filename); SfAudioMarkerList markers = file.Markers; DPF("----- BEFORE SORTING -----"); // iterates the markers list and just prints out some info for (int ii = 0; ii < markers.Count; ii++) { SfAudioMarker mkInfo = markers[ii]; if (mkInfo.IsRegion) DPF("Region: '{0}'", ii); else DPF("Marker: '{0}'", ii); DPF("Start: '{0}'", mkInfo.Start); } DPF("----- NOW SORTING -----"); // iterates the markers list // for each marker, iterate the REST of the list // if any of the later markers have an earlier start position, swap the start position for (int ii = 0; ii < markers.Count; ii++) { SfAudioMarker mk = markers[ii]; Int64 ccCurrentPosition = mk.Start; for (int jj = ii+1; jj < markers.Count; jj++) { SfAudioMarker mkCompare = markers[jj]; Int64 ccComparePosition = mkCompare.Start; //DPF("Comparing: Marker '{0}' with Marker '{1}'", ii, jj); //DPF("Comparing: Position '{0}' with Position '{1}'", ccCurrentPosition, ccComparePosition); if (ccComparePosition < ccCurrentPosition) { // SWAP the two positions, move the compared marker to an earlier pos DPF("Swapping: Marker '{0}' with Marker '{1}'", ii, jj); mk.Start = ccComparePosition; mkCompare.Start = ccCurrentPosition; } } } DPF("----- AFTER SORTING -----"); // iterates the sorted markers list // exports any markers with length > 2 for (int ii = 0; ii < markers.Count; ii++) { SfAudioMarker mkAfter = markers[ii]; if (mkAfter.IsRegion) DPF("Region: '{0}'", ii); else DPF("Marker: '{0}'", ii); Int64 ccCurrentPosition = mkAfter.Start; DPF("Start: '{0}'", mkAfter.Start); Int64 ccNext = file.Length; if (ii+1 < markers.Count) ccNext = markers[ii+1].Start; Int64 ccLength = ccNext - mkAfter.Start; if (ccLength > 2) //changed to 2 because 0 exported zero length markers { SfAudioSelection range = new SfAudioSelection(mkAfter.Start, ccLength); string szName = String.Format("{0} {1}.{2}", "Exported ", ii, render.Extension); szName = CleanFilename(szName); DPF("Queueing: '{0}'", szName); string szFullName = Path.Combine(szDir, szName); if (File.Exists(szFullName)) File.Delete(szFullName); file.RenderAs(szFullName, render.Name, "Default Template", range, RenderOptions.RenderOnly); DPF("Path: '{0}'", szFullName); } // end of if ccLength } // end of for loop DPF("Done."); return null; } public string CleanFilename(string szName) { szName = szName.Replace(':', '_'); szName = szName.Replace('\\', '_'); szName = szName.Replace('/', '_'); foreach (char ch in Path.InvalidPathChars) { szName = szName.Replace(ch, '_'); } return szName; } 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 Message last edited on6/3/2005 9:26:31 AM bySound Samurai. |
Subject:RE: Need some help with Marker/Region scripti
Reply by: _TJ
Date:6/3/2005 1:02:14 PM
You seem to have made quite a bit of progress on C# already. Be aware that that sort algorithm will get really slow with really large collections of markers. Also, it's gonna really mess up your Regions if you have any. (you don't move the lengths along with the starts). Anyway, If you carefully analyze the DPF() results from the swap, you will see what is going wrong here. After you swap mk with mkCompare, mk is at a different position, but you are still comparing in the inner loop with ccCurrentPosition from the original mk. so if your markers are arranged such that you do one swap per inner loop, your code works fine, but if you have more than on, you start swapping wrong. I think you just need to update ccCurrentPosition after you swap. Now, as for algorithms, There is a standard collection and should fit your needs perfectly. It's called a SortedList (System.Collections.SortedList) Off the top of my head, it would look something like this Int64 ccTotalStart = 0; // file.Window.EditRegion.Start; Int64 ccTotalEnd = file.Length; // file.Window.EditRegion.Start + file.Window.EditRegion.Length; System.Collections.SortedList list = new SortedList(file.MarkerCount); list.Add(ccTotalStart, "Total Start"); list.Add(ccTotalEnd, "Total End"); foreach (SfAudioMarker mk in file.Markers) { if (mk.IsRegion) { list.Add(mk.Start, mk.Name); // if including region ends (regions can still have 0 length...) if (mk.Length > 0) list.Add(mk.Start + mk.Length, mk.Name + " (end)"); } else { list.Add(mk.Start, mk.Name); } } Int64 ccPos = ccTotalStart; foreach (Int64 ccNext in list.Keys) { Int64 ccLength = ccNext - ccPos; if (ccLength > 0) { DPF("ccPos = {0} ccLength = {1} name = '{2}'", ccPos, ccLength, list[ccPos]); // do render ccPos, ccLength here ccPos = ccNext; } if (ccNext > ccTotalEnd) break; } |
Subject:RE: Need some help with Marker/Region scripti
Reply by: Sound Samurai
Date:6/4/2005 11:26:37 AM
I bow deeply to you Master TJ for your knowledge, kindness and patience. I now have a beautiful script that slices my files quickly and silently with no other effort than pressing a keyboard shortcut. It took me almost a week but i have learned a little C#, a little .NET programming and how powerful the scripting is! I just wished the .NET and Soundforge documentation was a bit more geared towards idiots like me I will just run it in daily usage for some days to make sure its robust and then post it here in case anyone else needs it |
Subject:RE: Need some help with Marker/Region scripti
Reply by: Angels
Date:6/5/2005 10:22:48 AM
" I just wished the .NET and Soundforge documentation was a bit more geared towards idiots like me," I second this ;) Angels |