Plugin API
QUP Karaoke Engine — Plugin API
For developers. If you are looking for how to turn plugins on and off, see Plugin Manager instead.
Every plugin QUP ships — SongShop, OBS Recording, Discord Party, MIDI Karaoke, AutoFiller and the rest — is built against this same public API. Nothing in the engine is reserved for first-party code.
Package: com.qupkaraoke.plugin, distributed as qup-plugin-api.jar. Current as of v2.7.0.
The shortest possible plugin
package com.example.myplugin;
import com.qupkaraoke.plugin.*;
import javax.swing.JPanel;
public class MyPlugin implements QUPPlugin {
private PluginContext ctx;
public String getId() { return "com.example.myplugin"; }
public String getName() { return "My Plugin"; }
public String getVersion() { return "1.0.0"; }
public String getAuthor() { return "Me"; }
public String getDescription() { return "Does a thing."; }
public void init(PluginContext context) {
this.ctx = context;
ctx.getLogger().info("hello from My Plugin");
}
public JPanel getConfigPanel() { return null; } // no settings UI
public void onConfigPanelClosed() { /* persist settings here */ }
public void shutdown() { /* release resources here */ }
}
Package it as a JAR containing a file at:
META-INF/services/com.qupkaraoke.plugin.QUPPlugin
whose single line is your fully-qualified class name. Drop the JAR in QUP's plugins/ folder and restart.
That service file is always com.qupkaraoke.plugin.QUPPlugin, even when you implement one of the specialized interfaces below. QUP discovers everything as a QUPPlugin and then sorts by type.
Third-party libraries your plugin needs go in plugins/lib/. Resolution is parent-first, so a library the engine already ships always wins over a copy you place there. You can also compile against engine classes — the shipped plugins use the engine's own widgets (Knob, VuMeter, SkinTheme) so their panels match the app.
Starting QUP with --safe-mode skips all plugin loading for that session without changing which plugins are enabled.
Lifecycle
QUPPlugin is the interface every plugin implements. QUP calls, in order:
init(PluginContext)— you receive your API surfacegetConfigPanel()— your settings panel is registered in the config tree under PluginsonConfigPanelClosed()— every time the KJ closes the config window, and again just before shutdownshutdown()— QUP is exiting
| Method | Notes |
|---|---|
String getId() |
Unique, reverse-DNS. Also names your data folder. |
String getName() |
Shown in the config tree. |
String getVersion() |
|
String getAuthor() |
|
String getDescription() |
One line. |
void init(PluginContext context) |
The context is the only supported way to talk to QUP. |
JPanel getConfigPanel() |
Return null for no settings UI. |
void onConfigPanelClosed() |
default no-op. v2.5.0 |
void shutdown() |
Save settings in onConfigPanelClosed(), not shutdown()
This is the single most important convention in the API.
A karaoke show runs for hours. If you only persist on shutdown, a crash or a forced quit loses everything the KJ changed since launch. Worse, QUP abandons any plugin that takes longer than 3 seconds to shut down so a stuck plugin cannot freeze the app on exit — which can kill a save queued behind slow teardown.
onConfigPanelClosed() runs on the EDT while the window is closing, so keep it cheap and never block. It is a default no-op, so plugins written before v2.5.0 are unaffected. An exception thrown here is caught and logged and cannot stop other plugins from saving.
Your config panel must be light-themed
QUP's config panel is always light regardless of the app skin, and QUP does not enforce this for you. Use white backgrounds, black text and blue accents, or your panel will look broken next to everything else.
Where your files live
| Location | |
|---|---|
| Your JAR | APP_HOME/plugins/ — read-only on installed builds |
| Third-party JARs | APP_HOME/plugins/lib/ — read-only |
| Your data folder | DATA_HOME/plugins/data/{yourPluginId}/ |
| Your private database | DATA_HOME/plugins/data/{yourPluginId}/plugin.db |
DATA_HOME is the per-user data directory — %LOCALAPPDATA%\QUPKaraoke on Windows, ~/Library/Application Support/QUPKaraoke on macOS, $XDG_DATA_HOME/QUPKaraoke on Linux. In dev and portable builds it is the same folder as the install. Never write next to your JAR; on an installed build that location is read-only.
getPluginDatabase() gives you a SQLite connection with WAL enabled. It is yours entirely — create whatever tables you like. QUP never reads from it.
PluginContext
The whole API surface. Every method is safe to call from any thread, and callbacks come back on the Swing EDT.
Library and import
boolean importSong(SongImport song)
boolean isSongInLibrary(String externalId)
String getSongHashByExternalId(String externalId) // null if not found
List<SongInfo> queryAllSongs(boolean fillerOnly) // never null
boolean removeSongByHash(String songHash)
boolean removeFillerTrack(String filePath)
boolean reimportSong(String filePath, String artist, String title,
String cdid, int importFormat)
void refreshFileList()
void refreshMediaLibrary()
You download the file; QUP handles hashing, metadata extraction and database insertion. Removal takes the database entry only — the file on disk is never deleted.
Remove and reimport do not refresh the UI, deliberately, so a batch stays fast. Call refreshFileList() once when the batch is done. refreshMediaLibrary() is the one to call after batch metadata edits; it marshals to the EDT itself.
Metadata, genre and BPM
boolean updateSongGenre(String songHash, String genre)
int updateSongGenres(Map<String, String> hashToGenre) // one transaction
boolean updateFillerGenre(String filePath, String genre)
boolean updateSongMetadata(String songHash, String artist, String title,
String genre, String year)
boolean updateFillerMetadata(String filePath, String artist, String title,
String album, String genre)
double detectBpm(String filePath) // 0 if undecodable or no tempo found
void updateSongBpm(String songHash, double bpm)
double getSongBpm(String songHash) // 0 if not yet analyzed
Pass null for any metadata field to leave it unchanged. updateSongBpm() will never overwrite a BPM the KJ entered by hand.
Transport — v2.0.57
void karaokePlay() void fillerPlay()
void karaokePause() void fillerPause()
void karaokeStop() void fillerStop()
void karaokeNext() void fillerNext()
void karaokeSeek(int seconds) void fillerSeek(int seconds)
boolean isKaraokePlaying()
int getKaraokePositionSeconds() // -1 if unknown
int getKaraokeLengthSeconds() // -1 if unknown
Every command is a safe no-op when the target player is idle, and all of them go through the same paths as the KJ's own controls, so rotation and queue logic are respected. fillerPause() is best-effort: filler is a continuous radio-style player, so it pauses the countdown rather than hard-muting.
Queues — know which one you want
This is the easiest thing to get wrong. QUP has two separate queues.
// The separate filler music player
boolean addToFillerPlaylist(String filePath)
int addToFillerPlaylist(List<String> filePaths) // returns count enqueued
boolean addFillerUrl(String url, String displayName) // v2.0.57
int getFillerPlaylistSize()
int getFillerPlaylistIndex()
void clearFillerPlaylist()
boolean isFillerPlaying()
List<String> getFillerDatabasePaths() // never null
Map<String, String> getFillerGenres() // never null
int rescanFillerTags()
// The main karaoke playlist
boolean queueSong(String singerHash, String songHash)
boolean enqueueFileToPlaylist(String filePath)
boolean queueUrlToPlaylist(String url, String displayName) // v2.0.57
int getKaraokeUpcomingCount()
addToFillerPlaylist() and addFillerUrl() feed the background music player. enqueueFileToPlaylist() and queueUrlToPlaylist() append to the main karaoke playlist as a filler row — singer name "Filler" — which bypasses rotation entirely.
getFillerGenres() maps absolute path to genre; untagged tracks map to null or empty. rescanFillerTags() reads files off disk, so call it from a background thread.
UI
void registerMediaTab(String tabTitle, JPanel panel)
void registerMediaTab(String tabTitle, JComponent panel, String category) // v2.1.4
void registerLibraryMenu(LibraryTarget target, LibraryMenuProvider provider)
void registerKioskTab(String tabTitle, JPanel panel)
category is "Audio" (shown first) or "General" (the default). You own your tab's UI completely.
LibraryMenuProvider is a functional interface called fresh every time the right-click menu is about to open, with the current selection — so your entry can be fully dynamic. Return null to contribute nothing. LibraryTarget is KARAOKE, FILLER or BOTH.
Dialogs and notifications
void showNotification(String message, boolean isError) // non-blocking
boolean showConfirmDialog(String title, String message)
Native file dialogs — v2.0.54
File openFile(Component parent, String title)
File openFile(Component parent, String title, String startDir, FileFilterSpec... filters)
List<File> openFiles(Component parent, String title, FileFilterSpec... filters)
File openFolder(Component parent, String title, String startDir)
File saveFile(Component parent, String title, String suggestedName, FileFilterSpec... filters)
Genuinely native — IFileDialog on Windows, NSOpenPanel on macOS, GtkFileChooserNative on Linux — and they follow the OS light/dark theme. parent may be null.
All return null on cancel except openFiles(), which returns an empty list. FileFilterSpec normalises patterns, so "csv", ".csv" and "*.csv" all mean the same thing.
These replaced the older createFileChooser / createFolderChooser methods, which are removed.
Theming
Map<String, Color> getSkinColors()
int getListFontSize()
void applyStandardListStyle(JTable table) // v2.0.27
void addListStyleToggle(JPopupMenu popup) // v2.0.27
Colour keys: panelBg, panelBgDark, panelBorder, textPrimary, textDim, textActive, vfdKaraokeOn, vfdFillerOn, accent.
applyStandardListStyle() gives a table the skin's colours, alternating rows, the user's font size and the right-click style toggle in one call. It is safe to call repeatedly. If your table already has its own popup menu, use addListStyleToggle() to add just the toggle item to it.
getListFontSize() does not apply to config panels.
Singer identity — v2.2.1
boolean verifySingerPin(String singerName, String pin)
boolean singerHasPin(String singerName)
boolean singerExists(String singerName)
The PIN is never exposed to plugins. You may verify one; you must never receive, cache, log or persist it.
verifySingerPin() returns false for an unknown singer, a wrong PIN, and a singer with no PIN on file. The three are deliberately indistinguishable so a plugin cannot leak which singer names exist.
singerHasPin() exists for KJ-facing diagnostics only — "PIN required but none set" is a configuration problem a KJ cannot fix if they cannot see it. Never reflect it back to whoever is authenticating, because it reveals the name exists.
Name matching runs through SingerNames.normalize(), the same rule the rotation uses at showtime, so "verified" and "matched on stage" cannot disagree.
Audio
AudioSink openAudioSink(AudioFormat format) // v2.1.4; null if audio unavailable
void setFillerAudioTap(AudioTap tap) // v2.2.1; null removes
void setKaraokeAudioTap(AudioTap tap) // v2.6.0; null removes
App info and automation
String getAppVersion()
int getAppMajorVersion()
Logger getLogger()
File getPluginDataDir()
Connection getPluginDatabase() // null if the SQLite driver is unavailable
Automation getAutomation() // v2.0.57
boolean isDjMode() // v2.8.0
isDjMode() — whether QUP is currently running in DJ mode rather than karaoke mode. DJ mode has no singer rotation, no filler bed, and no kiosk queue — a plugin whose menu entries or behavior assume any of those (an auto-fill queue, a karaoke-only transport) should query this and self-gate rather than have the host hide its items for it.
Specialized plugin interfaces
Each extends QUPPlugin, so implement those eight members too, and register under the same service file.
SongStorePlugin — sell and deliver songs
int fetchCatalog() // entry count, or -1 on error
boolean onPurchaseRequested(CatalogEntry entry, String singerHash)
int getDownloadProgress() // 0-100, or -1 when idle
List<CatalogEntry> getCachedCatalog()
boolean requiresKJApproval()
Both fetchCatalog() and onPurchaseRequested() run on background threads, so network I/O is fine. singerHash is null when the KJ initiated the purchase.
Two models are supported: URL-redirect, where the entry carries a purchaseUrl, you open it in a browser and watch a folder for the file; and server-mediated, where you talk to a vendor API, download, importSong(), then optionally queueSong().
If you only import local files, implement QUPPlugin directly instead.
PlaybackListenerPlugin — react to what is playing
void onSongStarted(PlaybackEvent event)
void onSongEnded()
default void onNextSongPrefetch(PlaybackEvent event) {}
default void onQueuePrefetch(List<PlaybackEvent> upcoming) {}
default File getVideoForSong(String songHash) { return null; }
default boolean isVideoProvider() { return false; }
default boolean startVideoWithAudio() { return false; } // v2.6.0
default float getVideoDimLevel() { return 0.5f; } // v2.6.0
Callbacks run on background threads — except getVideoForSong(), which runs on the playback thread and must return immediately. If the video is not cached, return null and fetch it in the background from onSongStarted or onNextSongPrefetch.
onQueuePrefetch() may be called repeatedly as the queue changes, so dedupe and skip what you have already cached.
startVideoWithAudio() holds the background on its first frame until karaoke audio is actually heard, then starts it and offsets the clock by the measured delay. Karaoke tracks often open with a second or more of silence, and because the background is slaved to the song clock — which starts at zero regardless — the picture otherwise runs ahead of the music for the whole song. getVideoDimLevel() is read live while compositing rather than latched at song start, so a brightness control responds while the KJ drags it. Both are ignored unless isVideoProvider() is also true.
Filler entries also fire onSongStarted, carrying the placeholder singer name "Filler". If your plugin acts on the singer — announcing, promoting, paging them — check event.isFiller() and skip those.
FillerPlugin — drive the background music
void onFillerPlaylistLow(int remainingSongs)
void onFillerSongEnded(SongInfo song) // song may be null for streams
default int getMinQueueDepth() { return 3; }
default boolean isActiveFillerSource() { return true; }
Respond to onFillerPlaylistLow() by calling addToFillerPlaylist(). Only one filler plugin is active at a time; if several are installed the KJ picks.
PlaybackSourcePlugin — add a new playable format — v2.1.4
Set<String> supportedExtensions() // lower-case, dot-prefixed
boolean canPlay(File file)
SourcePlayer open(File file, PlaybackParams params) // null declines
This is the seam that makes a new format a plugin instead of a core rewrite — MIDI karaoke is built entirely on it. When the KJ plays a file the engine cannot decode natively, the first source plugin that claims it supplies a SourcePlayer the engine drives exactly like a CDG or MP4 song.
Sniff the file header in canPlay() rather than trusting the extension. open() should parse and prepare but not start playing.
VisualizationPlugin — draw behind the lyrics — v2.6.0
default void onVisualizationStart(int width, int height) {}
BufferedImage renderFrame(int width, int height, double elapsedSeconds);
default void onVisualizationStop() {}
default boolean hasContentFor(String songHash) { return true; }
Whatever is drawn under the lyrics on the projector: a music video, an audio-reactive visualizer, a slideshow, a solid colour — anything that renders into a BufferedImage. Music Video Background is itself one of these.
Exactly one is active at a time, chosen by the KJ in Config → Plugins. Unlike FillerPlugin, plugins do not self-report being active: the engine owns the choice and persists it, so two installed visualizations can never fight over the one projector.
renderFrame() runs on the compositing thread at roughly 30fps. Return null to keep the previous frame, so a slow generator degrades to a lower frame rate rather than flickering. It must return promptly — that thread also composites the lyrics, so a slow frame stalls the whole projector image. An exception is caught and the visualization is dropped for the rest of the song rather than being re-entered every frame.
A negative elapsedSeconds means hold: the engine wants the background parked rather than advancing, used to sit out a track's leading silence. A generated visualization can treat it as zero; anything playing pre-recorded material should stay on its first frame until it goes positive.
To react to audio, register an AudioTap via setKaraokeAudioTap() in init() — a visualizer needs the song the room is hearing, not the filler bed underneath it. Buffer in the tap, do the analysis in renderFrame(); never run an FFT inside the tap itself.
Readability comes first. The KJ's brightness control is applied after renderFrame() returns, but a visualization that stays busy and high-contrast where the words sit will be unreadable no matter how far it is dimmed.
VisualizationPlugin — direct GL rendering — v2.8.0
default boolean supportsDirectGl() { return false; }
default boolean glAdopt(int width, int height) { return false; }
default void glRenderFrame(int width, int height, double elapsedSeconds) {}
default void glRelease() {}
default void publishHostFrame(BufferedImage frame) {}
The methods above (onVisualizationStart/renderFrame/onVisualizationStop) are the readback contract: the plugin renders somewhere of its own and hands back a BufferedImage, which the compositor draws with Java2D. That costs a full glReadPixels every frame, forever, for a visualization that was already OpenGL to begin with.
These methods let a host that owns a GL context invite the plugin to draw straight into it instead. The host makes its context current on its own render thread and calls glRenderFrame there; the plugin renders into whatever framebuffer is bound and does not create a context of its own. All five are defaulted, so every existing plugin compiles and loads unchanged — a host that gets false from supportsDirectGl() simply falls back to renderFrame().
supportsDirectGl()— answeringtrueis a promise about threading as much as about drawing: the plugin must be able to run its whole render on the host's thread, and must not create, destroy, or make-current any context of its own while the host's is adopted.glAdopt(width, height)— called once per context generation (startup, and again after a context loss and rebuild), on the thread that owns the context for its lifetime and is the only thread that will ever call the othergl*methods. Returntrueif ready to render into this context.glRenderFrame(width, height, elapsedSeconds)— render one frame into the currently bound framebuffer, on the thread that calledglAdopt. GL state may be left dirty; the host re-establishes everything it depends on each frame. Must not swap buffers or destroy the context.glRelease()— release GL resources, on the adopting thread, while that context is still current, before the host tears its own context down.publishHostFrame(frame)— a small copy of what the host just drew, sorenderFrame()keeps answering for any other consumer (a config pane preview, for instance) while the host owns the context — without this they'd see nothing, or start a second renderer beside the host's.
VisualizationPresets — let the KJ pick a look — v2.7.0
List<String> getPresetNames();
int getCurrentPresetIndex();
void selectPreset(int index);
void nextPreset();
void randomPreset();
boolean isCycling(); void setCycling(boolean cycling);
int getCycleSeconds(); void setCycleSeconds(int seconds);
boolean isShuffle(); void setShuffle(boolean shuffle);
default void rescanPresets() {}
Optional companion to VisualizationPlugin, for visualizations that have named looks. Implement it and QUP renders preset controls beside the live preview under Media Library → Plugins → Backgrounds: a searchable list, Next and Random, a cycle toggle with interval, and shuffle. A visualization with one fixed look simply does not implement it and no controls appear.
Every method is called on the Swing EDT, and selection is a request, not a switch. If your visualization holds a GPU context, that context belongs to one thread for its lifetime — loading a preset from a button handler is undefined behaviour that typically takes the whole JVM down rather than throwing. Record what was asked for and act on it from whatever thread owns your rendering. The bundled MilkDrop plugin stores a pending index in a volatile field and picks it up on its next render pass.
Two behaviours worth matching, because they are the difference between the control working and appearing not to: a manual pick should restart the cycle interval, so a deliberate choice is not replaced a second later by a change that was already due; and cycling off should pin the current preset indefinitely.
Persist the current preset by name, not by index. Preset folders gain and lose files between runs, and an index saved yesterday points somewhere else today.
VisualizationPresetPlaylists — named subsets of those presets — v2.7.0
List<String> getPlaylistNames();
boolean isPlaylistEnabled(String playlist);
void setPlaylistEnabled(String playlist, boolean enabled);
String createPlaylist(String playlist); // returns the stored name, or null
void deletePlaylist(String playlist);
void addToPlaylist(String playlist, int presetIndex);
void removeFromPlaylist(String playlist, int presetIndex);
List<String> getPlaylistContents(String playlist);
int getCyclePoolSize();
A real MilkDrop pack is around ten thousand presets. Left alone the shuffle wanders all of them, and a KJ who has found the forty that suit a Friday night has no way to say so. A playlist is a named list of presets; ticking one or more restricts cycling to their union, and with none ticked everything is in play.
Also EDT-only, and the same threading warning applies with more force: your render thread is reading the pool while these calls mutate it. Publish the pool as an immutable snapshot — build a new array and swap it in — rather than letting the render thread walk a collection the EDT is editing.
getPlaylistContents() returns names, not indices, for the same reason preset selection is persisted by name: an index list is stale the moment a rescan lands between the call and its use.
Two behaviours to match: an empty ticked playlist should fall back to the whole set rather than leaving the screen with nothing to cycle through, and a pool change should affect the next switch only — deleting the playlist that put the current preset on screen must not yank the picture out from under the room.
Every mutating method is expected to persist immediately. There is no commit or close hook: the KJ ticking a playlist mid-set is the decision.
AudioDspPlugin — process audio in the path
void onAudioReady()
void onAudioStopping()
For plugins that pull captured input, run DSP, and write the result as a summed input on the mains bus — the shipped mic reverb/delay strip is the reference implementation.
An audio-DSP plugin is an async writer. Never do per-sample DSP inside the real-time callback. Release your input taps and bus inputs in onAudioStopping().
Audio interfaces
AudioSink — emit audio — v2.1.4
Obtained from openAudioSink(). On ASIO it is a summed input on the ReaRoute mains bus alongside karaoke and filler; otherwise a normal output device.
void write(byte[] pcm, int off, int len) // blocks with the output
void setVolume(float volume) // 0..1
double getSampleRate()
int getPlaybackLatencyFrames() // 0 if unknown
void flush()
void close()
Write signed 16-bit little-endian, interleaved stereo at getSampleRate() — set your render format to that so nothing has to resample. Use getPlaybackLatencyFrames() to slave a lyric cursor exactly. Close the sink when your source stops.
AudioTap — observe audio — v2.2.1
void onAudio(byte[] pcm, int offset, int length, AudioFormat format)
The counterpart to AudioSink. The tap sits at the very end of the filler player's chain — post-EQ, post-crossfade, immediately before the buffer goes to the device — so it hears exactly what the room hears, including filler ducking as a karaoke song starts. It is the same code point on both output paths, so it behaves identically with or without ASIO.
Read this before implementing one. onAudio is called on the player's audio thread, inline in the decode/write loop. It must return promptly and must not block, allocate heavily, take contended locks, or touch Swing. Copy what you need into a bounded structure you drain elsewhere, and drop rather than wait when that structure is full — a slow tap will backpressure and glitch the venue's audio. A tap that throws repeatedly may be dropped.
The pcm array is reused on the next iteration, so copy anything you intend to keep. Sample rate, channel count and sample size vary per track: read them from format rather than assuming 44.1 kHz stereo.
Each plugin has at most one filler tap; setting a new one replaces the old.
SourcePlayer — play a format you added — v2.1.4
Returned from PlaybackSourcePlugin.open(). Your source owns the master clock; the engine slaves lyrics and graphics to it.
void play() void stop() void pause() void resume()
boolean isPlaying() boolean isPaused()
void seekTo(double seconds)
double getCurrentTimeMs() // note: milliseconds
double getSongLengthSeconds()
void setVolume(int percent) // 0..100
default void setCrossfadeGain(float gain) {} // 0..1, on top of setVolume
void setTranspose(int semitones)
void setTempoRatio(double ratio) // 1.0 = original
void setEndListener(Runnable onEnd)
boolean hasVisual()
void paint(Graphics2D g, int width, int height)
void close()
You must fire the end listener when the song finishes — it is how the engine advances the playlist. A source that never calls it stalls the rotation.
paint() is called on both the projector and preview surfaces at their own pixel sizes; read your own clock for the current position. seekTo() should replay prior controller state so timbres stay correct.
Data types
| Type | Notes |
|---|---|
SongInfo |
Immutable song metadata. Note getCDID() capitalisation. getFullPath() joins path + filename; filename already includes the extension. |
SongImport |
Describes a song to import. You download the file; QUP hashes and inserts it. pluginId is set for you. |
CatalogEntry |
A purchasable song. Maps to the OpenKJ apigetsongs_v2 shape. purchaseUrl is null for server-mediated stores. pluginId is set for you. |
PlaybackEvent |
Song + singer info for playback callbacks. Check isFiller(). |
PlaybackParams |
Public final fields pitchSemitones, tempoPercent. PlaybackParams.defaults() is (0, 0). |
FileFilterSpec |
new FileFilterSpec("Audio Files (*.mp3)", "mp3"). Accessors are description() and extensions(). |
LibraryTarget |
KARAOKE, FILLER, BOTH. |
SingerNames |
normalize(String), sameSinger(String, String). |
SkinnedDialog |
Undecorated dialog with a skin-painted title bar. Add to getContentPanel(), show with showCentered(w, h). |
SingerNames is deliberately not fuzzy
It folds Unicode to NFKC, reduces every Unicode space separator — including U+00A0, which \s does not match and phone keyboards insert freely — to plain blanks, collapses runs, trims and lower-cases.
It never computes edit distance, never drops punctuation and never matches a prefix. "Bob" and "Bobby" stay different people. Route your own singer matching through it so your plugin and the rotation always agree.
Automation — v2.0.57
getAutomation() returns a QA surface for driving and observing the engine with nobody watching or listening.
File captureScreen(File pngOut)
File captureWindow(String window, File pngOut) // "main", "cdg", "config"
List<String> configPaneNames()
void showConfigPane(String name)
void captureConfigPanes(File dir)
String diagnostics() // JSON
void playTestTone(double freqHz, double seconds)
void playTestSweep(double startHz, double endHz, double seconds)
void playTestNoise(double seconds)
void stopTestSignal()
Test signals route through the real karaoke DSP chain — SoundTouch pitch/tempo and EQ — so a loopback recording can be FFT-analysed to verify key change, EQ and "is there sound at all" objectively.
The whole surface is best-effort: methods return null or do nothing rather than throwing when a target is unavailable, such as in a headless run.
Threading, in one table
| Where | Thread | Rule |
|---|---|---|
PluginContext, all methods |
any | Safe from anywhere. |
Automation, all methods |
any | GUI actions marshal to the EDT. |
QUPPlugin.onConfigPanelClosed() |
EDT | Cheap, non-blocking. |
PlaybackListenerPlugin callbacks |
background | Network I/O fine. |
PlaybackListenerPlugin.getVideoForSong() |
playback | Return immediately. |
FillerPlugin callbacks |
background | Network I/O fine. |
SongStorePlugin catalog/purchase |
background | Network I/O fine. |
AudioTap.onAudio() |
audio | No blocking, no allocation, no Swing. |
AudioDspPlugin DSP |
your own | Never inside the RT callback. |
PluginContext.rescanFillerTags() |
caller's | Call it off the EDT; it reads files. |
VisualizationPlugin.renderFrame() |
compositing | Return promptly; it also draws the lyrics. |
Version history
| Version | Added |
|---|---|
| v2.8.0 | PluginContext.isDjMode(), VisualizationPlugin direct GL rendering (supportsDirectGl(), glAdopt(), glRenderFrame(), glRelease(), publishHostFrame()) |
| v2.7.0 | VisualizationPresets, VisualizationPresetPlaylists |
| v2.6.0 | VisualizationPlugin, setKaraokeAudioTap(), PlaybackListenerPlugin.startVideoWithAudio(), PlaybackListenerPlugin.getVideoDimLevel() |
| v2.5.0 | QUPPlugin.onConfigPanelClosed() |
| v2.2.1 | setFillerAudioTap(), AudioTap, verifySingerPin(), singerHasPin(), singerExists(), SingerNames, PlaybackEvent.isFiller() |
| v2.1.4 | registerMediaTab() with category, openAudioSink(), AudioSink, PlaybackSourcePlugin, SourcePlayer, PlaybackParams, SourcePlayer.setCrossfadeGain() |
| v2.0.57 | Transport control, addFillerUrl(), queueUrlToPlaylist(), getAutomation(), Automation |
| v2.0.54 | Native file dialogs, FileFilterSpec. Removed createFileChooser() / createFolderChooser(). |
| v2.0.27 | applyStandardListStyle(), addListStyleToggle() |