diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/AudioEngine.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/AudioEngine.kt index 093635e..f57f132 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/AudioEngine.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/AudioEngine.kt @@ -104,29 +104,14 @@ class AudioEngine( } } - /** One insert-effect instance bound to the toolbox slot that configures it. - * [effectR] is a second copy driving the RIGHT channel in experimental stereo-FX - * mode (null otherwise, and always null for natively-stereo effects like the - * delay); [effect] then drives the left. */ - private class FxUnit(val slot: ToolboxSlot, val effect: AudioEffect, val effectR: AudioEffect? = null) { + /** One insert-effect instance bound to the toolbox slot that configures it. */ + private class FxUnit(val slot: ToolboxSlot, val effect: AudioEffect) { // Last slot version / tempo pushed into the effect, so [updateFxParams] can // skip re-parsing params (which allocates) when nothing changed. var lastVersion = -1 var lastTempo = Float.NaN } - /** Experimental: process every insert in true stereo (a mono effect runs one copy - * per channel) so upstream stereo — e.g. a delay's width — survives into later - * effects like the reverb. Higher CPU; off by default. */ - @Volatile var stereoFx = false; private set - - /** Toggle stereo-FX mode and rebuild the chains (adds/removes the R-instances). */ - fun setStereoFx(enabled: Boolean) { - if (stereoFx == enabled) return - stereoFx = enabled - rebuildFxChains() - } - /** Per-channel effect chains (mixer FX slots resolved to real processors). * Rebuilt when routing changes; parameters refresh live every block. Stored as * arrays (not lists) because the audio thread iterates them per sample, and a @@ -273,10 +258,7 @@ class AudioEngine( val slot = proj.toolbox.getOrNull(slotIndex) ?: continue val type = slot.type ?: continue AudioEffect.create(type, sampleRate)?.let { fx -> - // In stereo-FX mode a mono effect gets a second instance for the - // right channel; natively-stereo effects stay single-instance. - val right = if (stereoFx && !fx.nativeStereo) AudioEffect.create(type, sampleRate) else null - units.add(FxUnit(slot, fx, right)) + units.add(FxUnit(slot, fx)) } } next[ch] = units.toTypedArray() @@ -490,17 +472,14 @@ class AudioEngine( } } - /** Per-channel FX stereo scratch (audio-thread only): io[0]=L, io[1]=R. */ - private val fxIo = FloatArray(2) - /** * Render [frames] frames of the whole mix into [out] as INTERLEAVED STEREO — * out[2*i] = left, out[2*i+1] = right, so [out] must hold at least frames*2 * samples. This is the single source of truth for audio: it is called by the * Kotlin AudioTrack loop AND by the native Oboe callback (via [NativeAudioBridge]), * so both backends produce identical sound. Runs on an audio thread — must not - * allocate. Voices are mono and feed a mono per-channel sum; stereo width comes - * from stereo-capable insert effects (the tape delay's per-head panning). + * allocate. Voices and inserts are mono, so both master channels carry the same + * sum; the output/recording path is nonetheless stereo (2 interleaved channels). */ fun fillBlock(out: FloatArray, frames: Int) { val proj = project @@ -517,7 +496,6 @@ class AudioEngine( // Snapshot the volatile chains once so a concurrent rebuild can't swap them // mid-block; indexing + array iteration below then allocate nothing. val chains = channelChains - val io = fxIo for (i in 0 until frames) { if (playing && proj != null) advanceSequencer(proj) @@ -537,27 +515,17 @@ class AudioEngine( } for (ch in 0 until Pattern.TRACK_COUNT) { val mc = proj.mixer.channels[ch] - val mono = if (mc.audible(anySolo)) channelSum[ch] * mc.volume else 0f - io[0] = mono; io[1] = mono + var s = if (mc.audible(anySolo)) channelSum[ch] * mc.volume else 0f val chain = chains[ch] - for (u in chain) { - val er = u.effectR - if (er != null) { - // Stereo-FX mode, mono effect: a copy per channel keeps width. - io[0] = u.effect.process(io[0]) - io[1] = er.process(io[1]) - } else { - u.effect.processStereo(io) - } - } + for (u in chain) s = u.effect.process(s) // Isolate a misbehaving effect: a non-finite sample from one // channel's FX must not poison the whole master sum. - var l = io[0]; var r = io[1] - if (!l.isFinite()) l = 0f - if (!r.isFinite()) r = 0f + if (!s.isFinite()) s = 0f // Optional per-bus soft-clip limiter (toolbar toggle). - if (mc.limiter) { l = softClip(l); r = softClip(r) } - mixL += l; mixR += r + if (mc.limiter) s = softClip(s) + // Voices and inserts are mono; both master channels get the same + // sum. The master output/recording path stays stereo (2 channels). + mixL += s; mixR += s } } for (lv in liveVoices) { val v = lv.render() * LIVE_GAIN; mixL += v; mixR += v } @@ -582,7 +550,6 @@ class AudioEngine( val v = u.slot.version if (v != u.lastVersion || tempo != u.lastTempo) { u.effect.update(u.slot, tempo) - u.effectR?.update(u.slot, tempo) // keep the right-channel copy in sync u.lastVersion = v u.lastTempo = tempo } diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/Effects.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/Effects.kt index 07a3160..605710b 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/Effects.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/audio/Effects.kt @@ -29,28 +29,6 @@ interface AudioEffect { /** Process one mono sample. */ fun process(x: Float): Float - /** - * True if this effect produces its own stereo width and must run as a SINGLE - * instance via [processStereo] (the tape delay's per-head panning). Effects that - * are inherently mono return false: in the experimental stereo-FX mode the engine - * runs two copies of them (one per channel) to keep upstream stereo intact. - */ - val nativeStereo: Boolean get() = false - - /** - * Process one STEREO frame in place ([io] is a 2-element scratch: io[0]=L, - * io[1]=R). The default runs the mono [process] on the average of the two - * channels and writes it to both — so a mono effect passes audio through as - * mono, collapsing any upstream stereo width. It's used when stereo-FX mode is - * off; when on, the engine calls [process] per channel on separate instances - * instead. Only [nativeStereo] effects override this to produce real width. - */ - fun processStereo(io: FloatArray) { - val m = process((io[0] + io[1]) * 0.5f) - io[0] = m - io[1] = m - } - companion object { fun create(type: ToolboxType, sampleRate: Int): AudioEffect? = when (type) { ToolboxType.DELAY -> TapeDelay(sampleRate) @@ -77,8 +55,6 @@ private class TapeDelay(private val sampleRate: Int) : AudioEffect { private var writeIndex = 0 private val delaySamples = IntArray(DelayDivisions.HEADS) { sampleRate / 2 } private val headOn = BooleanArray(DelayDivisions.HEADS) - private val balance = FloatArray(DelayDivisions.HEADS) // -1 = full left, +1 = full right - private val stereoScratch = FloatArray(2) // for the mono process() fallback private var feedback = 0.4f private var dryWet = 0.35f // Tape character: @@ -98,7 +74,6 @@ private class TapeDelay(private val sampleRate: Int) : AudioEffect { val idx = slot.float(HEAD_DIV_KEYS[h], DelayDivisions.DEFAULT.toFloat()).toInt() delaySamples[h] = (DelayDivisions.beatFraction(idx) * samplesPerBeat) .toInt().coerceIn(1, buffer.size - 2) - balance[h] = slot.float(HEAD_BAL_KEYS[h], 0f).coerceIn(-1f, 1f) } feedback = slot.float("feedback", 0.4f).coerceIn(0f, DelayDivisions.MAX_FEEDBACK) dryWet = slot.float("drywet", 0.35f).coerceIn(0f, 1f) @@ -109,26 +84,15 @@ private class TapeDelay(private val sampleRate: Int) : AudioEffect { flutterDepth = tape * sampleRate * 0.0015f // up to ~1.5 ms wow/flutter } - override val nativeStereo: Boolean get() = true // produces width via per-head panning - - // Mono path delegates to the stereo one (the chain always calls processStereo). override fun process(x: Float): Float { - stereoScratch[0] = x; stereoScratch[1] = x - processStereo(stereoScratch) - return (stereoScratch[0] + stereoScratch[1]) * 0.5f - } - - override fun processStereo(io: FloatArray) { - val x = (io[0] + io[1]) * 0.5f flutterPhase += flutterInc if (flutterPhase > 2 * PI) flutterPhase -= 2 * PI val flutter = sin(flutterPhase).toFloat() * flutterDepth - // Read each active head (fractional read + linear interpolation for flutter) - // and pan it by its balance. Linear pan (gainL+gainR == 1) keeps the summed - // level — and the feedback loop gain — independent of head count and pan. - var wetL = 0f - var wetR = 0f + // Read each active head (fractional read + linear interpolation for flutter). + // The wet signal is the AVERAGE of the active heads, so the summed level — and + // the feedback loop gain — stays independent of how many heads are on. + var wet = 0f var active = 0 for (h in 0 until DelayDivisions.HEADS) { if (!headOn[h]) continue @@ -138,31 +102,24 @@ private class TapeDelay(private val sampleRate: Int) : AudioEffect { val i0 = pos.toInt() % buffer.size val i1 = (i0 + 1) % buffer.size val frac = pos - pos.toInt() - val s = buffer[i0] + (buffer[i1] - buffer[i0]) * frac - val b = balance[h] - wetL += s * (1f - b) * 0.5f - wetR += s * (1f + b) * 0.5f + wet += buffer[i0] + (buffer[i1] - buffer[i0]) * frac } - if (active > 0) { wetL /= active; wetR /= active } + if (active > 0) wet /= active - // Feedback uses the mono wet (wetL + wetR == the pre-pan head average), so the - // loop behaves exactly as the mono delay did regardless of panning. - var fb = (wetL + wetR) * feedback + var fb = wet * feedback fb += satBlend * (tanh(fb) - fb) lpState += lpAlpha * (fb - lpState) fb = lpState buffer[writeIndex] = x + fb writeIndex = (writeIndex + 1) % buffer.size - io[0] = x * (1f - dryWet) + wetL * dryWet - io[1] = x * (1f - dryWet) + wetR * dryWet + return x * (1f - dryWet) + wet * dryWet } private companion object { const val FLUTTER_HZ = 5.0 val HEAD_ON_KEYS = Array(DelayDivisions.HEADS) { "head${it}On" } val HEAD_DIV_KEYS = Array(DelayDivisions.HEADS) { "head${it}Div" } - val HEAD_BAL_KEYS = Array(DelayDivisions.HEADS) { "head${it}Bal" } } } diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/io/SettingsStore.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/io/SettingsStore.kt index 3b91944..1fb2126 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/io/SettingsStore.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/io/SettingsStore.kt @@ -3,7 +3,6 @@ package com.reactorcoremeltdown.sizzletracker.io import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey @@ -24,8 +23,6 @@ data class AppSettings( val kbdChannel: Int = 0, /** Base octave for the on-screen keyboards. */ val kbdOctave: Int = 3, - /** Experimental: process every insert effect in true stereo (higher CPU). */ - val stereoFx: Boolean = false, ) /** @@ -47,7 +44,6 @@ class SettingsStore(private val context: Context) { recordTailBars = prefs[RECORD_TAIL] ?: 2, kbdChannel = prefs[KBD_CHANNEL] ?: 0, kbdOctave = prefs[KBD_OCTAVE] ?: 3, - stereoFx = prefs[STEREO_FX] ?: false, ) } @@ -74,17 +70,11 @@ class SettingsStore(private val context: Context) { } } - /** Persist the experimental stereo-effects toggle. */ - suspend fun saveStereoFx(enabled: Boolean) { - context.settingsDataStore.edit { prefs -> prefs[STEREO_FX] = enabled } - } - private companion object { val PALETTE = stringPreferencesKey("palette") val RECORD_DIR = stringPreferencesKey("recordDirUri") val RECORD_TAIL = intPreferencesKey("recordTailBars") val KBD_CHANNEL = intPreferencesKey("kbdChannel") val KBD_OCTAVE = intPreferencesKey("kbdOctave") - val STEREO_FX = booleanPreferencesKey("stereoFx") } } diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/model/Toolbox.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/model/Toolbox.kt index ea778f9..00befb7 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/model/Toolbox.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/model/Toolbox.kt @@ -118,8 +118,6 @@ enum class ToolboxType( add(ParamSpec("head${h}On", "Head ${h + 1} On", if (h == 0) 1f else 0f, 0f, 1f)) add(ParamSpec("head${h}Div", "Head ${h + 1} Div", headDefaults[h].toFloat(), 0f, (DelayDivisions.size - 1).toFloat())) - // Per-head stereo balance: -1 = full left, 0 = centre, +1 = full right. - add(ParamSpec("head${h}Bal", "Head ${h + 1} Bal", 0f, -1f, 1f)) } }, ), diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/AppViewModel.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/AppViewModel.kt index ab0d057..cf89b85 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/AppViewModel.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/AppViewModel.kt @@ -660,15 +660,6 @@ class AppViewModel( var nativeAudioError by mutableStateOf(null); private set fun dismissNativeAudioError() { nativeAudioError = null } - /** Experimental: process every insert effect in true stereo (higher CPU). */ - var stereoFx by mutableStateOf(false); private set - fun applyStereoFx(enabled: Boolean) { - stereoFx = enabled - engine.setStereoFx(enabled) - viewModelScope.launch { settingsStore.saveStereoFx(enabled) } - touched() - } - /** Choose the colour theme and persist the choice. */ fun selectPalette(p: com.reactorcoremeltdown.sizzletracker.ui.theme.RetroPalette) { palette = p @@ -747,9 +738,6 @@ class AppViewModel( // Restore the on-screen keyboard's channel + octave. kbdChannel = s.kbdChannel.coerceIn(0, Cell.MAX_CHANNEL) kbdOctave = s.kbdOctave.coerceIn(0, 8) - // Restore the experimental stereo-FX mode (rebuilds the FX chains). - stereoFx = s.stereoFx - engine.setStereoFx(s.stereoFx) engine.recTempDir = appContext.cacheDir engine.setRecordTailBars(recordTailBars) engine.onRecordingStarted = { diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/settings/SettingsScreen.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/settings/SettingsScreen.kt index 516b912..6149de1 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/settings/SettingsScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -363,24 +362,6 @@ private fun AudioDevicesCard(vm: AppViewModel) { DeviceRow("Output", outputs, vm.outputDeviceId) { vm.setAudioOutput(it) } HorizontalDivider() DeviceRow("Input", inputs, vm.inputDeviceId) { vm.setAudioInput(it) } - HorizontalDivider() - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column(Modifier.weight(1f)) { - Text("Stereo effects (experimental)", style = MaterialTheme.typography.bodyMedium) - Text( - "Process every insert in true stereo, so a delay's width carries " + - "into later effects (e.g. delay → reverb). Higher CPU — may cause " + - "dropouts on some devices.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch(checked = vm.stereoFx, onCheckedChange = { vm.applyStereoFx(it) }) - } } } diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/theme/Theme.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/theme/Theme.kt index c59d787..9a9c973 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/theme/Theme.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/theme/Theme.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect @@ -92,17 +93,55 @@ private val retroTypography = Typography( @Composable fun SizzleTheme( palette: RetroPalette = RetroPalette.AMBER, - @Suppress("UNUSED_PARAMETER") darkTheme: Boolean = isSystemInDarkTheme(), // always dark; kept for API symmetry + @Suppress("UNUSED_PARAMETER") darkTheme: Boolean = isSystemInDarkTheme(), // follows the palette, not the system content: @Composable () -> Unit, ) { - val scheme = darkColorScheme( - background = palette.background, - surface = palette.surface, + // Whether this palette reads as a light theme drives which Material base scheme + // we start from. That matters because Material components (Card, ElevatedCard, + // ListItem, DropdownMenu…) pull their container/variant colours from roles we + // don't override — and the dark defaults look wrong (dark cards, muddy dividers) + // on a bright palette. Starting from the matching base keeps those defaults sane; + // then we paint the palette over every role components actually read. + val isLight = palette.background.luminance() > 0.5f + + // Text/icon colour to sit *on top of* the accent and danger fills — chosen for + // contrast against the fill itself, not the page, so buttons stay legible on any + // palette (a light accent gets near-black ink, a dark accent gets near-white). + val onAccent = if (palette.accent.luminance() > 0.5f) Color(0xFF0B0D0E) else Color(0xFFF7F7F7) + val onDanger = if (palette.danger.luminance() > 0.5f) Color(0xFF0B0D0E) else Color(0xFFF7F7F7) + + val base = if (isLight) lightColorScheme() else darkColorScheme() + val scheme = base.copy( primary = palette.accent, - onPrimary = palette.background, + onPrimary = onAccent, + primaryContainer = palette.accent, + onPrimaryContainer = onAccent, + secondary = palette.accent, + onSecondary = onAccent, + background = palette.background, onBackground = palette.text, + surface = palette.surface, onSurface = palette.text, + // Card / sheet / menu container roles all map to the panel colour so every + // surface matches the toolbar instead of falling back to Material greys. + surfaceVariant = palette.surface, + onSurfaceVariant = palette.textDim, + surfaceContainerLowest = palette.surface, + surfaceContainerLow = palette.surface, + surfaceContainer = palette.surface, + surfaceContainerHigh = palette.surface, + surfaceContainerHighest = palette.surface, + surfaceBright = palette.surface, + surfaceDim = palette.surface, + outline = palette.grid, + outlineVariant = palette.grid, error = palette.danger, + onError = onDanger, + errorContainer = palette.danger, + onErrorContainer = onDanger, + // Disable Material's automatic elevation tint so cards keep the exact panel + // colour rather than being tinted toward the primary at higher elevations. + surfaceTint = palette.surface, ) // Tint the system navigation bar to match the app's bottom tab bar (also diff --git a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/DelayEditor.kt b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/DelayEditor.kt index 43ec0e9..b5c4cf2 100644 --- a/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/DelayEditor.kt +++ b/app/src/main/java/com/reactorcoremeltdown/sizzletracker/ui/toolbox/DelayEditor.kt @@ -135,60 +135,5 @@ private fun DelayHead(vm: AppViewModel, slot: ToolboxSlot, head: Int, rev: Int, color = if (on) c.text else c.textDim, fontFamily = FontFamily.Monospace, fontSize = 10.sp, ) - - // Horizontal stereo balance for this head: left … centre … right. - HeadBalance(vm, slot, head, on) - } -} - -/** A compact horizontal balance (pan) slider for one tape head. Drag/tap to set; - * snaps to centre near the middle. */ -@Composable -private fun HeadBalance(vm: AppViewModel, slot: ToolboxSlot, head: Int, on: Boolean) { - val c = LocalRetro.current - @Suppress("UNUSED_VARIABLE") val rev = vm.revision // defeat strong-skipping so the thumb/label refresh - val bal = slot.float("head${head}Bal", 0f).coerceIn(-1f, 1f) - - fun setBal(x: Float, w: Float) { - var b = (x / w * 2f - 1f).coerceIn(-1f, 1f) - if (kotlin.math.abs(b) < 0.06f) b = 0f // centre detent - slot.set("head${head}Bal", b); vm.bumpForToolbar() - } - - Canvas( - Modifier - .fillMaxWidth() - .height(16.dp) - .background(c.surface) - .border(1.dp, if (on) c.accent else c.grid, RectangleShape) - .pointerInput(head, slot.index) { - detectTapGestures { off -> setBal(off.x, size.width.toFloat()) } - } - .pointerInput(head, slot.index) { - detectDragGestures { change, _ -> change.consume(); setBal(change.position.x, size.width.toFloat()) } - }, - ) { - val w = size.width - val h = size.height - drawLine(c.grid, Offset(w / 2f, 0f), Offset(w / 2f, h), strokeWidth = 1f) // centre tick - val tx = ((bal + 1f) / 2f) * w - drawRect( - if (on) c.accent else c.textDim, - Offset((tx - 2f).coerceIn(0f, w - 4f), 0f), Size(4f, h), - ) - } - Text( - balanceLabel(bal), - color = if (on) c.text else c.textDim, - fontFamily = FontFamily.Monospace, fontSize = 9.sp, - ) -} - -private fun balanceLabel(bal: Float): String { - val pct = kotlin.math.round(bal * 100f).toInt() - return when { - pct == 0 -> "C" - pct < 0 -> "L${-pct}" - else -> "R$pct" } } diff --git a/tools/samplepack-builder.html b/tools/samplepack-builder.html new file mode 100644 index 0000000..ad8e316 --- /dev/null +++ b/tools/samplepack-builder.html @@ -0,0 +1,340 @@ + + + + + +Sizzletracker Sample Pack Builder + + + +

▚ SIZZLETRACKER · SAMPLE PACK BUILDER

+

Drop a WAV / MP3 / audio file on a pad, tap to audition, then download a Sampler .zip bundle ready to import in the app.

+ +
+
+ + +
+
+ 0 / 16 pads + + +
+ +
+ +
+ +

+ Pads are labelled with the note each triggers in the app (pad 1 = C-2, MIDI 36), reading left-to-right, top-to-bottom. + Files are re-encoded to 16-bit mono WAV. Dropping several files on a pad fills it and the pads after it. + Pads holding the same sample as another pad are tinted red (matched by audio content, not file name). + Import the downloaded zip via the Sampler's preset toolbar (Import). +

+ + + + + +