diff --git a/WaveSurferSB.sbi b/WaveSurferSB.sbi new file mode 100644 index 0000000..3da1e5a --- /dev/null +++ b/WaveSurferSB.sbi @@ -0,0 +1,577 @@ +; WaveSurferSB - A wavesurfer.js v7 wrapper for SpiderBasic +; +; This library wraps the wavesurfer.js audio waveform player for use in SpiderBasic. +; Includes support for Timeline, Minimap, and Spectrogram plugins. +; wavesurfer.js v7: https://wavesurfer.xyz / https://github.com/katspaugh/wavesurfer.js +; +; Usage: +; IncludeFile "WaveSurferSB/WaveSurferSB.sbi" +; +; ; Load core + plugins (comma-separated: "timeline,minimap,spectrogram") +; WaveSurferSB::Download(@MyCallback(), "timeline,minimap,spectrogram") +; +; Procedure MyCallback(Success) +; If Success +; Define ws = WaveSurferSB::Create("#myContainer", WaveSurferSB::#DragToSeek) +; WaveSurferSB::TimelineCreate(ws) +; WaveSurferSB::MinimapCreate(ws, 50, "#777", "#333") +; WaveSurferSB::SpectrogramCreate(ws, "", 128, #True) +; WaveSurferSB::Load(ws, "audio.mp3") +; EndIf +; EndProcedure +; +; For wavesurfer.js documentation, see: https://wavesurfer.xyz/docs + +DeclareModule WaveSurferSB + + ;{ Initialization + ; Plugins.s: comma-separated plugin names to load, e.g. "timeline,minimap,spectrogram" + Declare Download(*Callback, Plugins.s = "", UseLocalFiles = #False) + ;} + + ;{ Instance Management + Declare Create(Container.s, Flags = 0) + Declare Create_ex(Container.s, OptionsJSON.s) + Declare Destroy(Instance) + Declare Empty(Instance) + ;} + + ;{ Audio Loading + Declare Load(Instance, URL.s) + Declare LoadWithPeaks(Instance, URL.s, PeaksJSON.s, Duration.d = 0.0) + ;} + + ;{ Playback Control + Declare Play(Instance) + Declare PlayFrom(Instance, StartSeconds.d, EndSeconds.d = -1.0) + Declare Pause(Instance) + Declare PlayPause(Instance) + Declare Stop(Instance) + Declare IsPlaying(Instance) + ;} + + ;{ Navigation + Declare SetTime(Instance, TimeSeconds.d) + Declare.d GetCurrentTime(Instance) + Declare.d GetDuration(Instance) + Declare SeekTo(Instance, Progress.d) + ;} + + ;{ Volume & Speed + Declare SetVolume(Instance, Volume.d) + Declare.d GetVolume(Instance) + Declare SetMuted(Instance, Muted) + Declare SetPlaybackRate(Instance, Rate.d) + Declare.d GetPlaybackRate(Instance) + ;} + + ;{ Visual Options + Declare SetOptions(Instance, OptionsJSON.s) + Declare Zoom(Instance, MinPxPerSec.d) + ;} + + ;{ Generic Plugin Registration + Declare RegisterPlugin(Instance, PluginName.s, OptionsJSON.s = "{}") + Declare DestroyPlugin(PluginInstance) + ;} + + ;{ Timeline Plugin + Declare TimelineCreate(Instance, Container.s = "", Height = 20) + Declare TimelineCreate_ex(Instance, OptionsJSON.s) + Declare TimelineDestroy(PluginInstance) + ;} + + ;{ Minimap Plugin + Declare MinimapCreate(Instance, Height = 50, WaveColor.s = "#999", ProgressColor.s = "#555", OverlayColor.s = "") + Declare MinimapCreate_ex(Instance, OptionsJSON.s) + Declare MinimapDestroy(PluginInstance) + ;} + + ;{ Spectrogram Plugin + Declare SpectrogramCreate(Instance, Container.s = "", Height = 128, Labels = #False) + Declare SpectrogramCreate_ex(Instance, OptionsJSON.s) + Declare SpectrogramDestroy(PluginInstance) + ;} + + ;{ Events + Declare OnReady(Instance, *Callback) + Declare OnPlay(Instance, *Callback) + Declare OnPause(Instance, *Callback) + Declare OnFinish(Instance, *Callback) + Declare OnTimeUpdate(Instance, *Callback) + Declare OnSeeking(Instance, *Callback) + Declare OnInteraction(Instance, *Callback) + Declare OnClick(Instance, *Callback) + Declare OnDecode(Instance, *Callback) + Declare OnLoading(Instance, *Callback) + Declare OnDestroy(Instance, *Callback) + Declare OnEvent(Instance, EventName.s, *Callback) + ;} + + ;{ Data Export + Declare.s GetMediaElement(Instance) + ;} + + ;{ Creation Flag Constants + #Default = 0 + #Normalize = 1 << 0 + #AutoPlay = 1 << 1 + #DragToSeek = 1 << 2 + #HideScrollbar = 1 << 3 + #AutoScroll = 1 << 4 + #AutoCenter = 1 << 5 + #SplitChannels = 1 << 6 + #BarStyle = 1 << 7 + ;} + +EndDeclareModule + +Module WaveSurferSB + EnableExplicit + + ;{ Private Variables + Global download_callback + Global is_loaded = #False + Global plugins_to_load.s = "" + Global use_local.i = #False + ;} + + ;{ Private Declarations + Declare Handler_Download(url.s, success) + Declare Handler_PluginDownload(url.s, success) + Declare _HideAMD() + Declare _RestoreAMD() + Declare _LoadNextPlugin() + ;} + + ; AMD/UMD Helpers + Procedure _HideAMD() + !window._ws_saved_module = (typeof module !== 'undefined') ? module : undefined; + !window._ws_saved_exports = (typeof exports !== 'undefined') ? exports : undefined; + !window._ws_saved_define = (typeof define !== 'undefined') ? define : undefined; + !try { module = undefined; } catch(e) {} + !try { exports = undefined; } catch(e) {} + !try { define = undefined; } catch(e) {} + EndProcedure + + Procedure _RestoreAMD() + !try { if (window._ws_saved_module !== undefined) module = window._ws_saved_module; } catch(e) {} + !try { if (window._ws_saved_exports !== undefined) exports = window._ws_saved_exports; } catch(e) {} + !try { if (window._ws_saved_define !== undefined) define = window._ws_saved_define; } catch(e) {} + EndProcedure + + ; Plugin Loading Chain + Procedure _LoadNextPlugin() + Protected plugin.s, url.s + + ; Pop the first plugin from the comma-separated list + !v_plugin = wavesurfersb$g_plugins_to_load.split(',')[0] || ''; + !wavesurfersb$g_plugins_to_load = wavesurfersb$g_plugins_to_load.split(',').slice(1).join(','); + + !v_plugin = v_plugin.trim(); + + Protected check + !v_check = (v_plugin === ''); + + If check + ; All plugins loaded - restore AMD and call user callback + _RestoreAMD() + is_loaded = #True + !wavesurfersb$g_download_callback(1); + ProcedureReturn + EndIf + + If use_local + url = "LocalFiles/JS/wavesurfer-" + plugin + ".min.js" + Else + url = "https://unpkg.com/wavesurfer.js@7/dist/plugins/" + plugin + ".min.js" + EndIf + + LoadScript(url, @Handler_PluginDownload(), #PB_Script_JavaScript) + EndProcedure + + ; Initialization + Procedure Download(*Callback, Plugins.s = "", UseLocalFiles = #False) + download_callback = *Callback + plugins_to_load = Plugins + use_local = UseLocalFiles + + _HideAMD() + + If UseLocalFiles + LoadScript("LocalFiles/JS/wavesurfer.min.js", @Handler_Download(), #PB_Script_JavaScript) + Else + LoadScript("https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js", @Handler_Download(), #PB_Script_JavaScript) + EndIf + EndProcedure + + ; Instance Management + Procedure Create(Container.s, Flags = 0) + Protected Instance + + ; Build options object in JavaScript + !var _opts = { container: v_container }; + + If Flags & #Normalize + !_opts.normalize = true; + EndIf + + If Flags & #AutoPlay + !_opts.autoplay = true; + EndIf + + If Flags & #DragToSeek + !_opts.dragToSeek = true; + EndIf + + If Flags & #HideScrollbar + !_opts.hideScrollbar = true; + EndIf + + If Flags & #AutoScroll + !_opts.autoScroll = true; + EndIf + + If Flags & #AutoCenter + !_opts.autoCenter = true; + EndIf + + If Flags & #SplitChannels + !_opts.splitChannels = [{}]; + EndIf + + If Flags & #BarStyle + !_opts.barWidth = 2; + !_opts.barGap = 1; + !_opts.barRadius = 2; + EndIf + + !v_instance = WaveSurfer.create(_opts); + ProcedureReturn Instance + EndProcedure + + Procedure Create_ex(Container.s, OptionsJSON.s) + Protected Instance + + !var _opts; + !try { _opts = JSON.parse(v_optionsjson); } catch(e) { _opts = {}; } + !_opts.container = v_container; + !v_instance = WaveSurfer.create(_opts); + + ProcedureReturn Instance + EndProcedure + + Procedure Destroy(Instance) + !if (v_instance && typeof v_instance.destroy === 'function') { + ! v_instance.destroy(); + !} + EndProcedure + + Procedure Empty(Instance) + !if (v_instance && typeof v_instance.empty === 'function') { + ! v_instance.empty(); + !} + EndProcedure + + ; Audio Loading + Procedure Load(Instance, URL.s) + !if (v_instance) v_instance.load(v_url); + EndProcedure + + Procedure LoadWithPeaks(Instance, URL.s, PeaksJSON.s, Duration.d = 0.0) + !if (v_instance) { + ! var _peaks; + ! try { _peaks = JSON.parse(v_peaksjson); } catch(e) { _peaks = undefined; } + ! var _dur = v_duration > 0 ? v_duration : undefined; + ! v_instance.load(v_url, _peaks, _dur); + !} + EndProcedure + + ; Playback Control + Procedure Play(Instance) + !if (v_instance) v_instance.play(); + EndProcedure + + Procedure PlayFrom(Instance, StartSeconds.d, EndSeconds.d = -1.0) + !if (v_instance) { + ! if (v_endseconds >= 0) { + ! v_instance.play(v_startseconds, v_endseconds); + ! } else { + ! v_instance.play(v_startseconds); + ! } + !} + EndProcedure + + Procedure Pause(Instance) + !if (v_instance) v_instance.pause(); + EndProcedure + + Procedure PlayPause(Instance) + !if (v_instance) v_instance.playPause(); + EndProcedure + + Procedure Stop(Instance) + !if (v_instance) v_instance.stop(); + EndProcedure + + Procedure IsPlaying(Instance) + Protected Result = #False + !if (v_instance) v_result = v_instance.isPlaying(); + ProcedureReturn Result + EndProcedure + + ; Navigation + Procedure SetTime(Instance, TimeSeconds.d) + !if (v_instance) v_instance.setTime(v_timeseconds); + EndProcedure + + Procedure.d GetCurrentTime(Instance) + Protected.d Result = 0.0 + !if (v_instance) v_result = v_instance.getCurrentTime(); + ProcedureReturn Result + EndProcedure + + Procedure.d GetDuration(Instance) + Protected.d Result = 0.0 + !if (v_instance) v_result = v_instance.getDuration(); + ProcedureReturn Result + EndProcedure + + Procedure SeekTo(Instance, Progress.d) + !if (v_instance) v_instance.seekTo(v_progress); + EndProcedure + + ; Volume & Speed + Procedure SetVolume(Instance, Volume.d) + !if (v_instance) v_instance.setVolume(v_volume); + EndProcedure + + Procedure.d GetVolume(Instance) + Protected.d Result = 1.0 + !if (v_instance) v_result = v_instance.getVolume(); + ProcedureReturn Result + EndProcedure + + Procedure SetMuted(Instance, Muted) + !if (v_instance) v_instance.setMuted(!!v_muted); + EndProcedure + + Procedure SetPlaybackRate(Instance, Rate.d) + !if (v_instance) v_instance.setPlaybackRate(v_rate); + EndProcedure + + Procedure.d GetPlaybackRate(Instance) + Protected.d Result = 1.0 + !if (v_instance) v_result = v_instance.getPlaybackRate(); + ProcedureReturn Result + EndProcedure + + ; Visual Options + Procedure SetOptions(Instance, OptionsJSON.s) + !if (v_instance) { + ! var _opts; + ! try { _opts = JSON.parse(v_optionsjson); } catch(e) { _opts = {}; } + ! v_instance.setOptions(_opts); + !} + EndProcedure + + Procedure Zoom(Instance, MinPxPerSec.d) + !if (v_instance) v_instance.zoom(v_minpxpersec); + EndProcedure + + ; Generic Plugin Registration + Procedure RegisterPlugin(Instance, PluginName.s, OptionsJSON.s = "{}") + Protected PluginInstance + !if (v_instance && WaveSurfer[v_pluginname]) { + ! var _opts; + ! try { _opts = JSON.parse(v_optionsjson); } catch(e) { _opts = {}; } + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer[v_pluginname].create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure DestroyPlugin(PluginInstance) + !if (v_plugininstance && typeof v_plugininstance.destroy === 'function') { + ! v_plugininstance.destroy(); + !} + EndProcedure + + ; Timeline Plugin + Procedure TimelineCreate(Instance, Container.s = "", Height = 20) + Protected PluginInstance + !if (v_instance && WaveSurfer.Timeline) { + ! var _opts = { height: v_height }; + ! if (v_container && v_container !== '') _opts.container = v_container; + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer.Timeline.create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure TimelineCreate_ex(Instance, OptionsJSON.s) + Protected PluginInstance + !if (v_instance && WaveSurfer.Timeline) { + ! var _opts; + ! try { _opts = JSON.parse(v_optionsjson); } catch(e) { _opts = {}; } + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer.Timeline.create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure TimelineDestroy(PluginInstance) + DestroyPlugin(PluginInstance) + EndProcedure + + ; Minimap Plugin + Procedure MinimapCreate(Instance, Height = 50, WaveColor.s = "#999", ProgressColor.s = "#555", OverlayColor.s = "") + Protected PluginInstance + !if (v_instance && WaveSurfer.Minimap) { + ! var _opts = { + ! height: v_height, + ! waveColor: v_wavecolor, + ! progressColor: v_progresscolor + ! }; + ! if (v_overlaycolor && v_overlaycolor !== '') _opts.overlayColor = v_overlaycolor; + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer.Minimap.create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure MinimapCreate_ex(Instance, OptionsJSON.s) + Protected PluginInstance + !if (v_instance && WaveSurfer.Minimap) { + ! var _opts; + ! try { _opts = JSON.parse(v_optionsjson); } catch(e) { _opts = {}; } + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer.Minimap.create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure MinimapDestroy(PluginInstance) + DestroyPlugin(PluginInstance) + EndProcedure + + ; Spectrogram Plugin + Procedure SpectrogramCreate(Instance, Container.s = "", Height = 128, Labels = #False) + Protected PluginInstance + !if (v_instance && WaveSurfer.Spectrogram) { + ! var _opts = { + ! height: v_height, + ! labels: !!v_labels + ! }; + ! if (v_container && v_container !== '') _opts.container = v_container; + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer.Spectrogram.create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure SpectrogramCreate_ex(Instance, OptionsJSON.s) + Protected PluginInstance + !if (v_instance && WaveSurfer.Spectrogram) { + ! var _opts; + ! try { _opts = JSON.parse(v_optionsjson); } catch(e) { _opts = {}; } + ! v_plugininstance = v_instance.registerPlugin(WaveSurfer.Spectrogram.create(_opts)); + !} + ProcedureReturn PluginInstance + EndProcedure + + Procedure SpectrogramDestroy(PluginInstance) + DestroyPlugin(PluginInstance) + EndProcedure + + ; Events + Procedure OnReady(Instance, *Callback) + !if (v_instance) v_instance.on('ready', function(duration) { p_callback(duration); }); + EndProcedure + + Procedure OnPlay(Instance, *Callback) + !if (v_instance) v_instance.on('play', function() { p_callback(); }); + EndProcedure + + Procedure OnPause(Instance, *Callback) + !if (v_instance) v_instance.on('pause', function() { p_callback(); }); + EndProcedure + + Procedure OnFinish(Instance, *Callback) + !if (v_instance) v_instance.on('finish', function() { p_callback(); }); + EndProcedure + + Procedure OnTimeUpdate(Instance, *Callback) + !if (v_instance) v_instance.on('timeupdate', function(currentTime) { p_callback(currentTime); }); + EndProcedure + + Procedure OnSeeking(Instance, *Callback) + !if (v_instance) v_instance.on('seeking', function(currentTime) { p_callback(currentTime); }); + EndProcedure + + Procedure OnInteraction(Instance, *Callback) + !if (v_instance) v_instance.on('interaction', function(newTime) { p_callback(newTime); }); + EndProcedure + + Procedure OnClick(Instance, *Callback) + !if (v_instance) v_instance.on('click', function(relativeX) { p_callback(relativeX); }); + EndProcedure + + Procedure OnDecode(Instance, *Callback) + !if (v_instance) v_instance.on('decode', function(duration) { p_callback(duration); }); + EndProcedure + + Procedure OnLoading(Instance, *Callback) + !if (v_instance) v_instance.on('loading', function(percent) { p_callback(percent); }); + EndProcedure + + Procedure OnDestroy(Instance, *Callback) + !if (v_instance) v_instance.on('destroy', function() { p_callback(); }); + EndProcedure + + Procedure OnEvent(Instance, EventName.s, *Callback) + !if (v_instance) v_instance.on(v_eventname, function() { p_callback(); }); + EndProcedure + + ; Data Export + Procedure.s GetMediaElement(Instance) + Protected Result.s = "" + !if (v_instance) { + ! var _media = v_instance.getMediaElement(); + ! if (_media) v_result = _media.outerHTML || ''; + !} + ProcedureReturn Result + EndProcedure + + ; Private Procedures + Procedure Handler_Download(url.s, success) + If success + ; Core loaded - now load plugins if any + If plugins_to_load <> "" + _LoadNextPlugin() + Else + ; No plugins requested - all done + _RestoreAMD() + is_loaded = #True + !wavesurfersb$g_download_callback(1); + EndIf + Else + _RestoreAMD() + !wavesurfersb$g_download_callback(0); + EndIf + EndProcedure + + Procedure Handler_PluginDownload(url.s, success) + If success + ; Plugin loaded - continue chain with next plugin + _LoadNextPlugin() + Else + _RestoreAMD() + !wavesurfersb$g_download_callback(0); + EndIf + EndProcedure + +EndModule + +; IDE Options = SpiderBasic 3.20 (Windows - x86) +; Folding = BAAAAAAAAAAg +; iOSAppOrientation = 0 +; AndroidAppCode = 0 +; AndroidAppOrientation = 0 +; EnableXP +; DPIAware +; CompileSourceDirectory \ No newline at end of file diff --git a/WaveSurferSB_Plugins_Example_1.sb b/WaveSurferSB_Plugins_Example_1.sb new file mode 100644 index 0000000..a98bedd --- /dev/null +++ b/WaveSurferSB_Plugins_Example_1.sb @@ -0,0 +1,144 @@ +; WaveSurferSB Plugins Example +; Demonstrates Timeline, Minimap, and Spectrogram plugins +; +; This example loads wavesurfer.js core + all three plugins, +; creates a waveform with playback controls, and attaches each plugin. + +IncludeFile "WaveSurferSB.sbi" + +Global ws ; WaveSurfer instance +Global wsTimeline ; Timeline plugin instance +Global wsMinimap ; Minimap plugin instance +Global wsSpectro ; Spectrogram plugin instance + +Enumeration + #btnPlay + #btnPause + #btnStop + #txtStatus + #txtTime +EndEnumeration + +; ---- Event Callbacks ---- + +Procedure OnReady(Duration.d) + Debug "Audio ready! Duration: " + StrD(Duration, 2) + "s" + SetGadgetText(#txtStatus, "Ready - " + StrD(Duration, 1) + "s") +EndProcedure + +Procedure OnTimeUpdate(CurrentTime.d) + SetGadgetText(#txtTime, StrD(CurrentTime, 1) + " / " + StrD(WaveSurferSB::GetDuration(ws), 1)) +EndProcedure + +Procedure OnFinish() + SetGadgetText(#txtStatus, "Finished") +EndProcedure + +; ---- Button Events ---- + +Procedure OnEvent() + Select EventGadget() + Case #btnPlay + WaveSurferSB::Play(ws) + SetGadgetText(#txtStatus, "Playing") + + Case #btnPause + WaveSurferSB::Pause(ws) + SetGadgetText(#txtStatus, "Paused") + + Case #btnStop + WaveSurferSB::Stop(ws) + SetGadgetText(#txtStatus, "Stopped") + EndSelect +EndProcedure + +; ---- Download Callback ---- + +Procedure OnDownloadComplete(Success) + If Not Success + Debug "ERROR: Failed to load wavesurfer.js or plugins" + SetGadgetText(#txtStatus, "Load failed!") + ProcedureReturn + EndIf + + Debug "wavesurfer.js + plugins loaded successfully" + + ; Create the main WaveSurfer instance with drag-to-seek and bar style + ws = WaveSurferSB::Create("#waveform", WaveSurferSB::#DragToSeek | WaveSurferSB::#BarStyle) + + ; ---- Attach Plugins ---- + + ; Timeline: time labels below the waveform (default container, 20px height) + wsTimeline = WaveSurferSB::TimelineCreate(ws) + + ; Minimap: overview navigation bar with colored overlay + wsMinimap = WaveSurferSB::MinimapCreate(ws, 40, "#888", "#444", "rgba(100,100,200,0.3)") + + ; Spectrogram: frequency visualization with labels + ; Renders into the #spectrogram div, 150px tall, with frequency labels + wsSpectro = WaveSurferSB::SpectrogramCreate(ws, "#spectrogram", 150, #True) + + ; ---- Advanced plugin example using _ex (JSON options) ---- + ; If you need full control, use the _ex variants instead: + ; + ; wsTimeline = WaveSurferSB::TimelineCreate_ex(ws, ~"{\"height\": 25, \"timeInterval\": 5, \"primaryLabelInterval\": 10, \"insertPosition\": \"beforebegin\"}") + ; + ; wsSpectro = WaveSurferSB::SpectrogramCreate_ex(ws, ~"{\"container\": \"#spectrogram\", \"height\": 200, \"labels\": true, \"fftSamples\": 1024, \"scale\": \"mel\", \"colorMap\": \"roseus\"}") + + ; ---- Bind Events ---- + WaveSurferSB::OnReady(ws, @OnReady()) + WaveSurferSB::OnTimeUpdate(ws, @OnTimeUpdate()) + WaveSurferSB::OnFinish(ws, @OnFinish()) + + ; ---- Load Audio ---- + ; Replace with your audio URL + WaveSurferSB::Load(ws, "WaveSurferTestMusic.ogg") + + SetGadgetText(#txtStatus, "Loading audio...") +EndProcedure + +; ---- Main UI ---- + +Procedure Main() + + OpenWindow(0, 0, 0, 800, 600, "WaveSurferSB - Plugins Demo", #PB_Window_ScreenCentered) + + ; Create HTML containers for the waveform and spectrogram + ; The waveform div holds the main wave + timeline + minimap (auto-appended) + ; The spectrogram div is separate so it renders below + !var _wsRoot = document.body.firstElementChild || document.body; + !_wsRoot.insertAdjacentHTML('afterbegin', + ! '
' + + ! '
' + !); + + ; Transport controls + ButtonGadget(#btnPlay, 10, 420, 80, 30, "Play") + ButtonGadget(#btnPause, 100, 420, 80, 30, "Pause") + ButtonGadget(#btnStop, 190, 420, 80, 30, "Stop") + + TextGadget(#txtStatus, 290, 425, 200, 25, "Loading...") + TextGadget(#txtTime, 500, 425, 200, 25, "0.0 / 0.0") + + BindGadgetEvent(#btnPlay, @OnEvent()) + BindGadgetEvent(#btnPause, @OnEvent()) + BindGadgetEvent(#btnStop, @OnEvent()) + + ; Download core + all three plugins (loaded sequentially) + ; Plugin names must match the filenames on unpkg CDN + WaveSurferSB::Download(@OnDownloadComplete(), "timeline,minimap,spectrogram") + +EndProcedure + +Main() + +; IDE Options = SpiderBasic 3.20 (Windows - x86) +; CursorPosition = 94 +; FirstLine = 57 +; Folding = -- +; iOSAppOrientation = 0 +; AndroidAppCode = 0 +; AndroidAppOrientation = 0 +; EnableXP +; DPIAware +; CompileSourceDirectory \ No newline at end of file diff --git a/WaveSurferTestMusic.ogg b/WaveSurferTestMusic.ogg new file mode 100644 index 0000000..f510c39 Binary files /dev/null and b/WaveSurferTestMusic.ogg differ