Author Topic: New Voice Control Music Jukebox - Play Songs by Name  (Read 8735 times)

iceblast

  • Sr. Member
  • ****
  • Posts: 374
New Voice Control Music Jukebox - Play Songs by Name
« on: March 24, 2018, 01:25:46 AM »
New and Improved Music Jukebox, much, much easier to use.

I made a Video Tutorial, check it out.

https://youtu.be/RYVaHXthtoM


The video explains everything, pretty easy to use.

Hope someone gets some enjoyment out of it. :)




Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #1 on: March 24, 2018, 07:40:17 PM »
From your video I see you're looking to get files into a list, perhaps this is easier to work with:
Code: [Select]
using System;
using System.Windows.Forms;
using System.IO;

public class VAInline
{
public void main()
{
System.Collections.Specialized.StringCollection returnList = null;
if (Clipboard.ContainsFileDropList())
{
returnList = Clipboard.GetFileDropList();
}
else
{
return;
}

string path = "";

path = returnList[0];
if (File.Exists(path))
{
path = Path.GetDirectoryName(path);
}

if (Directory.Exists(path))
{
string[] files = Directory.GetFiles(path, "*.mp3");
string output = "";
foreach (string filePath in files)
{
output += Path.GetFileName(filePath) + "\r\n";
}
VA.SetText("songNames", output); //Put file name list into a VoiceAttack text value
Clipboard.SetText(output);//Or directly into the clipboard ready for pasting(or both if you prefer)
}
}
}

You could use it like this:
Code: [Select]
Inline C# Function: Get .mp3 files in folder, wait until execution finishes
Begin Text Compare : [songNames] Does Not Equal ''
    Write '[Blue] {TXT:songNames}' to log
End Condition
And hook it up to trigger when you press Ctrl + C

Or like this
Code: [Select]
Press Left Ctrl+C keys and hold for 0,06 seconds and release
Inline C# Function: Get .mp3 files in folder, wait until execution finishes
Begin Text Compare : [songNames] Does Not Equal ''
    Write '[Blue] {TXT:songNames}' to log
End Condition
To use it with any other key combination(or voice activation for that matter).


If you use it as it, it should tie into your workflow nicely, as all you have to do is press Ctrl + V to paste the entire list of file names, each on its own line, to a text file. As a happy coincidence, because a newline is added after each file name, you'll automatically have space for the album title at the bottom.


This could easily be expanded to index files in other audio formats, but as you're using mp3s as I assume most people are, it was simpler in implementation to limit it to .mp3(otherwise you'd have to use an additional condition to check the extension of each file, or at least a LINQ snippet).

Gary

  • Administrator
  • Hero Member
  • *****
  • Posts: 2831
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #2 on: March 24, 2018, 07:58:10 PM »
Lol, iceblast has a Southern accent like the rest of us ;)

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #3 on: March 24, 2018, 08:53:46 PM »
I never go anywhere, never thought I had a Southern accent. :)

Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #4 on: March 24, 2018, 09:16:45 PM »
Though I still need something that will copy the current directories path, for where the song files are stored.
Like c:\music\Queen

Ah. The inline function has that path already, it can easily be written to a VoiceAttack value(I didn't realize you still needed the folder when you had the file names).

Add
Code: [Select]
VA.SetText("songPath", path);
like this:
Code: [Select]
using System;
using System.Windows.Forms;
using System.IO;

public class VAInline
{
public void main()
{
System.Collections.Specialized.StringCollection returnList = null;
if (Clipboard.ContainsFileDropList())
{
returnList = Clipboard.GetFileDropList();
}
else
{
return;
}

string path = "";

path = returnList[0];
if (File.Exists(path))
{
path = Path.GetDirectoryName(path);
}
VA.SetText("songPath", path);

if (Directory.Exists(path))
{
string[] files = Directory.GetFiles(path, "*.mp3");
string output = "";
foreach (string filePath in files)
{
output += Path.GetFileName(filePath) + "\r\n";
}
VA.SetText("songNames", output); //Put file name list into a VoiceAttack text value
Clipboard.SetText(output);//Or directly into the clipboard ready for pasting(or both if you prefer)
}
}
}

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #5 on: March 25, 2018, 11:02:38 PM »
Completely remade my Voice Music Jukebox.

It's now twice as fast, and thanks to Pfeil for giving me a easier why to get the Path of the folder, and the names of the files.

I made another Tutorial Video. Much easier to do, and you don't need any reg edits or anything, just the profile.

Here's the Link for the Video
https://youtu.be/HD0RI-GMRiU

Well, I hope someone finds some enjoyment out of it.

Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #6 on: March 26, 2018, 02:30:42 AM »
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.
Code: [Select]
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]
Code: [Select]
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
Code: [Select]
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 name
Code: [Select]
Set 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
Code: [Select]
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}
Code: [Select]
Inline C# Function: Get song file path, wait until execution finishes
Play sound, '{TXT:~fileToPlay}'

Where "Get song file path" contains
Code: [Select]
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 Setup
Code: [Select]
Begin 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 :P



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
Code: [Select]
if (Regex.IsMatch(songId, @"[0-9a-zA-Z\s]+"))
{
songId = Regex.Replace(songId, @"[^0-9a-zA-Z\s]+", "");
}
to
Code: [Select]
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.
« Last Edit: March 26, 2018, 02:39:21 AM by Pfeil »

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #7 on: March 26, 2018, 03:36:27 AM »
Wow, you did a lot of work. I was bored when I built my jukebox profile, but I did have fun doing it.

You probably know by now, that I know nothing about programming. I read your code, and I don't understand anything. I can do a lot with computers, but programming just isn't one of them. Though I'm going to look at it again. I would love to make some C# code, but I'm not fool enough to think I'm going to be able to do anything with it.

