Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,14 @@ jobs:
run: |
jq -n --arg relay "$JAM_RELAY" '{ relay: $relay }' > build-web/dist/config.json

# The sample packs, from the release scripts/packs.py names (made by
# the `sample packs' workflow, packs.yml), into the site's packs/,
# which the page downloads from when asked. Before that release is
# published the site keeps the empty index the build wrote.
- name: The sample packs
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
run: python3 scripts/packs.py fetch build-web/dist/packs --missing-ok

- name: The packaged site, for GitHub Pages
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
uses: actions/upload-pages-artifact@v3
Expand Down Expand Up @@ -530,6 +538,7 @@ jobs:
build-web/gen
build-web/patches
build-web/icons
build-web/packs
build-web/cairo2d/cairo2dtest.js
build-web/cairo2d/cairo2dtest.wasm

Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/packs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# The sample packs, as a release of their own.
#
# scripts/packs.py builds them from their CC0 sources -- a couple of hundred
# megabytes of downloads and an ffmpeg pass -- and this publishes the result
# as the release scripts/packs.py names in RELEASE (packs-1, packs-2, ...):
# a tar per pack and the index, about 17 MB. The site's deploy and
# `packs.py install' download that and never touch the sources.
#
# By hand, and apart from a thinksynth release: the packs change when a pack
# is added or rebuilt, not when the program does. To publish, bump RELEASE,
# merge, and run this on master. It refuses a tag that already exists -- a
# release the deploy points at is not replaced under it -- marks its
# release as not the latest, so the program's own releases stay what
# /releases/latest is, and then runs the ci workflow on master, whose
# deploy is what puts the release on the site: the deploy that came with
# the merge ran before the release existed.

name: sample packs

on:
workflow_dispatch:

permissions:
contents: write
actions: write

jobs:
publish:
name: build and publish the sample packs
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: ffmpeg
run: |
sudo apt-get update -q
sudo apt-get install -y --no-install-recommends ffmpeg

# The tag, not only the release: a bare tag left behind would pass a
# release check and the release would be made on that old commit.
- name: The release this would make, not made already
env:
GH_TOKEN: ${{ github.token }}
run: |
tag=$(python3 scripts/packs.py tag)
# Free only on a 404: any other failure is not an answer.
if out=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" 2>&1); then
echo "::error::the tag $tag exists already; bump RELEASE in scripts/packs.py"
exit 1
elif ! grep -q "HTTP 404" <<< "$out"; then
echo "::error::could not look up the tag $tag: $out"
exit 1
fi
echo "TAG=$tag" >> "$GITHUB_ENV"

- name: The packs, from their sources
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 scripts/packs.py build out
python3 scripts/packs.py archive out

- name: The release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$TAG" out/*.tar out/index.json \
--target "$GITHUB_SHA" --latest=false \
--title "Sample packs ($TAG)" \
--notes "Recorded instruments for the sampled_*.dsp graphs, built by scripts/packs.py at $GITHUB_SHA. Public domain (CC0): VSCO 2 Community Edition (Versilian Studios / Sam Gossner) and Big Rusty Drums (Karoryfer Samples)."

- name: The site, deployed with it
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run ci.yml --ref master
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ browser, the page's MIDI out button does the same through Web MIDI
(Chromium and Firefox). Either can also send MIDI clock, so a drum machine or
a DAW follows the piece's tempo and transport.

The sampled instruments (`sampled_*.dsp`: strings, horn, trumpet, flute,
upright piano, a drum kit) play public-domain recordings that are not in this
repository: they are published as a release of their own, built by the
`sample packs` workflow. `scripts/packs.py install` (it needs ffmpeg)
downloads it into this user's data directory, where thinksynth looks —
`~/.local/share/thinksynth` on Linux, `~/Library/Application Support/thinksynth`
on macOS, `%LOCALAPPDATA%\thinksynth` on Windows, and the first of those
for the Flatpak too. A running thinksynth finds a newly installed pack the
next time it loads an instrument; the page offers each pack for download
when something in play needs it.

