Spaces:
Sleeping
Sleeping
Replace synthesized hits with a licensed sampled arcade kit
Browse files- README.md +5 -3
- softchart/assets/taiko/README.md +16 -0
- softchart/assets/taiko/balloon-01.wav +0 -0
- softchart/assets/taiko/balloon-02.wav +0 -0
- softchart/assets/taiko/don-01.wav +0 -0
- softchart/assets/taiko/don-02.wav +0 -0
- softchart/assets/taiko/don-big.wav +0 -0
- softchart/assets/taiko/ka-01.wav +0 -0
- softchart/assets/taiko/ka-02.wav +0 -0
- softchart/assets/taiko/ka-big.wav +0 -0
- softchart/assets/taiko/manifest.json +328 -0
- softchart/assets/taiko/roll-01.wav +0 -0
- softchart/assets/taiko/roll-02.wav +0 -0
- softchart/assets/taiko/roll-big-01.wav +0 -0
- softchart/assets/taiko/roll-big-02.wav +0 -0
- softchart/preview_audio.py +102 -162
README.md
CHANGED
|
@@ -29,9 +29,11 @@ The Space uses Gradio Server mode with a fully custom, responsive interface.
|
|
| 29 |
Upload a song, pick a difficulty, and get a full-song mel/plan preview, a
|
| 30 |
full TJA chart image, a `.tja` playable in TJAPlayer3 / OpenTaiko, and a
|
| 31 |
synchronized WAV mix that lets you hear the generated chart performed over
|
| 32 |
-
the source music immediately. The original, arcade-tuned
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
The generation API streams the real processing stages to the interface:
|
| 37 |
audio analysis, beat-grid fitting, song-level planning, note generation,
|
|
|
|
| 29 |
Upload a song, pick a difficulty, and get a full-song mel/plan preview, a
|
| 30 |
full TJA chart image, a `.tja` playable in TJAPlayer3 / OpenTaiko, and a
|
| 31 |
synchronized WAV mix that lets you hear the generated chart performed over
|
| 32 |
+
the source music immediately. The original, arcade-tuned kit is built from
|
| 33 |
+
hash-pinned CC0 head and rim recordings from the
|
| 34 |
+
[Chinese Bass Drum Dagu Oneshots](https://freesound.org/people/sazanami12/packs/45781/)
|
| 35 |
+
pack. SoftChart transforms those recordings offline and does not contain audio
|
| 36 |
+
taken from a commercial game.
|
| 37 |
|
| 38 |
The generation API streams the real processing stages to the interface:
|
| 39 |
audio analysis, beat-grid fitting, song-level planning, note generation,
|
softchart/assets/taiko/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SoftChart Arcade Taiko Kit
|
| 2 |
+
|
| 3 |
+
These one-shots are original SoftChart transformations of the **Chinese Bass
|
| 4 |
+
Drum Dagu Oneshots** pack recorded in Taipei by `sazanami12`:
|
| 5 |
+
|
| 6 |
+
- Source: https://freesound.org/people/sazanami12/packs/45781/
|
| 7 |
+
- License: CC0 1.0 — https://creativecommons.org/publicdomain/zero/1.0/
|
| 8 |
+
- Build recipe: `scripts/build_taiko_bank.py`
|
| 9 |
+
- Exact source and output hashes: `manifest.json`
|
| 10 |
+
|
| 11 |
+
The source instrument is a Chinese Huapengu / Dagu, selected as a
|
| 12 |
+
redistributable large East-Asian barrel-drum recording with isolated head and
|
| 13 |
+
rim articulations. It is not represented as a Japanese wadaiko recording.
|
| 14 |
+
|
| 15 |
+
No audio from Taiko no Tatsujin or any other commercial game is included,
|
| 16 |
+
transformed, embedded, or required by the build.
|
softchart/assets/taiko/balloon-01.wav
ADDED
|
Binary file (12 kB). View file
|
|
|
softchart/assets/taiko/balloon-02.wav
ADDED
|
Binary file (12 kB). View file
|
|
|
softchart/assets/taiko/don-01.wav
ADDED
|
Binary file (73.3 kB). View file
|
|
|
softchart/assets/taiko/don-02.wav
ADDED
|
Binary file (73.3 kB). View file
|
|
|
softchart/assets/taiko/don-big.wav
ADDED
|
Binary file (79 kB). View file
|
|
|
softchart/assets/taiko/ka-01.wav
ADDED
|
Binary file (35.9 kB). View file
|
|
|
softchart/assets/taiko/ka-02.wav
ADDED
|
Binary file (35.9 kB). View file
|
|
|
softchart/assets/taiko/ka-big.wav
ADDED
|
Binary file (35.9 kB). View file
|
|
|
softchart/assets/taiko/manifest.json
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 1,
|
| 3 |
+
"kit_id": "softchart-arcade-taiko-cc0",
|
| 4 |
+
"license": "CC0-1.0",
|
| 5 |
+
"license_url": "https://creativecommons.org/publicdomain/zero/1.0/",
|
| 6 |
+
"source_pack_url": "https://freesound.org/people/sazanami12/packs/45781/",
|
| 7 |
+
"source_creator": "sazanami12",
|
| 8 |
+
"source_instrument": "Chinese Huapengu / Dagu recorded in Taipei, Taiwan",
|
| 9 |
+
"source_encoding": "public Freesound HQ OGG preview of the CC0 original WAV",
|
| 10 |
+
"sources": [
|
| 11 |
+
{
|
| 12 |
+
"sound_id": 856549,
|
| 13 |
+
"label": "center-loud",
|
| 14 |
+
"sha256": "26d230aff7bb043fc88e823d7eecf00f807208f4c1d4d27c2cc1a7a2773b9d87",
|
| 15 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856549_17110092-hq.ogg"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"sound_id": 856550,
|
| 19 |
+
"label": "center-soft",
|
| 20 |
+
"sha256": "78ed9dea56102480811e90a9653c08fe20a0eca5793c9c936f1c2592042b9a46",
|
| 21 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856550_17110092-hq.ogg"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"sound_id": 856551,
|
| 25 |
+
"label": "edge-loud",
|
| 26 |
+
"sha256": "85ff2986eb902296e541a9d44b21bb61b9c2b8a05b5510be2fac9fbc99738399",
|
| 27 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856551_17110092-hq.ogg"
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"sound_id": 856552,
|
| 31 |
+
"label": "edge-soft",
|
| 32 |
+
"sha256": "85600ed1ddfab1c56fe07268f5fd68073706da8cd9a2f0cf7f16bde30bb8bb3e",
|
| 33 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856552_17110092-hq.ogg"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"sound_id": 856557,
|
| 37 |
+
"label": "rim-right",
|
| 38 |
+
"sha256": "6c24ee738d5995121fa85a432223a2afadb585f2a67b253747e2db0b3796bca0",
|
| 39 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856557_17110092-hq.ogg"
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"sound_id": 856558,
|
| 43 |
+
"label": "rim-left",
|
| 44 |
+
"sha256": "5ac219e4254f27c4a3b5b581f5c67e7c72fe91fa3d0fb2e03d021793febc1872",
|
| 45 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856558_17110092-hq.ogg"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"sound_id": 856559,
|
| 49 |
+
"label": "rim-both-loud",
|
| 50 |
+
"sha256": "840a559e5d979e483cec2afb64e4f71a27d31c753c1bb138395303c5bd0d8253",
|
| 51 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856559_17110092-hq.ogg"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"sound_id": 856560,
|
| 55 |
+
"label": "rim-both-soft",
|
| 56 |
+
"sha256": "4d66f33d35d2fe160da02c9a310231cdd6f2e8724dcfa7706de9c8409b5b7f97",
|
| 57 |
+
"preview_url": "https://cdn.freesound.org/previews/856/856560_17110092-hq.ogg"
|
| 58 |
+
}
|
| 59 |
+
],
|
| 60 |
+
"files": [
|
| 61 |
+
{
|
| 62 |
+
"filename": "balloon-01.wav",
|
| 63 |
+
"kind": "balloon",
|
| 64 |
+
"round_robin": 1,
|
| 65 |
+
"sha256": "241042aeb27bf7ca310c65fb1130554c3f5501b735d4178c9a65227a3c3962b7",
|
| 66 |
+
"pcm_sha256": "d86c418410b1f009dbc0a1f1cbc81dbe6ac52b1707002f7595c48c973fd309c0",
|
| 67 |
+
"sample_rate": 44100,
|
| 68 |
+
"channels": 1,
|
| 69 |
+
"frames": 5954,
|
| 70 |
+
"anchor_frame": 0,
|
| 71 |
+
"source_sound_ids": [
|
| 72 |
+
856552,
|
| 73 |
+
856559
|
| 74 |
+
],
|
| 75 |
+
"profile": {
|
| 76 |
+
"centroid_hz": 144.684,
|
| 77 |
+
"attack_centroid_hz": 2085.291,
|
| 78 |
+
"t90_seconds": 0.088254,
|
| 79 |
+
"t99_seconds": 0.101973,
|
| 80 |
+
"crest_db": 10.255
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"filename": "balloon-02.wav",
|
| 85 |
+
"kind": "balloon",
|
| 86 |
+
"round_robin": 2,
|
| 87 |
+
"sha256": "9e9b9ae2067e3b8c35d4bef011aea160ac6b2c955b861b48fcdd977e9784a96e",
|
| 88 |
+
"pcm_sha256": "90a5802c2b25e55fdfd39b394f505e302aa13e4435418c494aff807a51bdfefd",
|
| 89 |
+
"sample_rate": 44100,
|
| 90 |
+
"channels": 1,
|
| 91 |
+
"frames": 5954,
|
| 92 |
+
"anchor_frame": 0,
|
| 93 |
+
"source_sound_ids": [
|
| 94 |
+
856550,
|
| 95 |
+
856559
|
| 96 |
+
],
|
| 97 |
+
"profile": {
|
| 98 |
+
"centroid_hz": 154.871,
|
| 99 |
+
"attack_centroid_hz": 2160.392,
|
| 100 |
+
"t90_seconds": 0.087846,
|
| 101 |
+
"t99_seconds": 0.101927,
|
| 102 |
+
"crest_db": 11.393
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"filename": "don-01.wav",
|
| 107 |
+
"kind": "don",
|
| 108 |
+
"round_robin": 1,
|
| 109 |
+
"sha256": "b0ae8c300223ca2092da3ebac7d5318978926935cc5f5de63339ca06e4552c94",
|
| 110 |
+
"pcm_sha256": "2fcc51eed16cf540288ad5b99153afea9f4ba599dc4921595852792cd4ee2163",
|
| 111 |
+
"sample_rate": 44100,
|
| 112 |
+
"channels": 1,
|
| 113 |
+
"frames": 36603,
|
| 114 |
+
"anchor_frame": 0,
|
| 115 |
+
"source_sound_ids": [
|
| 116 |
+
856552,
|
| 117 |
+
856559
|
| 118 |
+
],
|
| 119 |
+
"profile": {
|
| 120 |
+
"centroid_hz": 106.922,
|
| 121 |
+
"attack_centroid_hz": 2085.291,
|
| 122 |
+
"t90_seconds": 0.279342,
|
| 123 |
+
"t99_seconds": 0.424104,
|
| 124 |
+
"crest_db": 13.942
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"filename": "don-02.wav",
|
| 129 |
+
"kind": "don",
|
| 130 |
+
"round_robin": 2,
|
| 131 |
+
"sha256": "668748702af4b40922954685d4b416f6402c905f1ef7e9172f13d1a201f05c14",
|
| 132 |
+
"pcm_sha256": "5116770059c6891ce90b2f0eb39e15ee9cc37a851d9df10165c9e15638f18ad7",
|
| 133 |
+
"sample_rate": 44100,
|
| 134 |
+
"channels": 1,
|
| 135 |
+
"frames": 36603,
|
| 136 |
+
"anchor_frame": 0,
|
| 137 |
+
"source_sound_ids": [
|
| 138 |
+
856550,
|
| 139 |
+
856559
|
| 140 |
+
],
|
| 141 |
+
"profile": {
|
| 142 |
+
"centroid_hz": 113.558,
|
| 143 |
+
"attack_centroid_hz": 2160.392,
|
| 144 |
+
"t90_seconds": 0.286984,
|
| 145 |
+
"t99_seconds": 0.403673,
|
| 146 |
+
"crest_db": 14.121
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"filename": "don-big.wav",
|
| 151 |
+
"kind": "don_big",
|
| 152 |
+
"round_robin": 1,
|
| 153 |
+
"sha256": "38d7bee2a772c45fe935bab378f7aedaf73746fb78972fc538cf37d8c81c20cd",
|
| 154 |
+
"pcm_sha256": "29823b5ff47f93217360db5019f2c805bf5e36c0658b7fb8bdf9fec051636ecf",
|
| 155 |
+
"sample_rate": 44100,
|
| 156 |
+
"channels": 1,
|
| 157 |
+
"frames": 39470,
|
| 158 |
+
"anchor_frame": 0,
|
| 159 |
+
"source_sound_ids": [
|
| 160 |
+
856550,
|
| 161 |
+
856559
|
| 162 |
+
],
|
| 163 |
+
"profile": {
|
| 164 |
+
"centroid_hz": 105.346,
|
| 165 |
+
"attack_centroid_hz": 2440.045,
|
| 166 |
+
"t90_seconds": 0.379365,
|
| 167 |
+
"t99_seconds": 0.557619,
|
| 168 |
+
"crest_db": 13.246
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"filename": "ka-01.wav",
|
| 173 |
+
"kind": "ka",
|
| 174 |
+
"round_robin": 1,
|
| 175 |
+
"sha256": "7091db93a0d8bcb9a58d08e00c1052c9c057784a72ed7a445e7e04e1fb854192",
|
| 176 |
+
"pcm_sha256": "c7d95d820c95ecaa955b0a47739b1c7ab025b4a99f9c076bbef7a1b18f30215d",
|
| 177 |
+
"sample_rate": 44100,
|
| 178 |
+
"channels": 1,
|
| 179 |
+
"frames": 17949,
|
| 180 |
+
"anchor_frame": 0,
|
| 181 |
+
"source_sound_ids": [
|
| 182 |
+
856559,
|
| 183 |
+
856557
|
| 184 |
+
],
|
| 185 |
+
"profile": {
|
| 186 |
+
"centroid_hz": 2083.859,
|
| 187 |
+
"attack_centroid_hz": 4187.299,
|
| 188 |
+
"t90_seconds": 0.061202,
|
| 189 |
+
"t99_seconds": 0.123469,
|
| 190 |
+
"crest_db": 20.018
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"filename": "ka-02.wav",
|
| 195 |
+
"kind": "ka",
|
| 196 |
+
"round_robin": 2,
|
| 197 |
+
"sha256": "6dfcaad357908e139e98e62d748659ef0484337b7f04b66db6a4f4c4071120a8",
|
| 198 |
+
"pcm_sha256": "b8d50d62a9a71db0effbb34421958aa40776e44f0573baa271f32cb6881e96ce",
|
| 199 |
+
"sample_rate": 44100,
|
| 200 |
+
"channels": 1,
|
| 201 |
+
"frames": 17949,
|
| 202 |
+
"anchor_frame": 0,
|
| 203 |
+
"source_sound_ids": [
|
| 204 |
+
856557,
|
| 205 |
+
856558
|
| 206 |
+
],
|
| 207 |
+
"profile": {
|
| 208 |
+
"centroid_hz": 1926.337,
|
| 209 |
+
"attack_centroid_hz": 3862.82,
|
| 210 |
+
"t90_seconds": 0.058141,
|
| 211 |
+
"t99_seconds": 0.107732,
|
| 212 |
+
"crest_db": 22.327
|
| 213 |
+
}
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"filename": "ka-big.wav",
|
| 217 |
+
"kind": "ka_big",
|
| 218 |
+
"round_robin": 1,
|
| 219 |
+
"sha256": "dd72eb84c579eaa9ec2f04fe1e78f1cc497b5bf425b32a75788398379b1f2964",
|
| 220 |
+
"pcm_sha256": "40e876586926aecc06b91bfc60a1d00d33832fd34f334799d7c46e16921a6ea4",
|
| 221 |
+
"sample_rate": 44100,
|
| 222 |
+
"channels": 1,
|
| 223 |
+
"frames": 17949,
|
| 224 |
+
"anchor_frame": 0,
|
| 225 |
+
"source_sound_ids": [
|
| 226 |
+
856559,
|
| 227 |
+
856557,
|
| 228 |
+
856558
|
| 229 |
+
],
|
| 230 |
+
"profile": {
|
| 231 |
+
"centroid_hz": 1986.528,
|
| 232 |
+
"attack_centroid_hz": 4484.582,
|
| 233 |
+
"t90_seconds": 0.061224,
|
| 234 |
+
"t99_seconds": 0.125624,
|
| 235 |
+
"crest_db": 19.782
|
| 236 |
+
}
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"filename": "roll-01.wav",
|
| 240 |
+
"kind": "roll",
|
| 241 |
+
"round_robin": 1,
|
| 242 |
+
"sha256": "12bdee9caf1c8b17f2c56cce90078f25837a9670c939ceaabdd4cc524ebd9100",
|
| 243 |
+
"pcm_sha256": "19c20b070f3f23eab146d1a3b864a5827e68afe52224ccd3d7b23a2413f242a6",
|
| 244 |
+
"sample_rate": 44100,
|
| 245 |
+
"channels": 1,
|
| 246 |
+
"frames": 6615,
|
| 247 |
+
"anchor_frame": 0,
|
| 248 |
+
"source_sound_ids": [
|
| 249 |
+
856552,
|
| 250 |
+
856559
|
| 251 |
+
],
|
| 252 |
+
"profile": {
|
| 253 |
+
"centroid_hz": 139.363,
|
| 254 |
+
"attack_centroid_hz": 2085.291,
|
| 255 |
+
"t90_seconds": 0.094785,
|
| 256 |
+
"t99_seconds": 0.113311,
|
| 257 |
+
"crest_db": 9.782
|
| 258 |
+
}
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"filename": "roll-02.wav",
|
| 262 |
+
"kind": "roll",
|
| 263 |
+
"round_robin": 2,
|
| 264 |
+
"sha256": "2abd129bdb4e8c73aa44d071c386a8fc643f613c9b0cec1c84a339073e8bc681",
|
| 265 |
+
"pcm_sha256": "d4f9ca68263e903188eed4844a70041c38bb8f1c026331c28802771de606274b",
|
| 266 |
+
"sample_rate": 44100,
|
| 267 |
+
"channels": 1,
|
| 268 |
+
"frames": 6615,
|
| 269 |
+
"anchor_frame": 0,
|
| 270 |
+
"source_sound_ids": [
|
| 271 |
+
856550,
|
| 272 |
+
856559
|
| 273 |
+
],
|
| 274 |
+
"profile": {
|
| 275 |
+
"centroid_hz": 154.227,
|
| 276 |
+
"attack_centroid_hz": 2160.392,
|
| 277 |
+
"t90_seconds": 0.096259,
|
| 278 |
+
"t99_seconds": 0.114195,
|
| 279 |
+
"crest_db": 11.106
|
| 280 |
+
}
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"filename": "roll-big-01.wav",
|
| 284 |
+
"kind": "roll_big",
|
| 285 |
+
"round_robin": 1,
|
| 286 |
+
"sha256": "23fea6acc0d2c554353af445ea8aa887555ce2df3268540256922f695158909a",
|
| 287 |
+
"pcm_sha256": "7d3c290e8c18c786dd46633483d03c7ae3b83dfd83410c1262bd3e2d4dd8e82e",
|
| 288 |
+
"sample_rate": 44100,
|
| 289 |
+
"channels": 1,
|
| 290 |
+
"frames": 7717,
|
| 291 |
+
"anchor_frame": 0,
|
| 292 |
+
"source_sound_ids": [
|
| 293 |
+
856550,
|
| 294 |
+
856559
|
| 295 |
+
],
|
| 296 |
+
"profile": {
|
| 297 |
+
"centroid_hz": 157.954,
|
| 298 |
+
"attack_centroid_hz": 2440.045,
|
| 299 |
+
"t90_seconds": 0.100317,
|
| 300 |
+
"t99_seconds": 0.129501,
|
| 301 |
+
"crest_db": 9.349
|
| 302 |
+
}
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"filename": "roll-big-02.wav",
|
| 306 |
+
"kind": "roll_big",
|
| 307 |
+
"round_robin": 2,
|
| 308 |
+
"sha256": "edfd7d6225ac7e14d554b7ec946455d1d942f87c00e370d0cae2b3e62a22e2fd",
|
| 309 |
+
"pcm_sha256": "20289804cdb38f0b6dc6e300e7e8b0b7f32c7cc485e990db4a0a53a0055c47e5",
|
| 310 |
+
"sample_rate": 44100,
|
| 311 |
+
"channels": 1,
|
| 312 |
+
"frames": 7717,
|
| 313 |
+
"anchor_frame": 0,
|
| 314 |
+
"source_sound_ids": [
|
| 315 |
+
856550,
|
| 316 |
+
856552,
|
| 317 |
+
856559
|
| 318 |
+
],
|
| 319 |
+
"profile": {
|
| 320 |
+
"centroid_hz": 153.15,
|
| 321 |
+
"attack_centroid_hz": 2438.852,
|
| 322 |
+
"t90_seconds": 0.094399,
|
| 323 |
+
"t99_seconds": 0.125283,
|
| 324 |
+
"crest_db": 9.633
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
]
|
| 328 |
+
}
|
softchart/assets/taiko/roll-01.wav
ADDED
|
Binary file (13.3 kB). View file
|
|
|
softchart/assets/taiko/roll-02.wav
ADDED
|
Binary file (13.3 kB). View file
|
|
|
softchart/assets/taiko/roll-big-01.wav
ADDED
|
Binary file (15.5 kB). View file
|
|
|
softchart/assets/taiko/roll-big-02.wav
ADDED
|
Binary file (15.5 kB). View file
|
|
|
softchart/preview_audio.py
CHANGED
|
@@ -1,28 +1,31 @@
|
|
| 1 |
-
"""Deterministic
|
| 2 |
|
| 3 |
The chart is the timing authority: :func:`parse_tja_events` converts the
|
| 4 |
repo-emitted TJA subset to audio-clock events, including OFFSET, measure-local
|
| 5 |
-
subdivisions, and piecewise BPM changes.
|
| 6 |
-
|
| 7 |
-
assets are used.
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
|
|
|
| 12 |
import math
|
| 13 |
import os
|
| 14 |
import wave
|
| 15 |
from dataclasses import dataclass
|
| 16 |
from fractions import Fraction
|
|
|
|
| 17 |
|
| 18 |
import numpy as np
|
| 19 |
-
from scipy.signal import
|
| 20 |
|
| 21 |
|
| 22 |
_HIT_KIND = {"1": "don", "2": "ka", "3": "don_big", "4": "ka_big"}
|
| 23 |
_SPAN_KIND = {"5": "roll", "6": "roll_big", "7": "balloon"}
|
| 24 |
_ALLOWED_COMMANDS = {"#GOGOSTART", "#GOGOEND"}
|
| 25 |
_PCM16_CEILING = int(math.floor(32767 * 10.0 ** (-1.0 / 20.0)))
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
@dataclass(frozen=True)
|
|
@@ -56,6 +59,12 @@ class _Strike:
|
|
| 56 |
autoplay: bool
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
def _finite_float(value: str, field: str) -> float:
|
| 60 |
try:
|
| 61 |
result = float(value)
|
|
@@ -217,140 +226,92 @@ def parse_tja_events(tja_text: str) -> list[TaikoEvent]:
|
|
| 217 |
return sorted(events, key=lambda event: event.time)
|
| 218 |
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
# Compact arcade feedback needs the mid-bass membrane cluster and bachi
|
| 253 |
-
# attack to survive small speakers. The frequencies are based on measured
|
| 254 |
-
# wadaiko mode relationships, deliberately omitting kick-like sub-bass and
|
| 255 |
-
# the large downward pitch sweep used by the previous voice.
|
| 256 |
-
duration = 0.105 if tap else (0.29 if big else 0.27)
|
| 257 |
-
n = int(round(duration * sample_rate))
|
| 258 |
-
t = np.arange(n, dtype=np.float64) / sample_rate
|
| 259 |
-
rng = np.random.default_rng(12_710 + 101 * variant + 1_009 * int(big) + 7_003 * int(tap))
|
| 260 |
-
pitch = (0.982, 0.994, 1.008, 1.019)[variant % 4]
|
| 261 |
-
attack = 1.0 - np.exp(-t / 0.00024)
|
| 262 |
-
|
| 263 |
-
modes = np.zeros(n, dtype=np.float64)
|
| 264 |
-
frequencies = (255.0, 295.0, 560.0, 690.0, 1_015.0)
|
| 265 |
-
weights = (0.82, 1.00, 0.47, 0.32, 0.17)
|
| 266 |
-
decays = (0.185, 0.165, 0.095, 0.068, 0.040)
|
| 267 |
-
decay_scale = 0.31 if tap else (1.06 if big else 1.0)
|
| 268 |
-
for frequency, weight, decay in zip(frequencies, weights, decays):
|
| 269 |
-
end_hz = frequency * pitch
|
| 270 |
-
modes += weight * _falling_mode(
|
| 271 |
-
t, end_hz * (1.012 + 0.002 * variant), end_hz,
|
| 272 |
-
0.009, decay * decay_scale,
|
| 273 |
-
)
|
| 274 |
-
|
| 275 |
-
skin = _filtered_noise(rng, n, sample_rate, 380.0, 5_400.0)
|
| 276 |
-
skin *= np.exp(-t / (0.0042 if tap else 0.0075))
|
| 277 |
-
stick = _filtered_noise(rng, n, sample_rate, 1_300.0, 9_500.0)
|
| 278 |
-
stick *= np.exp(-t / 0.0018)
|
| 279 |
-
signal = (
|
| 280 |
-
attack * modes
|
| 281 |
-
+ (0.30 if tap else 0.20) * skin
|
| 282 |
-
+ (0.17 if tap else 0.105) * stick
|
| 283 |
-
)
|
| 284 |
-
signal = _highpass(signal, sample_rate, 115.0 if tap else 62.0)
|
| 285 |
-
signal = _soft_clip(signal, 1.42)
|
| 286 |
-
signal *= np.exp(-np.maximum(t - duration * 0.72, 0.0) / 0.018)
|
| 287 |
-
target = 0.59 if tap else (0.90 if big else 0.84)
|
| 288 |
-
signal *= target / max(float(np.max(np.abs(signal))), 1e-12)
|
| 289 |
-
return signal.astype(np.float32)
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
def _make_ka(sample_rate: int, variant: int, big: bool = False) -> np.ndarray:
|
| 293 |
-
# Ka is a dry wooden rim crack: a millisecond-scale excitation followed by
|
| 294 |
-
# short, irregular high modes. Broadband noise is only the bachi contact,
|
| 295 |
-
# not the body of the sound.
|
| 296 |
-
duration = 0.16 if big else 0.14
|
| 297 |
-
n = int(round(duration * sample_rate))
|
| 298 |
-
t = np.arange(n, dtype=np.float64) / sample_rate
|
| 299 |
-
rng = np.random.default_rng(37_901 + 131 * variant + 2_003 * int(big))
|
| 300 |
-
pitch = (0.981, 0.995, 1.009, 1.021)[variant % 4]
|
| 301 |
-
attack = 1.0 - np.exp(-t / 0.00016)
|
| 302 |
-
|
| 303 |
-
rim = _filtered_noise(rng, n, sample_rate, 1_200.0, 10_500.0)
|
| 304 |
-
rim *= np.exp(-t / 0.0028)
|
| 305 |
-
modes = np.zeros(n, dtype=np.float64)
|
| 306 |
-
for frequency, weight, decay in (
|
| 307 |
-
(650.0, 0.42, 0.064), (1_050.0, 1.00, 0.048),
|
| 308 |
-
(1_700.0, 0.63, 0.034), (2_700.0, 0.35, 0.022),
|
| 309 |
-
(4_200.0, 0.19, 0.012),
|
| 310 |
-
):
|
| 311 |
-
phase = 2.0 * np.pi * frequency * pitch * t + variant * 0.31
|
| 312 |
-
modes += weight * np.sin(phase) * np.exp(-t / (decay * (1.04 if big else 1.0)))
|
| 313 |
-
signal = 0.22 * rim + attack * modes
|
| 314 |
-
signal = _highpass(signal, sample_rate, 390.0)
|
| 315 |
-
signal = _soft_clip(signal, 1.28)
|
| 316 |
-
signal *= np.exp(-np.maximum(t - duration * 0.68, 0.0) / 0.012)
|
| 317 |
-
target = 0.91 if big else 0.79
|
| 318 |
-
signal *= target / max(float(np.max(np.abs(signal))), 1e-12)
|
| 319 |
-
return signal.astype(np.float32)
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
_BANKS: dict[int, dict[str, tuple[np.ndarray, ...]]] = {}
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
def _sample_bank(sample_rate: int) -> dict[str, tuple[np.ndarray, ...]]:
|
| 326 |
bank = _BANKS.get(sample_rate)
|
| 327 |
-
if bank is None:
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
return bank
|
| 339 |
|
| 340 |
|
|
|
|
|
|
|
|
|
|
| 341 |
def _expand_strikes(events: list[TaikoEvent]) -> tuple[list[_Strike], int]:
|
| 342 |
strikes: list[_Strike] = []
|
| 343 |
autoplay_count = 0
|
| 344 |
ordinary_index = 0
|
| 345 |
for event in events:
|
| 346 |
if event.kind in {"don", "ka", "don_big", "ka_big"}:
|
| 347 |
-
if event.kind
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
pan = -0.14 if ordinary_index % 2 == 0 else 0.14
|
| 351 |
-
else:
|
| 352 |
-
pan = -0.02 if ordinary_index % 2 == 0 else 0.02
|
| 353 |
-
depth = 0.30 if event.kind.endswith("_big") else 0.22
|
| 354 |
strikes.append(_Strike(event.time, event.kind, pan, depth, False))
|
| 355 |
ordinary_index += 1
|
| 356 |
continue
|
|
@@ -371,7 +332,7 @@ def _expand_strikes(events: list[TaikoEvent]) -> tuple[list[_Strike], int]:
|
|
| 371 |
kind = event.kind
|
| 372 |
depth = 0.085 if event.kind == "roll_big" else 0.060
|
| 373 |
for index, time in enumerate(times):
|
| 374 |
-
pan_width = 0.
|
| 375 |
pan = -pan_width if index % 2 == 0 else pan_width
|
| 376 |
strikes.append(_Strike(float(time), kind, pan, depth, True))
|
| 377 |
autoplay_count += 1
|
|
@@ -412,9 +373,10 @@ def _load_stereo_audio(audio_path: str, sample_rate: int) -> np.ndarray:
|
|
| 412 |
return np.ascontiguousarray(audio, dtype=np.float32)
|
| 413 |
|
| 414 |
|
| 415 |
-
def _place_strike(drums: np.ndarray,
|
| 416 |
-
sample_rate: int
|
| 417 |
-
|
|
|
|
| 418 |
sample_start = max(0, -start)
|
| 419 |
output_start = max(0, start)
|
| 420 |
count = min(len(sample) - sample_start, len(drums) - output_start)
|
|
@@ -426,23 +388,6 @@ def _place_strike(drums: np.ndarray, sample: np.ndarray, strike: _Strike,
|
|
| 426 |
segment = sample[sample_start:sample_start + count]
|
| 427 |
drums[output_start:output_start + count, 0] += segment * left_gain
|
| 428 |
drums[output_start:output_start + count, 1] += segment * right_gain
|
| 429 |
-
|
| 430 |
-
# A big note represents both hands, not a lower-pitched or longer drum.
|
| 431 |
-
# Layer an independently synthesized variant on the opposite side a few
|
| 432 |
-
# milliseconds later while keeping the first transient exactly on-grid.
|
| 433 |
-
if layered_sample is not None:
|
| 434 |
-
layer_delay = int(round(0.0018 * sample_rate))
|
| 435 |
-
layer_start = output_start + layer_delay
|
| 436 |
-
layer_source_start = sample_start
|
| 437 |
-
layer_count = min(
|
| 438 |
-
len(layered_sample) - layer_source_start,
|
| 439 |
-
len(drums) - layer_start,
|
| 440 |
-
)
|
| 441 |
-
if layer_count > 0:
|
| 442 |
-
layer = layered_sample[layer_source_start:layer_source_start + layer_count]
|
| 443 |
-
layer_gain = 0.72 if strike.kind == "roll_big" else 0.92
|
| 444 |
-
drums[layer_start:layer_start + layer_count, 0] += layer * right_gain * layer_gain
|
| 445 |
-
drums[layer_start:layer_start + layer_count, 1] += layer * left_gain * layer_gain
|
| 446 |
return True
|
| 447 |
|
| 448 |
|
|
@@ -519,14 +464,15 @@ def synthesize_taiko_preview(
|
|
| 519 |
events = parse_tja_events(tja_text)
|
| 520 |
music = _load_stereo_audio(audio_path, sample_rate)
|
| 521 |
strikes, autoplay_count = _expand_strikes(events)
|
| 522 |
-
bank = _sample_bank(sample_rate)
|
| 523 |
source_frames = len(music)
|
| 524 |
output_frames = source_frames
|
| 525 |
for index, strike in enumerate(strikes):
|
| 526 |
-
|
|
|
|
| 527 |
output_frames = max(
|
| 528 |
output_frames,
|
| 529 |
-
max(0,
|
| 530 |
)
|
| 531 |
if output_frames > source_frames:
|
| 532 |
music = np.pad(music, ((0, output_frames - source_frames), (0, 0)))
|
|
@@ -534,17 +480,11 @@ def synthesize_taiko_preview(
|
|
| 534 |
audible_strikes: list[_Strike] = []
|
| 535 |
|
| 536 |
for index, strike in enumerate(strikes):
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
if strike.kind in {"don_big", "ka_big", "roll_big"}:
|
| 541 |
-
layered_sample = samples[(index + 1) % len(samples)]
|
| 542 |
-
if _place_strike(
|
| 543 |
-
drums, sample, strike, sample_rate, layered_sample=layered_sample,
|
| 544 |
-
):
|
| 545 |
audible_strikes.append(strike)
|
| 546 |
|
| 547 |
-
drums = _soft_clip(drums, 1.10).astype(np.float32)
|
| 548 |
duck_gain, max_duck_db = _duck_envelope(
|
| 549 |
len(music), audible_strikes, sample_rate, drum_gain,
|
| 550 |
)
|
|
|
|
| 1 |
+
"""Deterministic sample-based arcade Taiko preview mixing.
|
| 2 |
|
| 3 |
The chart is the timing authority: :func:`parse_tja_events` converts the
|
| 4 |
repo-emitted TJA subset to audio-clock events, including OFFSET, measure-local
|
| 5 |
+
subdivisions, and piecewise BPM changes. Drum timbres come from a bundled,
|
| 6 |
+
hash-validated CC0 large barrel-drum kit; no commercial-game audio is used.
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
+
import hashlib
|
| 12 |
+
import json
|
| 13 |
import math
|
| 14 |
import os
|
| 15 |
import wave
|
| 16 |
from dataclasses import dataclass
|
| 17 |
from fractions import Fraction
|
| 18 |
+
from pathlib import Path
|
| 19 |
|
| 20 |
import numpy as np
|
| 21 |
+
from scipy.signal import resample_poly
|
| 22 |
|
| 23 |
|
| 24 |
_HIT_KIND = {"1": "don", "2": "ka", "3": "don_big", "4": "ka_big"}
|
| 25 |
_SPAN_KIND = {"5": "roll", "6": "roll_big", "7": "balloon"}
|
| 26 |
_ALLOWED_COMMANDS = {"#GOGOSTART", "#GOGOEND"}
|
| 27 |
_PCM16_CEILING = int(math.floor(32767 * 10.0 ** (-1.0 / 20.0)))
|
| 28 |
+
_ASSET_DIR = Path(__file__).with_name("assets") / "taiko"
|
| 29 |
|
| 30 |
|
| 31 |
@dataclass(frozen=True)
|
|
|
|
| 59 |
autoplay: bool
|
| 60 |
|
| 61 |
|
| 62 |
+
@dataclass(frozen=True)
|
| 63 |
+
class _Voice:
|
| 64 |
+
samples: np.ndarray
|
| 65 |
+
anchor_frame: int
|
| 66 |
+
|
| 67 |
+
|
| 68 |
def _finite_float(value: str, field: str) -> float:
|
| 69 |
try:
|
| 70 |
result = float(value)
|
|
|
|
| 226 |
return sorted(events, key=lambda event: event.time)
|
| 227 |
|
| 228 |
|
| 229 |
+
_BANKS: dict[int, dict[str, tuple[_Voice, ...]]] = {}
|
| 230 |
+
_REQUIRED_VOICES = {
|
| 231 |
+
"don": 2,
|
| 232 |
+
"ka": 2,
|
| 233 |
+
"don_big": 1,
|
| 234 |
+
"ka_big": 1,
|
| 235 |
+
"roll": 2,
|
| 236 |
+
"roll_big": 2,
|
| 237 |
+
"balloon": 2,
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _load_pcm16_voice(path: Path, anchor_frame: int,
|
| 242 |
+
sample_rate: int) -> _Voice:
|
| 243 |
+
with wave.open(str(path), "rb") as source:
|
| 244 |
+
if source.getnchannels() != 1 or source.getsampwidth() != 2:
|
| 245 |
+
raise ValueError(f"Taiko asset must be mono PCM16: {path.name}")
|
| 246 |
+
source_rate = source.getframerate()
|
| 247 |
+
pcm = np.frombuffer(source.readframes(source.getnframes()), dtype="<i2")
|
| 248 |
+
if source_rate != 44_100:
|
| 249 |
+
raise ValueError(f"Taiko asset must be 44.1 kHz: {path.name}")
|
| 250 |
+
samples = pcm.astype(np.float32) / 32767.0
|
| 251 |
+
if sample_rate != source_rate:
|
| 252 |
+
ratio = Fraction(sample_rate, source_rate).limit_denominator()
|
| 253 |
+
samples = resample_poly(samples, ratio.numerator, ratio.denominator).astype(np.float32)
|
| 254 |
+
anchor_frame = int(round(anchor_frame * sample_rate / source_rate))
|
| 255 |
+
if not 0 <= anchor_frame < len(samples):
|
| 256 |
+
raise ValueError(f"Taiko asset anchor is outside the sample: {path.name}")
|
| 257 |
+
return _Voice(np.ascontiguousarray(samples), anchor_frame)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _sample_bank(sample_rate: int) -> dict[str, tuple[_Voice, ...]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
bank = _BANKS.get(sample_rate)
|
| 262 |
+
if bank is not None:
|
| 263 |
+
return bank
|
| 264 |
+
|
| 265 |
+
manifest_path = _ASSET_DIR / "manifest.json"
|
| 266 |
+
if not manifest_path.is_file():
|
| 267 |
+
raise FileNotFoundError(f"Taiko asset manifest is missing: {manifest_path}")
|
| 268 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 269 |
+
if manifest.get("schema_version") != 1 or manifest.get("license") != "CC0-1.0":
|
| 270 |
+
raise ValueError("Taiko asset manifest must be schema 1 and CC0-1.0")
|
| 271 |
+
|
| 272 |
+
grouped: dict[str, list[tuple[int, _Voice]]] = {kind: [] for kind in _REQUIRED_VOICES}
|
| 273 |
+
for item in manifest.get("files", []):
|
| 274 |
+
kind = item.get("kind")
|
| 275 |
+
if kind not in grouped:
|
| 276 |
+
raise ValueError(f"unsupported Taiko asset kind in manifest: {kind!r}")
|
| 277 |
+
filename = item.get("filename")
|
| 278 |
+
if not isinstance(filename, str) or Path(filename).name != filename:
|
| 279 |
+
raise ValueError("Taiko asset filename must be a plain basename")
|
| 280 |
+
path = _ASSET_DIR / filename
|
| 281 |
+
if not path.is_file():
|
| 282 |
+
raise FileNotFoundError(f"Taiko asset is missing: {path}")
|
| 283 |
+
file_hash = hashlib.sha256(path.read_bytes()).hexdigest()
|
| 284 |
+
if file_hash != item.get("sha256"):
|
| 285 |
+
raise ValueError(f"Taiko asset hash mismatch: {filename}")
|
| 286 |
+
voice = _load_pcm16_voice(path, int(item["anchor_frame"]), sample_rate)
|
| 287 |
+
grouped[kind].append((int(item["round_robin"]), voice))
|
| 288 |
+
|
| 289 |
+
bank = {}
|
| 290 |
+
for kind, expected_count in _REQUIRED_VOICES.items():
|
| 291 |
+
entries = sorted(grouped[kind], key=lambda entry: entry[0])
|
| 292 |
+
if len(entries) != expected_count:
|
| 293 |
+
raise ValueError(
|
| 294 |
+
f"Taiko kit requires {expected_count} {kind} voice(s), got {len(entries)}"
|
| 295 |
+
)
|
| 296 |
+
if [index for index, _ in entries] != list(range(1, expected_count + 1)):
|
| 297 |
+
raise ValueError(f"Taiko {kind} round-robin indices must be contiguous from 1")
|
| 298 |
+
bank[kind] = tuple(voice for _, voice in entries)
|
| 299 |
+
_BANKS[sample_rate] = bank
|
| 300 |
return bank
|
| 301 |
|
| 302 |
|
| 303 |
+
_DEFAULT_BANK = _sample_bank(44_100)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
def _expand_strikes(events: list[TaikoEvent]) -> tuple[list[_Strike], int]:
|
| 307 |
strikes: list[_Strike] = []
|
| 308 |
autoplay_count = 0
|
| 309 |
ordinary_index = 0
|
| 310 |
for event in events:
|
| 311 |
if event.kind in {"don", "ka", "don_big", "ka_big"}:
|
| 312 |
+
pan_width = 0.08 if event.kind in {"ka", "ka_big"} else 0.015
|
| 313 |
+
pan = -pan_width if ordinary_index % 2 == 0 else pan_width
|
| 314 |
+
depth = 0.26 if event.kind.endswith("_big") else 0.22
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
strikes.append(_Strike(event.time, event.kind, pan, depth, False))
|
| 316 |
ordinary_index += 1
|
| 317 |
continue
|
|
|
|
| 332 |
kind = event.kind
|
| 333 |
depth = 0.085 if event.kind == "roll_big" else 0.060
|
| 334 |
for index, time in enumerate(times):
|
| 335 |
+
pan_width = 0.10 if kind == "roll_big" else 0.06
|
| 336 |
pan = -pan_width if index % 2 == 0 else pan_width
|
| 337 |
strikes.append(_Strike(float(time), kind, pan, depth, True))
|
| 338 |
autoplay_count += 1
|
|
|
|
| 373 |
return np.ascontiguousarray(audio, dtype=np.float32)
|
| 374 |
|
| 375 |
|
| 376 |
+
def _place_strike(drums: np.ndarray, voice: _Voice, strike: _Strike,
|
| 377 |
+
sample_rate: int) -> bool:
|
| 378 |
+
sample = voice.samples
|
| 379 |
+
start = int(round(strike.time * sample_rate)) - voice.anchor_frame
|
| 380 |
sample_start = max(0, -start)
|
| 381 |
output_start = max(0, start)
|
| 382 |
count = min(len(sample) - sample_start, len(drums) - output_start)
|
|
|
|
| 388 |
segment = sample[sample_start:sample_start + count]
|
| 389 |
drums[output_start:output_start + count, 0] += segment * left_gain
|
| 390 |
drums[output_start:output_start + count, 1] += segment * right_gain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
return True
|
| 392 |
|
| 393 |
|
|
|
|
| 464 |
events = parse_tja_events(tja_text)
|
| 465 |
music = _load_stereo_audio(audio_path, sample_rate)
|
| 466 |
strikes, autoplay_count = _expand_strikes(events)
|
| 467 |
+
bank = _DEFAULT_BANK if sample_rate == 44_100 else _sample_bank(sample_rate)
|
| 468 |
source_frames = len(music)
|
| 469 |
output_frames = source_frames
|
| 470 |
for index, strike in enumerate(strikes):
|
| 471 |
+
voice = bank[strike.kind][index % len(bank[strike.kind])]
|
| 472 |
+
start = int(round(strike.time * sample_rate)) - voice.anchor_frame
|
| 473 |
output_frames = max(
|
| 474 |
output_frames,
|
| 475 |
+
max(0, start + len(voice.samples)),
|
| 476 |
)
|
| 477 |
if output_frames > source_frames:
|
| 478 |
music = np.pad(music, ((0, output_frames - source_frames), (0, 0)))
|
|
|
|
| 480 |
audible_strikes: list[_Strike] = []
|
| 481 |
|
| 482 |
for index, strike in enumerate(strikes):
|
| 483 |
+
voices = bank[strike.kind]
|
| 484 |
+
voice = voices[index % len(voices)]
|
| 485 |
+
if _place_strike(drums, voice, strike, sample_rate):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
audible_strikes.append(strike)
|
| 487 |
|
|
|
|
| 488 |
duck_gain, max_duck_db = _duck_envelope(
|
| 489 |
len(music), audible_strikes, sample_rate, drum_gain,
|
| 490 |
)
|