So, I wasn't going to, but I got bored and didn't feel like doing much else.
I built a few commands that will allow you to put .mp3 files into the clipboard, add them to a list of songs, and will automatically build a command that allows you to play each of them.
There is a caveat in that VoiceAttack currently handles changing dynamic(token based) command names by reloading the entire profile.
In itself this works well for this profile, but it does mean that if you embed these commands into another profile, any running commands will stop and any volatile variables will be discarded when you add songs.
Another caveat: There's no facility for adding a custom name/phrase for any given song.
It could be added, but because the dictionary collection used to hold the data can only easily be searched by key and not by value, it would mean removal for those songs(and duplicate checking while adding) would be difficult.
Names, and consequently command phrases, are taken from the ID3 tags of the added songs, in the format "artist title".
E.G. "Queen - We will rock you" would be played by speaking "play queen we will rock you".
Reading tags is done using "tagLib sharp", which is a C# wrapper for
tagLib.
I found a compiled copy of the latest version
here.
Virustotal found nothing objectionable in that file, so it appears to be safe(though use at your own risk).
You only need "taglib-sharp.dll", which must be placed in your VoiceAttack installation directory(where VoiceAttack.exe is also located).
Because it's not currently possible to use tokens in the "Referenced Assemblies" textbox, you need to add the path to the installation folder manually to each inline function, unless you've used the default installation path.
E.G.
Microsoft.CSharp.dll;System.dll;System.Core.dll;System.Data.dll;System.Data.DataSetExtensions.dll;System.Deployment.dll;System.Drawing.dll;System.Net.Http.dll; System.Windows.Forms.dll;System.Xml.dll;System.Xml.Linq.dll;C:\Program Files(x86)\VoiceAttack\taglib-sharp.dll
If you've added it correctly, you should not see any errors when you click the "Compile" button.
The commands included are the following:
[Add;Remove] [song;songs]Begin Text Compare : [{CMD}] Starts With 'Remove'
Set Boolean [~removeSong] to True
End Condition
Inline C# Function: Add/Remove songs, wait until execution finishes
Execute command, 'Build command name' (and wait until it completes)
Where "Add/Remove songs" contains
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.IO;
using TagLib;
using System.Text.RegularExpressions;
using File = System.IO.File; //Avoids ambiguity with TagLib "File" method
public class VAInline
{
public void main()
{
System.Collections.Specialized.StringCollection returnList = null;
if (Clipboard.ContainsFileDropList())
{
returnList = Clipboard.GetFileDropList();
}
else
{
VA.WriteToLog("Error: No files in clipboard", "red");
return;
}
Dictionary<string, string> songList = loadStoredDictionary(">>songList");
int songCount = 0;
bool? remove = VA.GetBoolean("~removeSong");
foreach (string i in returnList)
{
try
{
if (!File.Exists(i))
{
VA.WriteToLog("Error: The file '" + i + "' does not exist","red");
continue;
}
if (!i.EndsWith(".mp3"))
{
VA.WriteToLog("Error: The file '" + i + "' does not have the .mp3 extension","red");
continue;
}
string fileName = Path.GetFileName(i);
var file = TagLib.File.Create(i);
string songId = (file.Tag.FirstArtist + " " + file.Tag.Title).ToLower();
if (songId == " ")
{
VA.WriteToLog("File '" + fileName + "' does not have proper ID3 tags and was skipped", "orange");
continue;
}
if (Regex.IsMatch(songId, @"[0-9a-zA-Z\s]+"))
{
songId = Regex.Replace(songId, @"[^0-9a-zA-Z\s]+", "");
}
if (!songList.ContainsKey(songId))
{
if (remove != true)
{
songList.Add(songId, i);
songCount++;
}
else
{
VA.WriteToLog(fileName + " is not in the list and was skipped", "orange");
}
}
else
{
if (remove != true)
{
VA.WriteToLog(fileName + " is already in the list and was skipped", "orange");
}
else
{
songList.Remove(songId);
songCount++;
}
}
}
catch (Exception e)
{
VA.WriteToLog("operation failed: "+ e.Message, "red");
}
}
saveStoredDictionary(songList, ">>songList");
VA.WriteToLog(songCount + " songs " + ((remove != true) ? "added" : "removed") + ((returnList.Count - songCount != 0) ? "(" + (returnList.Count - songCount) + " skipped)" : ""), "green");
}
public Dictionary<string, string> loadStoredDictionary(string valueName)
{
Dictionary<string, string> d = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(VA.GetText(valueName)))
{
string[] pairs = VA.GetText(valueName).Split(new char[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
foreach (string singlePair in pairs)
{
string[] pair = singlePair.Split('>');
d.Add(pair[0], pair[1]);
}
}
return d;
}
public void saveStoredDictionary(Dictionary<string, string> d, string valueName)
{
string pairs = "";
foreach (var pair in d)
{
pairs += pair.Key + ">" + pair.Value + "|";
}
VA.SetText(valueName,pairs);
}
}
Build command nameSet Text [~playCommandPrefix] to 'Play'
Set Text [~playCommandSuffix] to ''
Inline C# Function: Build command name, wait until execution finishes
Set Text [>>songList] to [>>songList] (save value to profile)
Reset the active profile
The top two actions are there so you can easily change the command phrase, however if you add any dynamic command sections you'll need to change the "{CMDSEGMENT:1}" in the inline function of the play command to the relevant value.
Where "Build command name" contains
using System;
using System.Collections.Generic;
using System.IO;
public class VAInline
{
public void main()
{
Dictionary<string, string> songList = loadStoredDictionary(">>songList");
if (songList.Count == 0)
{
VA.WriteToLog("Error: The song list is empty, cannot build command name", "red");
return;
}
string commandPhrase = VA.GetText("~playCommandPrefix") + " [";
foreach (var songId in songList.Keys)
{
commandPhrase += songId + ";";
}
commandPhrase = commandPhrase.Remove(commandPhrase.Length -1); //Remove trailing ";"
commandPhrase += "] " + VA.GetText("~playCommandSuffix");
VA.SetText(">>playCommandName", commandPhrase);
}
public Dictionary<string, string> loadStoredDictionary(string valueName)
{
Dictionary<string, string> d = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(VA.GetText(valueName)))
{
string[] pairs = VA.GetText(valueName).Split(new char[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
foreach (string singlePair in pairs)
{
string[] pair = singlePair.Split('>');
d.Add(pair[0], pair[1]);
}
}
return d;
}
public void saveStoredDictionary(Dictionary<string, string> d, string valueName)
{
string pairs = "";
foreach (var pair in d)
{
pairs += pair.Key + ">" + pair.Value + "|";
}
VA.SetText(valueName,pairs);
}
}
{TXT:>>playCommandName}Inline C# Function: Get song file path, wait until execution finishes
Play sound, '{TXT:~fileToPlay}'
Where "Get song file path" contains
using System;
using System.Collections.Generic;
public class VAInline
{
public void main()
{
Dictionary<string, string> songList = loadStoredDictionary(">>songList");
string songId = VA.ParseTokens("{CMDSEGMENT:1}");
if (songList.ContainsKey(songId))
{
VA.SetText("~fileToPlay",songList[songId]);
}
else
{
VA.WriteToLog("Error: Song does not exist in list", "red");
}
}
public Dictionary<string, string> loadStoredDictionary(string valueName)
{
Dictionary<string, string> d = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(VA.GetText(valueName)))
{
string[] pairs = VA.GetText(valueName).Split(new char[] {'|'}, StringSplitOptions.RemoveEmptyEntries);
foreach (string singlePair in pairs)
{
string[] pair = singlePair.Split('>');
d.Add(pair[0], pair[1]);
}
}
return d;
}
public void saveStoredDictionary(Dictionary<string, string> d, string valueName)
{
string pairs = "";
foreach (var pair in d)
{
pairs += pair.Key + ">" + pair.Value + "|";
}
VA.SetText(valueName,pairs);
}
}
There's also a command that should be run whenever the profile is loaded, or whenever the profile including this profile is loaded(in which case you can execute it by name):
Music Player SetupBegin Text Compare : [>>songList] Has Not Been Set
Set Text [>>songList] to [Saved Value]
Execute command, 'Build command name' (and wait until it completes)
End Condition
This just serves to retrieve the saved value and build the command on the initial profile load after VoiceAttack (re)starts.
The workflow would be:
- Open a Windows Explorer window
- Navigate to the .mp3 files you wish to add
- Select all the files
- Press Ctrl + C
- Say "Add songs"
- Say "play" followed by the artist and full title of the song, and it should begin playing
Once you've added songs, the profile should remember that and load them in when you load the profile.
Again, there's some basic error checking in this, but it doesn't come with any guarantees, nor do I take responsibility for your eventual misfortunes
Remember, I just put this together out of boredom; If you want to modify it be my guest, but I'm not going to, any time soon.
EDIT: I should mention that the tags are stripped of any special characters so that the speech engine won't choke on them.
E.G. "art-ist song (album version)" would become "artist song album version".
You could have the function replace the characters with spaces(as I believe the speech engine ignores extras), if you find removing them outright causes issues.
Feel free to change
if (Regex.IsMatch(songId, @"[0-9a-zA-Z\s]+"))
{
songId = Regex.Replace(songId, @"[^0-9a-zA-Z\s]+", "");
}
to
songId = Regex.Replace(songId, @"[^0-9a-zA-Z\s]+", " ");
While you're in there.
Initially you'd get a log message every time it modified the song name, but I figured that's not necessary and removed it, so the check is superfluous.