If you want JACK on Linux, start `jackd` before thinksynth — RtAudio will use
the running server.

Expand Down
2 changes: 1 addition & 1 deletion docs/NODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ Sample Player (a wav at a voice's pitch)

| Arg | Dir | Description | Default | Range | Units | Values |
|---|---|---|---|---|---|---|
| `file` | in | The wav to play, found under samples/ on THINK_DSP_PATH | | | | |
| `file` | in | The wav to play, found under samples/ on THINK_DSP_PATH; or zones, `a.wav@48 b.wav@55', the one nearest the note played from its own | | | | |
| `file2` | in | Middle layer wav; an empty slot uses file | | | | |
| `file3` | in | Upper layer wav; an empty slot uses file2 or file | | | | |
| `freq` | in | The note to play it at | | | Hz | |
Expand Down
70 changes: 70 additions & 0 deletions dsp/sampled_cellos.dsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Cello section (sampled) -- recorded, from the `cellos' sample pack.
#
# VSCO 2 Community Edition, Versilian Studios / Sam Gossner; CC0. The
# recordings are not in this repository: scripts/packs.py builds them
# from their source into a release of their own, and the page offers the
# pack for download when something in play needs it. Without it, this
# plays silence.
#
# One osc::sample, its `file' a set of zones -- each recording with the
# note it was made at -- so every note is played from the nearest
# recording. 2 velocity layers, chosen by how hard the note is played.
#
# The recordings are trimmed to a few seconds, and the last second
# loops with a 300 ms crossfade, so a held note holds.

name "Cello section (sampled)";
author "Misha Nasledov";
description "Cello section, recorded (VSCO 2 Community Edition, Versilian Studios / Sam Gossner).";
category "Strings and pads";

@a = 120 ms;
@a.widget = 1;
@a.min = 1 ms;
@a.max = 2000 ms;
@a.label = "Attack";

@r = 450 ms;
@r.widget = 1;
@r.min = 70 ms;
@r.max = 4000 ms;
@r.label = "Release";

node ionode {
channels = 2;
out0 = vca->out;
out1 = vca->out;
play = env->play;
};

node freq misc::midi2freq {
note = ionode->note;
};

node smp osc::sample {
file = "cellos/cellos_36_1.wav@36 cellos/cellos_40_1.wav@40 cellos/cellos_43_1.wav@43 cellos/cellos_47_1.wav@47 cellos/cellos_50_1.wav@50 cellos/cellos_53_1.wav@53 cellos/cellos_57_1.wav@57 cellos/cellos_60_1.wav@60 cellos/cellos_64_1.wav@64 cellos/cellos_67_1.wav@67 cellos/cellos_71_1.wav@71 cellos/cellos_74_1.wav@74 cellos/cellos_77_1.wav@77";
file2 = "cellos/cellos_36_2.wav@36 cellos/cellos_40_2.wav@40 cellos/cellos_43_2.wav@43 cellos/cellos_47_2.wav@47 cellos/cellos_50_2.wav@50 cellos/cellos_53_2.wav@53 cellos/cellos_57_2.wav@57 cellos/cellos_60_2.wav@60 cellos/cellos_64_2.wav@64 cellos/cellos_67_2.wav@67 cellos/cellos_71_2.wav@71 cellos/cellos_74_2.wav@74 cellos/cellos_77_2.wav@77";
freq = freq->out;
trigger = ionode->trigger;
select = ionode->velocity;
split1 = 0.6;
split2 = 1;
loop = 1000 ms;
xfade = 300 ms;
};

node env env::adsr {
a = @a;
d = 0;
s = 0.5 + 0.5 * ionode->velocity;
r = @r;
p = 0.5 + 0.5 * ionode->velocity;
trigger = ionode->trigger;
};

node vca mixer::mul {
in0 = smp->out;
in1 = env->out * 0.75;
};

io ionode;
56 changes: 56 additions & 0 deletions dsp/sampled_drums.dsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Drum kit (sampled) -- recorded, from the `drums' sample pack.
#
# Big Rusty Drums, Karoryfer Samples; CC0. The recordings are not in
# this repository: scripts/packs.py builds them from their source into a
# release of their own, and the page offers the pack for download when
# something in play needs it. Without it, this plays silence.
#
# One osc::sample, its `file' a set of zones -- each recording with the
# note it was made at -- so every note is played from the nearest
# recording. 3 velocity layers, chosen by how hard the note is played.
#
# General MIDI drum notes: 36 kick, 38 snare, 41 43 45 48 toms,
# 42 closed and 46 open hi-hat, 49 crash, 51 ride. Every other
# note plays its nearest drum, retuned.

name "Drum kit (sampled)";
author "Misha Nasledov";
description "Drum kit, recorded (Big Rusty Drums, Karoryfer Samples).";
category "Drums";

node ionode {
channels = 2;
poly = 16;
out0 = vca->out;
out1 = vca->out;
play = max(smp->play, least->play);
};

# At least a tenth of a second, so a voice whose file is not
# there yet still lives long enough to be one.
node least env::ad {
a = 0;
d = 100 ms;
};

node freq misc::midi2freq {
note = ionode->note;
};

node smp osc::sample {
file = "drums/k_36_1.wav@36 drums/sn_center_38_1.wav@38 drums/t22_41_1.wav@41 drums/ht_cl_42_1.wav@42 drums/t18_43_1.wav@43 drums/t15_45_1.wav@45 drums/ht_open_46_1.wav@46 drums/t14_48_1.wav@48 drums/cr_49_1.wav@49 drums/rd_51_1.wav@51";
file2 = "drums/k_36_2.wav@36 drums/sn_center_38_2.wav@38 drums/t22_41_2.wav@41 drums/ht_cl_42_2.wav@42 drums/t18_43_2.wav@43 drums/t15_45_2.wav@45 drums/ht_open_46_2.wav@46 drums/t14_48_2.wav@48 drums/cr_49_2.wav@49 drums/rd_51_2.wav@51";
file3 = "drums/k_36_3.wav@36 drums/sn_center_38_3.wav@38 drums/t22_41_3.wav@41 drums/ht_cl_42_3.wav@42 drums/t18_43_3.wav@43 drums/t15_45_3.wav@45 drums/ht_open_46_3.wav@46 drums/t14_48_3.wav@48 drums/cr_49_3.wav@49 drums/rd_51_3.wav@51";
freq = freq->out;
trigger = 1;
select = ionode->velocity;
split1 = 0.4;
split2 = 0.75;
};

node vca mixer::mul {
in0 = smp->out;
in1 = (0.4 + 0.6 * ionode->velocity) * 0.85;
};

io ionode;
70 changes: 70 additions & 0 deletions dsp/sampled_flute.dsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Flute (sampled) -- recorded, from the `flute' sample pack.
#
# VSCO 2 Community Edition, Versilian Studios / Sam Gossner; CC0. The
# recordings are not in this repository: scripts/packs.py builds them
# from their source into a release of their own, and the page offers the
# pack for download when something in play needs it. Without it, this
# plays silence.
#
# One osc::sample, its `file' a set of zones -- each recording with the
# note it was made at -- so every note is played from the nearest
# recording. 2 velocity layers, chosen by how hard the note is played.
#
# The recordings are trimmed to a few seconds, and the last second
# loops with a 300 ms crossfade, so a held note holds.

name "Flute (sampled)";
author "Misha Nasledov";
description "Flute, recorded (VSCO 2 Community Edition, Versilian Studios / Sam Gossner).";
category "Leads and stabs";

@a = 30 ms;
@a.widget = 1;
@a.min = 1 ms;
@a.max = 2000 ms;
@a.label = "Attack";

@r = 200 ms;
@r.widget = 1;
@r.min = 70 ms;
@r.max = 4000 ms;
@r.label = "Release";

node ionode {
channels = 2;
out0 = vca->out;
out1 = vca->out;
play = env->play;
};

node freq misc::midi2freq {
note = ionode->note;
};

node smp osc::sample {
file = "flute/flute_60_1.wav@60 flute/flute_64_1.wav@64 flute/flute_69_1.wav@69 flute/flute_72_1.wav@72 flute/flute_76_1.wav@76 flute/flute_81_1.wav@81 flute/flute_84_1.wav@84 flute/flute_88_1.wav@88 flute/flute_93_1.wav@93 flute/flute_96_1.wav@96";
file2 = "flute/flute_60_2.wav@60 flute/flute_64_2.wav@64 flute/flute_69_2.wav@69 flute/flute_72_2.wav@72 flute/flute_76_2.wav@76 flute/flute_81_2.wav@81 flute/flute_84_1.wav@84 flute/flute_88_1.wav@88 flute/flute_93_2.wav@93 flute/flute_96_1.wav@96";
freq = freq->out;
trigger = ionode->trigger;
select = ionode->velocity;
split1 = 0.6;
split2 = 1;
loop = 1000 ms;
xfade = 300 ms;
};

node env env::adsr {
a = @a;
d = 0;
s = 0.5 + 0.5 * ionode->velocity;
r = @r;
p = 0.5 + 0.5 * ionode->velocity;
trigger = ionode->trigger;
};

node vca mixer::mul {
in0 = smp->out;
in1 = env->out * 3.2;
};

io ionode;
71 changes: 71 additions & 0 deletions dsp/sampled_horn.dsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# French horn (sampled) -- recorded, from the `horn' sample pack.
#
# VSCO 2 Community Edition, Versilian Studios / Sam Gossner; CC0. The
# recordings are not in this repository: scripts/packs.py builds them
# from their source into a release of their own, and the page offers the
# pack for download when something in play needs it. Without it, this
# plays silence.
#
# One osc::sample, its `file' a set of zones -- each recording with the
# note it was made at -- so every note is played from the nearest
# recording. 3 velocity layers, chosen by how hard the note is played.
#
# The recordings are trimmed to a few seconds, and the last second
# loops with a 300 ms crossfade, so a held note holds.

name "French horn (sampled)";
author "Misha Nasledov";
description "French horn, recorded (VSCO 2 Community Edition, Versilian Studios / Sam Gossner).";
category "Leads and stabs";

@a = 40 ms;
@a.widget = 1;
@a.min = 1 ms;
@a.max = 2000 ms;
@a.label = "Attack";

@r = 250 ms;
@r.widget = 1;
@r.min = 70 ms;
@r.max = 4000 ms;
@r.label = "Release";

node ionode {
channels = 2;
out0 = vca->out;
out1 = vca->out;
play = env->play;
};

node freq misc::midi2freq {
note = ionode->note;
};

node smp osc::sample {
file = "horn/horn_36_1.wav@36 horn/horn_39_1.wav@39 horn/horn_43_1.wav@43 horn/horn_46_1.wav@46 horn/horn_50_1.wav@50 horn/horn_53_1.wav@53 horn/horn_57_1.wav@57 horn/horn_60_1.wav@60";
file2 = "horn/horn_36_2.wav@36 horn/horn_39_2.wav@39 horn/horn_43_2.wav@43 horn/horn_46_2.wav@46 horn/horn_50_2.wav@50 horn/horn_53_2.wav@53 horn/horn_57_2.wav@57 horn/horn_60_2.wav@60";
file3 = "horn/horn_36_2.wav@36 horn/horn_39_2.wav@39 horn/horn_43_2.wav@43 horn/horn_46_2.wav@46 horn/horn_50_3.wav@50 horn/horn_53_2.wav@53 horn/horn_57_2.wav@57 horn/horn_60_3.wav@60";
freq = freq->out;
trigger = ionode->trigger;
select = ionode->velocity;
split1 = 0.4;
split2 = 0.75;
loop = 1000 ms;
xfade = 300 ms;
};

node env env::adsr {
a = @a;
d = 0;
s = 0.5 + 0.5 * ionode->velocity;
r = @r;
p = 0.5 + 0.5 * ionode->velocity;
trigger = ionode->trigger;
};

node vca mixer::mul {
in0 = smp->out;
in1 = env->out * 0.5;
};

io ionode;
Loading
Loading