Skip to main content

Drag & Drop Samples

Drop audio files from Finder or Explorer onto the instrument UI and load them into a Kontakt group as new zones.

Result​

A drop area that highlights while files hover over it. On drop, every .wav file in the drag is loaded as a kontakt.Sample and added as a zone to the group named Dropped Samples. Files of other types are ignored, and the area reports what was loaded.

info

The uri and path packages as well as kontakt.load_sample require Kontakt 8.12 or later.

Full example​

import { Rectangle, Text, Frame, Overlay, Drop } from ui
import { parse_uri_list } from uri
import { path_from_uri } from path
import { instrument, load_sample } from kontakt

export component SampleDropZone {
status: String = "Drop samples here"
is_target: Bool = false

load_samples(from uri_list: String) -> (String) {
var group = instrument.group(named: "Dropped Samples")
if group == nil {
return "No group named 'Dropped Samples'"
}

var loaded: [String] = []
for uri in parse_uri_list(uri_list) {
var maybe_path = path_from_uri(uri)
if maybe_path != nil {
var path = maybe_path!
if path.extension.to_string().lowercased() == ".wav" {
var sample = load_sample(path)
if sample != nil {
group!.add_zone(sample!)
loaded.append(path.filename.to_string())
}
}
}
}

if loaded.is_empty {
return "No .wav files loaded"
}
return "Loaded: \{loaded.joined(separator: ", ")}"
}

Rectangle(color: self.is_target ? Color(0xFF50A0C0) : Color(0xFF80C0D0), radius: 4) with {
Frame(width: 400, height: 120)
Overlay {
Text(self.status)
}
Drop(
of: ["text/uri-list"],
enter: fun (event) { self.is_target = true },
exit: fun () { self.is_target = false },
perform: fun (event, action) {
self.status = self.load_samples(from: event.data(format: "text/uri-list"))
self.is_target = false
return true
},
)
}
}

export var main: Component = SampleDropZone()

How it works​

1. Accept file drops​

Both macOS and Windows describe dragged files with the text/uri-list MIME type, so the ui.Drop modifier only has to declare that format:

Drop(
of: ["text/uri-list"],
enter: fun (event) { self.is_target = true },
exit: fun () { self.is_target = false },
perform: fun (event, action) { … },
)

ui.Drop.constructor-by-mime-type-param-enter and ui.Drop.constructor-by-mime-type-param-exit drive the highlight; ui.Drop.constructor-by-mime-type-param-perform does the work and returns true to report that the drop was handled.

2. Parse the URI list​

data returns the payload as a single string. A text/uri-list payload holds one URI per line, so uri.parse_uri_list turns it into a list of uri.URI values:

for uri in parse_uri_list(uri_list) { … }

Comment lines starting with # and empty lines are dropped by the parser.

3. Convert URIs to paths​

A file:// URI is not a filesystem path — percent-escapes and, on Windows, the leading slash have to be resolved first. path_from_uri does that and returns nil for anything that isn't convertible, such as an https:// URI dragged in from a browser:

var maybe_path = path_from_uri(uri)
if maybe_path != nil {
var path = maybe_path!
…
}

With a path.Path in hand, extension filters by file type. It includes the leading dot (.wav), and lowercased makes the comparison case-insensitive so .WAV is accepted too.

4. Load the samples​

kontakt.load_sample reads the file and returns a kontakt.Sample, or nil if the file cannot be loaded — an unsupported format, or a file that no longer exists. Each loaded sample becomes a new zone via add_zone:

var sample = load_sample(path)
if sample != nil {
group!.add_zone(sample!)
}

group looks the target group up by name, so the instrument needs a group called Dropped Samples. Zones created this way are destroyable, so they can be removed again with destroy.

Variations​

  • Show file names before loading. stem gives the file name without its extension, filename with it.
  • Reject unsupported files while dragging. Use the predicate constructor and inspect the payload in can_accept, so the cursor already shows that the drop will not be accepted.
  • Replace instead of append. Iterate zones and call destroy on the existing zones before adding new ones.

See also​