It's a shame it keeps erroring out though.

I got the taglib-sharp.dll file, but I'm using the beta .25, so I changed the path in the inline C# to C:\Program Files (x86)\VoiceAttack Beta\taglib-sharp.dll

The Compiler says it passed.

The error I'm getting is Error: The song list is empty, cannot build command name

VoiceAttack really doesn't like this error apparently. :) I had to close VoiceAttack and start it with profiles off.

Anyway, I figured you would have some idea why the error.

Thanks again for all you do! :)


Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #8 on: March 26, 2018, 05:44:59 PM »
The error I'm getting is Error: The song list is empty, cannot build command name
This happens when the stored value for ">>songList" is empty, which is a condition I didn't check for after adding the startup command.

VoiceAttack really doesn't like this error apparently. :) I had to close VoiceAttack and start it with profiles off.
Yeah, it'll go into a loop and keep going. VoiceAttack can be hard to stop when that happens.


Change the command to
Code: [Select]
Begin Text Compare : [>>songList] Has Not Been Set
    Set Text [>>songList] to [Saved Value]
    Begin Text Compare : [>>songList] Has Not Been Set
        Write '[Orange] The song list is empty, please add some songs to use the music player' to log
    End Condition - Exit when condition met
    Execute command, 'Build command name' (and wait until it completes)
End Condition


Or import the attached copy of the profile, which contains that fix.

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #9 on: April 03, 2018, 07:18:51 AM »
Hey Pfeil, I've been playing with your Music Player.

Everything was going great, till I was forced to close VA. After VA started, all the songs I added where gone.

Well, I thought I had backed them up, but it's not working.

Here's what I did. Every time I added songs, I made VA copy the same songs to a txt file, by using {TXT:>>songList}. I figured, I could just load the txt file back into >>songList if it forgot the list.

Apparently, this isn't working though. Was hoping you can help me with this. Is there a way to use my txt file loaded with mp3 paths back into player? I'm guessing a txt file is the wrong format, I have not idea.

Exergist

  • Global Moderator
  • Sr. Member
  • *****
  • Posts: 405
  • Ride the lightning
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #10 on: April 03, 2018, 08:01:26 AM »
This is great stuff! When I have time I'll sift through all the C# functions and create links to the content in the Inline Functions section :)

Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #11 on: April 03, 2018, 02:03:35 PM »
I made VA copy the same songs to a txt file, by using {TXT:>>songList}. I figured, I could just load the txt file back into >>songList if it forgot the list.

Apparently, this isn't working though. Was hoping you can help me with this. Is there a way to use my txt file loaded with mp3 paths back into player? I'm guessing a txt file is the wrong format, I have not idea.

I tested this, and it should work. Both a text file created by VoiceAttack, and one manually created, load properly on my machine.

You should be able to load the text file using a single action:
Code: [Select]
Set Text [>>songList] to [{VA_DIR}\songlist.txt] (save value to profile)Modify the path as required.


Are you getting any error messages?

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #12 on: April 03, 2018, 05:34:14 PM »
Hey Pfeil,

The way I've been trying to do this is by running a command called Load Song List.
So it runs this.
Code: [Select]
Set Text [>>songList] to [{VA_DIR}\songlist.txt] (save value to profile)
It is throwing a error:

 Inline function execution exception: Exception has been thrown by the target of an invocation. An item with the same key has already been added.

While trying different things, I've see this error as well. Error with launched file: [The system cannot find the file specified]

You're getting it to work, which is great to hear! But I'm having no luck. So, if it's not the file type being the problem, is it when I ask it to load the songlist.txt.

My songlist.txt is over 50megs now. Probably takes a bit of time to load as well.

So, how or when do you load the txt file into >>songList back into the command.

I figured it should be a one time command only used if the >>songList goes empty. Just say the command and bang, your songlist is back. Yet, that's not working.

Any idea what I'm doing wrong?



Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #13 on: April 04, 2018, 04:27:36 AM »
If you're getting that error it means you have a song duplicated somewhere in the list. Did you append the text file at any point, instead of overwriting it?

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #14 on: April 04, 2018, 04:34:09 AM »
I had it set to append, no over write.

Pfeil

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 4781
  • RTFM
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #15 on: April 04, 2018, 04:39:29 AM »
Append will add the entire list to itself each time, so it'll have a lot of duplicates.

I made an edited command with an inline function to automate undoing this:
Load song list from file
Code: [Select]
Set Text [>>songList] to [{VA_DIR}\songlist.txt]
Inline C# Function: Sanitize song list, wait until execution finishes
Set Text [>>songList] to [>>songList] (save value to profile)

Code: [Select]
using System;
using System.Collections.Generic;
using System.IO;

public class VAInline
{
public void main()
{
saveStoredDictionary(sanitizeStoredDictionary(">>songList"), ">>songList");
}

public Dictionary<string, string> sanitizeStoredDictionary(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('>');

if (!d.ContainsKey(pair[0]))
{
d.Add(pair[0], pair[1]);
}
else
{
VA.WriteToLog("Ignoring duplicate song name '" + pair[0] + "'", "orange");
}
}
}

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);
}
}

iceblast

  • Sr. Member
  • ****
  • Posts: 374
Re: New Voice Control Music Jukebox - Play Songs by Name
« Reply #16 on: April 04, 2018, 10:07:08 AM »
Thanks again Pfeil! Your Sanitize song list command did it's job, but I think I must of damaged the file or something, because it didn't work. Turns out the file was huge compared to loading all my songs in it.

After changing it to overwrite, I was able to reload a new list with no problem. So, I just reloaded all my songs, and now everything is good. Your program works great! Added just about 9,000 songs into it. :)

Again, thanks for all your help! :)