Download it from the add-on link here (https://getmusicbee.com/addons/plugins/517/ab-repeat-toggle-hotkey/)
NB: I'm yet to address one aspect of the plugin that'll currently prevent you from setting up the AB Repeat for the last played song from the previous MB session.
I initially planned to add this function to the other hotkey plugin I shared a couple of weeks ago, but I've decided otherwise.
Splitting them up will help with discoverability and ensure users get to download only the features they desire.
The function was requested here and I've been enjoying it myself too:
It would be great if AB repeat functionality could be added to Musicbee.
(https://i.imgur.com/si9GsiD.png)
(https://i.imgur.com/R4wwmPn.png)
The entire code for anyone interested in taking a peak.
using System;
using System.IO;
using System.Timers;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace MusicBeePlugin
{
public partial class Plugin
{
MusicBeeApiInterface mbApi;
System.Timers.Timer myTimer = new Timer();
ToolTip status = new ToolTip();
string dataFolder;
string logPath;
bool timer_Active = false;
bool pauseOn = false;
int mode = 0;
int start_time;
int end_time;
int streamHandle = -1;
double position;
public PluginInfo Initialise(IntPtr apiInterfacePtr)
{
mbApi = new MusicBeeApiInterface();
PluginInfo info = new PluginInfo();
mbApi.Initialise(apiInterfacePtr);
info.PluginInfoVersion = PluginInfoVersion;
info.Name = "AB_Repeat Hotkey Toggle";
info.Description = "Triggers a repeat state between two points in a song";
info.Author = "Mayibongwe (2024)";
info.TargetApplication = "";
info.Type = PluginType.General;
info.VersionMajor = 1;
info.VersionMinor = 3;
info.Revision = 0;
info.MinInterfaceVersion = MinInterfaceVersion;
info.MinApiRevision = MinApiRevision;
info.ReceiveNotifications = ReceiveNotificationFlags.PlayerEvents | ReceiveNotificationFlags.TagEvents;
info.ConfigurationPanelHeight = -1;
dataFolder = Path.Combine(mbApi.Setting_GetPersistentStoragePath(), "mb_RepeatAB");
if (Directory.Exists(dataFolder) == false) Directory.CreateDirectory(dataFolder);
logPath = Path.Combine(dataFolder, "Activity.log");
return info;
}
public bool Configure(IntPtr panelHandle)
{
return false;
}
public void SaveSettings(){}
public void Close(PluginCloseReason reason)
{
myTimer.Dispose();
status.Dispose();
}
public void Uninstall()
{
if (Directory.Exists(dataFolder)) Directory.Delete(dataFolder, true);
}
public void writeToLog(string data)
{
File.AppendAllText(logPath, data + Environment.NewLine);
}
public void ReceiveNotification(string sourceFileUrl, NotificationType type)
{
switch(type)
{
case NotificationType.PluginStartup:
File.WriteAllText(logPath, "Plugin in effect" + Environment.NewLine);
mbApi.MB_AddMenuItem(null, "Player: Toggle AB Repeat", repeatHandler);
break;
case NotificationType.TrackChanged:
if(end_time != 0)
{
resetPoints();
writeToLog("Repeat off / New playing track");
}
break;
case NotificationType.PlayStateChanged:
if(end_time != 0)
{
PlayState status = mbApi.Player_GetPlayState();
if(timer_Active == true)
{
mode = 0;
if(status == PlayState.Paused)
{
pauseOn = true;
writeToLog("Playback paused");
}
if(status == PlayState.Stopped) writeToLog("Playback stopped");
}
else
{
if(status == PlayState.Playing && pauseOn == true)
{
mode = 2;
pauseOn = false;
myTimer.Enabled = true;
writeToLog("Playback resumed");
}
}
}
break;
}
}
public void repeatHandler(object sender, EventArgs e)
{
if(streamHandle != -1 && mbApi.Player_GetPlayState() != PlayState.Stopped)
{
mode += 1;
switch(mode)
{
case 1:
start_time = mbApi.Player_GetPosition();
writeToLog("Start Time: " + start_time);
break;
case 2:
end_time = mbApi.Player_GetPosition();
if(end_time < start_time)
{
writeToLog("End Time Invalid: " + end_time);
resetPoints();
writeToLog("AB points captured discarded: Retry");
}
else
{
writeToLog("End Time: " + end_time);
repeatTrack();
showTooltip("on");
}
break;
case 3:
resetPoints();
writeToLog("Repeat turned off");
showTooltip("off");
break;
}
}
else
{
writeToLog("Cannot proceed without stream handle");
showTooltip("handle");
}
}
private void showTooltip(string enabled)
{
if(enabled == "on") enabled = "AB Repeat On";
else if (enabled == "off") enabled = "AB Repeat Off";
else enabled = "Missing Stream Handle";
status.Show(enabled, Application.OpenForms[0], Cursor.Position.X-42, Cursor.Position.Y-25, 1750);
}
private void resetPoints()
{
mode = 0;
start_time = 0;
end_time = 0;
}
private void repeatTrack()
{
switch(mbApi.Player_GetPlayState())
{
case PlayState.Playing:
mbApi.Player_SetPosition(start_time);
myTimer = new System.Timers.Timer();
myTimer.Interval = 1;
myTimer.Enabled = true;
myTimer.Elapsed += timer_Tick;
writeToLog("AB Repeat Commenced: While Playing");
break;
case PlayState.Paused:
pauseOn = true;
mbApi.Player_SetPosition(start_time);
BASS_ChannelPause(streamHandle);
writeToLog("AB Repeat Commenced: While Paused");
break;
}
}
private void timer_Tick(object sender, ElapsedEventArgs e)
{
if(mode == 2)
{
timer_Active = true;
position = toSeconds(BASS_ChannelGetPosition(streamHandle, 0));
if(position > end_time || position < start_time)
{
writeToLog("Loop ended: " + position);
mbApi.Player_SetPosition(start_time);
}
}
else
{
timer_Active = false;
writeToLog("Timing disabled");
myTimer.Enabled = false; // stop timing if repeat is disabled
}
}
private double toSeconds(long byteData)
{
double seconds = BASS_ChannelBytes2Seconds(streamHandle, byteData);
return seconds*1000;
}
public void InitialiseStream(int handle)
{
streamHandle = handle;
writeToLog("Stream Initialised: " + handle);
}
[System.Security.SuppressUnmanagedCodeSecurity()]
[DllImport("bass.dll", CharSet = CharSet.Auto)]
public static extern long BASS_ChannelGetPosition(int handle, int mode);
[System.Security.SuppressUnmanagedCodeSecurity()]
[DllImport("bass.dll", CharSet = CharSet.Auto)]
public static extern bool BASS_ChannelPause(int handle);
[System.Security.SuppressUnmanagedCodeSecurity()]
[DllImport("bass.dll", CharSet = CharSet.Auto)]
public static extern double BASS_ChannelBytes2Seconds(int handle, long byteData);
}
}