@rel-packages/osu-beatmap-parser 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prebuildrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "upload": "gh release upload {tag_name} {file_path} --repo={owner}/{repo} --clobber",
3
+ "download": "https://github.com/{owner}/{repo}/releases/download/{tag}/{file_name}",
4
+ "tag-prefix": "v",
5
+ "backend": "cmake-js"
6
+ }
package/CMakeLists.txt ADDED
@@ -0,0 +1,68 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+
3
+ if(WIN32)
4
+ if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
5
+ if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
6
+ set(CMAKE_TOOLCHAIN_FILE "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
7
+ elseif(EXISTS "C:/vcpkg/scripts/buildsystems/vcpkg.cmake")
8
+ set(CMAKE_TOOLCHAIN_FILE "C:/vcpkg/scripts/buildsystems/vcpkg.cmake")
9
+ endif()
10
+ endif()
11
+ set(VCPKG_TARGET_TRIPLET "x64-windows-static")
12
+ endif()
13
+
14
+ project(osu-beatmap-parser VERSION 0.0.1)
15
+
16
+ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
17
+ set(CMAKE_CXX_STANDARD 17)
18
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
19
+ set(BUILD_TESTING OFF CACHE BOOL "Build tests" FORCE)
20
+ set(BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries")
21
+
22
+ file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/native/*.cpp")
23
+ file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS "src/native/*.hpp")
24
+
25
+ if(WIN32)
26
+ find_package(SndFile CONFIG REQUIRED)
27
+ set(SNDFILE_LIBRARIES SndFile::sndfile)
28
+ else()
29
+ find_package(PkgConfig REQUIRED)
30
+ pkg_check_modules(SNDFILE REQUIRED sndfile)
31
+ endif()
32
+
33
+ execute_process(
34
+ COMMAND node -p "require('node-addon-api').include"
35
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
36
+ OUTPUT_VARIABLE NODE_ADDON_API_DIR
37
+ OUTPUT_STRIP_TRAILING_WHITESPACE
38
+ )
39
+
40
+ string(REPLACE "\"" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
41
+
42
+ add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS} ${CMAKE_JS_SRC})
43
+
44
+ set_target_properties(${PROJECT_NAME} PROPERTIES
45
+ PREFIX ""
46
+ SUFFIX ".node"
47
+ CXX_STANDARD 17
48
+ CXX_STANDARD_REQUIRED ON
49
+ )
50
+
51
+ if (MSVC)
52
+ set(MSVC_RUNTIME_LIBRARY OFF)
53
+ add_compile_definitions(_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
54
+ execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
55
+ target_compile_options(${PROJECT_NAME} PRIVATE /Zc:__cplusplus)
56
+ endif()
57
+
58
+ target_include_directories(${PROJECT_NAME} PRIVATE
59
+ ${NODE_ADDON_API_DIR}
60
+ ${CMAKE_JS_INC}
61
+ ${SNDFILE_INCLUDE_DIRS}
62
+ )
63
+
64
+ target_link_libraries(
65
+ ${PROJECT_NAME}
66
+ ${CMAKE_JS_LIB}
67
+ ${SNDFILE_LIBRARIES}
68
+ )
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ ## osu-beatmap-parser
2
+
3
+ .osu parser for nodejs used by [osu-stuff](https://github.com/mezleca/osu-stuff)
4
+
5
+ ## installation
6
+
7
+ ```bash
8
+ npm install osu-beatmap-parser
9
+ ```
10
+
11
+ ## development
12
+
13
+ ```bash
14
+ npm install
15
+ npm run compile
16
+ ```
@@ -0,0 +1,11 @@
1
+ import { OsuKey, OsuInput } from "./types";
2
+ export declare function get_property(location: string, key: OsuKey): string;
3
+ export declare function get_properties(input: string | OsuInput, keys: OsuKey[]): Record<OsuKey, string> & {
4
+ id?: string;
5
+ };
6
+ export declare function process_beatmaps(inputs: (string | OsuInput)[], keys: OsuKey[]): Promise<(Record<OsuKey, string> & {
7
+ id?: string;
8
+ })[]>;
9
+ export declare function get_duration(location: string): number;
10
+ export declare function get_audio_duration(location: string): number;
11
+ export { OsuKey, OsuInput };
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.get_property = get_property;
7
+ exports.get_properties = get_properties;
8
+ exports.process_beatmaps = process_beatmaps;
9
+ exports.get_duration = get_duration;
10
+ exports.get_audio_duration = get_audio_duration;
11
+ const bindings_1 = __importDefault(require("bindings"));
12
+ const native = (0, bindings_1.default)("osu-beatmap-parser");
13
+ function get_property(location, key) {
14
+ return native.get_property(location, key);
15
+ }
16
+ ;
17
+ function get_properties(input, keys) {
18
+ const location = typeof input === "string" ? input : input.path;
19
+ const result = native.get_properties(location, keys);
20
+ if (typeof input !== "string" && input.id) {
21
+ return { ...result, id: input.id };
22
+ }
23
+ return result;
24
+ }
25
+ ;
26
+ async function process_beatmaps(inputs, keys) {
27
+ const locations = inputs.map(i => typeof i === "string" ? i : i.path);
28
+ const results = await native.process_beatmaps(locations, keys);
29
+ return results.map((res, i) => {
30
+ const input = inputs[i];
31
+ if (typeof input !== "string" && input.id) {
32
+ return { ...res, id: input.id };
33
+ }
34
+ return res;
35
+ });
36
+ }
37
+ ;
38
+ function get_duration(location) {
39
+ return native.get_duration(location);
40
+ }
41
+ ;
42
+ function get_audio_duration(location) {
43
+ return native.get_audio_duration(location);
44
+ }
45
+ ;
@@ -0,0 +1,12 @@
1
+ export type OsuKey = "AudioFilename" | "AudioLeadIn" | "PreviewTime" | "Countdown" | "SampleSet" | "StackLeniency" | "Mode" | "LetterboxInBreaks" | "WidescreenStoryboard" | "Bookmarks" | "DistanceSpacing" | "BeatDivisor" | "GridSize" | "TimelineZoom" | "Title" | "TitleUnicode" | "Artist" | "ArtistUnicode" | "Creator" | "Version" | "Source" | "Tags" | "BeatmapID" | "BeatmapSetID" | "HPDrainRate" | "CircleSize" | "OverallDifficulty" | "ApproachRate" | "SliderMultiplier" | "SliderTickRate" | "Background" | "Video" | "Storyboard" | "Duration";
2
+ export interface OsuInput {
3
+ path: string;
4
+ id?: string;
5
+ }
6
+ export interface INativeExporter {
7
+ get_property(location: string, key: string): string;
8
+ get_properties(location: string, keys: string[]): Record<string, string>;
9
+ process_beatmaps(locations: string[], keys: string[]): Promise<Record<string, string>[]>;
10
+ get_duration(location: string): number;
11
+ get_audio_duration(location: string): number;
12
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@rel-packages/osu-beatmap-parser",
3
+ "version": "0.1.7",
4
+ "description": ".osu parser for nodejs",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "homepage": "https://github.com/mezleca/osu-beatmap-parser",
8
+ "scripts": {
9
+ "install": "prebuild-install || npm run compile",
10
+ "dev": "npm run compile:native && tsx scripts/verify.ts",
11
+ "compile": "npm run compile:native && npm run compile:tsc",
12
+ "compile:tsc": "tsc",
13
+ "compile:native": "cmake-js build",
14
+ "prebuild": "prebuild --backend cmake-js --strip --verbose"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "repository": {
20
+ "url": "https://github.com/mezleca/osu-beatmap-parser.git"
21
+ },
22
+ "binary": {
23
+ "module_name": "osu-beatmap-parser",
24
+ "module_path": "./build/Release/",
25
+ "host": "https://github.com/mezleca/osu-beatmap-parser/releases/download/",
26
+ "remote_path": "{version}"
27
+ },
28
+ "type": "commonjs",
29
+ "dependencies": {
30
+ "bindings": "^1.5.0",
31
+ "prebuild-install": "^7.1.2"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bindings": "^1.5.5",
35
+ "@types/node": "^24.10.1",
36
+ "cmake-js": "^7.4.0",
37
+ "node-addon-api": "^8.5.0",
38
+ "prebuild": "^13.0.1",
39
+ "tsx": "^4.21.0",
40
+ "typescript": "^5.9.3"
41
+ }
42
+ }
@@ -0,0 +1,265 @@
1
+ #include <filesystem>
2
+ #include <napi.h>
3
+ #include <iostream>
4
+ #include <sndfile.h>
5
+ #include <string>
6
+ #include <thread>
7
+ #include <fstream>
8
+ #include <vector>
9
+ #include <atomic>
10
+ #include "osu/parser.hpp"
11
+ #include "osu/audio.hpp"
12
+ #include "addon.hpp"
13
+ #include "pool.hpp"
14
+
15
+ #define NOOP_FUNC(env) Napi::Function::New(env, [](const Napi::CallbackInfo& info){})
16
+
17
+ AudioAnalyzer audio_analizer;
18
+
19
+ std::filesystem::path dirname(const std::string &path) {
20
+ std::filesystem::path p(path);
21
+ return p.parent_path();
22
+ }
23
+
24
+ std::string read_file(const std::string& path) {
25
+ std::ifstream file(path, std::ios::binary | std::ios::ate);
26
+
27
+ if (!file.is_open()) {
28
+ return "";
29
+ }
30
+
31
+ std::streamsize size = file.tellg();
32
+ file.seekg(0, std::ios::beg);
33
+
34
+ std::string buffer(size, ' ');
35
+
36
+ if (file.read(buffer.data(), size)) {
37
+ return buffer;
38
+ }
39
+
40
+ return "";
41
+ }
42
+
43
+ Napi::Value ParserAddon::get_property(const Napi::CallbackInfo& info) {
44
+ if (info.Length() < 2) {
45
+ return Napi::String::New(info.Env(), "");
46
+ }
47
+
48
+ if (!info[0].IsString() || !info[1].IsString()) {
49
+ return Napi::String::New(info.Env(), "");
50
+ }
51
+
52
+ std::string location = info[0].As<Napi::String>().Utf8Value();
53
+ std::string key = info[1].As<Napi::String>().Utf8Value();
54
+ std::string content = read_file(location);
55
+
56
+ if (content.empty()) {
57
+ return Napi::String::New(info.Env(), "");
58
+ }
59
+
60
+ return Napi::String::From(info.Env(), osu_parser::get_property(content, key));
61
+ }
62
+
63
+ Napi::Value ParserAddon::get_properties(const Napi::CallbackInfo& info) {
64
+ if (info.Length() < 2) {
65
+ return Napi::Object::New(info.Env());
66
+ }
67
+
68
+ std::string location = info[0].As<Napi::String>().Utf8Value();
69
+ Napi::Array keys_array = info[1].As<Napi::Array>();
70
+
71
+ std::vector<std::string> keys;
72
+
73
+ for (uint32_t i = 0; i < keys_array.Length(); i++) {
74
+ Napi::Value val = keys_array[i];
75
+ if (val.IsString()) {
76
+ keys.push_back(val.As<Napi::String>().Utf8Value());
77
+ }
78
+ }
79
+
80
+ std::string content = read_file(location);
81
+
82
+ if (content.empty()) {
83
+ return Napi::Object::New(info.Env());
84
+ }
85
+
86
+ auto results = osu_parser::get_properties(content, keys);
87
+
88
+ Napi::Object obj = Napi::Object::New(info.Env());
89
+
90
+ for (const auto& [k, v] : results) {
91
+ obj.Set(k, v);
92
+ }
93
+
94
+ return obj;
95
+ }
96
+
97
+ struct BatchContext {
98
+ std::vector<std::string> paths;
99
+ std::vector<std::string> keys;
100
+ std::vector<std::unordered_map<std::string, std::string>> results;
101
+ std::atomic<size_t> completed_count{0};
102
+ Napi::ThreadSafeFunction tsfn;
103
+ Napi::Promise::Deferred deferred;
104
+ bool needs_duration = false;
105
+
106
+ BatchContext(Napi::Env env) : deferred(Napi::Promise::Deferred::New(env)) {}
107
+ };
108
+
109
+ void process_chunk(BatchContext* context, size_t start, size_t end) {
110
+ for (size_t i = start; i < end; i++) {
111
+ std::string content = read_file(context->paths[i]);
112
+ if (!content.empty()) {
113
+ context->results[i] = osu_parser::get_properties(content, context->keys);
114
+
115
+ if (context->needs_duration) {
116
+ std::string audio_file_name = osu_parser::get_property(content, "AudioFilename");
117
+ if (!audio_file_name.empty()) {
118
+ std::filesystem::path audio_path = dirname(context->paths[i]) / audio_file_name;
119
+ double duration = audio_analizer.get_audio_duration(audio_path.string());
120
+ context->results[i]["Duration"] = std::to_string(duration);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ size_t completed = context->completed_count.fetch_add(end - start) + (end - start);
127
+
128
+ if (completed == context->paths.size()) {
129
+ auto callback = [context](Napi::Env env, Napi::Function jsCallback) {
130
+ Napi::Array result_array = Napi::Array::New(env, context->results.size());
131
+
132
+ for (size_t i = 0; i < context->results.size(); i++) {
133
+ Napi::Object obj = Napi::Object::New(env);
134
+ for (const auto& [k, v] : context->results[i]) {
135
+ obj.Set(k, v);
136
+ }
137
+ result_array[i] = obj;
138
+ }
139
+
140
+ context->deferred.Resolve(result_array);
141
+ };
142
+
143
+ context->tsfn.BlockingCall(callback);
144
+ context->tsfn.Release();
145
+ }
146
+ }
147
+
148
+ Napi::Value ParserAddon::process_beatmaps(const Napi::CallbackInfo& info) {
149
+ Napi::Env env = info.Env();
150
+
151
+ if (info.Length() < 2) {
152
+ auto deferred = Napi::Promise::Deferred::New(env);
153
+ deferred.Reject(Napi::String::New(env, "Invalid arguments"));
154
+ return deferred.Promise();
155
+ }
156
+
157
+ Napi::Array paths_array = info[0].As<Napi::Array>();
158
+ Napi::Array keys_array = info[1].As<Napi::Array>();
159
+
160
+ auto context = new BatchContext(env);
161
+
162
+ for (uint32_t i = 0; i < paths_array.Length(); i++) {
163
+ context->paths.push_back(Napi::Value(paths_array[i]).As<Napi::String>().Utf8Value());
164
+ }
165
+
166
+ for (uint32_t i = 0; i < keys_array.Length(); i++) {
167
+ std::string key = Napi::Value(keys_array[i]).As<Napi::String>().Utf8Value();
168
+ context->keys.push_back(key);
169
+ if (key == "Duration") {
170
+ context->needs_duration = true;
171
+ }
172
+ }
173
+
174
+ context->results.resize(context->paths.size());
175
+
176
+ context->tsfn = Napi::ThreadSafeFunction::New(
177
+ env,
178
+ NOOP_FUNC(env),
179
+ "ProcessBeatmaps",
180
+ 0,
181
+ 1,
182
+ [context](Napi::Env) {
183
+ delete context;
184
+ }
185
+ );
186
+
187
+ size_t total_files = context->paths.size();
188
+ size_t thread_count = std::thread::hardware_concurrency();
189
+
190
+ if (thread_count == 0) {
191
+ thread_count = 1;
192
+ }
193
+
194
+ size_t chunk_size = (total_files + thread_count - 1) / thread_count;
195
+
196
+ for (size_t t = 0; t < thread_count; t++) {
197
+ size_t start = t * chunk_size;
198
+ size_t end = std::min(start + chunk_size, total_files);
199
+
200
+ if (start >= end) {
201
+ break;
202
+ }
203
+
204
+ pool.enqueue([context, start, end]() {
205
+ process_chunk(context, start, end);
206
+ });
207
+ }
208
+
209
+ return context->deferred.Promise();
210
+ }
211
+
212
+ Napi::Value ParserAddon::get_duration(const Napi::CallbackInfo& info) {
213
+ if (info.Length() < 1) {
214
+ return Napi::Number::New(info.Env(), 0.0);
215
+ }
216
+
217
+ if (!info[0].IsString()) {
218
+ return Napi::String::New(info.Env(), "");
219
+ }
220
+
221
+ std::string location = info[0].As<Napi::String>().Utf8Value();
222
+ std::string content = read_file(location);
223
+
224
+ if (content.empty()) {
225
+ return Napi::Number::New(info.Env(), 0.0);
226
+ }
227
+
228
+ std::string audio_file_name = osu_parser::get_property(content, "AudioFilename");
229
+ std::filesystem::path audio_path = dirname(location) / audio_file_name;
230
+
231
+ return Napi::Number::From(info.Env(), audio_analizer.get_audio_duration(audio_path.string()));
232
+ }
233
+
234
+ Napi::Value ParserAddon::get_audio_duration(const Napi::CallbackInfo& info) {
235
+ if (info.Length() < 1) {
236
+ return Napi::Number::New(info.Env(), 0.0);
237
+ }
238
+
239
+ if (!info[0].IsString()) {
240
+ return Napi::String::New(info.Env(), "");
241
+ }
242
+
243
+ std::string location = info[0].As<Napi::String>().Utf8Value();
244
+ std::filesystem::path audio_path(location);
245
+
246
+ return Napi::Number::From(info.Env(), audio_analizer.get_audio_duration(audio_path.string()));
247
+ }
248
+
249
+ Napi::Value ParserAddon::test_promise(const Napi::CallbackInfo& info) {
250
+ auto deffered = Napi::Promise::Deferred::New(info.Env());
251
+ auto tsfn = Napi::ThreadSafeFunction::New(info.Env(), NOOP_FUNC(info.Env()), "tsfn", 0, 1);
252
+
253
+ std::thread([tsfn, deffered]() {
254
+ tsfn.NonBlockingCall([deffered](Napi::Env env, Napi::Function) {
255
+ std::this_thread::sleep_for(std::chrono::milliseconds(2000));
256
+ deffered.Resolve(Napi::Boolean::New(env, true));
257
+ });
258
+
259
+ tsfn.Release();
260
+ }).detach();
261
+
262
+ return deffered.Promise();
263
+ }
264
+
265
+ NODE_API_ADDON(ParserAddon)
@@ -0,0 +1,26 @@
1
+ #pragma once
2
+
3
+ #include <napi.h>
4
+ #include "pool.hpp"
5
+
6
+ class ParserAddon : public Napi::Addon<ParserAddon> {
7
+ public:
8
+ ParserAddon(Napi::Env env, Napi::Object exports) {
9
+ pool.initialize(std::thread::hardware_concurrency());
10
+ DefineAddon(exports, {
11
+ InstanceMethod("get_property", &ParserAddon::get_property),
12
+ InstanceMethod("get_properties", &ParserAddon::get_properties),
13
+ InstanceMethod("process_beatmaps", &ParserAddon::process_beatmaps),
14
+ InstanceMethod("get_duration", &ParserAddon::get_duration),
15
+ InstanceMethod("get_audio_duration", &ParserAddon::get_audio_duration),
16
+ InstanceMethod("test_promise", &ParserAddon::test_promise)
17
+ });
18
+ }
19
+
20
+ Napi::Value get_property(const Napi::CallbackInfo& info);
21
+ Napi::Value get_properties(const Napi::CallbackInfo& info);
22
+ Napi::Value process_beatmaps(const Napi::CallbackInfo& info);
23
+ Napi::Value get_duration(const Napi::CallbackInfo& info);
24
+ Napi::Value get_audio_duration(const Napi::CallbackInfo& info);
25
+ Napi::Value test_promise(const Napi::CallbackInfo& info);
26
+ };
@@ -0,0 +1,56 @@
1
+ #pragma once
2
+ #include <string>
3
+ #include <unordered_set>
4
+ #include <unordered_map>
5
+
6
+ enum OSU_SECTIONS {
7
+ General = 0,
8
+ Editor,
9
+ Metadata,
10
+ Difficulty,
11
+ Events,
12
+ TimingPoints,
13
+ Colours,
14
+ HitObjects
15
+ };
16
+
17
+ const std::unordered_map<std::string_view, std::string_view> KEY_TO_SECTION = {
18
+ {"AudioFilename", "[General]"},
19
+ {"AudioLeadIn", "[General]"},
20
+ {"PreviewTime", "[General]"},
21
+ {"Countdown", "[General]"},
22
+ {"SampleSet", "[General]"},
23
+ {"StackLeniency", "[General]"},
24
+ {"Mode", "[General]"},
25
+ {"LetterboxInBreaks", "[General]"},
26
+ {"WidescreenStoryboard", "[General]"},
27
+ {"Bookmarks", "[Editor]"},
28
+ {"DistanceSpacing", "[Editor]"},
29
+ {"BeatDivisor", "[Editor]"},
30
+ {"GridSize", "[Editor]"},
31
+ {"TimelineZoom", "[Editor]"},
32
+ {"Title", "[Metadata]"},
33
+ {"TitleUnicode", "[Metadata]"},
34
+ {"Artist", "[Metadata]"},
35
+ {"ArtistUnicode", "[Metadata]"},
36
+ {"Creator", "[Metadata]"},
37
+ {"Version", "[Metadata]"},
38
+ {"Source", "[Metadata]"},
39
+ {"Tags", "[Metadata]"},
40
+ {"BeatmapID", "[Metadata]"},
41
+ {"BeatmapSetID", "[Metadata]"},
42
+ {"HPDrainRate", "[Difficulty]"},
43
+ {"CircleSize", "[Difficulty]"},
44
+ {"OverallDifficulty", "[Difficulty]"},
45
+ {"ApproachRate", "[Difficulty]"},
46
+ {"SliderMultiplier", "[Difficulty]"},
47
+ {"SliderTickRate", "[Difficulty]"},
48
+ // special ones (doenst have keys)
49
+ {"Background", "[Events]"},
50
+ {"Video", "[Events]"},
51
+ {"Storyboard", "[Events]"},
52
+ // extra special ones (why not)
53
+ {"Duration", "[General]"}
54
+ };
55
+
56
+ const std::unordered_set<std::string_view> SPECIAL_KEYS {"Background", "Video", "Storyboard"};
@@ -0,0 +1,30 @@
1
+ #include <iostream>
2
+ #include <filesystem>
3
+ #include <sndfile.h>
4
+ #include "audio.hpp"
5
+
6
+ double AudioAnalyzer::get_audio_duration(std::string location) {
7
+ double duration = 0.0;
8
+
9
+ if (get_cache(location, duration)) {
10
+ return duration;
11
+ }
12
+
13
+ if (!std::filesystem::exists(location)) {
14
+ std::cout << location << " does not exists\n";
15
+ return 0.0;
16
+ }
17
+
18
+ SF_INFO sfinfo{};
19
+ SNDFILE *file = sf_open(location.c_str(), SFM_READ, &sfinfo);
20
+
21
+ if (!file) {
22
+ return 0.0;
23
+ }
24
+
25
+ duration = static_cast<double>(sfinfo.frames) / sfinfo.samplerate;
26
+
27
+ sf_close(file);
28
+ set_cache(location, duration);
29
+ return duration;
30
+ }
@@ -0,0 +1,34 @@
1
+ #pragma once
2
+ #include <mutex>
3
+ #include <string>
4
+ #include <unordered_map>
5
+
6
+ class AudioAnalyzer
7
+ {
8
+ private:
9
+ std::unordered_map<std::string, double> cache;
10
+ mutable std::mutex cache_mutex;
11
+
12
+ bool get_cache(const std::string &key, double &duration) const {
13
+ std::lock_guard<std::mutex> lock(cache_mutex);
14
+ auto it = cache.find(key);
15
+ if (it != cache.end())
16
+ {
17
+ duration = it->second;
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ void set_cache(const std::string &key, double duration) {
24
+ std::lock_guard<std::mutex> lock(cache_mutex);
25
+ cache[key] = duration;
26
+ }
27
+
28
+ void clear_cache() {
29
+ std::lock_guard<std::mutex> lock(cache_mutex);
30
+ cache.clear();
31
+ }
32
+ public:
33
+ double get_audio_duration(std::string location);
34
+ };
@@ -0,0 +1,294 @@
1
+ #include <algorithm>
2
+ #include <cstdio>
3
+ #include <iostream>
4
+ #include <optional>
5
+ #include <string>
6
+ #include <string_view>
7
+ #include <unordered_set>
8
+ #include <unordered_map>
9
+ #include "./parser.hpp"
10
+ #include "../definitions.hpp"
11
+
12
+ const std::unordered_set<std::string_view> VIDEO_EXTENSIONS = {
13
+ ".mp4", ".avi", ".flv", ".mov", ".wmv", ".m4v", ".mpg", ".mpeg"
14
+ };
15
+
16
+ const std::unordered_set<std::string_view> IMAGE_EXTENSIONS = {
17
+ ".jpg", ".jpeg", ".png", ".bmp", ".gif"
18
+ };
19
+
20
+ std::string_view trim_view(std::string_view s) {
21
+ size_t start = s.find_first_not_of(" \t\r\n");
22
+
23
+ if (start == std::string_view::npos) {
24
+ return "";
25
+ }
26
+
27
+ size_t end = s.find_last_not_of(" \t\r\n");
28
+ return s.substr(start, end - start + 1);
29
+ }
30
+
31
+ std::string trim(const std::string &s) {
32
+ std::string_view result = trim_view(s);
33
+ return std::string(result);
34
+ }
35
+
36
+ std::vector<std::string_view> split_view(std::string_view s, char delim) {
37
+ std::vector<std::string_view> result;
38
+
39
+ size_t start = 0;
40
+ size_t end = s.find(delim);
41
+
42
+ while (end != std::string_view::npos) {
43
+ result.push_back(s.substr(start, end - start));
44
+ start = end + 1;
45
+ end = s.find(delim, start);
46
+ }
47
+
48
+ result.push_back(s.substr(start));
49
+ return result;
50
+ }
51
+
52
+ std::string normalize_path(const std::string &path) {
53
+ #ifdef _WIN32
54
+ std::string normalized = path;
55
+ std::replace(normalized.begin(), normalized.end(), '/', '\\');
56
+ return normalized;
57
+ #else
58
+ return path;
59
+ #endif
60
+ }
61
+
62
+ std::string_view get_extension(std::string_view filename) {
63
+ size_t dot_pos = filename.find_last_of('.');
64
+
65
+ if (dot_pos == std::string_view::npos) {
66
+ return "";
67
+ }
68
+
69
+ return filename.substr(dot_pos);
70
+ }
71
+
72
+ std::optional<std::string> get_special_key(std::string_view key, std::string_view line) {
73
+ if (key == "Background" || key == "Video") {
74
+ std::vector<std::string_view> parts = split_view(line, ',');
75
+
76
+ if (parts.size() < 3) {
77
+ return std::nullopt;
78
+ }
79
+
80
+ std::string_view event_type = trim_view(parts[0]);
81
+ std::string_view start_time = trim_view(parts[1]);
82
+
83
+ if (event_type != "0" || start_time != "0") {
84
+ return std::nullopt;
85
+ }
86
+
87
+ std::string_view filename = trim_view(parts[2]);
88
+
89
+ // remove quotes if present
90
+ if (filename.size() >= 2 && filename.front() == '"' && filename.back() == '"') {
91
+ filename = filename.substr(1, filename.size() - 2);
92
+ }
93
+
94
+ std::string_view ext = get_extension(filename);
95
+ std::string ext_lower(ext);
96
+ std::transform(ext_lower.begin(), ext_lower.end(), ext_lower.begin(), ::tolower);
97
+
98
+ if (key == "Video") {
99
+ if (VIDEO_EXTENSIONS.count(ext_lower) == 0) {
100
+ return std::nullopt;
101
+ }
102
+ } else {
103
+ if (IMAGE_EXTENSIONS.count(ext_lower) == 0) {
104
+ return std::nullopt;
105
+ }
106
+ }
107
+
108
+ return normalize_path(std::string(filename));
109
+ } else if (key == "Storyboard") {
110
+ // TODO:
111
+ return std::nullopt;
112
+ }
113
+
114
+ return std::nullopt;
115
+ }
116
+
117
+ std::string osu_parser::get_property(std::string_view content, std::string_view key) {
118
+ bool is_special_key = SPECIAL_KEYS.count(key) > 0;
119
+ std::string current_section;
120
+ std::string special_section;
121
+
122
+ if (is_special_key) {
123
+ if (KEY_TO_SECTION.find(key) == KEY_TO_SECTION.end()) {
124
+ std::cout << "failed to find special key: " << key << "\n";
125
+ return "";
126
+ }
127
+ special_section = KEY_TO_SECTION.at(key);
128
+ }
129
+
130
+ size_t start = 0;
131
+ size_t end = content.find('\n');
132
+
133
+ while (end != std::string_view::npos) {
134
+ std::string_view line_view = trim_view(content.substr(start, end - start));
135
+ start = end + 1;
136
+ end = content.find('\n', start);
137
+
138
+ if (line_view.empty() || line_view[0] == '/') {
139
+ continue;
140
+ }
141
+
142
+ if (line_view[0] == '[') {
143
+ current_section = std::string(line_view);
144
+ continue;
145
+ }
146
+
147
+ if (is_special_key) {
148
+ if (special_section != current_section) {
149
+ continue;
150
+ }
151
+
152
+ auto result = get_special_key(key, line_view);
153
+
154
+ if (result.has_value()) {
155
+ return result.value();
156
+ }
157
+
158
+ continue;
159
+ }
160
+
161
+ size_t delimiter_i = line_view.find(':');
162
+
163
+ if (delimiter_i == std::string_view::npos) {
164
+ continue;
165
+ }
166
+
167
+ std::string_view current_key = trim_view(line_view.substr(0, delimiter_i));
168
+
169
+ if (current_key == key) {
170
+ std::string_view value = trim_view(line_view.substr(delimiter_i + 1));
171
+ return std::string(value);
172
+ }
173
+ }
174
+
175
+ // check last line if no newline at end
176
+ if (start < content.size()) {
177
+ std::string_view line_view = trim_view(content.substr(start));
178
+ if (line_view.empty() || line_view[0] == '/') {
179
+ return "";
180
+ }
181
+
182
+ // section change at end
183
+ if (line_view[0] == '[') {
184
+ return "";
185
+ }
186
+
187
+ if (is_special_key) {
188
+ if (special_section == current_section) {
189
+ auto result = get_special_key(key, line_view);
190
+ if (result.has_value()) {
191
+ return result.value();
192
+ }
193
+ }
194
+ return "";
195
+ }
196
+
197
+ size_t delimiter_i = line_view.find(':');
198
+
199
+ if (delimiter_i != std::string_view::npos) {
200
+ std::string_view current_key = trim_view(line_view.substr(0, delimiter_i));
201
+ if (current_key == key) {
202
+ std::string_view value = trim_view(line_view.substr(delimiter_i + 1));
203
+ return std::string(value);
204
+ }
205
+ }
206
+ }
207
+
208
+ return "";
209
+ }
210
+
211
+ std::unordered_map<std::string, std::string> osu_parser::get_properties(std::string_view content, const std::vector<std::string>& keys) {
212
+ std::unordered_map<std::string, std::string> results;
213
+ std::unordered_map<std::string_view, std::string_view> key_to_section_map;
214
+ std::unordered_set<std::string_view> keys_to_find;
215
+
216
+ for (const auto& key : keys) {
217
+ keys_to_find.insert(key);
218
+ if (KEY_TO_SECTION.count(key)) {
219
+ key_to_section_map[key] = KEY_TO_SECTION.at(key);
220
+ }
221
+ }
222
+
223
+ std::string current_section;
224
+ size_t start = 0;
225
+ size_t end = content.find('\n');
226
+
227
+ auto process_line = [&](std::string_view line_view) {
228
+ if (line_view.empty() || line_view[0] == '/') {
229
+ return;
230
+ }
231
+
232
+ if (line_view[0] == '[') {
233
+ current_section = std::string(line_view);
234
+ return;
235
+ }
236
+
237
+ size_t delimiter_i = line_view.find(':');
238
+ if (delimiter_i != std::string_view::npos) {
239
+ std::string_view current_key = trim_view(line_view.substr(0, delimiter_i));
240
+
241
+ if (keys_to_find.count(current_key)) {
242
+ bool correct_section = !key_to_section_map.count(current_key) ||
243
+ key_to_section_map.at(current_key) == current_section;
244
+
245
+ bool not_found_yet = results.find(std::string(current_key)) == results.end();
246
+
247
+ if (correct_section && not_found_yet) {
248
+ std::string_view value = trim_view(line_view.substr(delimiter_i + 1));
249
+ results[std::string(current_key)] = std::string(value);
250
+ }
251
+ }
252
+ }
253
+
254
+ for (const auto& key : keys) {
255
+ if (!SPECIAL_KEYS.count(key)) {
256
+ continue;
257
+ }
258
+
259
+ if (results.find(key) != results.end()) {
260
+ continue;
261
+ }
262
+
263
+ if (key_to_section_map.count(key) && key_to_section_map.at(key) == current_section) {
264
+ auto result = get_special_key(key, line_view);
265
+ if (result.has_value()) {
266
+ results[key] = result.value();
267
+ }
268
+ }
269
+ }
270
+ };
271
+
272
+ while (end != std::string_view::npos) {
273
+ std::string_view line_view = trim_view(content.substr(start, end - start));
274
+ process_line(line_view);
275
+ start = end + 1;
276
+ end = content.find('\n', start);
277
+ }
278
+
279
+ if (start < content.size()) {
280
+ std::string_view line_view = trim_view(content.substr(start));
281
+ process_line(line_view);
282
+ }
283
+
284
+ return results;
285
+ }
286
+
287
+ std::vector<std::string> osu_parser::get_section() {
288
+ std::vector<std::string> result;
289
+ return result;
290
+ }
291
+
292
+ std::map<std::string, std::string> osu_parser::parse(std::string_view content) {
293
+ return {};
294
+ }
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+ #include <map>
3
+ #include <string>
4
+ #include <vector>
5
+ #include <unordered_map>
6
+
7
+ namespace osu_parser {
8
+ std::string get_property(std::string_view content, std::string_view key);
9
+ std::unordered_map<std::string, std::string> get_properties(std::string_view content, const std::vector<std::string>& keys);
10
+
11
+ // TODO:
12
+ std::vector<std::string> get_section();
13
+ std::map<std::string, std::string> parse(std::string_view content);
14
+ };
@@ -0,0 +1,80 @@
1
+ #pragma once
2
+
3
+ #include <condition_variable>
4
+ #include <functional>
5
+ #include <mutex>
6
+ #include <queue>
7
+ #include <thread>
8
+ #include <vector>
9
+ #include <atomic>
10
+
11
+ class ThreadPool {
12
+ public:
13
+ void initialize(int count) {
14
+ for (int i = 0; i < count; i++) {
15
+ // add new worker
16
+ workers.emplace_back([this]() {
17
+ // initialize task loop
18
+ while (true) {
19
+ std::function<void()> task;
20
+ // wait for new task
21
+ {
22
+ std::unique_lock<std::mutex> lock(queue_mutex);
23
+
24
+ // use condition variable to make threads go zzz until we actually have a task to do
25
+ cv.wait(lock, [this] {
26
+ return stop || !tasks.empty();
27
+ });
28
+
29
+ // ensure we have a task
30
+ if (stop && tasks.empty()) {
31
+ break;
32
+ }
33
+
34
+ // move task from queue to var
35
+ task = std::move(tasks.front());
36
+
37
+ // if i recall correctly "move" moves the pointer data from the queue
38
+ // but the pointer is still there pointing to nothing
39
+ // so remove it anyway
40
+ tasks.pop();
41
+ }
42
+
43
+ // execute it :)
44
+ task();
45
+ }
46
+ });
47
+ }
48
+ }
49
+
50
+ ~ThreadPool() {
51
+ stop = true;
52
+
53
+ // wake up all lazy ass threads
54
+ cv.notify_all();
55
+
56
+ // free them
57
+ for (std::thread& t : workers) {
58
+ if (t.joinable()) {
59
+ t.join();
60
+ }
61
+ }
62
+ }
63
+
64
+ template<class T>
65
+ void enqueue(T&& f) {
66
+ {
67
+ std::unique_lock<std::mutex> lock(queue_mutex);
68
+ tasks.emplace(std::forward<T>(f));
69
+ }
70
+ cv.notify_one();
71
+ }
72
+ private:
73
+ std::atomic<bool> stop{false};
74
+ std::vector<std::thread> workers;
75
+ std::queue<std::function<void()>> tasks;
76
+ std::mutex queue_mutex;
77
+ std::condition_variable cv;
78
+ };
79
+
80
+ inline ThreadPool pool;
package/src/types.ts ADDED
@@ -0,0 +1,48 @@
1
+ export type OsuKey =
2
+ | "AudioFilename"
3
+ | "AudioLeadIn"
4
+ | "PreviewTime"
5
+ | "Countdown"
6
+ | "SampleSet"
7
+ | "StackLeniency"
8
+ | "Mode"
9
+ | "LetterboxInBreaks"
10
+ | "WidescreenStoryboard"
11
+ | "Bookmarks"
12
+ | "DistanceSpacing"
13
+ | "BeatDivisor"
14
+ | "GridSize"
15
+ | "TimelineZoom"
16
+ | "Title"
17
+ | "TitleUnicode"
18
+ | "Artist"
19
+ | "ArtistUnicode"
20
+ | "Creator"
21
+ | "Version"
22
+ | "Source"
23
+ | "Tags"
24
+ | "BeatmapID"
25
+ | "BeatmapSetID"
26
+ | "HPDrainRate"
27
+ | "CircleSize"
28
+ | "OverallDifficulty"
29
+ | "ApproachRate"
30
+ | "SliderMultiplier"
31
+ | "SliderTickRate"
32
+ | "Background"
33
+ | "Video"
34
+ | "Storyboard"
35
+ | "Duration";
36
+
37
+ export interface OsuInput {
38
+ path: string;
39
+ id?: string;
40
+ }
41
+
42
+ export interface INativeExporter {
43
+ get_property(location: string, key: string): string;
44
+ get_properties(location: string, keys: string[]): Record<string, string>;
45
+ process_beatmaps(locations: string[], keys: string[]): Promise<Record<string, string>[]>;
46
+ get_duration(location: string): number;
47
+ get_audio_duration(location: string): number;
48
+ }