Skip to content

Commit

Permalink
Better drum groups (#99)
Browse files Browse the repository at this point in the history
* fix: don't replace variation separator

* feat: add sampleGroups

* feat: getGroupVariations and README

* feat: CHANGELOG

* feat: add tests

* chore: rename and deprecate

* fix: readme

* docs: README on drum sample variations
  • Loading branch information
danigb authored Dec 18, 2024
1 parent 781fbc2 commit d17711d
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 52 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# smplr

## 0.16.x

#### DrumMachine sample groups

DrumMachines group different samples with same prefix under the same group. For example `tom-1.ogg` and `tom-2.ogg` forms the `tom` group:

```js
const drums = new DrumMachine(context, { instrument: "TR-808" });
drum.getSampleNames(); // => ['kick-1', 'kick-2', 'snare-1', 'snare-2', ...]
drum.getGroupNames(); // => ['kick', 'snare']
drum.getSampleNamesForGroup('kick') => // => ['kick-1', 'kick-2']
```

**Deprecations:**

- `drum.sampleNames` is deprecated in favour of `drum.getSampleNames()` or `drum.getGroupNames()`
- `drum.getVariations` is now called `drum.getSampleNamesForGroup`

## 0.15.x

#### Disable scheduler with `disableScheduler` option
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,11 +467,14 @@ const context = new AudioContext();
const drums = new DrumMachine(context, { instrument: "TR-808" });
drums.start({ note: "kick" });

// Drum samples could have variations:
const now = context.currentTime;
drums.getVariations("kick").forEach((variation, index) => {
drums.start({ note: variation, time: now + index });
});
// Drum samples are grouped and can have sample variations:
drums.getSampleNames(); // => ['kick-1', 'kick-2', 'snare-1', 'snare-2', ...]
drums.getGroupNames(); // => ['kick', 'snare']
drums.getSampleNamesForGroup("kick") => // => ['kick-1', 'kick-2']

// You can trigger samples by group name or specific sample
drums.start("kick"); // Play the first sample of the group
drums.start("kick-1"); // Play this specific sample
```

### Smolken double bass
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "smplr",
"version": "0.15.1",
"version": "0.16.0",
"homepage": "https://github.com/danigb/smplr#readme",
"description": "A Sampled collection of instruments",
"main": "dist/index.js",
Expand Down
14 changes: 7 additions & 7 deletions site/src/DrumMachineExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,29 @@ export function DrumMachineExample({ className }: { className?: string }) {
/>
</div>
<div className="grid grid-cols-6 gap-1">
{drums?.sampleNames.map((sample) => (
<div key={sample} className="bg-zinc-900 rounded px-2 pb-2">
{drums?.getGroupNames().map((group) => (
<div key={group} className="bg-zinc-900 rounded px-2 pb-2">
<div className="flex">
<button
className="text-left flex-grow"
onClick={() => {
drums?.start({
note: sample,
note: group,
detune: 50 * (Math.random() - 0.5),
});
}}
>
{sample}
{group}
</button>
</div>
<div className="flex flex-wrap gap-1 mt-1">
{drums?.getVariations(sample).map((variation) => (
{drums?.getSampleNamesForGroup(group).map((sample) => (
<button
key={variation}
key={sample}
className="bg-zinc-600 w-4 h-4 rounded"
onMouseDown={() => {
drums?.start({
note: variation,
note: sample,
});
}}
></button>
Expand Down
43 changes: 21 additions & 22 deletions src/drum-machine/dm-instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ export function isDrumMachineInstrument(
typeof instrument.baseUrl === "string" &&
typeof instrument.name === "string" &&
Array.isArray(instrument.samples) &&
Array.isArray(instrument.sampleNames) &&
typeof instrument.nameToSample === "object" &&
typeof instrument.sampleNameVariations === "object"
Array.isArray(instrument.groupNames) &&
typeof instrument.nameToSampleName === "object" &&
typeof instrument.sampleGroupVariations === "object"
);
}

export type DrumMachineInstrument = {
baseUrl: string;
name: string;
samples: string[];
sampleNames: string[];
nameToSample: Record<string, string | undefined>;
sampleNameVariations: Record<string, string[]>;
groupNames: string[];
nameToSampleName: Record<string, string | undefined>;
sampleGroupVariations: Record<string, string[]>;
};
export const EMPTY_INSTRUMENT: DrumMachineInstrument = {
baseUrl: "",
name: "",
samples: [],
sampleNames: [],
nameToSample: {},
sampleNameVariations: {},
groupNames: [],
nameToSampleName: {},
sampleGroupVariations: {},
};

export async function fetchDrumMachineInstrument(
Expand All @@ -39,21 +39,20 @@ export async function fetchDrumMachineInstrument(
const json = await res.json();
// need to fix json
json.baseUrl = url.replace("/dm.json", "");
json.sampleNames = [];
json.nameToSample = {};
json.sampleNameVariations = {};
for (const sampleSrc of json.samples) {
const sample =
sampleSrc.indexOf("/") !== -1 ? sampleSrc : sampleSrc.replace("-", "/");
json.nameToSample[sample] = sample;
const [base, variation] = sample.split("/");
if (!json.sampleNames.includes(base)) {
json.sampleNames.push(base);
json.groupNames = [];
json.nameToSampleName = {};
json.sampleGroupVariations = {};
for (const sample of json.samples) {
json.nameToSampleName[sample] = sample;
const separator = sample.indexOf("/") !== -1 ? "/" : "-";
const [base, variation] = sample.split(separator);
if (!json.groupNames.includes(base)) {
json.groupNames.push(base);
}
json.nameToSample[base] ??= sample;
json.sampleNameVariations[base] ??= [];
json.nameToSampleName[base] ??= sample;
json.sampleGroupVariations[base] ??= [];
if (variation) {
json.sampleNameVariations[base].push(`${base}/${variation}`);
json.sampleGroupVariations[base].push(`${base}${separator}${variation}`);
}
}

Expand Down
23 changes: 18 additions & 5 deletions src/drum-machine/drum-machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ function setup() {
"https://smpldsnds.github.io/drum-machines/TR-808/dm.json": {
baseUrl: "",
name: "",
samples: ["kick/low"],
sampleNames: [],
nameToSample: { kick: "kick/low" },
sampleNameVariations: {},
samples: ["kick/low", "kick/mid", "kick/high"],
},
"https://smpldsnds.github.io/drum-machines/TR-808/kick/low.ogg": "kick",
"https://smpldsnds.github.io/drum-machines/TR-808/kick/low.ogg": "kick-l",
"https://smpldsnds.github.io/drum-machines/TR-808/kick/mid.ogg": "kick-m",
"https://smpldsnds.github.io/drum-machines/TR-808/kick/high.ogg": "kick-h",
});
const mock = createAudioContextMock();
const context = mock.context;
Expand All @@ -32,6 +31,20 @@ describe("Drum machine", () => {
expect(start).toHaveBeenCalledWith({ note: "kick/low", stopId: "kick" });
});

it("returns all samples", async () => {
const { context } = setup();
const dm = await new DrumMachine(context, {
instrument: "TR-808",
}).load;
expect(dm.getSampleNames()).toEqual(["kick/low", "kick/mid", "kick/high"]);
expect(dm.getGroupNames()).toEqual(["kick"]);
expect(dm.getSampleNamesForGroup("kick")).toEqual([
"kick/low",
"kick/mid",
"kick/high",
]);
});

it("calls underlying player on stop", () => {
const { context } = setup();
const dm = new DrumMachine(context, { instrument: "TR-808" });
Expand Down
37 changes: 25 additions & 12 deletions src/drum-machine/drum-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,20 @@ export class DrumMachine {
});
}

async loaded() {
console.warn("deprecated: use load instead");
return this.load;
getSampleNames(): string[] {
return this.#instrument.samples.slice();
}

get sampleNames(): string[] {
return this.#instrument.sampleNames;
getGroupNames(): string[] {
return this.#instrument.groupNames.slice();
}

getVariations(name: string): string[] {
return this.#instrument.sampleNameVariations[name] ?? [];
getSampleNamesForGroup(groupName: string): string[] {
return this.#instrument.sampleGroupVariations[groupName] ?? [];
}

start(sample: SampleStart) {
const sampleName = this.#instrument.nameToSample[sample.note];
const sampleName = this.#instrument.nameToSampleName[sample.note];
return this.player.start({
...sample,
note: sampleName ? sampleName : sample.note,
Expand All @@ -105,6 +104,22 @@ export class DrumMachine {
stop(sample: SampleStop) {
return this.player.stop(sample);
}

/** @deprecated */
async loaded() {
console.warn("deprecated: use load instead");
return this.load;
}
/** @deprecated */
get sampleNames(): string[] {
console.log("deprecated: Use getGroupNames instead");
return this.#instrument.groupNames.slice();
}
/** @deprecated */
getVariations(groupName: string): string[] {
console.warn("deprecated: use getSampleNamesForGroup");
return this.#instrument.sampleGroupVariations[groupName] ?? [];
}
}

function drumMachineLoader(
Expand All @@ -116,10 +131,8 @@ function drumMachineLoader(
const format = findFirstSupportedFormat(["ogg", "m4a"]) ?? "ogg";
return instrument.then((data) =>
Promise.all(
data.samples.map(async (sample) => {
const url = `${data.baseUrl}/${sample}.${format}`;
const sampleName =
sample.indexOf("/") !== -1 ? sample : sample.replace("-", "/");
data.samples.map(async (sampleName) => {
const url = `${data.baseUrl}/${sampleName}.${format}`;
const buffer = await loadAudioBuffer(context, url, storage);
if (buffer) buffers[sampleName] = buffer;
})
Expand Down

0 comments on commit d17711d

Please sign in to comment.