From 1174b328e5a764ef787d77ad9714b8bd3e1ccbfa Mon Sep 17 00:00:00 2001 From: JotaChina Date: Sun, 26 Oct 2025 16:43:01 -0300 Subject: [PATCH] sync mode on --- assets/css/style.css | 46 + assets/js/creations/audio.js | 9 +- assets/js/creations/audio/audio_audio.js | 230 ++- assets/js/creations/audio/audio_clipboard.js | 118 ++ assets/js/creations/audio/audio_state.js | 173 +- assets/js/creations/audio/audio_ui.js | 1368 ++++++++------ assets/js/creations/config.js | 1 - assets/js/creations/file.js | 43 +- assets/js/creations/main.js | 555 ++++-- assets/js/creations/pattern/pattern_audio.js | 67 +- assets/js/creations/pattern/pattern_bounce.js | 108 ++ assets/js/creations/pattern/pattern_state.js | 151 +- assets/js/creations/pattern/pattern_ui.js | 327 ++-- assets/js/creations/recording.js | 11 +- assets/js/creations/server/package-lock.json | 1330 ++++++++++++++ assets/js/creations/server/package.json | 19 + assets/js/creations/server/server.js | 514 ++++++ assets/js/creations/socket.js | 805 +++++++++ assets/js/creations/state.js | 105 +- assets/js/creations/ui.js | 66 +- creation.html | 68 +- node_modules/.bin/pino | 1 + node_modules/.package-lock.json | 122 ++ .../@pinojs/redact/.github/dependabot.yml | 13 + .../@pinojs/redact/.github/workflows/ci.yml | 48 + .../.github/workflows/publish-release.yml | 43 + node_modules/@pinojs/redact/LICENSE | 21 + node_modules/@pinojs/redact/README.md | 350 ++++ .../@pinojs/redact/benchmarks/basic.js | 184 ++ node_modules/@pinojs/redact/eslint.config.js | 1 + node_modules/@pinojs/redact/index.d.ts | 52 + node_modules/@pinojs/redact/index.js | 529 ++++++ node_modules/@pinojs/redact/index.test-d.ts | 22 + node_modules/@pinojs/redact/package.json | 37 + .../@pinojs/redact/scripts/sync-version.mjs | 20 + .../test/actual-redact-comparison.test.js | 211 +++ .../@pinojs/redact/test/index.test.js | 824 +++++++++ .../@pinojs/redact/test/integration.test.js | 390 ++++ .../redact/test/multiple-wildcards.test.js | 227 +++ .../redact/test/prototype-pollution.test.js | 223 +++ .../redact/test/selective-clone.test.js | 115 ++ node_modules/@pinojs/redact/tsconfig.json | 19 + node_modules/atomic-sleep/.travis.yml | 11 + node_modules/atomic-sleep/LICENSE | 22 + node_modules/atomic-sleep/index.js | 38 + node_modules/atomic-sleep/package.json | 37 + node_modules/atomic-sleep/readme.md | 58 + node_modules/atomic-sleep/test.js | 47 + .../on-exit-leak-free/.github/dependabot.yml | 12 + .../.github/workflows/ci.yml | 46 + node_modules/on-exit-leak-free/LICENSE | 21 + node_modules/on-exit-leak-free/README.md | 54 + node_modules/on-exit-leak-free/index.js | 108 ++ node_modules/on-exit-leak-free/package.json | 37 + .../on-exit-leak-free/test/base.test.js | 30 + .../test/event-emitter-leak.test.js | 23 + .../test/fixtures/beforeExit.js | 33 + .../on-exit-leak-free/test/fixtures/close.js | 21 + .../test/fixtures/gc-not-close.js | 24 + .../test/fixtures/unregister.js | 24 + .../.github/dependabot.yml | 13 + .../.github/workflows/ci.yml | 97 + .../pino-abstract-transport/.husky/pre-commit | 4 + node_modules/pino-abstract-transport/LICENSE | 21 + .../pino-abstract-transport/README.md | 172 ++ .../pino-abstract-transport/index.d.ts | 122 ++ node_modules/pino-abstract-transport/index.js | 128 ++ .../pino-abstract-transport/package.json | 40 + .../pino-abstract-transport/test/base.test.js | 445 +++++ .../fixtures/transport-async-iteration.js | 22 + .../test/fixtures/transport-on-data.js | 22 + .../test/fixtures/transport-transform.js | 24 + .../test/fixtures/worker-pipeline.js | 15 + .../test/types/index.test-d.ts | 31 + .../test/worker.test.js | 357 ++++ .../pino-std-serializers/.editorconfig | 13 + .../pino-std-serializers/.eslintignore | 2 + .../.github/dependabot.yml | 13 + .../.github/workflows/ci.yml | 81 + node_modules/pino-std-serializers/LICENSE | 7 + node_modules/pino-std-serializers/Readme.md | 182 ++ node_modules/pino-std-serializers/index.d.ts | 145 ++ node_modules/pino-std-serializers/index.js | 36 + .../pino-std-serializers/lib/err-helpers.js | 118 ++ .../pino-std-serializers/lib/err-proto.js | 48 + .../lib/err-with-cause.js | 48 + node_modules/pino-std-serializers/lib/err.js | 45 + node_modules/pino-std-serializers/lib/req.js | 100 ++ node_modules/pino-std-serializers/lib/res.js | 47 + .../pino-std-serializers/package.json | 47 + .../test/err-with-cause.test.js | 187 ++ .../pino-std-serializers/test/err.test.js | 200 +++ .../pino-std-serializers/test/req.test.js | 477 +++++ .../pino-std-serializers/test/res.test.js | 120 ++ .../test/types/index.test-d.ts | 71 + .../pino-std-serializers/tsconfig.json | 13 + node_modules/pino/.nojekyll | 0 node_modules/pino/.prettierignore | 1 + node_modules/pino/CNAME | 1 + node_modules/pino/CONTRIBUTING.md | 30 + node_modules/pino/LICENSE | 21 + node_modules/pino/README.md | 177 ++ node_modules/pino/SECURITY.md | 68 + node_modules/pino/benchmarks/basic.bench.js | 95 + .../pino/benchmarks/child-child.bench.js | 52 + .../pino/benchmarks/child-creation.bench.js | 73 + node_modules/pino/benchmarks/child.bench.js | 62 + .../pino/benchmarks/deep-object.bench.js | 88 + .../pino/benchmarks/formatters.bench.js | 50 + .../pino/benchmarks/internal/custom-levels.js | 67 + .../internal/just-pino-heavy.bench.js | 76 + .../benchmarks/internal/just-pino.bench.js | 182 ++ .../internal/parent-vs-child.bench.js | 75 + .../pino/benchmarks/internal/redact.bench.js | 86 + .../pino/benchmarks/long-string.bench.js | 81 + .../pino/benchmarks/multi-arg.bench.js | 193 ++ node_modules/pino/benchmarks/multistream.js | 98 + node_modules/pino/benchmarks/object.bench.js | 82 + .../utils/generate-benchmark-doc.js | 36 + .../pino/benchmarks/utils/runbench.js | 138 ++ .../pino/benchmarks/utils/wrap-log-level.js | 55 + node_modules/pino/bin.js | 6 + node_modules/pino/browser.js | 505 ++++++ node_modules/pino/build/sync-version.js | 25 + node_modules/pino/docs/api.md | 1588 +++++++++++++++++ node_modules/pino/docs/asynchronous.md | 40 + node_modules/pino/docs/benchmarks.md | 55 + node_modules/pino/docs/browser.md | 242 +++ node_modules/pino/docs/bundling.md | 40 + node_modules/pino/docs/child-loggers.md | 95 + node_modules/pino/docs/diagnostics.md | 16 + node_modules/pino/docs/ecosystem.md | 86 + node_modules/pino/docs/help.md | 345 ++++ node_modules/pino/docs/lts.md | 64 + node_modules/pino/docs/pretty.md | 35 + node_modules/pino/docs/redaction.md | 135 ++ node_modules/pino/docs/transports.md | 1263 +++++++++++++ node_modules/pino/docs/web.md | 309 ++++ node_modules/pino/eslint.config.js | 19 + node_modules/pino/examples/basic.js | 43 + node_modules/pino/examples/transport.js | 68 + node_modules/pino/favicon.ico | Bin 0 -> 15086 bytes node_modules/pino/file.js | 12 + node_modules/pino/inc-version.sh | 42 + node_modules/pino/index.html | 55 + node_modules/pino/lib/caller.js | 30 + node_modules/pino/lib/constants.js | 28 + node_modules/pino/lib/deprecations.js | 8 + node_modules/pino/lib/levels.js | 241 +++ node_modules/pino/lib/meta.js | 3 + node_modules/pino/lib/multistream.js | 203 +++ node_modules/pino/lib/proto.js | 256 +++ node_modules/pino/lib/redaction.js | 114 ++ node_modules/pino/lib/symbols.js | 74 + node_modules/pino/lib/time.js | 39 + node_modules/pino/lib/tools.js | 423 +++++ node_modules/pino/lib/transport-stream.js | 56 + node_modules/pino/lib/transport.js | 167 ++ node_modules/pino/lib/worker.js | 194 ++ node_modules/pino/package.json | 120 ++ node_modules/pino/pino.d.ts | 904 ++++++++++ node_modules/pino/pino.js | 234 +++ node_modules/pino/test/basic.test.js | 886 +++++++++ node_modules/pino/test/broken-pipe.test.js | 59 + node_modules/pino/test/browser-child.test.js | 132 ++ .../pino/test/browser-disabled.test.js | 87 + .../test/browser-early-console-freeze.test.js | 12 + .../test/browser-is-level-enabled.test.js | 101 ++ node_modules/pino/test/browser-levels.test.js | 241 +++ .../pino/test/browser-serializers.test.js | 352 ++++ .../pino/test/browser-timestamp.test.js | 88 + .../pino/test/browser-transmit.test.js | 417 +++++ node_modules/pino/test/browser.test.js | 679 +++++++ .../pino/test/complex-objects.test.js | 36 + node_modules/pino/test/crlf.test.js | 34 + node_modules/pino/test/custom-levels.test.js | 267 +++ node_modules/pino/test/diagnostics.test.js | 107 ++ node_modules/pino/test/error-key.test.js | 37 + node_modules/pino/test/error.test.js | 403 +++++ node_modules/pino/test/escaping.test.js | 93 + node_modules/pino/test/esm/esm.mjs | 14 + node_modules/pino/test/esm/index.test.js | 21 + node_modules/pino/test/esm/named-exports.mjs | 29 + node_modules/pino/test/exit.test.js | 79 + .../pino/test/fixtures/broken-pipe/basic.js | 9 + .../test/fixtures/broken-pipe/destination.js | 10 + .../test/fixtures/broken-pipe/syncfalse.js | 12 + .../pino/test/fixtures/console-transport.js | 13 + .../pino/test/fixtures/crashing-transport.js | 13 + .../pino/test/fixtures/default-exit.js | 8 + .../pino/test/fixtures/destination-exit.js | 8 + node_modules/pino/test/fixtures/eval/index.js | 13 + .../fixtures/eval/node_modules/14-files.js | 3 + .../fixtures/eval/node_modules/2-files.js | 3 + .../test/fixtures/eval/node_modules/file1.js | 5 + .../test/fixtures/eval/node_modules/file10.js | 5 + .../test/fixtures/eval/node_modules/file11.js | 5 + .../test/fixtures/eval/node_modules/file12.js | 5 + .../test/fixtures/eval/node_modules/file13.js | 5 + .../test/fixtures/eval/node_modules/file14.js | 11 + .../test/fixtures/eval/node_modules/file2.js | 5 + .../test/fixtures/eval/node_modules/file3.js | 5 + .../test/fixtures/eval/node_modules/file4.js | 5 + .../test/fixtures/eval/node_modules/file5.js | 5 + .../test/fixtures/eval/node_modules/file6.js | 5 + .../test/fixtures/eval/node_modules/file7.js | 5 + .../test/fixtures/eval/node_modules/file8.js | 5 + .../test/fixtures/eval/node_modules/file9.js | 5 + .../pino/test/fixtures/noop-transport.js | 10 + .../test/fixtures/pretty/null-prototype.js | 8 + .../test/fixtures/stdout-hack-protection.js | 11 + .../pino/test/fixtures/syncfalse-child.js | 6 + .../pino/test/fixtures/syncfalse-exit.js | 9 + .../test/fixtures/syncfalse-flush-exit.js | 10 + node_modules/pino/test/fixtures/syncfalse.js | 6 + .../pino/test/fixtures/syntax-error-esm.mjs | 2 + .../to-file-transport-with-transform.js | 20 + .../pino/test/fixtures/to-file-transport.js | 13 + .../pino/test/fixtures/to-file-transport.mjs | 8 + ...nsport-exit-immediately-with-async-dest.js | 16 + .../fixtures/transport-exit-immediately.js | 11 + .../test/fixtures/transport-exit-on-ready.js | 12 + .../pino/test/fixtures/transport-main.js | 9 + .../test/fixtures/transport-many-lines.js | 29 + .../test/fixtures/transport-string-stdout.js | 9 + .../pino/test/fixtures/transport-transform.js | 21 + .../fixtures/transport-uses-pino-config.js | 33 + .../test/fixtures/transport-with-on-exit.js | 12 + .../test/fixtures/transport-worker-data.js | 19 + .../pino/test/fixtures/transport-worker.js | 15 + .../fixtures/transport-wrong-export-type.js | 3 + .../pino/test/fixtures/transport/index.js | 12 + .../pino/test/fixtures/transport/package.json | 5 + .../ts/to-file-transport-with-transform.ts | 18 + .../test/fixtures/ts/to-file-transport.ts | 11 + .../pino/test/fixtures/ts/transpile.cjs | 36 + ...nsport-exit-immediately-with-async-dest.ts | 15 + .../fixtures/ts/transport-exit-immediately.ts | 10 + .../fixtures/ts/transport-exit-on-ready.ts | 11 + .../pino/test/fixtures/ts/transport-main.ts | 8 + .../fixtures/ts/transport-string-stdout.ts | 8 + .../pino/test/fixtures/ts/transport-worker.ts | 14 + node_modules/pino/test/formatters.test.js | 364 ++++ node_modules/pino/test/helper.d.ts | 4 + node_modules/pino/test/helper.js | 155 ++ node_modules/pino/test/hooks.test.js | 114 ++ node_modules/pino/test/http.test.js | 214 +++ .../pino/test/internals/version.test.js | 17 + .../pino/test/is-level-enabled.test.js | 179 ++ node_modules/pino/test/jest/basic.spec.js | 10 + node_modules/pino/test/levels.test.js | 810 +++++++++ node_modules/pino/test/metadata.test.js | 120 ++ .../pino/test/mixin-merge-strategy.test.js | 57 + node_modules/pino/test/mixin.test.js | 241 +++ node_modules/pino/test/multistream.test.js | 729 ++++++++ node_modules/pino/test/redact.test.js | 893 +++++++++ node_modules/pino/test/serializers.test.js | 257 +++ .../pino/test/stdout-protection.test.js | 41 + node_modules/pino/test/syncfalse.test.js | 186 ++ node_modules/pino/test/timestamp-nano.test.js | 37 + node_modules/pino/test/timestamp.test.js | 124 ++ .../pino/test/transport-stream.test.js | 40 + node_modules/pino/test/transport/big.test.js | 42 + .../test/transport/bundlers-support.test.js | 99 + .../pino/test/transport/caller.test.js | 24 + node_modules/pino/test/transport/core.test.js | 654 +++++++ .../test/transport/core.transpiled.test.ts | 114 ++ .../pino/test/transport/crash.test.js | 36 + .../pino/test/transport/module-link.test.js | 241 +++ .../pino/test/transport/pipeline.test.js | 137 ++ node_modules/pino/test/transport/repl.test.js | 15 + .../pino/test/transport/sync-false.test.js | 70 + .../pino/test/transport/sync-true.test.js | 57 + .../pino/test/transport/targets.test.js | 48 + .../test/transport/uses-pino-config.test.js | 166 ++ .../pino/test/types/pino-import.test-d.cts | 30 + .../test/types/pino-multistream.test-d.ts | 28 + .../pino/test/types/pino-top-export.test-d.ts | 36 + .../pino/test/types/pino-transport.test-d.ts | 145 ++ .../pino/test/types/pino-type-only.test-d.ts | 66 + node_modules/pino/test/types/pino.test-d.ts | 596 +++++++ node_modules/pino/test/types/pino.ts | 90 + node_modules/pino/tsconfig.json | 14 + node_modules/process-warning/.gitattributes | 2 + .../process-warning/.github/dependabot.yml | 13 + .../process-warning/.github/workflows/ci.yml | 22 + node_modules/process-warning/LICENSE | 21 + node_modules/process-warning/README.md | 118 ++ .../process-warning/benchmarks/warn.js | 25 + node_modules/process-warning/eslint.config.js | 6 + .../process-warning/examples/example.js | 11 + node_modules/process-warning/index.js | 124 ++ node_modules/process-warning/package.json | 73 + .../test/emit-interpolated-string.test.js | 34 + .../test/emit-once-only.test.js | 33 + .../process-warning/test/emit-reset.test.js | 40 + .../process-warning/test/emit-set.test.js | 35 + .../test/emit-unlimited.test.js | 42 + .../process-warning/test/index.test.js | 99 + .../process-warning/test/issue-88.test.js | 38 + .../process-warning/test/jest.test.js | 24 + .../process-warning/test/no-warnings.test.js | 80 + node_modules/process-warning/test/promise.js | 10 + node_modules/process-warning/types/index.d.ts | 37 + .../process-warning/types/index.test-d.ts | 36 + .../.github/workflows/ci.yml | 21 + node_modules/quick-format-unescaped/LICENSE | 21 + .../quick-format-unescaped/benchmark.js | 24 + node_modules/quick-format-unescaped/index.js | 109 ++ .../quick-format-unescaped/package.json | 29 + node_modules/quick-format-unescaped/readme.md | 66 + .../quick-format-unescaped/test/index.js | 136 ++ node_modules/real-require/LICENSE.md | 21 + node_modules/real-require/README.md | 51 + node_modules/real-require/package.json | 49 + node_modules/real-require/src/index.js | 14 + node_modules/safe-stable-stringify/LICENSE | 21 + .../safe-stable-stringify/esm/package.json | 4 + .../safe-stable-stringify/esm/wrapper.d.ts | 4 + .../safe-stable-stringify/esm/wrapper.js | 6 + node_modules/safe-stable-stringify/index.d.ts | 22 + node_modules/safe-stable-stringify/index.js | 625 +++++++ .../safe-stable-stringify/package.json | 65 + node_modules/safe-stable-stringify/readme.md | 179 ++ node_modules/sonic-boom/.eslintignore | 2 + node_modules/sonic-boom/.taprc | 3 + node_modules/sonic-boom/LICENSE | 21 + node_modules/sonic-boom/README.md | 152 ++ node_modules/sonic-boom/bench.js | 98 + node_modules/sonic-boom/check.js | 18 + node_modules/sonic-boom/example.js | 8 + node_modules/sonic-boom/fixtures/firehose.js | 22 + node_modules/sonic-boom/index.js | 719 ++++++++ node_modules/sonic-boom/package.json | 52 + node_modules/sonic-boom/test/destroy.test.js | 49 + node_modules/sonic-boom/test/end.test.js | 98 + .../sonic-boom/test/flush-sync.test.js | 140 ++ node_modules/sonic-boom/test/flush.test.js | 419 +++++ node_modules/sonic-boom/test/fsync.test.js | 63 + node_modules/sonic-boom/test/helper.js | 42 + .../sonic-boom/test/minlength.test.js | 35 + node_modules/sonic-boom/test/mode.test.js | 116 ++ .../sonic-boom/test/periodicflush.test.js | 61 + node_modules/sonic-boom/test/reopen.test.js | 239 +++ node_modules/sonic-boom/test/retry.test.js | 414 +++++ node_modules/sonic-boom/test/sync.test.js | 261 +++ node_modules/sonic-boom/test/write.test.js | 465 +++++ node_modules/sonic-boom/types/index.d.ts | 63 + node_modules/sonic-boom/types/tests/test.ts | 4 + node_modules/split2/LICENSE | 13 + node_modules/split2/README.md | 85 + node_modules/split2/bench.js | 27 + node_modules/split2/index.js | 141 ++ node_modules/split2/package.json | 39 + node_modules/split2/test.js | 409 +++++ .../thread-stream/.github/dependabot.yml | 13 + .../thread-stream/.github/workflows/ci.yml | 94 + node_modules/thread-stream/.husky/pre-commit | 4 + node_modules/thread-stream/.taprc | 4 + node_modules/thread-stream/LICENSE | 21 + node_modules/thread-stream/README.md | 135 ++ node_modules/thread-stream/bench.js | 85 + node_modules/thread-stream/index.d.ts | 92 + node_modules/thread-stream/index.js | 537 ++++++ node_modules/thread-stream/lib/indexes.js | 9 + node_modules/thread-stream/lib/wait.js | 61 + node_modules/thread-stream/lib/worker.js | 174 ++ node_modules/thread-stream/package.json | 57 + node_modules/thread-stream/test/base.test.js | 285 +++ node_modules/thread-stream/test/bench.test.js | 38 + .../thread-stream/test/bundlers.test.js | 60 + .../thread-stream/test/close-on-gc.js | 37 + .../test/commonjs-fallback.test.js | 80 + .../thread-stream/test/context.test.js | 21 + .../thread-stream/test/create-and-exit.js | 16 + .../thread-stream/test/custom-worker.js | 9 + .../test/dir with spaces/test-package.zip | Bin 0 -> 349 bytes node_modules/thread-stream/test/emit-event.js | 22 + node_modules/thread-stream/test/end.test.js | 61 + node_modules/thread-stream/test/error.js | 14 + node_modules/thread-stream/test/esm.test.mjs | 47 + node_modules/thread-stream/test/event.test.js | 23 + node_modules/thread-stream/test/exit.js | 14 + .../thread-stream/test/get-context.js | 22 + node_modules/thread-stream/test/helper.d.ts | 1 + node_modules/thread-stream/test/helper.js | 35 + .../thread-stream/test/indexes.test.js | 11 + .../test/multibyte-chars.test.mjs | 74 + .../thread-stream/test/never-drain.test.js | 57 + node_modules/thread-stream/test/on-message.js | 18 + node_modules/thread-stream/test/pkg/index.js | 37 + .../thread-stream/test/pkg/pkg.config.json | 15 + .../thread-stream/test/pkg/pkg.test.js | 46 + node_modules/thread-stream/test/port.js | 16 + .../thread-stream/test/post-message.test.js | 24 + .../thread-stream/test/string-limit-2.test.js | 41 + .../thread-stream/test/string-limit.test.js | 42 + .../thread-stream/test/syntax-error.mjs | 2 + .../test/thread-management.test.js | 121 ++ .../thread-stream/test/to-file-on-destroy.js | 23 + .../thread-stream/test/to-file-on-final.js | 24 + node_modules/thread-stream/test/to-file.js | 12 + node_modules/thread-stream/test/to-file.mjs | 8 + node_modules/thread-stream/test/to-next.js | 9 + .../thread-stream/test/transpiled.test.js | 30 + .../test/ts-commonjs-default-export.zip | Bin 0 -> 397 bytes node_modules/thread-stream/test/ts.test.ts | 33 + node_modules/thread-stream/test/ts/to-file.ts | 10 + .../thread-stream/test/ts/transpile.sh | 19 + .../thread-stream/test/uncaughtException.js | 21 + .../thread-stream/test/unhandledRejection.js | 21 + node_modules/thread-stream/test/yarnrc.yml | 7 + node_modules/thread-stream/tsconfig.json | 8 + package-lock.json | 127 ++ package.json | 5 + 415 files changed, 44294 insertions(+), 1182 deletions(-) create mode 100644 assets/js/creations/audio/audio_clipboard.js create mode 100644 assets/js/creations/pattern/pattern_bounce.js create mode 100644 assets/js/creations/server/package-lock.json create mode 100644 assets/js/creations/server/package.json create mode 100644 assets/js/creations/server/server.js create mode 100644 assets/js/creations/socket.js create mode 120000 node_modules/.bin/pino create mode 100644 node_modules/.package-lock.json create mode 100644 node_modules/@pinojs/redact/.github/dependabot.yml create mode 100644 node_modules/@pinojs/redact/.github/workflows/ci.yml create mode 100644 node_modules/@pinojs/redact/.github/workflows/publish-release.yml create mode 100644 node_modules/@pinojs/redact/LICENSE create mode 100644 node_modules/@pinojs/redact/README.md create mode 100644 node_modules/@pinojs/redact/benchmarks/basic.js create mode 100644 node_modules/@pinojs/redact/eslint.config.js create mode 100644 node_modules/@pinojs/redact/index.d.ts create mode 100644 node_modules/@pinojs/redact/index.js create mode 100644 node_modules/@pinojs/redact/index.test-d.ts create mode 100644 node_modules/@pinojs/redact/package.json create mode 100644 node_modules/@pinojs/redact/scripts/sync-version.mjs create mode 100644 node_modules/@pinojs/redact/test/actual-redact-comparison.test.js create mode 100644 node_modules/@pinojs/redact/test/index.test.js create mode 100644 node_modules/@pinojs/redact/test/integration.test.js create mode 100644 node_modules/@pinojs/redact/test/multiple-wildcards.test.js create mode 100644 node_modules/@pinojs/redact/test/prototype-pollution.test.js create mode 100644 node_modules/@pinojs/redact/test/selective-clone.test.js create mode 100644 node_modules/@pinojs/redact/tsconfig.json create mode 100644 node_modules/atomic-sleep/.travis.yml create mode 100644 node_modules/atomic-sleep/LICENSE create mode 100644 node_modules/atomic-sleep/index.js create mode 100644 node_modules/atomic-sleep/package.json create mode 100644 node_modules/atomic-sleep/readme.md create mode 100644 node_modules/atomic-sleep/test.js create mode 100644 node_modules/on-exit-leak-free/.github/dependabot.yml create mode 100644 node_modules/on-exit-leak-free/.github/workflows/ci.yml create mode 100644 node_modules/on-exit-leak-free/LICENSE create mode 100644 node_modules/on-exit-leak-free/README.md create mode 100644 node_modules/on-exit-leak-free/index.js create mode 100644 node_modules/on-exit-leak-free/package.json create mode 100644 node_modules/on-exit-leak-free/test/base.test.js create mode 100644 node_modules/on-exit-leak-free/test/event-emitter-leak.test.js create mode 100644 node_modules/on-exit-leak-free/test/fixtures/beforeExit.js create mode 100644 node_modules/on-exit-leak-free/test/fixtures/close.js create mode 100644 node_modules/on-exit-leak-free/test/fixtures/gc-not-close.js create mode 100644 node_modules/on-exit-leak-free/test/fixtures/unregister.js create mode 100644 node_modules/pino-abstract-transport/.github/dependabot.yml create mode 100644 node_modules/pino-abstract-transport/.github/workflows/ci.yml create mode 100755 node_modules/pino-abstract-transport/.husky/pre-commit create mode 100644 node_modules/pino-abstract-transport/LICENSE create mode 100644 node_modules/pino-abstract-transport/README.md create mode 100644 node_modules/pino-abstract-transport/index.d.ts create mode 100644 node_modules/pino-abstract-transport/index.js create mode 100644 node_modules/pino-abstract-transport/package.json create mode 100644 node_modules/pino-abstract-transport/test/base.test.js create mode 100644 node_modules/pino-abstract-transport/test/fixtures/transport-async-iteration.js create mode 100644 node_modules/pino-abstract-transport/test/fixtures/transport-on-data.js create mode 100644 node_modules/pino-abstract-transport/test/fixtures/transport-transform.js create mode 100644 node_modules/pino-abstract-transport/test/fixtures/worker-pipeline.js create mode 100644 node_modules/pino-abstract-transport/test/types/index.test-d.ts create mode 100644 node_modules/pino-abstract-transport/test/worker.test.js create mode 100644 node_modules/pino-std-serializers/.editorconfig create mode 100644 node_modules/pino-std-serializers/.eslintignore create mode 100644 node_modules/pino-std-serializers/.github/dependabot.yml create mode 100644 node_modules/pino-std-serializers/.github/workflows/ci.yml create mode 100644 node_modules/pino-std-serializers/LICENSE create mode 100644 node_modules/pino-std-serializers/Readme.md create mode 100644 node_modules/pino-std-serializers/index.d.ts create mode 100644 node_modules/pino-std-serializers/index.js create mode 100644 node_modules/pino-std-serializers/lib/err-helpers.js create mode 100644 node_modules/pino-std-serializers/lib/err-proto.js create mode 100644 node_modules/pino-std-serializers/lib/err-with-cause.js create mode 100644 node_modules/pino-std-serializers/lib/err.js create mode 100644 node_modules/pino-std-serializers/lib/req.js create mode 100644 node_modules/pino-std-serializers/lib/res.js create mode 100644 node_modules/pino-std-serializers/package.json create mode 100644 node_modules/pino-std-serializers/test/err-with-cause.test.js create mode 100644 node_modules/pino-std-serializers/test/err.test.js create mode 100644 node_modules/pino-std-serializers/test/req.test.js create mode 100644 node_modules/pino-std-serializers/test/res.test.js create mode 100644 node_modules/pino-std-serializers/test/types/index.test-d.ts create mode 100644 node_modules/pino-std-serializers/tsconfig.json create mode 100644 node_modules/pino/.nojekyll create mode 100644 node_modules/pino/.prettierignore create mode 100644 node_modules/pino/CNAME create mode 100644 node_modules/pino/CONTRIBUTING.md create mode 100644 node_modules/pino/LICENSE create mode 100644 node_modules/pino/README.md create mode 100644 node_modules/pino/SECURITY.md create mode 100644 node_modules/pino/benchmarks/basic.bench.js create mode 100644 node_modules/pino/benchmarks/child-child.bench.js create mode 100644 node_modules/pino/benchmarks/child-creation.bench.js create mode 100644 node_modules/pino/benchmarks/child.bench.js create mode 100644 node_modules/pino/benchmarks/deep-object.bench.js create mode 100644 node_modules/pino/benchmarks/formatters.bench.js create mode 100644 node_modules/pino/benchmarks/internal/custom-levels.js create mode 100644 node_modules/pino/benchmarks/internal/just-pino-heavy.bench.js create mode 100644 node_modules/pino/benchmarks/internal/just-pino.bench.js create mode 100644 node_modules/pino/benchmarks/internal/parent-vs-child.bench.js create mode 100644 node_modules/pino/benchmarks/internal/redact.bench.js create mode 100644 node_modules/pino/benchmarks/long-string.bench.js create mode 100644 node_modules/pino/benchmarks/multi-arg.bench.js create mode 100644 node_modules/pino/benchmarks/multistream.js create mode 100644 node_modules/pino/benchmarks/object.bench.js create mode 100644 node_modules/pino/benchmarks/utils/generate-benchmark-doc.js create mode 100644 node_modules/pino/benchmarks/utils/runbench.js create mode 100644 node_modules/pino/benchmarks/utils/wrap-log-level.js create mode 100755 node_modules/pino/bin.js create mode 100644 node_modules/pino/browser.js create mode 100644 node_modules/pino/build/sync-version.js create mode 100644 node_modules/pino/docs/api.md create mode 100644 node_modules/pino/docs/asynchronous.md create mode 100644 node_modules/pino/docs/benchmarks.md create mode 100644 node_modules/pino/docs/browser.md create mode 100644 node_modules/pino/docs/bundling.md create mode 100644 node_modules/pino/docs/child-loggers.md create mode 100644 node_modules/pino/docs/diagnostics.md create mode 100644 node_modules/pino/docs/ecosystem.md create mode 100644 node_modules/pino/docs/help.md create mode 100644 node_modules/pino/docs/lts.md create mode 100644 node_modules/pino/docs/pretty.md create mode 100644 node_modules/pino/docs/redaction.md create mode 100644 node_modules/pino/docs/transports.md create mode 100644 node_modules/pino/docs/web.md create mode 100644 node_modules/pino/eslint.config.js create mode 100644 node_modules/pino/examples/basic.js create mode 100644 node_modules/pino/examples/transport.js create mode 100644 node_modules/pino/favicon.ico create mode 100644 node_modules/pino/file.js create mode 100755 node_modules/pino/inc-version.sh create mode 100644 node_modules/pino/index.html create mode 100644 node_modules/pino/lib/caller.js create mode 100644 node_modules/pino/lib/constants.js create mode 100644 node_modules/pino/lib/deprecations.js create mode 100644 node_modules/pino/lib/levels.js create mode 100644 node_modules/pino/lib/meta.js create mode 100644 node_modules/pino/lib/multistream.js create mode 100644 node_modules/pino/lib/proto.js create mode 100644 node_modules/pino/lib/redaction.js create mode 100644 node_modules/pino/lib/symbols.js create mode 100644 node_modules/pino/lib/time.js create mode 100644 node_modules/pino/lib/tools.js create mode 100644 node_modules/pino/lib/transport-stream.js create mode 100644 node_modules/pino/lib/transport.js create mode 100644 node_modules/pino/lib/worker.js create mode 100644 node_modules/pino/package.json create mode 100644 node_modules/pino/pino.d.ts create mode 100644 node_modules/pino/pino.js create mode 100644 node_modules/pino/test/basic.test.js create mode 100644 node_modules/pino/test/broken-pipe.test.js create mode 100644 node_modules/pino/test/browser-child.test.js create mode 100644 node_modules/pino/test/browser-disabled.test.js create mode 100644 node_modules/pino/test/browser-early-console-freeze.test.js create mode 100644 node_modules/pino/test/browser-is-level-enabled.test.js create mode 100644 node_modules/pino/test/browser-levels.test.js create mode 100644 node_modules/pino/test/browser-serializers.test.js create mode 100644 node_modules/pino/test/browser-timestamp.test.js create mode 100644 node_modules/pino/test/browser-transmit.test.js create mode 100644 node_modules/pino/test/browser.test.js create mode 100644 node_modules/pino/test/complex-objects.test.js create mode 100644 node_modules/pino/test/crlf.test.js create mode 100644 node_modules/pino/test/custom-levels.test.js create mode 100644 node_modules/pino/test/diagnostics.test.js create mode 100644 node_modules/pino/test/error-key.test.js create mode 100644 node_modules/pino/test/error.test.js create mode 100644 node_modules/pino/test/escaping.test.js create mode 100644 node_modules/pino/test/esm/esm.mjs create mode 100644 node_modules/pino/test/esm/index.test.js create mode 100644 node_modules/pino/test/esm/named-exports.mjs create mode 100644 node_modules/pino/test/exit.test.js create mode 100644 node_modules/pino/test/fixtures/broken-pipe/basic.js create mode 100644 node_modules/pino/test/fixtures/broken-pipe/destination.js create mode 100644 node_modules/pino/test/fixtures/broken-pipe/syncfalse.js create mode 100644 node_modules/pino/test/fixtures/console-transport.js create mode 100644 node_modules/pino/test/fixtures/crashing-transport.js create mode 100644 node_modules/pino/test/fixtures/default-exit.js create mode 100644 node_modules/pino/test/fixtures/destination-exit.js create mode 100644 node_modules/pino/test/fixtures/eval/index.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/14-files.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/2-files.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file1.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file10.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file11.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file12.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file13.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file14.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file2.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file3.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file4.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file5.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file6.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file7.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file8.js create mode 100644 node_modules/pino/test/fixtures/eval/node_modules/file9.js create mode 100644 node_modules/pino/test/fixtures/noop-transport.js create mode 100644 node_modules/pino/test/fixtures/pretty/null-prototype.js create mode 100644 node_modules/pino/test/fixtures/stdout-hack-protection.js create mode 100644 node_modules/pino/test/fixtures/syncfalse-child.js create mode 100644 node_modules/pino/test/fixtures/syncfalse-exit.js create mode 100644 node_modules/pino/test/fixtures/syncfalse-flush-exit.js create mode 100644 node_modules/pino/test/fixtures/syncfalse.js create mode 100644 node_modules/pino/test/fixtures/syntax-error-esm.mjs create mode 100644 node_modules/pino/test/fixtures/to-file-transport-with-transform.js create mode 100644 node_modules/pino/test/fixtures/to-file-transport.js create mode 100644 node_modules/pino/test/fixtures/to-file-transport.mjs create mode 100644 node_modules/pino/test/fixtures/transport-exit-immediately-with-async-dest.js create mode 100644 node_modules/pino/test/fixtures/transport-exit-immediately.js create mode 100644 node_modules/pino/test/fixtures/transport-exit-on-ready.js create mode 100644 node_modules/pino/test/fixtures/transport-main.js create mode 100644 node_modules/pino/test/fixtures/transport-many-lines.js create mode 100644 node_modules/pino/test/fixtures/transport-string-stdout.js create mode 100644 node_modules/pino/test/fixtures/transport-transform.js create mode 100644 node_modules/pino/test/fixtures/transport-uses-pino-config.js create mode 100644 node_modules/pino/test/fixtures/transport-with-on-exit.js create mode 100644 node_modules/pino/test/fixtures/transport-worker-data.js create mode 100644 node_modules/pino/test/fixtures/transport-worker.js create mode 100644 node_modules/pino/test/fixtures/transport-wrong-export-type.js create mode 100644 node_modules/pino/test/fixtures/transport/index.js create mode 100644 node_modules/pino/test/fixtures/transport/package.json create mode 100644 node_modules/pino/test/fixtures/ts/to-file-transport-with-transform.ts create mode 100644 node_modules/pino/test/fixtures/ts/to-file-transport.ts create mode 100755 node_modules/pino/test/fixtures/ts/transpile.cjs create mode 100644 node_modules/pino/test/fixtures/ts/transport-exit-immediately-with-async-dest.ts create mode 100644 node_modules/pino/test/fixtures/ts/transport-exit-immediately.ts create mode 100644 node_modules/pino/test/fixtures/ts/transport-exit-on-ready.ts create mode 100644 node_modules/pino/test/fixtures/ts/transport-main.ts create mode 100644 node_modules/pino/test/fixtures/ts/transport-string-stdout.ts create mode 100644 node_modules/pino/test/fixtures/ts/transport-worker.ts create mode 100644 node_modules/pino/test/formatters.test.js create mode 100644 node_modules/pino/test/helper.d.ts create mode 100644 node_modules/pino/test/helper.js create mode 100644 node_modules/pino/test/hooks.test.js create mode 100644 node_modules/pino/test/http.test.js create mode 100644 node_modules/pino/test/internals/version.test.js create mode 100644 node_modules/pino/test/is-level-enabled.test.js create mode 100644 node_modules/pino/test/jest/basic.spec.js create mode 100644 node_modules/pino/test/levels.test.js create mode 100644 node_modules/pino/test/metadata.test.js create mode 100644 node_modules/pino/test/mixin-merge-strategy.test.js create mode 100644 node_modules/pino/test/mixin.test.js create mode 100644 node_modules/pino/test/multistream.test.js create mode 100644 node_modules/pino/test/redact.test.js create mode 100644 node_modules/pino/test/serializers.test.js create mode 100644 node_modules/pino/test/stdout-protection.test.js create mode 100644 node_modules/pino/test/syncfalse.test.js create mode 100644 node_modules/pino/test/timestamp-nano.test.js create mode 100644 node_modules/pino/test/timestamp.test.js create mode 100644 node_modules/pino/test/transport-stream.test.js create mode 100644 node_modules/pino/test/transport/big.test.js create mode 100644 node_modules/pino/test/transport/bundlers-support.test.js create mode 100644 node_modules/pino/test/transport/caller.test.js create mode 100644 node_modules/pino/test/transport/core.test.js create mode 100644 node_modules/pino/test/transport/core.transpiled.test.ts create mode 100644 node_modules/pino/test/transport/crash.test.js create mode 100644 node_modules/pino/test/transport/module-link.test.js create mode 100644 node_modules/pino/test/transport/pipeline.test.js create mode 100644 node_modules/pino/test/transport/repl.test.js create mode 100644 node_modules/pino/test/transport/sync-false.test.js create mode 100644 node_modules/pino/test/transport/sync-true.test.js create mode 100644 node_modules/pino/test/transport/targets.test.js create mode 100644 node_modules/pino/test/transport/uses-pino-config.test.js create mode 100644 node_modules/pino/test/types/pino-import.test-d.cts create mode 100644 node_modules/pino/test/types/pino-multistream.test-d.ts create mode 100644 node_modules/pino/test/types/pino-top-export.test-d.ts create mode 100644 node_modules/pino/test/types/pino-transport.test-d.ts create mode 100644 node_modules/pino/test/types/pino-type-only.test-d.ts create mode 100644 node_modules/pino/test/types/pino.test-d.ts create mode 100644 node_modules/pino/test/types/pino.ts create mode 100644 node_modules/pino/tsconfig.json create mode 100644 node_modules/process-warning/.gitattributes create mode 100644 node_modules/process-warning/.github/dependabot.yml create mode 100644 node_modules/process-warning/.github/workflows/ci.yml create mode 100644 node_modules/process-warning/LICENSE create mode 100644 node_modules/process-warning/README.md create mode 100644 node_modules/process-warning/benchmarks/warn.js create mode 100644 node_modules/process-warning/eslint.config.js create mode 100644 node_modules/process-warning/examples/example.js create mode 100644 node_modules/process-warning/index.js create mode 100644 node_modules/process-warning/package.json create mode 100644 node_modules/process-warning/test/emit-interpolated-string.test.js create mode 100644 node_modules/process-warning/test/emit-once-only.test.js create mode 100644 node_modules/process-warning/test/emit-reset.test.js create mode 100644 node_modules/process-warning/test/emit-set.test.js create mode 100644 node_modules/process-warning/test/emit-unlimited.test.js create mode 100644 node_modules/process-warning/test/index.test.js create mode 100644 node_modules/process-warning/test/issue-88.test.js create mode 100644 node_modules/process-warning/test/jest.test.js create mode 100644 node_modules/process-warning/test/no-warnings.test.js create mode 100644 node_modules/process-warning/test/promise.js create mode 100644 node_modules/process-warning/types/index.d.ts create mode 100644 node_modules/process-warning/types/index.test-d.ts create mode 100644 node_modules/quick-format-unescaped/.github/workflows/ci.yml create mode 100644 node_modules/quick-format-unescaped/LICENSE create mode 100644 node_modules/quick-format-unescaped/benchmark.js create mode 100644 node_modules/quick-format-unescaped/index.js create mode 100644 node_modules/quick-format-unescaped/package.json create mode 100644 node_modules/quick-format-unescaped/readme.md create mode 100644 node_modules/quick-format-unescaped/test/index.js create mode 100644 node_modules/real-require/LICENSE.md create mode 100644 node_modules/real-require/README.md create mode 100644 node_modules/real-require/package.json create mode 100644 node_modules/real-require/src/index.js create mode 100644 node_modules/safe-stable-stringify/LICENSE create mode 100644 node_modules/safe-stable-stringify/esm/package.json create mode 100644 node_modules/safe-stable-stringify/esm/wrapper.d.ts create mode 100644 node_modules/safe-stable-stringify/esm/wrapper.js create mode 100644 node_modules/safe-stable-stringify/index.d.ts create mode 100644 node_modules/safe-stable-stringify/index.js create mode 100644 node_modules/safe-stable-stringify/package.json create mode 100644 node_modules/safe-stable-stringify/readme.md create mode 100644 node_modules/sonic-boom/.eslintignore create mode 100644 node_modules/sonic-boom/.taprc create mode 100644 node_modules/sonic-boom/LICENSE create mode 100644 node_modules/sonic-boom/README.md create mode 100644 node_modules/sonic-boom/bench.js create mode 100644 node_modules/sonic-boom/check.js create mode 100644 node_modules/sonic-boom/example.js create mode 100644 node_modules/sonic-boom/fixtures/firehose.js create mode 100644 node_modules/sonic-boom/index.js create mode 100644 node_modules/sonic-boom/package.json create mode 100644 node_modules/sonic-boom/test/destroy.test.js create mode 100644 node_modules/sonic-boom/test/end.test.js create mode 100644 node_modules/sonic-boom/test/flush-sync.test.js create mode 100644 node_modules/sonic-boom/test/flush.test.js create mode 100644 node_modules/sonic-boom/test/fsync.test.js create mode 100644 node_modules/sonic-boom/test/helper.js create mode 100644 node_modules/sonic-boom/test/minlength.test.js create mode 100644 node_modules/sonic-boom/test/mode.test.js create mode 100644 node_modules/sonic-boom/test/periodicflush.test.js create mode 100644 node_modules/sonic-boom/test/reopen.test.js create mode 100644 node_modules/sonic-boom/test/retry.test.js create mode 100644 node_modules/sonic-boom/test/sync.test.js create mode 100644 node_modules/sonic-boom/test/write.test.js create mode 100644 node_modules/sonic-boom/types/index.d.ts create mode 100644 node_modules/sonic-boom/types/tests/test.ts create mode 100644 node_modules/split2/LICENSE create mode 100644 node_modules/split2/README.md create mode 100644 node_modules/split2/bench.js create mode 100644 node_modules/split2/index.js create mode 100644 node_modules/split2/package.json create mode 100644 node_modules/split2/test.js create mode 100644 node_modules/thread-stream/.github/dependabot.yml create mode 100644 node_modules/thread-stream/.github/workflows/ci.yml create mode 100755 node_modules/thread-stream/.husky/pre-commit create mode 100644 node_modules/thread-stream/.taprc create mode 100644 node_modules/thread-stream/LICENSE create mode 100644 node_modules/thread-stream/README.md create mode 100644 node_modules/thread-stream/bench.js create mode 100644 node_modules/thread-stream/index.d.ts create mode 100644 node_modules/thread-stream/index.js create mode 100644 node_modules/thread-stream/lib/indexes.js create mode 100644 node_modules/thread-stream/lib/wait.js create mode 100644 node_modules/thread-stream/lib/worker.js create mode 100644 node_modules/thread-stream/package.json create mode 100644 node_modules/thread-stream/test/base.test.js create mode 100644 node_modules/thread-stream/test/bench.test.js create mode 100644 node_modules/thread-stream/test/bundlers.test.js create mode 100644 node_modules/thread-stream/test/close-on-gc.js create mode 100644 node_modules/thread-stream/test/commonjs-fallback.test.js create mode 100644 node_modules/thread-stream/test/context.test.js create mode 100644 node_modules/thread-stream/test/create-and-exit.js create mode 100644 node_modules/thread-stream/test/custom-worker.js create mode 100644 node_modules/thread-stream/test/dir with spaces/test-package.zip create mode 100644 node_modules/thread-stream/test/emit-event.js create mode 100644 node_modules/thread-stream/test/end.test.js create mode 100644 node_modules/thread-stream/test/error.js create mode 100644 node_modules/thread-stream/test/esm.test.mjs create mode 100644 node_modules/thread-stream/test/event.test.js create mode 100644 node_modules/thread-stream/test/exit.js create mode 100644 node_modules/thread-stream/test/get-context.js create mode 100644 node_modules/thread-stream/test/helper.d.ts create mode 100644 node_modules/thread-stream/test/helper.js create mode 100644 node_modules/thread-stream/test/indexes.test.js create mode 100644 node_modules/thread-stream/test/multibyte-chars.test.mjs create mode 100644 node_modules/thread-stream/test/never-drain.test.js create mode 100644 node_modules/thread-stream/test/on-message.js create mode 100644 node_modules/thread-stream/test/pkg/index.js create mode 100644 node_modules/thread-stream/test/pkg/pkg.config.json create mode 100644 node_modules/thread-stream/test/pkg/pkg.test.js create mode 100644 node_modules/thread-stream/test/port.js create mode 100644 node_modules/thread-stream/test/post-message.test.js create mode 100644 node_modules/thread-stream/test/string-limit-2.test.js create mode 100644 node_modules/thread-stream/test/string-limit.test.js create mode 100644 node_modules/thread-stream/test/syntax-error.mjs create mode 100644 node_modules/thread-stream/test/thread-management.test.js create mode 100644 node_modules/thread-stream/test/to-file-on-destroy.js create mode 100644 node_modules/thread-stream/test/to-file-on-final.js create mode 100644 node_modules/thread-stream/test/to-file.js create mode 100644 node_modules/thread-stream/test/to-file.mjs create mode 100644 node_modules/thread-stream/test/to-next.js create mode 100644 node_modules/thread-stream/test/transpiled.test.js create mode 100644 node_modules/thread-stream/test/ts-commonjs-default-export.zip create mode 100644 node_modules/thread-stream/test/ts.test.ts create mode 100644 node_modules/thread-stream/test/ts/to-file.ts create mode 100755 node_modules/thread-stream/test/ts/transpile.sh create mode 100644 node_modules/thread-stream/test/uncaughtException.js create mode 100644 node_modules/thread-stream/test/unhandledRejection.js create mode 100644 node_modules/thread-stream/test/yarnrc.yml create mode 100644 node_modules/thread-stream/tsconfig.json create mode 100644 package-lock.json create mode 100644 package.json diff --git a/assets/css/style.css b/assets/css/style.css index c136f74..be2cbea 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -186,6 +186,52 @@ body.sidebar-hidden .sample-browser { border: 1px solid var(--border-color-light); } +/* Estilo para o novo menu de contexto da régua */ +#ruler-context-menu { + display: none; + position: fixed; + z-index: 10000; + background-color: var(--background-dark); + border: 1px solid var(--border-color-dark); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + font-size: 0.8rem; + padding: 5px 0; + min-width: 150px; +} + +#ruler-context-menu > div { + padding: 6px 15px; + cursor: pointer; + color: var(--text-light); +} + +#ruler-context-menu > div:hover { + background-color: var(--accent-blue); + color: #fff; +} + +/* Estilo para clipes "recortados" (cut) */ +.timeline-clip.cut { + opacity: 0.5; + border-style: dashed; +} + +/* =============================================== */ +/* SALA COMPARTILHADA +/* =============================================== */ +#create-room-btn { + width: auto; /* Permite que o botão se ajuste ao texto */ + padding-left: 12px; + padding-right: 16px; + color: var(--text-light); /* Ou a cor que preferir */ +} + +#create-room-btn:hover { + background-color: var(--accent-color); /* Destaque ao passar o mouse */ + color: var(--text-dark); +} + /* =============================================== */ /* EDITOR DE BASES (BEAT EDITOR / STEP SEQUENCER) /* =============================================== */ diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index 4dfed83..2cdee54 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -1,5 +1,7 @@ // js/audio.js +import * as Tone from "https://esm.sh/tone"; + // O contexto de áudio agora será gerenciado principalmente pelo Tone.js. // Esta função garante que ele seja iniciado por uma interação do usuário. export function initializeAudioContext() { @@ -9,10 +11,13 @@ export function initializeAudioContext() { } } -// Funções de acesso ao contexto global do Tone.js +// ✅ DEPOIS: devolve o *raw* AudioContext (nativo do Web Audio) export function getAudioContext() { - return Tone.context; + // compatível com versões novas/antigas do Tone + const ctx = typeof Tone.getContext === 'function' ? Tone.getContext() : Tone.context; + return ctx.rawContext || ctx; // rawContext quando existir } + export function getMainGainNode() { return Tone.Destination; } diff --git a/assets/js/creations/audio/audio_audio.js b/assets/js/creations/audio/audio_audio.js index 0c8e8e9..a92d980 100644 --- a/assets/js/creations/audio/audio_audio.js +++ b/assets/js/creations/audio/audio_audio.js @@ -1,8 +1,10 @@ // js/audio/audio_audio.js import { appState } from "../state.js"; import { updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./audio_ui.js"; -import { initializeAudioContext, getAudioContext } from "../audio.js"; +import { initializeAudioContext, getAudioContext, getMainGainNode } from "../audio.js"; import { getPixelsPerSecond } from "../utils.js"; +// 🔊 ADIÇÃO: usar a MESMA instância do Tone que o projeto usa +import * as Tone from "https://esm.sh/tone"; // --- Configurações do Scheduler --- const LOOKAHEAD_INTERVAL_MS = 25.0; @@ -16,16 +18,17 @@ let animationFrameId = null; // Sincronização de Tempo let startTime = 0; -let seekTime = 0; -let logicalPlaybackTime = 0; +// (seek/logical ficam em appState.audio) // Configurações de Loop let isLoopActive = false; let loopStartTimeSec = 0; let loopEndTimeSec = 8; +// estado runtime const runtimeClipState = new Map(); -const scheduledNodes = new Map(); +// ⚠️ agora armazenamos Tone.Player em vez de BufferSource +const scheduledNodes = new Map(); // eventId -> { player, clipId } let nextEventId = 0; const callbacks = { @@ -42,49 +45,78 @@ function _getSecondsPerBeat() { return 60.0 / _getBpm(); } function _convertBeatToSeconds(beat) { return beat * _getSecondsPerBeat(); } function _convertSecondsToBeat(seconds) { return seconds / _getSecondsPerBeat(); } - +// garante um único contexto — o rawContext do Tone function _initContext() { if (!audioCtx) { initializeAudioContext(); - audioCtx = getAudioContext(); + audioCtx = getAudioContext(); // deve ser o rawContext do Tone } } -// --- Lógica Principal do Scheduler (sem alterações) --- +// helper: normaliza AudioBuffer → ToneAudioBuffer (mesmo contexto) +function _toToneBuffer(buffer) { + if (!buffer) return null; + if (buffer._buffer) return buffer; // já é Tone.ToneAudioBuffer + const tab = new Tone.ToneAudioBuffer(); + tab._buffer = buffer; // injeta o AudioBuffer (já no rawContext do Tone) + return tab; +} + +// --- Lógica Principal do Scheduler (mantida) --- function _scheduleClip(clip, absolutePlayTime, durationSec) { if (!clip.buffer) { console.warn(`Clip ${clip.id} não possui áudio buffer carregado.`); return; } - if (!clip.gainNode || !clip.pannerNode) { - console.warn(`Clip ${clip.id} não possui gainNode ou pannerNode.`); - return; - } - const source = new Tone.BufferSource(clip.buffer); - source.connect(clip.gainNode); - - // --- CORREÇÃO: Aplica o pitch (que pode ser de stretch ou wheel) --- - if (clip.pitch && clip.pitch !== 0) { - source.playbackRate.value = Math.pow(2, clip.pitch / 12); - } else { - source.playbackRate.value = 1.0; // Garante que o modo 'trim' toque normal - } - // --- FIM DA CORREÇÃO --- + // usamos Player .sync() conectando no mesmo grafo do Tone + const toneBuf = _toToneBuffer(clip.buffer); + if (!toneBuf) return; + + // cadeia de ganho/pan por clipe (se já tiver no estado, use; aqui garantimos) + const gain = clip.gainNode instanceof Tone.Gain ? clip.gainNode : new Tone.Gain(clip.volume ?? 1); + const pan = clip.pannerNode instanceof Tone.Panner ? clip.pannerNode : new Tone.Panner(clip.pan ?? 0); + + // conecta no destino principal (é um ToneAudioNode) + try { + gain.disconnect(); // evita duplicatas caso exista de execuções anteriores + } catch {} + try { + pan.disconnect(); + } catch {} + gain.connect(pan).connect(getMainGainNode()); + + // player sincronizado no Transport + const player = new Tone.Player(toneBuf).sync().connect(gain); + + // aplica pitch como rate (semitons → rate) + const rate = (clip.pitch && clip.pitch !== 0) ? Math.pow(2, clip.pitch / 12) : 1; + player.playbackRate = rate; + + // calculamos o "when" no tempo do Transport: + // absolutePlayTime é em audioCtx.currentTime; o "zero" lógico foi quando demos play: + // logical = (now - startTime) + seek; => occurrence = (absolutePlayTime - startTime) + seek + const occurrenceInTransportSec = (absolutePlayTime - startTime) + (appState.audio.audioEditorSeekTime || 0); + + const offset = clip.offsetInSeconds ?? clip.offset ?? 0; + const dur = durationSec ?? toneBuf.duration; + + // agenda + player.start(occurrenceInTransportSec, offset, dur); const eventId = nextEventId++; - const clipOffset = clip.offsetInSeconds || clip.offset || 0; - source.start(absolutePlayTime, clipOffset, durationSec); - scheduledNodes.set(eventId, { sourceNode: source, clipId: clip.id }); + scheduledNodes.set(eventId, { player, clipId: clip.id }); if (callbacks.onClipScheduled) { callbacks.onClipScheduled(clip); } - source.onended = () => { + // quando parar naturalmente, limpamos runtime + player.onstop = () => { _handleClipEnd(eventId, clip.id); - source.dispose(); + try { player.unsync(); } catch {} + try { player.dispose(); } catch {} }; } @@ -94,7 +126,7 @@ function _handleClipEnd(eventId, clipId) { if (callbacks.onClipPlayed) { const clip = appState.audio.clips.find(c => c.id == clipId); - if(clip) callbacks.onClipPlayed(clip); + if (clip) callbacks.onClipPlayed(clip); } } @@ -102,26 +134,18 @@ function _schedulerTick() { if (!isPlaying || !audioCtx) return; const now = audioCtx.currentTime; - const logicalTime = (now - startTime) + seekTime; + const logicalTime = (now - startTime) + (appState.audio.audioEditorSeekTime || 0); const scheduleWindowStartSec = logicalTime; const scheduleWindowEndSec = logicalTime + SCHEDULE_AHEAD_TIME_SEC; for (const clip of appState.audio.clips) { const clipRuntime = runtimeClipState.get(clip.id) || { isScheduled: false }; - - if (clipRuntime.isScheduled) { - continue; - } - if (!clip.buffer) { - continue; - } + if (clipRuntime.isScheduled) continue; + if (!clip.buffer) continue; const clipStartTimeSec = clip.startTimeInSeconds; const clipDurationSec = clip.durationInSeconds; - - if (typeof clipStartTimeSec === 'undefined' || typeof clipDurationSec === 'undefined') { - continue; - } + if (typeof clipStartTimeSec === 'undefined' || typeof clipDurationSec === 'undefined') continue; let occurrenceStartTimeSec = clipStartTimeSec; @@ -137,11 +161,12 @@ function _schedulerTick() { occurrenceStartTimeSec += loopsMissed * loopDuration; } } + if ( occurrenceStartTimeSec >= scheduleWindowStartSec && occurrenceStartTimeSec < scheduleWindowEndSec ) { - const absolutePlayTime = startTime + (occurrenceStartTimeSec - seekTime); + const absolutePlayTime = startTime + (occurrenceStartTimeSec - (appState.audio.audioEditorSeekTime || 0)); _scheduleClip(clip, absolutePlayTime, clipDurationSec); clipRuntime.isScheduled = true; runtimeClipState.set(clip.id, clipRuntime); @@ -149,23 +174,26 @@ function _schedulerTick() { } } -// --- Loop de Animação (sem alterações) --- +// --- Loop de Animação (mantido) --- function _animationLoop() { if (!isPlaying) { animationFrameId = null; return; } const now = audioCtx.currentTime; - let newLogicalTime = (now - startTime) + seekTime; + let newLogicalTime = (now - startTime) + (appState.audio.audioEditorSeekTime || 0); + if (isLoopActive) { if (newLogicalTime >= loopEndTimeSec) { const loopDuration = loopEndTimeSec - loopStartTimeSec; newLogicalTime = loopStartTimeSec + ((newLogicalTime - loopStartTimeSec) % loopDuration); startTime = now; - seekTime = newLogicalTime; + appState.audio.audioEditorSeekTime = newLogicalTime; } } - logicalPlaybackTime = newLogicalTime; + + appState.audio.audioEditorLogicalTime = newLogicalTime; + if (!isLoopActive) { let maxTime = 0; appState.audio.clips.forEach(clip => { @@ -174,15 +202,14 @@ function _animationLoop() { const endTime = clipStartTime + clipDuration; if (endTime > maxTime) maxTime = endTime; }); - - if (maxTime > 0 && logicalPlaybackTime >= maxTime) { + if (maxTime > 0 && appState.audio.audioEditorLogicalTime >= maxTime) { stopAudioEditorPlayback(true); // Rebobina no fim resetPlayheadVisual(); return; } } const pixelsPerSecond = getPixelsPerSecond(); - const newPositionPx = logicalPlaybackTime * pixelsPerSecond; + const newPositionPx = appState.audio.audioEditorLogicalTime * pixelsPerSecond; updatePlayheadVisual(newPositionPx); animationFrameId = requestAnimationFrame(_animationLoop); } @@ -196,79 +223,111 @@ export function updateTransportLoop() { runtimeClipState.clear(); - scheduledNodes.forEach(nodeData => { - // --- CORREÇÃO BUG 1: Remove a linha 'onended = null' --- - nodeData.sourceNode.stop(0); - nodeData.sourceNode.dispose(); + // parar e descartar players agendados + scheduledNodes.forEach(({ player }) => { + try { player.unsync(); } catch {} + try { player.stop(); } catch {} + try { player.dispose(); } catch {} }); scheduledNodes.clear(); } -export function startAudioEditorPlayback() { +export async function startAudioEditorPlayback(seekTime) { // 1. Aceita 'seekTime' como parâmetro if (isPlaying) return; - _initContext(); + + // garante contexto ativo do Tone (gesto do usuário já ocorreu antes) + await Tone.start(); if (audioCtx.state === 'suspended') { - audioCtx.resume(); + await audioCtx.resume(); } isPlaying = true; - // --- CORREÇÃO BUG 2: Atualiza o estado global --- appState.global.isAudioEditorPlaying = true; - + + // alinhamento de relógio próprio (mantido para o seu scheduler) startTime = audioCtx.currentTime; + + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Bugs 1 & 2) + // ================================================================= - updateTransportLoop(); + // 1. Determine o tempo de início: + // Use o 'seekTime' recebido (da ação global) se for um número válido (>= 0). + // Caso contrário, use o tempo de seek local atual. + const timeToStart = (seekTime !== null && seekTime !== undefined && !isNaN(seekTime)) + ? seekTime + : (appState.audio.audioEditorSeekTime || 0); // 👈 Usa sua variável de estado + + // 2. Atualize o estado global (para a agulha pular) + // Isso garante que o estado local E o Tone estejam sincronizados. + appState.audio.audioEditorSeekTime = timeToStart; + + // 3. Alinhe o Tone.Transport a esse tempo + try { + Tone.Transport.seconds = timeToStart; // 👈 Usa o tempo sincronizado + } catch {} + // ================================================================= + // 👆 FIM DA CORREÇÃO + // ================================================================= + + updateTransportLoop(); + console.log("%cIniciando Playback...", "color: #3498db;"); - _schedulerTick(); + // inicia o Transport (para disparar os Players .sync()) + try { + Tone.Transport.start(); + } catch {} + + // mantém seu scheduler/animador + _schedulerTick(); schedulerIntervalId = setInterval(_schedulerTick, LOOKAHEAD_INTERVAL_MS); animationFrameId = requestAnimationFrame(_animationLoop); updateAudioEditorUI(); const playBtn = document.getElementById("audio-editor-play-btn"); - if (playBtn) { - playBtn.className = 'fa-solid fa-pause'; - } + if (playBtn) playBtn.className = "fa-solid fa-pause"; } export function stopAudioEditorPlayback(rewind = false) { if (!isPlaying) return; isPlaying = false; - // --- CORREÇÃO BUG 2: Atualiza o estado global --- appState.global.isAudioEditorPlaying = false; console.log(`%cParando Playback... (Rewind: ${rewind})`, "color: #d9534f;"); + // para o Transport (para Players .sync()) + try { Tone.Transport.stop(); } catch {} + clearInterval(schedulerIntervalId); schedulerIntervalId = null; cancelAnimationFrame(animationFrameId); animationFrameId = null; - seekTime = logicalPlaybackTime; - logicalPlaybackTime = 0; - + appState.audio.audioEditorSeekTime = appState.audio.audioEditorLogicalTime || 0; + appState.audio.audioEditorLogicalTime = 0; if (rewind) { - seekTime = 0; + appState.audio.audioEditorSeekTime = 0; + try { Tone.Transport.seconds = 0; } catch {} } - scheduledNodes.forEach(nodeData => { - // --- CORREÇÃO BUG 1: Remove a linha 'onended = null' --- - nodeData.sourceNode.stop(0); - nodeData.sourceNode.dispose(); + // parar e descartar players agendados + scheduledNodes.forEach(({ player }) => { + try { player.unsync(); } catch {} + try { player.stop(); } catch {} + try { player.dispose(); } catch {} }); scheduledNodes.clear(); runtimeClipState.clear(); updateAudioEditorUI(); const playBtn = document.getElementById("audio-editor-play-btn"); - if (playBtn) { - playBtn.className = 'fa-solid fa-play'; - } + if (playBtn) playBtn.className = 'fa-solid fa-play'; if (rewind) { - resetPlayheadVisual(); + resetPlayheadVisual(); } } @@ -284,21 +343,26 @@ export function seekAudioEditor(newTime) { if (wasPlaying) { stopAudioEditorPlayback(false); // Pausa } - seekTime = newTime; - logicalPlaybackTime = newTime; + + appState.audio.audioEditorSeekTime = newTime; + appState.audio.audioEditorLogicalTime = newTime; + + try { Tone.Transport.seconds = newTime; } catch {} + const pixelsPerSecond = getPixelsPerSecond(); const newPositionPx = newTime * pixelsPerSecond; updatePlayheadVisual(newPositionPx); + if (wasPlaying) { startAudioEditorPlayback(); } } export function registerCallbacks(newCallbacks) { - if (newCallbacks.onClipScheduled) { - callbacks.onClipScheduled = newCallbacks.onClipScheduled; - } - if (newCallbacks.onClipPlayed) { - callbacks.onClipPlayed = newCallbacks.onClipPlayed; - } -} \ No newline at end of file + if (newCallbacks.onClipScheduled) { + callbacks.onClipScheduled = newCallbacks.onClipScheduled; + } + if (newCallbacks.onClipPlayed) { + callbacks.onClipPlayed = newCallbacks.onClipPlayed; + } +} diff --git a/assets/js/creations/audio/audio_clipboard.js b/assets/js/creations/audio/audio_clipboard.js new file mode 100644 index 0000000..20d0b1b --- /dev/null +++ b/assets/js/creations/audio/audio_clipboard.js @@ -0,0 +1,118 @@ +// js/audio/audio_clipboard.js +import { appState } from '../state.js'; +import { removeAudioClip, loadAudioForClip } from './audio_state.js'; +import { renderAudioEditor } from './audio_ui.js'; +import { getMainGainNode } from '../audio.js'; + +/** + * Copia o clipe selecionado para a área de transferência global. + */ +export function copyAudioClip() { + const clipId = appState.global.selectedClipId; + if (!clipId) return; + + const clip = appState.audio.clips.find(c => c.id == clipId); + if (!clip) return; + + // Remove a marca de "recortado" se houver + if (appState.global.clipboard?.cutSourceId) { + appState.global.clipboard.cutSourceId = null; + } + + // Cria uma cópia limpa dos dados do clipe + const clipData = { ...clip }; + // Remove referências a nós de áudio, que devem ser únicos + delete clipData.gainNode; + delete clipData.pannerNode; + delete clipData.player; + + appState.global.clipboard = { + type: 'audio', + clip: clipData, + cutSourceId: null + }; + + console.log("Clipe copiado:", appState.global.clipboard.clip.name); + renderAudioEditor(); // Re-renderiza para remover o visual "cut" +} + +/** + * Recorta o clipe selecionado para a área de transferência global. + */ +export function cutAudioClip() { + const clipId = appState.global.selectedClipId; + if (!clipId) return; + + // Limpa o "cut" anterior + if (appState.global.clipboard?.cutSourceId) { + appState.global.clipboard.cutSourceId = null; + } + + const clip = appState.audio.clips.find(c => c.id == clipId); + if (!clip) return; + + // Cria uma cópia limpa dos dados do clipe + const clipData = { ...clip }; + delete clipData.gainNode; + delete clipData.pannerNode; + delete clipData.player; + + appState.global.clipboard = { + type: 'audio', + clip: clipData, + cutSourceId: clipId // Marca o ID original para exclusão + }; + + console.log("Clipe recortado:", appState.global.clipboard.clip.name); + renderAudioEditor(); // Re-renderiza para adicionar o visual "cut" +} + +/** + * Cola o clipe da área de transferência na timeline. + * @param {number} targetTrackId - O ID da pista onde colar. + * @param {number} targetTimeInSeconds - O tempo (em segundos) onde colar. + */ +export function pasteAudioClip(targetTrackId, targetTimeInSeconds) { + const clipboard = appState.global.clipboard; + if (!clipboard || clipboard.type !== 'audio' || !clipboard.clip) { + console.warn("Área de transferência vazia ou inválida para colar áudio."); + return; + } + + const clipToPaste = clipboard.clip; + + // Se for um "recorte", primeiro remove o clipe original + if (clipboard.cutSourceId) { + removeAudioClip(clipboard.cutSourceId); // + clipboard.cutSourceId = null; // Limpa para que não remova de novo + } + + // Cria um novo clipe a partir dos dados copiados + const newClip = { + ...clipToPaste, + id: Date.now() + Math.random(), // ID novo e único + trackId: targetTrackId, + startTimeInSeconds: targetTimeInSeconds, + + // Cria novos nós de áudio + gainNode: new Tone.Gain(Tone.gainToDb(clipToPaste.volume || 1.0)), + pannerNode: new Tone.Panner(clipToPaste.pan || 0), + + player: null, + // O buffer será copiado/referenciado + }; + + newClip.gainNode.connect(newClip.pannerNode); + newClip.pannerNode.connect(getMainGainNode()); // + + appState.audio.clips.push(newClip); + + // Como o buffer já deve existir no clipe original, + // não precisamos de 'loadAudioForClip', mas chamamos + // para garantir (caso a fonte seja um 'sourcePath'). + // A função 'loadAudioForClip' precisa ser inteligente + // para não recarregar se o buffer já existir. + loadAudioForClip(newClip).then(() => { // + renderAudioEditor(); + }); +} \ No newline at end of file diff --git a/assets/js/creations/audio/audio_state.js b/assets/js/creations/audio/audio_state.js index 5cb416d..4fe2301 100644 --- a/assets/js/creations/audio/audio_state.js +++ b/assets/js/creations/audio/audio_state.js @@ -1,18 +1,89 @@ -// js/audio_state.js +// js/audio/audio_state.js import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js"; import { renderAudioEditor } from "./audio_ui.js"; -import { getMainGainNode } from "../audio.js"; -import { getAudioContext } from "../audio.js"; +import { getMainGainNode, getAudioContext } from "../audio.js"; +import * as Tone from "https://esm.sh/tone"; export let audioState = { tracks: [], clips: [], + // --- TEMPOS MOVIDOS DO audio_audio.js PARA O ESTADO GLOBAL --- + audioEditorSeekTime: 0, + audioEditorLogicalTime: 0, + // --- FIM DA MUDANÇA --- audioEditorStartTime: 0, audioEditorAnimationId: null, audioEditorPlaybackTime: 0, isAudioEditorLoopEnabled: false, }; +// ==== SNAPSHOT: exportação do estado atual (tracks + clips) ==== +export function getAudioSnapshot() { + // Se seu estado “oficial” é audioState.* use ele; + // se for appState.audio.* troque abaixo. + const tracks = (audioState.tracks || []).map(t => ({ + id: t.id, name: t.name + })); + + const clips = (audioState.clips || []).map(c => ({ + id: c.id, + trackId: c.trackId, + name: c.name, + sourcePath: c.sourcePath || null, // URL do asset (precisa ser acessível) + startTimeInSeconds: c.startTimeInSeconds || 0, + durationInSeconds: c.durationInSeconds || (c.buffer?.duration || 0), + offset: c.offset || 0, + pitch: c.pitch || 0, + volume: c.volume ?? 1, + pan: c.pan ?? 0, + originalDuration: c.originalDuration || (c.buffer?.duration || 0), + })); + + return { tracks, clips }; +} + +// ==== SNAPSHOT: aplicação do estado recebido ==== +export async function applyAudioSnapshot(snapshot) { + if (!snapshot) return; + + // aplica trilhas (mantém ids/nome) + if (Array.isArray(snapshot.tracks) && snapshot.tracks.length) { + audioState.tracks = snapshot.tracks.map(t => ({ id: t.id, name: t.name })); + } + + // insere clipes usando os MESMOS ids do emissor (idempotente) + if (Array.isArray(snapshot.clips)) { + for (const c of snapshot.clips) { + // evita duplicar se já existir (idempotência) + if (audioState.clips.some(x => String(x.id) === String(c.id))) continue; + + // usa a própria função de criação (agora ela aceita id e nome) + // assinatura: addAudioClipToTimeline(samplePath, trackId, start, clipId, name) + addAudioClipToTimeline(c.sourcePath, c.trackId, c.startTimeInSeconds, c.id, c.name); + + // aplica propriedades adicionais (dur/offset/pitch/vol/pan) no mesmo id + const idx = audioState.clips.findIndex(x => String(x.id) === String(c.id)); + if (idx >= 0) { + const clip = audioState.clips[idx]; + clip.durationInSeconds = c.durationInSeconds; + clip.offset = c.offset; + clip.pitch = c.pitch; + clip.volume = c.volume; + clip.pan = c.pan; + clip.originalDuration = c.originalDuration; + + // reflete nos nós Tone já criados + if (clip.gainNode) clip.gainNode.gain.value = clip.volume ?? 1; + if (clip.pannerNode) clip.pannerNode.pan.value = clip.pan ?? 0; + } + } + } + + // re-render geral do editor + renderAudioEditor(); +} + + export function initializeAudioState() { audioState.clips.forEach(clip => { if (clip.pannerNode) clip.pannerNode.dispose(); @@ -21,6 +92,10 @@ export function initializeAudioState() { Object.assign(audioState, { tracks: [], clips: [], + // --- ADICIONADO --- + audioEditorSeekTime: 0, + audioEditorLogicalTime: 0, + // --- FIM --- audioEditorStartTime: 0, audioEditorAnimationId: null, audioEditorPlaybackTime: 0, @@ -29,6 +104,16 @@ export function initializeAudioState() { } export async function loadAudioForClip(clip) { + // --- ADIÇÃO --- + // Se já temos um buffer (do bounce ou colagem), não faz fetch + if (clip.buffer) { + // Garante que as durações estão corretas + if (clip.originalDuration === 0) clip.originalDuration = clip.buffer.duration; + if (clip.durationInSeconds === 0) clip.durationInSeconds = clip.buffer.duration; + return clip; + } + // --- FIM DA ADIÇÃO --- + if (!clip.sourcePath) return clip; const audioCtx = getAudioContext(); @@ -58,51 +143,82 @@ export async function loadAudioForClip(clip) { return clip; } -export function addAudioClipToTimeline(samplePath, trackId = 1, startTime = 0, clipName = null) { +// helper de id (fallback se o emissor não mandar) +function genClipId() { + return (crypto?.randomUUID?.() || `clip_${Date.now()}_${Math.floor(Math.random()*1e6)}`); +} + +// --- FUNÇÃO MODIFICADA --- +// agora aceita clipId e clipName vindos do emissor; mantém compat com chamadas antigas +export function addAudioClipToTimeline(samplePath, trackId = 1, startTime = 0, clipIdOrName = null, nameOrBuffer = null, maybeBuffer = null) { + // compat: se passaram (filePath, trackId, start, clipId) + // mas versões antigas chamavam (filePath, trackId, start) ou (filePath, trackId, start, name, buffer) + let incomingId = null; + let clipName = null; + let existingBuffer = null; + + // heurística: se clipIdOrName parece um UUID/clip_ → é id, senão é nome + if (typeof clipIdOrName === 'string' && (clipIdOrName.startsWith('clip_') || clipIdOrName.length >= 16)) { + incomingId = clipIdOrName; + clipName = (typeof nameOrBuffer === 'string') ? nameOrBuffer : null; + existingBuffer = maybeBuffer || (nameOrBuffer && typeof nameOrBuffer !== 'string' ? nameOrBuffer : null); + } else { + // assinatura antiga: 4º arg era nome + clipName = (typeof clipIdOrName === 'string') ? clipIdOrName : null; + existingBuffer = (nameOrBuffer && typeof nameOrBuffer !== 'string') ? nameOrBuffer : null; + } + + const finalId = incomingId || genClipId(); + + // idempotência: se o id já existe, não duplica + if (audioState.clips.some(c => String(c.id) === String(finalId))) { + return; + } + const newClip = { - id: Date.now() + Math.random(), + id: finalId, trackId: trackId, - sourcePath: samplePath, - - // --- MODIFICAÇÃO AQUI --- - // Usa o nome fornecido, ou extrai do caminho se não for fornecido - name: clipName || samplePath.split('/').pop(), + sourcePath: samplePath, // Pode ser null se existingBuffer for fornecido + name: clipName || (samplePath ? String(samplePath).split('/').pop() : 'Bounced Clip'), startTimeInSeconds: startTime, offset: 0, durationInSeconds: 0, - originalDuration: 0, // Será preenchido pelo loadAudioForClip + originalDuration: 0, pitch: 0, volume: DEFAULT_VOLUME, pan: DEFAULT_PAN, - - gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)), - pannerNode: new Tone.Panner(DEFAULT_PAN), - buffer: null, + buffer: existingBuffer || null, player: null, }; + // volume linear (0–1) + newClip.gainNode = new Tone.Gain(DEFAULT_VOLUME); + newClip.pannerNode = new Tone.Panner(DEFAULT_PAN); + + // conecta tudo no grafo do Tone (mesmo contexto) newClip.gainNode.connect(newClip.pannerNode); newClip.pannerNode.connect(getMainGainNode()); audioState.clips.push(newClip); + // loadAudioForClip agora vai lidar com 'existingBuffer' loadAudioForClip(newClip).then(() => { renderAudioEditor(); }); } export function updateAudioClipProperties(clipId, properties) { - const clip = audioState.clips.find(c => c.id == clipId); + const clip = audioState.clips.find(c => String(c.id) == String(clipId)); if (clip) { Object.assign(clip, properties); } } export function sliceAudioClip(clipId, sliceTimeInTimeline) { - const originalClip = audioState.clips.find(c => c.id == clipId); + const originalClip = audioState.clips.find(c => String(c.id) == String(clipId)); if (!originalClip || sliceTimeInTimeline <= originalClip.startTimeInSeconds || @@ -115,7 +231,7 @@ export function sliceAudioClip(clipId, sliceTimeInTimeline) { const cutPointInClip = sliceTimeInTimeline - originalClip.startTimeInSeconds; const newClip = { - id: Date.now() + Math.random(), + id: genClipId(), trackId: originalClip.trackId, sourcePath: originalClip.sourcePath, name: originalClip.name, @@ -132,7 +248,7 @@ export function sliceAudioClip(clipId, sliceTimeInTimeline) { volume: originalClip.volume, pan: originalClip.pan, - gainNode: new Tone.Gain(Tone.gainToDb(originalClip.volume)), + gainNode: new Tone.Gain(originalClip.volume), pannerNode: new Tone.Panner(originalClip.pan), player: null @@ -148,20 +264,19 @@ export function sliceAudioClip(clipId, sliceTimeInTimeline) { console.log("Clipe dividido. Original:", originalClip, "Novo:", newClip); } -// ... (resto do arquivo 'audio_state.js' sem alterações) ... export function updateClipVolume(clipId, volume) { - const clip = audioState.clips.find((c) => c.id == clipId); + const clip = audioState.clips.find((c) => String(c.id) == String(clipId)); if (clip) { const clampedVolume = Math.max(0, Math.min(1.5, volume)); clip.volume = clampedVolume; if (clip.gainNode) { - clip.gainNode.gain.value = Tone.gainToDb(clampedVolume); + clip.gainNode.gain.value = clampedVolume; } } } export function updateClipPan(clipId, pan) { - const clip = audioState.clips.find((c) => c.id == clipId); + const clip = audioState.clips.find((c) => String(c.id) == String(clipId)); if (clip) { const clampedPan = Math.max(-1, Math.min(1, pan)); clip.pan = clampedPan; @@ -177,19 +292,19 @@ export function addAudioTrackLane() { } export function removeAudioClip(clipId) { - const clipIndex = audioState.clips.findIndex(c => c.id == clipId); + const clipIndex = audioState.clips.findIndex(c => String(c.id) == String(clipId)); if (clipIndex === -1) return false; // Retorna false se não encontrou const clip = audioState.clips[clipIndex]; // 1. Limpa os nós de áudio do Tone.js if (clip.gainNode) { - clip.gainNode.disconnect(); - clip.gainNode.dispose(); + try { clip.gainNode.disconnect(); } catch {} + try { clip.gainNode.dispose(); } catch {} } if (clip.pannerNode) { - clip.pannerNode.disconnect(); - clip.pannerNode.dispose(); + try { clip.pannerNode.disconnect(); } catch {} + try { clip.pannerNode.dispose(); } catch {} } // 2. Remove o clipe do array de estado @@ -197,4 +312,4 @@ export function removeAudioClip(clipId) { // 3. Retorna true para o chamador (Controller) return true; -} \ No newline at end of file +} diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js index 3ae0072..71b50b7 100644 --- a/assets/js/creations/audio/audio_ui.js +++ b/assets/js/creations/audio/audio_ui.js @@ -1,615 +1,819 @@ // js/audio/audio_ui.js import { appState } from "../state.js"; -import { - addAudioClipToTimeline, - updateAudioClipProperties, - sliceAudioClip, +import { + addAudioClipToTimeline, + updateAudioClipProperties, + sliceAudioClip, + removeAudioClip, } from "./audio_state.js"; -import { seekAudioEditor, restartAudioEditorIfPlaying, updateTransportLoop } from "./audio_audio.js"; +import { + seekAudioEditor, + restartAudioEditorIfPlaying, + updateTransportLoop, +} from "./audio_audio.js"; import { drawWaveform } from "../waveform.js"; import { PIXELS_PER_BAR, PIXELS_PER_STEP, ZOOM_LEVELS } from "../config.js"; -import { getPixelsPerSecond, quantizeTime, getBeatsPerBar, getSecondsPerStep } from "../utils.js"; +import { + getPixelsPerSecond, + quantizeTime, + getBeatsPerBar, + getSecondsPerStep, +} from "../utils.js"; +import { sendAction } from "../socket.js"; export function renderAudioEditor() { - const audioEditor = document.querySelector('.audio-editor'); - const existingTrackContainer = document.getElementById('audio-track-container'); - if (!audioEditor || !existingTrackContainer) return; + const audioEditor = document.querySelector(".audio-editor"); + const existingTrackContainer = document.getElementById( + "audio-track-container" + ); + if (!audioEditor || !existingTrackContainer) return; - // --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA --- - let rulerWrapper = audioEditor.querySelector('.ruler-wrapper'); - if (!rulerWrapper) { - rulerWrapper = document.createElement('div'); - rulerWrapper.className = 'ruler-wrapper'; - rulerWrapper.innerHTML = ` + // --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA --- + let rulerWrapper = audioEditor.querySelector(".ruler-wrapper"); + if (!rulerWrapper) { + rulerWrapper = document.createElement("div"); + rulerWrapper.className = "ruler-wrapper"; + rulerWrapper.innerHTML = `
`; - audioEditor.insertBefore(rulerWrapper, existingTrackContainer); + audioEditor.insertBefore(rulerWrapper, existingTrackContainer); + } + + const ruler = rulerWrapper.querySelector(".timeline-ruler"); + ruler.innerHTML = ""; + + const pixelsPerSecond = getPixelsPerSecond(); + + let maxTime = appState.global.loopEndTime; + appState.audio.clips.forEach((clip) => { + const endTime = + (clip.startTimeInSeconds || 0) + (clip.durationInSeconds || 0); + if (endTime > maxTime) maxTime = endTime; + }); + + const containerWidth = existingTrackContainer.offsetWidth; + const contentWidth = maxTime * pixelsPerSecond; + const totalWidth = Math.max(contentWidth, containerWidth, 2000); + + ruler.style.width = `${totalWidth}px`; + + const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex]; + const beatsPerBar = getBeatsPerBar(); + const stepWidthPx = PIXELS_PER_STEP * zoomFactor; + const beatWidthPx = stepWidthPx * 4; + const barWidthPx = beatWidthPx * beatsPerBar; + + if (barWidthPx > 0) { + const numberOfBars = Math.ceil(totalWidth / barWidthPx); + for (let i = 1; i <= numberOfBars; i++) { + const marker = document.createElement("div"); + marker.className = "ruler-marker"; + marker.textContent = i; + marker.style.left = `${(i - 1) * barWidthPx}px`; + ruler.appendChild(marker); + } + } + + const loopRegion = document.createElement("div"); + loopRegion.id = "loop-region"; + loopRegion.style.left = `${ + appState.global.loopStartTime * pixelsPerSecond + }px`; + loopRegion.style.width = `${ + (appState.global.loopEndTime - appState.global.loopStartTime) * + pixelsPerSecond + }px`; + loopRegion.innerHTML = `
`; + loopRegion.classList.toggle("visible", appState.global.isLoopActive); + ruler.appendChild(loopRegion); + + // --- LISTENER DA RÉGUA (MODIFICADO para enviar Ações de Loop/Seek) --- + const newRuler = ruler.cloneNode(true); + ruler.parentNode.replaceChild(newRuler, ruler); + + newRuler.addEventListener("mousedown", (e) => { + // Esconde menus + document.getElementById("timeline-context-menu").style.display = "none"; + document.getElementById("ruler-context-menu").style.display = "none"; + + const currentPixelsPerSecond = getPixelsPerSecond(); + const loopHandle = e.target.closest(".loop-handle"); + const loopRegionBody = e.target.closest("#loop-region:not(.loop-handle)"); + + // Drag Handle Loop + if (loopHandle) { + e.preventDefault(); + e.stopPropagation(); + const handleType = loopHandle.classList.contains("left") + ? "left" + : "right"; + const initialMouseX = e.clientX; + const initialStart = appState.global.loopStartTime; + const initialEnd = appState.global.loopEndTime; + const onMouseMove = (moveEvent) => { + const deltaX = moveEvent.clientX - initialMouseX; + const deltaTime = deltaX / currentPixelsPerSecond; + let newStart = appState.global.loopStartTime; + let newEnd = appState.global.loopEndTime; + if (handleType === "left") { + newStart = Math.max(0, initialStart + deltaTime); + newStart = Math.min(newStart, appState.global.loopEndTime - 0.1); + appState.global.loopStartTime = newStart; + } else { + newEnd = Math.max( + appState.global.loopStartTime + 0.1, + initialEnd + deltaTime + ); + appState.global.loopEndTime = newEnd; + } + updateTransportLoop(); + const loopRegionEl = newRuler.querySelector("#loop-region"); + if (loopRegionEl) { + loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`; + loopRegionEl.style.width = `${ + (newEnd - newStart) * currentPixelsPerSecond + }px`; + } + }; + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Sincronia de Loop Drag Handle) + // ================================================================= + sendAction({ + type: "SET_LOOP_STATE", + isLoopActive: appState.global.isLoopActive, + loopStartTime: appState.global.loopStartTime, + loopEndTime: appState.global.loopEndTime, + }); + // renderAudioEditor(); // Removido + // ================================================================= + // 👆 FIM DA CORREÇÃO + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; } - const ruler = rulerWrapper.querySelector('.timeline-ruler'); - ruler.innerHTML = ''; - - const pixelsPerSecond = getPixelsPerSecond(); - - let maxTime = appState.global.loopEndTime; - appState.audio.clips.forEach(clip => { - const endTime = (clip.startTimeInSeconds || 0) + (clip.durationInSeconds || 0); - if (endTime > maxTime) maxTime = endTime; - }); - - const containerWidth = existingTrackContainer.offsetWidth; - const contentWidth = maxTime * pixelsPerSecond; - const totalWidth = Math.max(contentWidth, containerWidth, 2000); - - ruler.style.width = `${totalWidth}px`; - - const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex]; - const beatsPerBar = getBeatsPerBar(); - const stepWidthPx = PIXELS_PER_STEP * zoomFactor; - const beatWidthPx = stepWidthPx * 4; - const barWidthPx = beatWidthPx * beatsPerBar; - - if (barWidthPx > 0) { - const numberOfBars = Math.ceil(totalWidth / barWidthPx); - for (let i = 1; i <= numberOfBars; i++) { - const marker = document.createElement('div'); - marker.className = 'ruler-marker'; - marker.textContent = i; - marker.style.left = `${(i - 1) * barWidthPx}px`; - ruler.appendChild(marker); - } + // Drag Body Loop + if (loopRegionBody) { + e.preventDefault(); + e.stopPropagation(); + const initialMouseX = e.clientX; + const initialStart = appState.global.loopStartTime; + const initialEnd = appState.global.loopEndTime; + const initialDuration = initialEnd - initialStart; + const onMouseMove = (moveEvent) => { + const deltaX = moveEvent.clientX - initialMouseX; + const deltaTime = deltaX / currentPixelsPerSecond; + let newStart = Math.max(0, initialStart + deltaTime); + let newEnd = newStart + initialDuration; + appState.global.loopStartTime = newStart; + appState.global.loopEndTime = newEnd; + updateTransportLoop(); + const loopRegionEl = newRuler.querySelector("#loop-region"); + if (loopRegionEl) + loopRegionEl.style.left = `${newStart * currentPixelsPerSecond}px`; + }; + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Sincronia de Loop Drag Body) + // ================================================================= + sendAction({ + type: "SET_LOOP_STATE", + isLoopActive: appState.global.isLoopActive, + loopStartTime: appState.global.loopStartTime, + loopEndTime: appState.global.loopEndTime, + }); + // renderAudioEditor(); // Removido + // ================================================================= + // 👆 FIM DA CORREÇÃO + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; } - const loopRegion = document.createElement('div'); - loopRegion.id = 'loop-region'; - loopRegion.style.left = `${appState.global.loopStartTime * pixelsPerSecond}px`; - loopRegion.style.width = `${(appState.global.loopEndTime - appState.global.loopStartTime) * pixelsPerSecond}px`; - loopRegion.innerHTML = `
`; - loopRegion.classList.toggle("visible", appState.global.isLoopActive); - ruler.appendChild(loopRegion); + // Seek na Régua + e.preventDefault(); + const handleSeek = (event) => { + const rect = newRuler.getBoundingClientRect(); + const scrollLeft = newRuler.scrollLeft; + const clickX = event.clientX - rect.left; + const absoluteX = clickX + scrollLeft; + const newTime = absoluteX / currentPixelsPerSecond; + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Régua) + // ================================================================= + sendAction({ type: "SET_SEEK_TIME", seekTime: newTime }); + // seekAudioEditor(newTime); // 👈 Substituído + // ================================================================= + // 👆 FIM DA CORREÇÃO + }; + handleSeek(e); // Aplica no mousedown + const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); + const onMouseUpSeek = () => { + document.removeEventListener("mousemove", onMouseMoveSeek); + document.removeEventListener("mouseup", onMouseUpSeek); + }; + document.addEventListener("mousemove", onMouseMoveSeek); + document.addEventListener("mouseup", onMouseUpSeek); + }); - // --- LISTENER DA RÉGUA (sem alterações) --- - ruler.addEventListener('mousedown', (e) => { - const currentPixelsPerSecond = getPixelsPerSecond(); - const loopHandle = e.target.closest('.loop-handle'); - const loopRegionBody = e.target.closest('#loop-region:not(.loop-handle)'); + // Menu Contexto Régua (sem alterações) + newRuler.addEventListener("contextmenu", (e) => { + e.preventDefault(); + document.getElementById("timeline-context-menu").style.display = "none"; + const menu = document.getElementById("ruler-context-menu"); + const currentPixelsPerSecond = getPixelsPerSecond(); + const rect = newRuler.getBoundingClientRect(); + const scrollLeft = newRuler.scrollLeft; + const clickX = e.clientX - rect.left; + const absoluteX = clickX + scrollLeft; + const clickTime = absoluteX / currentPixelsPerSecond; + appState.global.lastRulerClickTime = clickTime; + menu.style.display = "block"; + menu.style.left = `${e.clientX}px`; + menu.style.top = `${e.clientY}px`; + }); - if (loopHandle) { /* ... lógica de loop ... */ - e.preventDefault(); e.stopPropagation(); - const handleType = loopHandle.classList.contains('left') ? 'left' : 'right'; - const initialMouseX = e.clientX; - const initialStart = appState.global.loopStartTime; - const initialEnd = appState.global.loopEndTime; + // Recriação Container Pistas (sem alterações) + const newTrackContainer = existingTrackContainer.cloneNode(false); + audioEditor.replaceChild(newTrackContainer, existingTrackContainer); - const onMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - initialMouseX; - const deltaTime = deltaX / currentPixelsPerSecond; - let newStart = appState.global.loopStartTime; - let newEnd = appState.global.loopEndTime; - - if (handleType === 'left') { - newStart = Math.max(0, initialStart + deltaTime); - newStart = Math.min(newStart, appState.global.loopEndTime - 0.1); - appState.global.loopStartTime = newStart; - } else { - newEnd = Math.max(appState.global.loopStartTime + 0.1, initialEnd + deltaTime); - appState.global.loopEndTime = newEnd; - } - - updateTransportLoop(); - loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`; - loopRegion.style.width = `${(newEnd - newStart) * currentPixelsPerSecond}px`; - }; - - const onMouseUp = () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - renderAudioEditor(); - }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return; - } - if (loopRegionBody) { /* ... lógica de mover loop ... */ - e.preventDefault(); e.stopPropagation(); - const initialMouseX = e.clientX; - const initialStart = appState.global.loopStartTime; - const initialEnd = appState.global.loopEndTime; - const initialDuration = initialEnd - initialStart; - - const onMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - initialMouseX; - const deltaTime = deltaX / currentPixelsPerSecond; - let newStart = Math.max(0, initialStart + deltaTime); - let newEnd = newStart + initialDuration; - appState.global.loopStartTime = newStart; - appState.global.loopEndTime = newEnd; - updateTransportLoop(); - loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`; - }; - - const onMouseUp = () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - renderAudioEditor(); - }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return; - } - - e.preventDefault(); - const handleSeek = (event) => { /* ... lógica de seek ... */ - const rect = ruler.getBoundingClientRect(); - const scrollLeft = ruler.scrollLeft; - const clickX = event.clientX - rect.left; - const absoluteX = clickX + scrollLeft; - const newTime = absoluteX / currentPixelsPerSecond; - seekAudioEditor(newTime); - }; - handleSeek(e); - const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); - const onMouseUpSeek = () => { document.removeEventListener('mousemove', onMouseMoveSeek); document.removeEventListener('mouseup', onMouseUpSeek); }; - document.addEventListener('mousemove', onMouseMoveSeek); - document.addEventListener('mouseup', onMouseUpSeek); - }); - - // --- RECRIAÇÃO DO CONTAINER DE PISTAS (sem alterações) --- - const newTrackContainer = existingTrackContainer.cloneNode(false); - audioEditor.replaceChild(newTrackContainer, existingTrackContainer); - - if (appState.audio.tracks.length === 0) { - appState.audio.tracks.push({ id: Date.now(), name: "Pista de Áudio 1" }); - } - - // --- RENDERIZAÇÃO DAS PISTAS INDIVIDUAIS (sem alterações) --- - appState.audio.tracks.forEach(trackData => { - const audioTrackLane = document.createElement('div'); - audioTrackLane.className = 'audio-track-lane'; - audioTrackLane.dataset.trackId = trackData.id; - audioTrackLane.innerHTML = ` + // Render Pistas (sem alterações) + appState.audio.tracks.forEach((trackData) => { + const audioTrackLane = document.createElement("div"); + audioTrackLane.className = "audio-track-lane"; + audioTrackLane.dataset.trackId = trackData.id; + audioTrackLane.innerHTML = `
-
- - ${trackData.name} -
-
+
${trackData.name}
-
-
- VOL -
-
-
- PAN -
+
VOL
+
PAN
-
-
-
- `; - newTrackContainer.appendChild(audioTrackLane); - - const timelineContainer = audioTrackLane.querySelector('.timeline-container'); - - timelineContainer.addEventListener("dragover", (e) => { e.preventDefault(); audioTrackLane.classList.add('drag-over'); }); - timelineContainer.addEventListener("dragleave", () => audioTrackLane.classList.remove('drag-over')); - - timelineContainer.addEventListener("drop", (e) => { - e.preventDefault(); - audioTrackLane.classList.remove('drag-over'); - const filePath = e.dataTransfer.getData("text/plain"); - if (!filePath) return; - const rect = timelineContainer.getBoundingClientRect(); - const dropX = e.clientX - rect.left + timelineContainer.scrollLeft; - let startTimeInSeconds = dropX / pixelsPerSecond; - startTimeInSeconds = quantizeTime(startTimeInSeconds); - addAudioClipToTimeline(filePath, trackData.id, startTimeInSeconds); +
+ `; + newTrackContainer.appendChild(audioTrackLane); + const timelineContainer = audioTrackLane.querySelector( + ".timeline-container" + ); + timelineContainer.addEventListener("dragover", (e) => { + e.preventDefault(); + audioTrackLane.classList.add("drag-over"); + }); + timelineContainer.addEventListener("dragleave", () => + audioTrackLane.classList.remove("drag-over") + ); + timelineContainer.addEventListener("drop", (e) => { + e.preventDefault(); + audioTrackLane.classList.remove("drag-over"); + const filePath = e.dataTransfer.getData("text/plain"); + if (!filePath) return; + const rect = timelineContainer.getBoundingClientRect(); + const dropX = e.clientX - rect.left + timelineContainer.scrollLeft; + let startTimeInSeconds = dropX / pixelsPerSecond; + startTimeInSeconds = quantizeTime(startTimeInSeconds); + if ( + !trackData.id || + startTimeInSeconds == null || + isNaN(startTimeInSeconds) + ) { + console.error("Drop inválido. Ignorando.", { + id: trackData.id, + time: startTimeInSeconds, }); - - const grid = timelineContainer.querySelector('.spectrogram-view-grid'); - grid.style.setProperty('--step-width', `${stepWidthPx}px`); - grid.style.setProperty('--beat-width', `${beatWidthPx}px`); - grid.style.setProperty('--bar-width', `${barWidthPx}px`); - }); - - // --- RENDERIZAÇÃO DOS CLIPS (COM MODIFICAÇÃO PARA SELEÇÃO) --- - appState.audio.clips.forEach(clip => { - const parentGrid = newTrackContainer.querySelector(`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`); - if (!parentGrid) return; - - const clipElement = document.createElement('div'); - clipElement.className = 'timeline-clip'; - clipElement.dataset.clipId = clip.id; - - // --- INÍCIO DA MODIFICAÇÃO --- - // Adiciona a classe 'selected' se o ID corresponder ao estado global - if (clip.id === appState.global.selectedClipId) { - clipElement.classList.add('selected'); - } - // --- FIM DA MODIFICAÇÃO --- - - clipElement.style.left = `${(clip.startTimeInSeconds || 0) * pixelsPerSecond}px`; - clipElement.style.width = `${(clip.durationInSeconds || 0) * pixelsPerSecond}px`; - - let pitchStr = clip.pitch > 0 ? `+${clip.pitch.toFixed(1)}` : `${clip.pitch.toFixed(1)}`; - if (clip.pitch === 0) pitchStr = ''; - clipElement.innerHTML = ` -
- ${clip.name} ${pitchStr} - -
- `; - parentGrid.appendChild(clipElement); - - if (clip.buffer) { - const canvas = clipElement.querySelector('.waveform-canvas-clip'); - const canvasWidth = (clip.durationInSeconds || 0) * pixelsPerSecond; - if (canvasWidth > 0) { - canvas.width = canvasWidth; - canvas.height = 40; - const audioBuffer = clip.buffer; - - // --- INÍCIO DA CORREÇÃO --- - // Verifica se o clipe está esticado (pitch != 0) ou aparado (pitch == 0). - const isStretched = clip.pitch !== 0; - - // Se for 'stretch', devemos usar a duração original do buffer como fonte. - // Se for 'trim', usamos a duração e offset atuais do clipe. - const sourceOffset = isStretched ? 0 : (clip.offset || 0); - const sourceDuration = isStretched ? clip.originalDuration : clip.durationInSeconds; - - // Chama drawWaveform para desenhar o segmento-fonte (source) - // dentro do canvas (que já tem a largura de destino). - drawWaveform(canvas, audioBuffer, 'var(--accent-green)', sourceOffset, sourceDuration); - // --- FIM DA CORREÇÃO --- - } - } - - clipElement.addEventListener('wheel', (e) => { - e.preventDefault(); - const clipToUpdate = appState.audio.clips.find(c => c.id == clipElement.dataset.clipId); - if (!clipToUpdate) return; - const direction = e.deltaY < 0 ? 1 : -1; - let newPitch = clipToUpdate.pitch + direction; - newPitch = Math.max(-24, Math.min(24, newPitch)); - updateAudioClipProperties(clipToUpdate.id, { pitch: newPitch }); - renderAudioEditor(); - restartAudioEditorIfPlaying(); + return; + } + const clipId = + crypto?.randomUUID?.() || + `clip_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; + addAudioClipToTimeline( + filePath, + trackData.id, + startTimeInSeconds, + clipId + ); + try { + sendAction({ + type: "ADD_AUDIO_CLIP", + filePath, + trackId: trackData.id, + startTimeInSeconds, + clipId, + name: String(filePath).split(/[\\/]/).pop(), }); + } catch (err) { + console.warn("[SYNC] Falha ao emitir ADD_AUDIO_CLIP", err); + } }); - - // --- SINCRONIZAÇÃO DE SCROLL (sem alterações) --- - newTrackContainer.addEventListener('scroll', () => { - const scrollPos = newTrackContainer.scrollLeft; - if (ruler.scrollLeft !== scrollPos) { - ruler.scrollLeft = scrollPos; - } - }); + const grid = timelineContainer.querySelector(".spectrogram-view-grid"); + grid.style.setProperty("--step-width", `${stepWidthPx}px`); + grid.style.setProperty("--beat-width", `${beatWidthPx}px`); + grid.style.setProperty("--bar-width", `${barWidthPx}px`); + }); - // --- EVENT LISTENER PRINCIPAL (COM MODIFICAÇÃO PARA SELEÇÃO) --- - newTrackContainer.addEventListener('mousedown', (e) => { - // --- INÍCIO DA MODIFICAÇÃO --- - // Esconde o menu de contexto se estiver aberto - const menu = document.getElementById('timeline-context-menu'); - if (menu) menu.style.display = 'none'; - - const clipElement = e.target.closest('.timeline-clip'); - - // Desseleciona se clicar fora de um clipe (e não for clique direito) - if (!clipElement && e.button !== 2) { - if (appState.global.selectedClipId) { - appState.global.selectedClipId = null; - - // Remove a classe de todos os clipes (para resposta visual imediata) - newTrackContainer.querySelectorAll('.timeline-clip.selected').forEach(c => { - c.classList.remove('selected'); - }); - } - } - // --- FIM DA MODIFICAÇÃO --- - - const currentPixelsPerSecond = getPixelsPerSecond(); - const handle = e.target.closest('.clip-resize-handle'); - // const clipElement = e.target.closest('.timeline-clip'); // <-- Já definido acima - - if (appState.global.sliceToolActive && clipElement) { - e.preventDefault(); - e.stopPropagation(); - const clipId = clipElement.dataset.clipId; - const timelineContainer = clipElement.closest('.timeline-container'); - const rect = timelineContainer.getBoundingClientRect(); - const clickX = e.clientX - rect.left; - const absoluteX = clickX + timelineContainer.scrollLeft; - let sliceTimeInTimeline = absoluteX / currentPixelsPerSecond; - sliceTimeInTimeline = quantizeTime(sliceTimeInTimeline); - sliceAudioClip(clipId, sliceTimeInTimeline); - renderAudioEditor(); - return; - } - - // --- CORREÇÃO: LÓGICA DE REDIMENSIONAMENTO DIVIDIDA --- - if (handle) { - e.preventDefault(); - e.stopPropagation(); - - const clipId = clipElement.dataset.clipId; - const clip = appState.audio.clips.find(c => c.id == clipId); - if (!clip || !clip.buffer) return; - - const handleType = handle.classList.contains('left') ? 'left' : 'right'; - const initialMouseX = e.clientX; - const secondsPerStep = getSecondsPerStep(); - - const initialLeftPx = clipElement.offsetLeft; - const initialWidthPx = clipElement.offsetWidth; - - const initialStartTime = clip.startTimeInSeconds; - const initialDuration = clip.durationInSeconds; - const initialOffset = clip.offset || 0; - const initialPitch = clip.pitch || 0; - const initialOriginalDuration = clip.originalDuration || clip.buffer.duration; - - // O tempo "zero" absoluto do buffer (seu início) - const bufferStartTime = initialStartTime - initialOffset; - - const onMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - initialMouseX; - - // --- MODO 2: TRIMMING --- - if (appState.global.resizeMode === 'trim') { - if (handleType === 'right') { - let newWidthPx = initialWidthPx + deltaX; - let newDuration = newWidthPx / currentPixelsPerSecond; - let newEndTime = quantizeTime(initialStartTime + newDuration); - - newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); - - const maxEndTime = bufferStartTime + initialOriginalDuration; - newEndTime = Math.min(newEndTime, maxEndTime); - - clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`; - - } else if (handleType === 'left') { - let newLeftPx = initialLeftPx + deltaX; - let newStartTime = newLeftPx / currentPixelsPerSecond; - newStartTime = quantizeTime(newStartTime); - - const minStartTime = (initialStartTime + initialDuration) - secondsPerStep; - newStartTime = Math.min(newStartTime, minStartTime); - - newStartTime = Math.max(bufferStartTime, newStartTime); - - const newLeftFinalPx = newStartTime * currentPixelsPerSecond; - const newWidthFinalPx = ((initialStartTime + initialDuration) - newStartTime) * currentPixelsPerSecond; - clipElement.style.left = `${newLeftFinalPx}px`; - clipElement.style.width = `${newWidthFinalPx}px`; - } - } - // --- MODO 1: STRETCHING --- - else if (appState.global.resizeMode === 'stretch') { - if (handleType === 'right') { - let newWidthPx = initialWidthPx + deltaX; - let newDuration = newWidthPx / currentPixelsPerSecond; - let newEndTime = quantizeTime(initialStartTime + newDuration); - - newEndTime = Math.max(initialStartTime + secondsPerStep, newEndTime); - - clipElement.style.width = `${(newEndTime - initialStartTime) * currentPixelsPerSecond}px`; - - } else if (handleType === 'left') { - let newLeftPx = initialLeftPx + deltaX; - let newStartTime = newLeftPx / currentPixelsPerSecond; - newStartTime = quantizeTime(newStartTime); - - const minStartTime = (initialStartTime + initialDuration) - secondsPerStep; - newStartTime = Math.min(newStartTime, minStartTime); - - const newLeftFinalPx = newStartTime * currentPixelsPerSecond; - const newWidthFinalPx = ((initialStartTime + initialDuration) - newStartTime) * currentPixelsPerSecond; - clipElement.style.left = `${newLeftFinalPx}px`; - clipElement.style.width = `${newWidthFinalPx}px`; - } - } - }; - - const onMouseUp = (upEvent) => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - - const finalLeftPx = clipElement.offsetLeft; - const finalWidthPx = clipElement.offsetWidth; - - const newStartTime = finalLeftPx / currentPixelsPerSecond; - const newDuration = finalWidthPx / currentPixelsPerSecond; - - // --- MODO 2: TRIMMING --- - if (appState.global.resizeMode === 'trim') { - const newOffset = newStartTime - bufferStartTime; - - if (handleType === 'right') { - updateAudioClipProperties(clipId, { - durationInSeconds: newDuration, - pitch: 0 // Reseta o pitch - }); - } else if (handleType === 'left') { - updateAudioClipProperties(clipId, { - startTimeInSeconds: newStartTime, - durationInSeconds: newDuration, - offset: newOffset, - pitch: 0 // Reseta o pitch - }); - } - } - // --- MODO 1: STRETCHING --- - else if (appState.global.resizeMode === 'stretch') { - // Calcula o novo pitch baseado na mudança de duração - // Usa a duração *do buffer* (originalDuration) como base - const newPlaybackRate = initialOriginalDuration / newDuration; - const newPitch = 12 * Math.log2(newPlaybackRate); - - if (handleType === 'right') { - updateAudioClipProperties(clipId, { - durationInSeconds: newDuration, - pitch: newPitch, - offset: 0 // Stretch sempre reseta o offset - }); - } else if (handleType === 'left') { - updateAudioClipProperties(clipId, { - startTimeInSeconds: newStartTime, - durationInSeconds: newDuration, - pitch: newPitch, - offset: 0 // Stretch sempre reseta o offset - }); - } - } - - restartAudioEditorIfPlaying(); - renderAudioEditor(); - }; - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return; - } - // --- FIM DA CORREÇÃO --- - - - if (clipElement) { - // --- INÍCIO DA MODIFICAÇÃO (SELEÇÃO NO DRAG) --- - const clipId = clipElement.dataset.clipId; - // Se o clipe clicado não for o já selecionado, atualiza a seleção - if (appState.global.selectedClipId !== clipId) { - appState.global.selectedClipId = clipId; // Define o estado global - - // Atualiza visualmente - newTrackContainer.querySelectorAll('.timeline-clip.selected').forEach(c => { - c.classList.remove('selected'); - }); - clipElement.classList.add('selected'); - } - // --- FIM DA MODIFICAÇÃO --- - - // Lógica de 'drag' (sem alterações) - e.preventDefault(); - // const clipId = clipElement.dataset.clipId; // <-- Já definido acima - const clickOffsetInClip = e.clientX - clipElement.getBoundingClientRect().left; - clipElement.classList.add('dragging'); - let lastOverLane = clipElement.closest('.audio-track-lane'); - const onMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - e.clientX; - clipElement.style.transform = `translateX(${deltaX}px)`; - const overElement = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY); - const overLane = overElement ? overElement.closest('.audio-track-lane') : null; - if (overLane && overLane !== lastOverLane) { - if(lastOverLane) lastOverLane.classList.remove('drag-over'); - overLane.classList.add('drag-over'); - lastOverLane = overLane; - } - }; - const onMouseUp = (upEvent) => { - clipElement.classList.remove('dragging'); - if (lastOverLane) lastOverLane.classList.remove('drag-over'); - clipElement.style.transform = ''; - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - const finalLane = lastOverLane; - if (!finalLane) return; - const newTrackId = finalLane.dataset.trackId; - const timelineContainer = finalLane.querySelector('.timeline-container'); - const wrapperRect = timelineContainer.getBoundingClientRect(); - const newLeftPx = (upEvent.clientX - wrapperRect.left) - clickOffsetInClip + timelineContainer.scrollLeft; - - const constrainedLeftPx = Math.max(0, newLeftPx); - let newStartTime = constrainedLeftPx / currentPixelsPerSecond; - newStartTime = quantizeTime(newStartTime); - - updateAudioClipProperties(clipId, { trackId: Number(newTrackId), startTimeInSeconds: newStartTime }); - renderAudioEditor(); - }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return; - } - - const timelineContainer = e.target.closest('.timeline-container'); - if (timelineContainer) { - // Lógica de 'seek' (sem alterações) - e.preventDefault(); - const handleSeek = (event) => { - const rect = timelineContainer.getBoundingClientRect(); - const scrollLeft = timelineContainer.scrollLeft; - const clickX = event.clientX - rect.left; - const absoluteX = clickX + scrollLeft; - const newTime = absoluteX / currentPixelsPerSecond; - seekAudioEditor(newTime); - }; - handleSeek(e); - const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); - const onMouseUpSeek = () => { document.removeEventListener('mousemove', onMouseMoveSeek); document.removeEventListener('mouseup', onMouseUpSeek); }; - document.addEventListener('mousemove', onMouseMoveSeek); - document.addEventListener('mouseup', onMouseUpSeek); - } - }); - - // --- LISTENER ADICIONADO (Menu de Contexto) --- - newTrackContainer.addEventListener('contextmenu', (e) => { - e.preventDefault(); // Impede o menu de contexto padrão do navegador - const menu = document.getElementById('timeline-context-menu'); - if (!menu) return; - - const clipElement = e.target.closest('.timeline-clip'); - - if (clipElement) { - // 1. Seleciona o clipe clicado (define o estado) - const clipId = clipElement.dataset.clipId; - if (appState.global.selectedClipId !== clipId) { - appState.global.selectedClipId = clipId; - - // Atualiza visualmente - newTrackContainer.querySelectorAll('.timeline-clip.selected').forEach(c => { - c.classList.remove('selected'); - }); - clipElement.classList.add('selected'); - } - - // 2. Posiciona e mostra o menu - menu.style.display = 'block'; - menu.style.left = `${e.clientX}px`; - menu.style.top = `${e.clientY}px`; - } else { - // Esconde o menu se clicar com o botão direito fora de um clipe - menu.style.display = 'none'; - } - }); - // --- FIM DO LISTENER ADICIONADO --- -} - -// --- Funções de UI (sem alterações) --- - -export function updateAudioEditorUI() { - const playBtn = document.getElementById('audio-editor-play-btn'); - if (!playBtn) return; - if (appState.global.isAudioEditorPlaying) { - playBtn.classList.remove('fa-play'); - playBtn.classList.add('fa-pause'); - } else { - playBtn.classList.remove('fa-pause'); - playBtn.classList.add('fa-play'); + // Render Clips (sem alterações) + appState.audio.clips.forEach((clip) => { + const parentGrid = newTrackContainer.querySelector( + `.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid` + ); + if (!parentGrid) return; + const clipElement = document.createElement("div"); + clipElement.className = "timeline-clip"; + clipElement.dataset.clipId = clip.id; + if (clip.id === appState.global.selectedClipId) + clipElement.classList.add("selected"); + if (appState.global.clipboard?.cutSourceId === clip.id) + clipElement.classList.add("cut"); + clipElement.style.left = `${ + (clip.startTimeInSeconds || 0) * pixelsPerSecond + }px`; + clipElement.style.width = `${ + (clip.durationInSeconds || 0) * pixelsPerSecond + }px`; + let pitchStr = + clip.pitch > 0 ? `+${clip.pitch.toFixed(1)}` : `${clip.pitch.toFixed(1)}`; + if (clip.pitch === 0) pitchStr = ""; + clipElement.innerHTML = `
${clip.name} ${pitchStr}
`; + parentGrid.appendChild(clipElement); + if (clip.buffer) { + const canvas = clipElement.querySelector(".waveform-canvas-clip"); + const canvasWidth = (clip.durationInSeconds || 0) * pixelsPerSecond; + if (canvasWidth > 0) { + canvas.width = canvasWidth; + canvas.height = 40; + const audioBuffer = clip.buffer; + const isStretched = clip.pitch !== 0; + const sourceOffset = isStretched ? 0 : clip.offset || 0; + const sourceDuration = isStretched + ? clip.originalDuration + : clip.durationInSeconds; + drawWaveform( + canvas, + audioBuffer, + "var(--accent-green)", + sourceOffset, + sourceDuration + ); + } } + clipElement.addEventListener("wheel", (e) => { + e.preventDefault(); + const clipToUpdate = appState.audio.clips.find( + (c) => c.id == clipElement.dataset.clipId + ); + if (!clipToUpdate) return; + const direction = e.deltaY < 0 ? 1 : -1; + let newPitch = clipToUpdate.pitch + direction; + newPitch = Math.max(-24, Math.min(24, newPitch)); + updateAudioClipProperties(clipToUpdate.id, { pitch: newPitch }); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId: clipToUpdate.id, + props: { pitch: newPitch }, + }); + } catch (err) { + console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (wheel)", err); + } + renderAudioEditor(); + restartAudioEditorIfPlaying(); + }); + }); + + // Sync Scroll (sem alterações) + newTrackContainer.addEventListener("scroll", () => { + const scrollPos = newTrackContainer.scrollLeft; + const mainRuler = document.querySelector(".timeline-ruler"); + if (mainRuler && mainRuler.scrollLeft !== scrollPos) { + mainRuler.scrollLeft = scrollPos; + } + }); + + // Event Listener Principal (mousedown no container de pistas) + newTrackContainer.addEventListener("mousedown", (e) => { + // Esconde menus + document.getElementById("timeline-context-menu").style.display = "none"; + document.getElementById("ruler-context-menu").style.display = "none"; + const clipElement = e.target.closest(".timeline-clip"); + // Desseleciona se clicar fora + if (!clipElement && e.button !== 2) { + if (appState.global.selectedClipId) { + appState.global.selectedClipId = null; + newTrackContainer + .querySelectorAll(".timeline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + } + } + + const currentPixelsPerSecond = getPixelsPerSecond(); + const handle = e.target.closest(".clip-resize-handle"); + + // Slice Tool + if (appState.global.sliceToolActive && clipElement) { + e.preventDefault(); + e.stopPropagation(); + const clipId = clipElement.dataset.clipId; + const timelineContainer = clipElement.closest(".timeline-container"); + const rect = timelineContainer.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const absoluteX = clickX + timelineContainer.scrollLeft; + let sliceTimeInTimeline = absoluteX / currentPixelsPerSecond; + sliceTimeInTimeline = quantizeTime(sliceTimeInTimeline); + sliceAudioClip(clipId, sliceTimeInTimeline); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { __operation: "slice", sliceTimeInTimeline }, + }); + } catch (err) { + console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (slice)", err); + } + renderAudioEditor(); + return; + } + + // Resize Handle + if (handle) { + e.preventDefault(); + e.stopPropagation(); + const clipId = clipElement.dataset.clipId; + const clip = appState.audio.clips.find((c) => c.id == clipId); + if (!clip || !clip.buffer) return; + const handleType = handle.classList.contains("left") ? "left" : "right"; + const initialMouseX = e.clientX; + const secondsPerStep = getSecondsPerStep(); + const initialLeftPx = clipElement.offsetLeft; + const initialWidthPx = clipElement.offsetWidth; + const initialStartTime = clip.startTimeInSeconds; + const initialDuration = clip.durationInSeconds; + const initialOffset = clip.offset || 0; + const initialOriginalDuration = + clip.originalDuration || clip.buffer.duration; + const bufferStartTime = initialStartTime - initialOffset; + const onMouseMove = (moveEvent) => { + const deltaX = moveEvent.clientX - initialMouseX; + // Trim Mode + if (appState.global.resizeMode === "trim") { + if (handleType === "right") { + let newWidthPx = initialWidthPx + deltaX; + let newDuration = newWidthPx / currentPixelsPerSecond; + let newEndTime = quantizeTime(initialStartTime + newDuration); + newEndTime = Math.max( + initialStartTime + secondsPerStep, + newEndTime + ); + const maxEndTime = bufferStartTime + initialOriginalDuration; + newEndTime = Math.min(newEndTime, maxEndTime); + clipElement.style.width = `${ + (newEndTime - initialStartTime) * currentPixelsPerSecond + }px`; + } else if (handleType === "left") { + let newLeftPx = initialLeftPx + deltaX; + let newStartTime = newLeftPx / currentPixelsPerSecond; + newStartTime = quantizeTime(newStartTime); + const minStartTime = + initialStartTime + initialDuration - secondsPerStep; + newStartTime = Math.min(newStartTime, minStartTime); + newStartTime = Math.max(bufferStartTime, newStartTime); + const newLeftFinalPx = newStartTime * currentPixelsPerSecond; + const newWidthFinalPx = + (initialStartTime + initialDuration - newStartTime) * + currentPixelsPerSecond; + clipElement.style.left = `${newLeftFinalPx}px`; + clipElement.style.width = `${newWidthFinalPx}px`; + } + } + // Stretch Mode + else if (appState.global.resizeMode === "stretch") { + if (handleType === "right") { + let newWidthPx = initialWidthPx + deltaX; + let newDuration = newWidthPx / currentPixelsPerSecond; + let newEndTime = quantizeTime(initialStartTime + newDuration); + newEndTime = Math.max( + initialStartTime + secondsPerStep, + newEndTime + ); + clipElement.style.width = `${ + (newEndTime - initialStartTime) * currentPixelsPerSecond + }px`; + } else if (handleType === "left") { + let newLeftPx = initialLeftPx + deltaX; + let newStartTime = newLeftPx / currentPixelsPerSecond; + newStartTime = quantizeTime(newStartTime); + const minStartTime = + initialStartTime + initialDuration - secondsPerStep; + newStartTime = Math.min(newStartTime, minStartTime); + const newLeftFinalPx = newStartTime * currentPixelsPerSecond; + const newWidthFinalPx = + (initialStartTime + initialDuration - newStartTime) * + currentPixelsPerSecond; + clipElement.style.left = `${newLeftFinalPx}px`; + clipElement.style.width = `${newWidthFinalPx}px`; + } + } + }; + const onMouseUp = (upEvent) => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + const finalLeftPx = clipElement.offsetLeft; + const finalWidthPx = clipElement.offsetWidth; + const newStartTime = finalLeftPx / currentPixelsPerSecond; + const newDuration = finalWidthPx / currentPixelsPerSecond; + // Trim Mode + if (appState.global.resizeMode === "trim") { + const newOffset = newStartTime - bufferStartTime; + if (handleType === "right") { + updateAudioClipProperties(clipId, { + durationInSeconds: newDuration, + pitch: 0, + }); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { durationInSeconds: newDuration, pitch: 0 }, + }); + } catch (err) { + console.warn( + "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (trim-right)", + err + ); + } + } else if (handleType === "left") { + updateAudioClipProperties(clipId, { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + offset: newOffset, + pitch: 0, + }); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + offset: newOffset, + pitch: 0, + }, + }); + } catch (err) { + console.warn( + "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (trim-left)", + err + ); + } + } + } + // Stretch Mode + else if (appState.global.resizeMode === "stretch") { + const newPlaybackRate = initialOriginalDuration / newDuration; + const newPitch = 12 * Math.log2(newPlaybackRate); + if (handleType === "right") { + updateAudioClipProperties(clipId, { + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }, + }); + } catch (err) { + console.warn( + "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (stretch-right)", + err + ); + } + } else if (handleType === "left") { + updateAudioClipProperties(clipId, { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { + startTimeInSeconds: newStartTime, + durationInSeconds: newDuration, + pitch: newPitch, + offset: 0, + }, + }); + } catch (err) { + console.warn( + "[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (stretch-left)", + err + ); + } + } + } + restartAudioEditorIfPlaying(); + renderAudioEditor(); + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; + } + + // Drag Clip + if (clipElement) { + const clipId = clipElement.dataset.clipId; + if (appState.global.selectedClipId !== clipId) { + appState.global.selectedClipId = clipId; + newTrackContainer + .querySelectorAll(".timeline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + clipElement.classList.add("selected"); + } + e.preventDefault(); + const clickOffsetInClip = + e.clientX - clipElement.getBoundingClientRect().left; + clipElement.classList.add("dragging"); + let lastOverLane = clipElement.closest(".audio-track-lane"); + const onMouseMove = (moveEvent) => { + const deltaX = moveEvent.clientX - e.clientX; + clipElement.style.transform = `translateX(${deltaX}px)`; + const overElement = document.elementFromPoint( + moveEvent.clientX, + moveEvent.clientY + ); + const overLane = overElement + ? overElement.closest(".audio-track-lane") + : null; + if (overLane && overLane !== lastOverLane) { + if (lastOverLane) lastOverLane.classList.remove("drag-over"); + overLane.classList.add("drag-over"); + lastOverLane = overLane; + } + }; + const onMouseUp = (upEvent) => { + clipElement.classList.remove("dragging"); + if (lastOverLane) lastOverLane.classList.remove("drag-over"); + clipElement.style.transform = ""; + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + const finalLane = lastOverLane; + if (!finalLane) return; + const newTrackId = finalLane.dataset.trackId; // (é uma string) + const timelineContainer = finalLane.querySelector( + ".timeline-container" + ); + const wrapperRect = timelineContainer.getBoundingClientRect(); + const newLeftPx = + upEvent.clientX - + wrapperRect.left - + clickOffsetInClip + + timelineContainer.scrollLeft; + const constrainedLeftPx = Math.max(0, newLeftPx); + let newStartTime = constrainedLeftPx / currentPixelsPerSecond; + newStartTime = quantizeTime(newStartTime); + // (Correção Bug 4 - remove Number()) + updateAudioClipProperties(clipId, { + trackId: newTrackId, + startTimeInSeconds: newStartTime, + }); + try { + sendAction({ + type: "UPDATE_AUDIO_CLIP", + clipId, + props: { trackId: newTrackId, startTimeInSeconds: newStartTime }, + }); + } catch (err) { + console.warn("[SYNC] Falha ao emitir UPDATE_AUDIO_CLIP (move)", err); + } + renderAudioEditor(); + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return; + } + + // Seek na Pista + const timelineContainer = e.target.closest(".timeline-container"); + if (timelineContainer) { + e.preventDefault(); + const handleSeek = (event) => { + const rect = timelineContainer.getBoundingClientRect(); + const scrollLeft = timelineContainer.scrollLeft; + const clickX = event.clientX - rect.left; + const absoluteX = clickX + scrollLeft; + const newTime = absoluteX / currentPixelsPerSecond; + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Sincronia de Seek na Pista) + // ================================================================= + sendAction({ type: "SET_SEEK_TIME", seekTime: newTime }); + // seekAudioEditor(newTime); // 👈 Substituído + // ================================================================= + // 👆 FIM DA CORREÇÃO + }; + handleSeek(e); // Aplica no mousedown + const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent); + const onMouseUpSeek = () => { + document.removeEventListener("mousemove", onMouseMoveSeek); + document.removeEventListener("mouseup", onMouseUpSeek); + }; + document.addEventListener("mousemove", onMouseMoveSeek); + document.addEventListener("mouseup", onMouseUpSeek); + } + }); + + // Menu Contexto Pista (sem alterações) + newTrackContainer.addEventListener("contextmenu", (e) => { + e.preventDefault(); + document.getElementById("ruler-context-menu").style.display = "none"; + const menu = document.getElementById("timeline-context-menu"); + if (!menu) return; + const clipElement = e.target.closest(".timeline-clip"); + const copyItem = document.getElementById("copy-clip"); + const cutItem = document.getElementById("cut-clip"); + const pasteItem = document.getElementById("paste-clip"); + const deleteItem = document.getElementById("delete-clip"); + const canPaste = appState.global.clipboard?.type === "audio"; + pasteItem.style.display = canPaste ? "block" : "none"; + if (clipElement) { + const clipId = clipElement.dataset.clipId; + if (appState.global.selectedClipId !== clipId) { + appState.global.selectedClipId = clipId; + newTrackContainer + .querySelectorAll(".timeline-clip.selected") + .forEach((c) => c.classList.remove("selected")); + clipElement.classList.add("selected"); + } + copyItem.style.display = "block"; + cutItem.style.display = "block"; + deleteItem.style.display = "block"; + menu.style.display = "block"; + menu.style.left = `${e.clientX}px`; + menu.style.top = `${e.clientY}px`; + if (!deleteItem.__synced) { + deleteItem.__synced = true; + deleteItem.addEventListener("click", () => { + const id = appState.global.selectedClipId; + if (!id) return; + const ok = removeAudioClip(id); + try { + sendAction({ type: "REMOVE_AUDIO_CLIP", clipId: id }); + } catch (err) { + console.warn("[SYNC] Falha ao emitir REMOVE_AUDIO_CLIP", err); + } + if (ok) renderAudioEditor(); + menu.style.display = "none"; + }); + } + } else { + copyItem.style.display = "none"; + cutItem.style.display = "none"; + deleteItem.style.display = "none"; + if (canPaste) { + menu.style.display = "block"; + menu.style.left = `${e.clientX}px`; + menu.style.top = `${e.clientY}px`; + } else { + menu.style.display = "none"; + } + } + }); } +// Funções de UI (sem alterações) +export function updateAudioEditorUI() { + const playBtn = document.getElementById("audio-editor-play-btn"); + if (!playBtn) return; + if (appState.global.isAudioEditorPlaying) { + playBtn.classList.remove("fa-play"); + playBtn.classList.add("fa-pause"); + } else { + playBtn.classList.remove("fa-pause"); + playBtn.classList.add("fa-play"); + } +} export function updatePlayheadVisual(pixels) { - document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => { - ph.style.left = `${pixels}px`; - }); + document.querySelectorAll(".audio-track-lane .playhead").forEach((ph) => { + ph.style.left = `${pixels}px`; + }); } - export function resetPlayheadVisual() { - document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => { - ph.style.left = '0px'; - }); -} \ No newline at end of file + document.querySelectorAll(".audio-track-lane .playhead").forEach((ph) => { + ph.style.left = "0px"; + }); +} diff --git a/assets/js/creations/config.js b/assets/js/creations/config.js index f264d34..b7a5a2c 100644 --- a/assets/js/creations/config.js +++ b/assets/js/creations/config.js @@ -8,7 +8,6 @@ export const NOTE_LENGTH = 12; export const DEFAULT_VOLUME = 0.8; export const DEFAULT_PAN = 0.0; -// --- ADICIONADO --- // Constantes para o layout do editor de áudio export const PIXELS_PER_STEP = 32; // Cada step (1/16) terá 32px de largura export const PIXELS_PER_BAR = 512; // 16 steps * 32px/step = 512px por compasso (bar) diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js index 2c41b63..79acc0b 100644 --- a/assets/js/creations/file.js +++ b/assets/js/creations/file.js @@ -4,6 +4,10 @@ import { loadAudioForTrack } from "./pattern/pattern_state.js"; import { renderAll, getSamplePathMap } from "./ui.js"; import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH } from "./config.js"; import { initializeAudioContext, getAudioContext, getMainGainNode } from "./audio.js"; +import * as Tone from "https://esm.sh/tone"; + +// --- NOVA IMPORTAÇÃO --- +import { sendAction } from "./socket.js"; export async function handleFileLoad(file) { let xmlContent = ""; @@ -20,7 +24,12 @@ export async function handleFileLoad(file) { } else { xmlContent = await file.text(); } - await parseMmpContent(xmlContent); + + // ANTES: await parseMmpContent(xmlContent); + // DEPOIS: + // Envia o XML para o servidor, que o transmitirá para todos (incluindo nós) + sendAction({ type: 'LOAD_PROJECT', xml: xmlContent }); + } catch (error) { console.error("Erro ao carregar o projeto:", error); alert(`Erro ao carregar projeto: ${error.message}`); @@ -34,8 +43,16 @@ export async function loadProjectFromServer(fileName) { throw new Error(`Não foi possível carregar o arquivo ${fileName}`); const xmlContent = await response.text(); - await parseMmpContent(xmlContent); - return true; + + // ANTES: + // await parseMmpContent(xmlContent); + // return true; + + // DEPOIS: + // Envia o XML para o servidor + sendAction({ type: 'LOAD_PROJECT', xml: xmlContent }); + return true; // Retorna true para que o modal de UI feche + } catch (error) { console.error("Erro ao carregar projeto do servidor:", error); console.error(error); @@ -44,6 +61,10 @@ export async function loadProjectFromServer(fileName) { } } +// --- NENHUMA MUDANÇA DAQUI PARA BAIXO --- +// 'parseMmpContent' agora é chamado pelo 'socket.js' +// quando ele recebe a ação 'LOAD_PROJECT' ou 'load_project_state'. + export async function parseMmpContent(xmlString) { resetProjectState(); initializeAudioContext(); @@ -175,12 +196,12 @@ export async function parseMmpContent(xmlString) { let isFirstTrackWithNotes = true; newTracks.forEach(track => { // --- INÍCIO DA CORREÇÃO --- - // Cria os nós de áudio usando os construtores do Tone.js - track.gainNode = new Tone.Gain(Tone.gainToDb(track.volume)); + // Agora usando Volume em dB (Opção B) + track.volumeNode = new Tone.Volume(Tone.gainToDb(track.volume)); track.pannerNode = new Tone.Panner(track.pan); - // Conecta a cadeia de áudio: Gain -> Panner -> Saída Principal (Destination) - track.gainNode.connect(track.pannerNode); + // Cadeia de áudio: Volume(dB) -> Panner -> Saída Principal + track.volumeNode.connect(track.pannerNode); track.pannerNode.connect(getMainGainNode()); // --- FIM DA CORREÇÃO --- @@ -206,7 +227,13 @@ export async function parseMmpContent(xmlString) { appState.pattern.tracks = newTracks; appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null; + + // força atualização total da UI e dos editores de pattern + await Promise.resolve(); // garante que os tracks estejam no estado renderAll(); + + console.log('[UI] Projeto renderizado após parseMmpContent'); + } export function generateMmpFile() { @@ -314,4 +341,4 @@ function downloadFile(content, fileName) { a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); -} \ No newline at end of file +} diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index 93b5c92..cc80184 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -1,43 +1,65 @@ -// js/main.js +// js/main.js (ESM com import absoluto de socket.js + ROOM_NAME local) + import { appState, resetProjectState } from "./state.js"; -import { addTrackToState, removeLastTrackFromState } from "./pattern/pattern_state.js"; -// --- CORREÇÃO AQUI --- -import { toggleRecording } from "./recording.js"; -import { addAudioTrackLane, removeAudioClip } from "./audio/audio_state.js"; -import { updateTransportLoop } from "./audio/audio_audio.js"; import { - togglePlayback, - stopPlayback, - rewindPlayback, -} from "./pattern/pattern_audio.js"; -import { - startAudioEditorPlayback, - stopAudioEditorPlayback, + updateTransportLoop, restartAudioEditorIfPlaying, } from "./audio/audio_audio.js"; import { initializeAudioContext } from "./audio.js"; import { handleFileLoad, generateMmpFile } from "./file.js"; -import { renderAll, loadAndRenderSampleBrowser, showOpenProjectModal, closeOpenProjectModal } from "./ui.js"; +import { + renderAll, + loadAndRenderSampleBrowser, + showOpenProjectModal, + closeOpenProjectModal, +} from "./ui.js"; import { renderAudioEditor } from "./audio/audio_ui.js"; import { adjustValue, enforceNumericInput } from "./utils.js"; import { ZOOM_LEVELS } from "./config.js"; -// --- NOVA FUNÇÃO --- -// Atualiza a aparência dos botões de ferramenta +// ⚠️ IMPORT ABSOLUTO para evitar 404/text/html quando a página estiver em /creation/ ou fora dela. +// Ajuste o prefixo abaixo para o caminho real onde seus assets vivem no servidor: +import { sendAction, joinRoom, setUserName } from "./socket.js"; + +// Descobre a sala pela URL (local ao main.js) e expõe no window para debug +const ROOM_NAME = new URLSearchParams(window.location.search).get("room"); +window.ROOM_NAME = ROOM_NAME; + +// ✅ NOVO: se tem sala na URL, entra já na sala (independe do áudio) + +if (ROOM_NAME) { + // entra na sala para receber estado/broadcasts imediatamente + joinRoom(); +} + +// Função util para alternar estado dos botões de ferramenta function updateToolButtons() { - const sliceToolBtn = document.getElementById("slice-tool-btn"); - const trimToolBtn = document.getElementById("resize-tool-trim"); - const stretchToolBtn = document.getElementById("resize-tool-stretch"); + const sliceToolBtn = document.getElementById("slice-tool-btn"); + const trimToolBtn = document.getElementById("resize-tool-trim"); + const stretchToolBtn = document.getElementById("resize-tool-stretch"); - if(sliceToolBtn) sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive); - if(trimToolBtn) trimToolBtn.classList.toggle("active", !appState.global.sliceToolActive && appState.global.resizeMode === 'trim'); - if(stretchToolBtn) stretchToolBtn.classList.toggle("active", !appState.global.sliceToolActive && appState.global.resizeMode === 'stretch'); + if (sliceToolBtn) + sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive); + if (trimToolBtn) + trimToolBtn.classList.toggle( + "active", + !appState.global.sliceToolActive && appState.global.resizeMode === "trim" + ); + if (stretchToolBtn) + stretchToolBtn.classList.toggle( + "active", + !appState.global.sliceToolActive && + appState.global.resizeMode === "stretch" + ); - // Desativa a ferramenta de corte se outra for selecionada - document.body.classList.toggle("slice-tool-active", appState.global.sliceToolActive); + document.body.classList.toggle( + "slice-tool-active", + appState.global.sliceToolActive + ); } document.addEventListener("DOMContentLoaded", () => { + // Botões e elementos const newProjectBtn = document.getElementById("new-project-btn"); const openMmpBtn = document.getElementById("open-mmp-btn"); const saveMmpBtn = document.getElementById("save-mmp-btn"); @@ -53,12 +75,9 @@ document.addEventListener("DOMContentLoaded", () => { const rewindBtn = document.getElementById("rewind-btn"); const metronomeBtn = document.getElementById("metronome-btn"); const sliceToolBtn = document.getElementById("slice-tool-btn"); - - // --- NOVOS BOTÕES --- const resizeToolTrimBtn = document.getElementById("resize-tool-trim"); const resizeToolStretchBtn = document.getElementById("resize-tool-stretch"); - const recordBtn = document.getElementById("record-btn"); - + const createRoomBtn = document.getElementById("create-room-btn"); const mmpFileInput = document.getElementById("mmp-file-input"); const sampleFileInput = document.getElementById("sample-file-input"); const openProjectModal = document.getElementById("open-project-modal"); @@ -68,192 +87,354 @@ document.addEventListener("DOMContentLoaded", () => { const addBarBtn = document.getElementById("add-bar-btn"); const zoomInBtn = document.getElementById("zoom-in-btn"); const zoomOutBtn = document.getElementById("zoom-out-btn"); + const deleteClipBtn = document.getElementById("delete-clip"); - // --- LISTENERS ADICIONADOS (COM LÓGICA DE CONTROLLER) --- - - // --- NOVO LISTENER PARA O BOTÃO DE GRAVAR --- - if (recordBtn) { - recordBtn.addEventListener("click", () => { - // Garante que o contexto de áudio foi iniciado por um gesto do usuário - initializeAudioContext(); - toggleRecording(); + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Botão de Sincronia - Agora envia Ação) + // ================================================================= + const syncModeBtn = document.getElementById("sync-mode-btn"); // + if (syncModeBtn) { + // + // Define o estado inicial (global por padrão) + appState.global.syncMode = "global"; // + syncModeBtn.classList.add("active"); // + syncModeBtn.textContent = "Global"; // + + syncModeBtn.addEventListener("click", () => { + // + // 1. Determina qual será o *novo* modo + const newMode = + appState.global.syncMode === "global" ? "local" : "global"; // + + // 2. Envia a ação para sincronizar. O handleActionBroadcast + // cuidará de atualizar o appState, o botão e mostrar o toast. + sendAction({ + type: "SET_SYNC_MODE", + mode: newMode, }); - } - // Listener para o botão "Excluir Clipe" no menu de contexto - const deleteClipBtn = document.getElementById('delete-clip'); - if (deleteClipBtn) { - deleteClipBtn.addEventListener('click', () => { - const clipId = appState.global.selectedClipId; // 1. Lê o estado - if (clipId) { - if (removeAudioClip(clipId)) { // 2. Chama a função de state - appState.global.selectedClipId = null; // 3. Atualiza o estado global - renderAudioEditor(); // 4. Renderiza a mudança - } - } - // Esconde o menu - const menu = document.getElementById('timeline-context-menu'); - if (menu) menu.style.display = 'none'; - }); - } - - // Listener global para a tecla Delete/Backspace - document.addEventListener('keydown', (e) => { - // Ignora se estiver digitando em um input - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { - return; - } - - const clipId = appState.global.selectedClipId; // 1. Lê o estado - // Verifica se há um clipe selecionado e a tecla pressionada é Delete ou Backspace - if ((e.key === 'Delete' || e.key === 'Backspace') && clipId) { - e.preventDefault(); // Impede o navegador de voltar a página (ação do Backspace) - - if (removeAudioClip(clipId)) { // 2. Chama a função de state - appState.global.selectedClipId = null; // 3. Atualiza o estado global - renderAudioEditor(); // 4. Renderiza a mudança - } - } + // Lógica antiga removida daqui (movida para o handler) + /* + const isNowLocal = appState.global.syncMode === "global"; + appState.global.syncMode = isNowLocal ? "local" : "global"; + syncModeBtn.classList.toggle("active", !isNowLocal); + syncModeBtn.textContent = isNowLocal ? "Local" : "Global"; + showToast( `🎧 Modo de Playback: ${isNowLocal ? "Local" : "Global"}`, "info" ); + */ }); - // Listener global para fechar menu de contexto ou desselecionar clipe - document.addEventListener('click', (e) => { - // Esconde o menu de contexto se clicar fora dele - const menu = document.getElementById('timeline-context-menu'); - if (menu && !e.target.closest('#timeline-context-menu')) { - menu.style.display = 'none'; - } - }); - - // --- FIM DOS LISTENERS ADICIONADOS --- + // Esconde o botão se não estiver em uma sala (lógica movida do socket.js) + if (!ROOM_NAME) { + // + //syncModeBtn.style.display = 'none'; // REMOVIDO PARA TESTE VISUAL + } + } + // ================================================================= + // 👆 FIM DA CORREÇÃO + // ================================================================= - newProjectBtn.addEventListener("click", () => { - if ((appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) && !confirm("Você tem certeza? Alterações não salvas serão perdidas.")) return; - resetProjectState(); - document.getElementById('bpm-input').value = 140; - document.getElementById('bars-input').value = 1; - document.getElementById('compasso-a-input').value = 4; - document.getElementById('compasso-b-input').value = 4; - const titleElement = document.getElementById('beat-bassline-title'); - if(titleElement) titleElement.textContent = 'Novo Projeto'; - renderAll(); + // Excluir clipe + if (deleteClipBtn) { + deleteClipBtn.addEventListener("click", () => { + initializeAudioContext(); + const clipId = appState.global.selectedClipId; + if (clipId) { + sendAction({ type: "REMOVE_AUDIO_CLIP", clipId }); + appState.global.selectedClipId = null; + } + const menu = document.getElementById("timeline-context-menu"); + if (menu) menu.style.display = "none"; + }); + } + + // Delete/Backspace + document.addEventListener("keydown", (e) => { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; + const clipId = appState.global.selectedClipId; + if ((e.key === "Delete" || e.key === "Backspace") && clipId) { + e.preventDefault(); + sendAction({ type: "REMOVE_AUDIO_CLIP", clipId }); + appState.global.selectedClipId = null; + } }); - addBarBtn.addEventListener("click", () => { + // Fechar menu contexto + document.addEventListener("click", (e) => { + const menu = document.getElementById("timeline-context-menu"); + if (menu && !e.target.closest("#timeline-context-menu")) { + menu.style.display = "none"; + } + }); + + // Ações principais (broadcast) + newProjectBtn?.addEventListener("click", () => { + initializeAudioContext(); + if ( + (appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) && + !confirm( + "Você tem certeza? Isso irá resetar o projeto para TODOS na sala." + ) + ) + return; + sendAction({ type: "RESET_PROJECT" }); + }); + + addBarBtn?.addEventListener("click", () => { const barsInput = document.getElementById("bars-input"); - if (barsInput) adjustValue(barsInput, 1); - }); - - openMmpBtn.addEventListener("click", showOpenProjectModal); - loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click()); - mmpFileInput.addEventListener("change", (event) => { const file = event.target.files[0]; if (file) { handleFileLoad(file).then(() => closeOpenProjectModal()); } }); - uploadSampleBtn.addEventListener("click", () => sampleFileInput.click()); - saveMmpBtn.addEventListener("click", generateMmpFile); - addInstrumentBtn.addEventListener("click", addTrackToState); - removeInstrumentBtn.addEventListener("click", removeLastTrackFromState); - playBtn.addEventListener("click", togglePlayback); - stopBtn.addEventListener("click", stopPlayback); - rewindBtn.addEventListener("click", rewindPlayback); - metronomeBtn.addEventListener("click", () => { initializeAudioContext(); appState.global.metronomeEnabled = !appState.global.metronomeEnabled; metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled); }); - - // --- LISTENERS DE FERRAMENTAS ATUALIZADOS --- - if(sliceToolBtn) { - sliceToolBtn.addEventListener("click", () => { - appState.global.sliceToolActive = !appState.global.sliceToolActive; - updateToolButtons(); - }); - } - if(resizeToolTrimBtn) { - resizeToolTrimBtn.addEventListener("click", () => { - appState.global.resizeMode = 'trim'; - appState.global.sliceToolActive = false; - updateToolButtons(); - }); - } - if(resizeToolStretchBtn) { - resizeToolStretchBtn.addEventListener("click", () => { - appState.global.resizeMode = 'stretch'; - appState.global.sliceToolActive = false; - updateToolButtons(); - }); - } - - openModalCloseBtn.addEventListener("click", closeOpenProjectModal); - - sidebarToggle.addEventListener("click", () => { - document.body.classList.toggle("sidebar-hidden"); - const icon = sidebarToggle.querySelector("i"); - if (icon) { - icon.className = document.body.classList.contains("sidebar-hidden") ? "fa-solid fa-caret-right" : "fa-solid fa-caret-left"; + if (barsInput) { + adjustValue(barsInput, 1); + barsInput.dispatchEvent(new Event("change", { bubbles: true })); } }); - + + openMmpBtn?.addEventListener("click", showOpenProjectModal); + loadFromComputerBtn?.addEventListener("click", () => mmpFileInput?.click()); + mmpFileInput?.addEventListener("change", (event) => { + const file = event.target.files[0]; + if (file) handleFileLoad(file).then(() => closeOpenProjectModal()); + }); + uploadSampleBtn?.addEventListener("click", () => sampleFileInput?.click()); + saveMmpBtn?.addEventListener("click", generateMmpFile); + + addInstrumentBtn?.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "ADD_TRACK" }); + }); + removeInstrumentBtn?.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "REMOVE_LAST_TRACK" }); + }); + + playBtn?.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "TOGGLE_PLAYBACK" }); + }); + stopBtn?.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "STOP_PLAYBACK" }); + }); + rewindBtn?.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "REWIND_PLAYBACK" }); + }); + + metronomeBtn?.addEventListener("click", () => { + initializeAudioContext(); + appState.global.metronomeEnabled = !appState.global.metronomeEnabled; + metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled); + }); + + // Ferramentas locais + if (sliceToolBtn) { + sliceToolBtn.addEventListener("click", () => { + appState.global.sliceToolActive = !appState.global.sliceToolActive; + updateToolButtons(); + }); + } + if (resizeToolTrimBtn) { + resizeToolTrimBtn.addEventListener("click", () => { + appState.global.resizeMode = "trim"; + appState.global.sliceToolActive = false; + updateToolButtons(); + }); + } + if (resizeToolStretchBtn) { + resizeToolStretchBtn.addEventListener("click", () => { + appState.global.resizeMode = "stretch"; + appState.global.sliceToolActive = false; + updateToolButtons(); + }); + } + + openModalCloseBtn?.addEventListener("click", closeOpenProjectModal); + sidebarToggle?.addEventListener("click", () => { + document.body.classList.toggle("sidebar-hidden"); + const icon = sidebarToggle.querySelector("i"); + if (icon) { + icon.className = document.body.classList.contains("sidebar-hidden") + ? "fa-solid fa-caret-right" + : "fa-solid fa-caret-left"; + } + }); + const inputs = document.querySelectorAll(".value-input"); inputs.forEach((input) => { input.addEventListener("input", (event) => { enforceNumericInput(event); - if (appState.global.isPlaying && (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input')) { stopPlayback(); } - if (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input' || event.target.id === 'bpm-input') { renderAll(); } + if ( + appState.global.isPlaying && + (event.target.id.startsWith("compasso-") || + event.target.id === "bars-input") + ) { + sendAction({ type: "STOP_PLAYBACK" }); + } + }); + + input.addEventListener("change", (event) => { + const target = event.target; + if (target.id === "bpm-input") { + sendAction({ type: "SET_BPM", value: target.value }); + } else if (target.id === "bars-input") { + sendAction({ type: "SET_BARS", value: target.value }); + } else if (target.id === "compasso-a-input") { + sendAction({ type: "SET_TIMESIG_A", value: target.value }); + } else if (target.id === "compasso-b-input") { + sendAction({ type: "SET_TIMESIG_B", value: target.value }); + } + }); + + input.addEventListener("wheel", (event) => { + event.preventDefault(); + const step = event.deltaY < 0 ? 1 : -1; + adjustValue(event.target, step); + event.target.dispatchEvent(new Event("change", { bubbles: true })); }); - input.addEventListener("wheel", (event) => { event.preventDefault(); const step = event.deltaY < 0 ? 1 : -1; adjustValue(event.target, step); }); }); const buttons = document.querySelectorAll(".adjust-btn"); - buttons.forEach((button) => { button.addEventListener("click", () => { const targetId = button.dataset.target + "-input"; const targetInput = document.getElementById(targetId); const step = parseInt(button.dataset.step, 10) || 1; if (targetInput) { adjustValue(targetInput, step); } }); }); - - if (zoomInBtn) { - zoomInBtn.addEventListener("click", () => { - if (appState.global.zoomLevelIndex < ZOOM_LEVELS.length - 1) { - appState.global.zoomLevelIndex++; - renderAll(); - } + buttons.forEach((button) => { + button.addEventListener("click", () => { + const targetId = button.dataset.target + "-input"; + const targetInput = document.getElementById(targetId); + const step = parseInt(button.dataset.step, 10) || 1; + if (targetInput) { + adjustValue(targetInput, step); + targetInput.dispatchEvent(new Event("change", { bubbles: true })); + } }); - } - if (zoomOutBtn) { - zoomOutBtn.addEventListener("click", () => { - if (appState.global.zoomLevelIndex > 0) { - appState.global.zoomLevelIndex--; - renderAll(); - } - }); - } - - audioEditorPlayBtn.addEventListener("click", () => { - if (appState.global.isAudioEditorPlaying) { - stopAudioEditorPlayback(false); // Pausa - } else { - startAudioEditorPlayback(); - } }); - audioEditorStopBtn.addEventListener("click", () => stopAudioEditorPlayback(true)); // Stop (rebobina) - - audioEditorLoopBtn.addEventListener("click", () => { - appState.global.isLoopActive = !appState.global.isLoopActive; - appState.audio.isAudioEditorLoopEnabled = appState.global.isLoopActive; - audioEditorLoopBtn.classList.toggle("active", appState.global.isLoopActive); - updateTransportLoop(); - const loopArea = document.getElementById("loop-region"); - if (loopArea) { - loopArea.classList.toggle("visible", appState.global.isLoopActive); + + // Zoom local + zoomInBtn?.addEventListener("click", () => { + if (appState.global.zoomLevelIndex < ZOOM_LEVELS.length - 1) { + appState.global.zoomLevelIndex++; + renderAll(); + } + }); + zoomOutBtn?.addEventListener("click", () => { + if (appState.global.zoomLevelIndex > 0) { + appState.global.zoomLevelIndex--; + renderAll(); } - restartAudioEditorIfPlaying(); }); - - if (addAudioTrackBtn) { addAudioTrackBtn.addEventListener("click", () => { addAudioTrackLane(); renderAudioEditor(); }); } + // Editor de Áudio + audioEditorPlayBtn?.addEventListener("click", () => { + initializeAudioContext(); + if (appState.global.isAudioEditorPlaying) { + sendAction({ type: "STOP_AUDIO_PLAYBACK", rewind: false }); + } else { + sendAction({ + type: "START_AUDIO_PLAYBACK", + seekTime: appState.audio.audioEditorSeekTime, // Corrigido + loopState: { + isLoopActive: appState.global.isLoopActive, + loopStartTime: appState.global.loopStartTime, + loopEndTime: appState.global.loopEndTime, + }, + }); + } + }); + audioEditorStopBtn?.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "STOP_AUDIO_PLAYBACK", rewind: true }); + }); + + // Loop Button (agora envia ação) + audioEditorLoopBtn?.addEventListener("click", () => { + initializeAudioContext(); // Garante contexto + const newLoopState = !appState.global.isLoopActive; + sendAction({ + type: "SET_LOOP_STATE", + isLoopActive: newLoopState, + loopStartTime: appState.global.loopStartTime, + loopEndTime: appState.global.loopEndTime, + }); + }); + + if (addAudioTrackBtn) { + addAudioTrackBtn.addEventListener("click", () => { + initializeAudioContext(); + sendAction({ type: "ADD_AUDIO_LANE" }); + }); + } + + // Navegador de Samples (local) loadAndRenderSampleBrowser(); - const browserContent = document.getElementById('browser-content'); + const browserContent = document.getElementById("browser-content"); if (browserContent) { - browserContent.addEventListener('click', function(event) { - const folderName = event.target.closest('.folder-name'); - if (folderName) { - const folderItem = folderName.parentElement; - folderItem.classList.toggle('open'); - } - }); + browserContent.addEventListener("click", function (event) { + const folderName = event.target.closest(".folder-name"); + if (folderName) { + const folderItem = folderName.parentElement; + folderItem.classList.toggle("open"); + } + }); + } + + // Criar sala (gera link com ?room=...) + if (createRoomBtn) { + createRoomBtn.addEventListener("click", () => { + initializeAudioContext(); + const currentParams = new URLSearchParams(window.location.search); + if (currentParams.has("room")) { + alert( + `Você já está na sala: ${currentParams.get( + "room" + )}\n\nCopie o link da barra de endereços para convidar.` + ); + return; + } + const defaultName = `sessao-${Math.random() + .toString(36) + .substring(2, 7)}`; + const roomName = prompt( + "Digite um nome para a sala compartilhada:", + defaultName + ); + if (!roomName) return; + const currentUrl = window.location.origin + window.location.pathname; + const shareableLink = `${currentUrl}?room=${encodeURIComponent( + roomName + )}`; + try { + navigator.clipboard.writeText(shareableLink); + alert( + `Link da sala copiado para a área de transferência!\n\n${shareableLink}\n\nA página será recarregada agora para entrar na nova sala.` + ); + } catch (err) { + alert( + `Link da sala: ${shareableLink}\n\nA página será recarregada agora para entrar na nova sala.` + ); + } + window.location.href = shareableLink; + }); + } + + // Modal “destravar áudio” + entrar na sala + const audioUnlockModal = document.getElementById("audio-unlock-modal"); + const audioUnlockBtn = document.getElementById("audio-unlock-btn"); + + if (ROOM_NAME && audioUnlockModal && audioUnlockBtn) { + audioUnlockModal.style.display = "flex"; + audioUnlockBtn.addEventListener("click", () => { + const userName = prompt( + "Qual o seu nome?", + `Alicer-${Math.floor(Math.random() * 999)}` + ); + if (!userName) return; + setUserName(userName); + initializeAudioContext(); + // joinRoom() já foi chamado no início se ROOM_NAME existe + audioUnlockModal.style.display = "none"; + }); + } else { + console.log("Modo local. Áudio será iniciado no primeiro clique."); + // Comentado para permitir teste visual + // if (syncModeBtn) syncModeBtn.style.display = "none"; } renderAll(); - updateToolButtons(); // Define o estado inicial dos botões -}); \ No newline at end of file + updateToolButtons(); +}); diff --git a/assets/js/creations/pattern/pattern_audio.js b/assets/js/creations/pattern/pattern_audio.js index 3eca9f6..7135637 100644 --- a/assets/js/creations/pattern/pattern_audio.js +++ b/assets/js/creations/pattern/pattern_audio.js @@ -1,4 +1,6 @@ // js/pattern_audio.js +import * as Tone from "https://esm.sh/tone"; + import { appState } from "../state.js"; import { highlightStep } from "./pattern_ui.js"; import { getTotalSteps } from "../utils.js"; @@ -21,24 +23,39 @@ export function playMetronomeSound(isDownbeat) { synth.triggerAttackRelease(freq, "8n", Tone.now()); } -// --- FUNÇÃO CORRIGIDA E EFICIENTE --- +// Dispara o sample de uma track, garantindo que o player esteja roteado corretamente export function playSample(filePath, trackId) { initializeAudioContext(); const track = trackId ? appState.pattern.tracks.find((t) => t.id == trackId) : null; - // Se a faixa existe e tem um player pré-carregado, apenas o dispara. + // Se a faixa existe e tem um player pré-carregado if (track && track.player) { - // Atualiza o volume/pan caso tenham sido alterados - track.gainNode.gain.value = Tone.gainToDb(track.volume); - track.pannerNode.pan.value = track.pan; - - // Dispara o som imediatamente. Esta operação é instantânea. - track.player.start(Tone.now()); - } - // Fallback para preview de samples no navegador (sem trackId) + if (track.player.loaded) { + // Ajusta volume/pan sempre que tocar (robustez a alterações em tempo real) + if (track.volumeNode) { + track.volumeNode.volume.value = + track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume); + } + if (track.pannerNode) { + track.pannerNode.pan.value = track.pan ?? 0; + } + + // Garante conexão: player -> volumeNode (não usar mais gainNode) + try { track.player.disconnect(); } catch {} + if (track.volumeNode) { + track.player.connect(track.volumeNode); + } + + // Dispara imediatamente + track.player.start(Tone.now()); + } else { + console.warn(`Player da trilha "${track.name}" ainda não carregado — pulando este tick.`); + } + } + // Fallback para preview de sample sem trackId else if (!trackId && filePath) { - const previewPlayer = new Tone.Player(filePath).toDestination(); - previewPlayer.autostart = true; + const previewPlayer = new Tone.Player(filePath).toDestination(); + previewPlayer.autostart = true; } } @@ -59,6 +76,7 @@ function tick() { timerDisplay.textContent = formatTime(currentTime); } + // Metrônomo if (appState.global.metronomeEnabled) { const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4; const stepsPerBeat = 16 / noteValue; @@ -67,12 +85,16 @@ function tick() { } } + // Percorre tracks e toca o step atual se ativo appState.pattern.tracks.forEach((track) => { if (!track.patterns || track.patterns.length === 0) return; - - const activePattern = track.patterns[appState.pattern.activePatternIndex]; - if (activePattern && activePattern.steps[appState.global.currentStep] && track.samplePath) { + // IMPORTANTE: usar o pattern ativo da PRÓPRIA TRILHA + const activePattern = track.patterns[track.activePatternIndex]; + + if (activePattern && + activePattern.steps[appState.global.currentStep] && + track.samplePath) { playSample(track.samplePath, track.id); } }); @@ -84,9 +106,9 @@ function tick() { export function startPlayback() { if (appState.global.isPlaying || appState.pattern.tracks.length === 0) return; initializeAudioContext(); - + if (appState.global.currentStep === 0) { - rewindPlayback(); + rewindPlayback(); } const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; @@ -96,15 +118,18 @@ export function startPlayback() { if (appState.global.playbackIntervalId) clearInterval(appState.global.playbackIntervalId); appState.global.isPlaying = true; - document.getElementById("play-btn").classList.remove("fa-play"); - document.getElementById("play-btn").classList.add("fa-pause"); + const playBtn = document.getElementById("play-btn"); + if (playBtn) { + playBtn.classList.remove("fa-play"); + playBtn.classList.add("fa-pause"); + } tick(); appState.global.playbackIntervalId = setInterval(tick, stepInterval); } export function stopPlayback() { - if(appState.global.playbackIntervalId) { + if (appState.global.playbackIntervalId) { clearInterval(appState.global.playbackIntervalId); } appState.global.playbackIntervalId = null; @@ -138,4 +163,4 @@ export function togglePlayback() { appState.global.currentStep = 0; startPlayback(); } -} \ No newline at end of file +} diff --git a/assets/js/creations/pattern/pattern_bounce.js b/assets/js/creations/pattern/pattern_bounce.js new file mode 100644 index 0000000..7634779 --- /dev/null +++ b/assets/js/creations/pattern/pattern_bounce.js @@ -0,0 +1,108 @@ +// js/pattern/pattern_bounce.js +import { appState } from '../state.js'; +import { getSecondsPerStep } from '../utils.js'; +import { addAudioClipToTimeline, addAudioTrackLane } from '../audio/audio_state.js'; +import { getAudioContext } from '../audio.js'; +import { renderAudioEditor } from '../audio/audio_ui.js'; + +/** + * Renderiza (bounce) o pattern de beat atualmente ativo para uma nova pista de áudio. + */ +export async function bounceActivePatternToAudio() { + console.log("Iniciando 'bounce' do pattern..."); + + // 1. Encontrar o pattern ativo + const activeTrackId = appState.pattern.activeTrackId; + + // --- DEBUG --- + console.log(`[DEBUG bounce] activeTrackId lido do estado: ${activeTrackId}`); + + const activeTrack = appState.pattern.tracks.find(t => t.id === activeTrackId); + + // --- DEBUG --- + if (activeTrack) { + console.log(`[DEBUG bounce] Pista ativa encontrada:`, activeTrack.name); + console.log(`[DEBUG bounce] Verificando track.buffer:`, activeTrack.buffer); + } else { + console.error(`[DEBUG bounce] NENHUMA PISTA ATIVA ENCONTRADA. activeTrackId é nulo ou inválido.`); + console.log(`[DEBUG bounce] Pistas disponíveis no estado:`, appState.pattern.tracks.map(t => ({id: t.id, name: t.name}))); + } + // --- FIM DEBUG --- + + if (!activeTrack) { + alert('Nenhuma pista de pattern selecionada para renderizar.'); + return; + } + + const activePattern = activeTrack.patterns[activeTrack.activePatternIndex]; + if (!activePattern) { + alert('Nenhum pattern ativo encontrado na pista.'); + return; + } + + const trackBuffer = activeTrack.buffer; + if (!trackBuffer) { + alert('O áudio (sample) desta pista ainda não foi carregado.'); + return; + } + + // 2. Calcular a duração do pattern + const steps = activePattern.steps; + const totalSteps = steps.length; + const secondsPerStep = getSecondsPerStep(); // + const duration = totalSteps * secondsPerStep; + + if (duration <= 0) { + alert("Pattern está vazio ou com duração zero."); + return; + } + + try { + // 3. Usar Tone.Offline para renderizar o áudio + const audioBuffer = await Tone.Offline(async (offlineCtx) => { + + const gainNode = new Tone.Gain(Tone.gainToDb(activeTrack.volume)).connect(offlineCtx.destination); + const pannerNode = new Tone.Panner(activeTrack.pan).connect(gainNode); // + + const now = offlineCtx.currentTime; + + // --- INÍCIO DA CORREÇÃO (que estava na versão anterior) --- + const offlineBuffer = offlineCtx.createBuffer( + trackBuffer.numberOfChannels, + trackBuffer.length, + trackBuffer.sampleRate + ); + + for (let i = 0; i < trackBuffer.numberOfChannels; i++) { + offlineBuffer.copyToChannel(trackBuffer.getChannelData(i), i); + } + // --- FIM DA CORREÇÃO --- + + // Agendar todas as notas (steps) + steps.forEach((isActive, index) => { + if (isActive) { + const time = now + (index * secondsPerStep); + const source = new Tone.BufferSource(offlineBuffer).connect(pannerNode); + source.start(time); + } + }); + + }, duration); + + // 4. Áudio renderizado. Agora, adicione-o ao editor de áudio. + + addAudioTrackLane(); // + const newTrack = appState.audio.tracks[appState.audio.tracks.length - 1]; // + const newTrackId = newTrack.id; + + const clipName = `${activeTrack.name}_(${activePattern.name})`; + + addAudioClipToTimeline(null, newTrackId, 0, clipName, audioBuffer); // + + console.log("Pattern renderizado com sucesso!"); + + } catch (err) { + console.error("Erro ao renderizar o pattern:", err); + alert(`Erro ao renderizar pattern: ${err.message}`); + } +} \ No newline at end of file diff --git a/assets/js/creations/pattern/pattern_state.js b/assets/js/creations/pattern/pattern_state.js index c584c6d..df65e21 100644 --- a/assets/js/creations/pattern/pattern_state.js +++ b/assets/js/creations/pattern/pattern_state.js @@ -1,97 +1,148 @@ // js/pattern_state.js +import * as Tone from "https://esm.sh/tone"; + +import { appState } from "../state.js"; import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js"; -import { getAudioContext, getMainGainNode } from "../audio.js"; -import { renderPatternEditor } from "./pattern_ui.js"; +import { getMainGainNode } from "../audio.js"; import { getTotalSteps } from "../utils.js"; -const initialState = { - tracks: [], - activeTrackId: null, - activePatternIndex: 0, -}; - -export let patternState = { ...initialState }; - export function initializePatternState() { - Object.assign(patternState, initialState, { tracks: [] }); + // Limpa players/buffers existentes + appState.pattern.tracks.forEach(track => { + try { track.player?.dispose(); } catch {} + try { track.buffer?.dispose?.(); } catch {} + }); + + // Reseta estado do editor de pattern + appState.pattern.tracks = []; + appState.pattern.activeTrackId = null; + appState.pattern.activePatternIndex = 0; } -// --- FUNÇÃO CORRIGIDA --- -// Agora, esta função cria e pré-carrega um Tone.Player para a faixa. export async function loadAudioForTrack(track) { if (!track.samplePath) return track; + try { - // Se já existir um player antigo, o descartamos para liberar memória. - if (track.player) { - track.player.dispose(); - } - - // Cria um novo Tone.Player e o conecta à cadeia de áudio da faixa. - // O 'await' garante que o áudio seja totalmente carregado antes de prosseguirmos. - track.player = await new Tone.Player(track.samplePath).toDestination(); - track.player.chain(track.gainNode, track.pannerNode, getMainGainNode()); - - } catch (error) { - console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error); + // Descartar player/buffer anteriores com segurança + try { track.player?.dispose(); } catch {} track.player = null; + try { track.buffer?.dispose?.(); } catch {} + track.buffer = null; + + // Garante nós de volume/pan (Opção B: Volume em dB) + if (!track.volumeNode) { + track.volumeNode = new Tone.Volume( + track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume) + ); + } else { + track.volumeNode.volume.value = + track.volume === 0 ? -Infinity : Tone.gainToDb(track.volume); + } + + if (!track.pannerNode) { + track.pannerNode = new Tone.Panner(track.pan ?? 0); + } else { + track.pannerNode.pan.value = track.pan ?? 0; + } + + // Encadeia: Volume(dB) -> Panner -> Master + try { track.volumeNode.disconnect(); } catch {} + try { track.pannerNode.disconnect(); } catch {} + track.volumeNode.connect(track.pannerNode); + track.pannerNode.connect(getMainGainNode()); + + // Cria e carrega o Player + const player = new Tone.Player({ url: track.samplePath, autostart: false }); + await player.load(track.samplePath); // garante buffer carregado + + // Conecta o player ao volumeNode + player.connect(track.volumeNode); + + // Buffer separado (se você usar waveform em outro lugar) + const buffer = new Tone.Buffer(); + await buffer.load(track.samplePath); + + // Atribuições finais + track.player = player; + track.buffer = buffer; + + } catch (error) { + console.error('Erro ao carregar sample:', track.samplePath); + console.error(`Falha ao carregar áudio para a trilha "${track.name}":`, error); + try { track.player?.dispose(); } catch {} + try { track.buffer?.dispose?.(); } catch {} + track.player = null; + track.buffer = null; } return track; } export function addTrackToState() { - const mainGainNode = getMainGainNode(); const totalSteps = getTotalSteps(); - const referenceTrack = patternState.tracks[0]; + const referenceTrack = appState.pattern.tracks[0]; const newTrack = { - id: Date.now(), + id: Date.now() + Math.random(), name: "novo instrumento", samplePath: null, - player: null, // <-- ADICIONADO: O player começará como nulo - patterns: referenceTrack - ? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos })) + player: null, + buffer: null, + patterns: referenceTrack + ? referenceTrack.patterns.map(p => ({ + name: p.name, + steps: new Array(p.steps.length).fill(false), + pos: p.pos + })) : [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }], activePatternIndex: 0, volume: DEFAULT_VOLUME, pan: DEFAULT_PAN, - gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)), + // Opção B: controlar volume em dB + volumeNode: new Tone.Volume(Tone.gainToDb(DEFAULT_VOLUME)), pannerNode: new Tone.Panner(DEFAULT_PAN), }; - newTrack.gainNode.chain(newTrack.pannerNode, mainGainNode); + // Cadeia de áudio nova + newTrack.volumeNode.connect(newTrack.pannerNode); + newTrack.pannerNode.connect(getMainGainNode()); - patternState.tracks.push(newTrack); - renderPatternEditor(); + appState.pattern.tracks.push(newTrack); + appState.pattern.activeTrackId = newTrack.id; } export function removeLastTrackFromState() { - if (patternState.tracks.length > 0) { - const trackToRemove = patternState.tracks[patternState.tracks.length - 1]; - if (trackToRemove.player) trackToRemove.player.dispose(); - if (trackToRemove.pannerNode) trackToRemove.pannerNode.dispose(); - if (trackToRemove.gainNode) trackToRemove.gainNode.dispose(); - - patternState.tracks.pop(); - renderPatternEditor(); + if (appState.pattern.tracks.length > 0) { + const trackToRemove = appState.pattern.tracks[appState.pattern.tracks.length - 1]; + + try { trackToRemove.player?.dispose(); } catch {} + try { trackToRemove.buffer?.dispose?.(); } catch {} + try { trackToRemove.pannerNode?.disconnect(); } catch {} + try { trackToRemove.volumeNode?.disconnect(); } catch {} + + appState.pattern.tracks.pop(); + if (appState.pattern.activeTrackId === trackToRemove.id) { + appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id ?? null; + } } } -export async function updateTrackSample(trackId, samplePath) { - const track = patternState.tracks.find((t) => t.id == trackId); +export async function updateTrackSample(trackIndex, samplePath) { + const track = appState.pattern.tracks[trackIndex]; if (track) { track.samplePath = samplePath; track.name = samplePath.split("/").pop(); - await loadAudioForTrack(track); // Carrega o novo player - renderPatternEditor(); + + // (re)carrega e reconecta corretamente o player nesta trilha + await loadAudioForTrack(track); } } -export function toggleStepState(trackId, stepIndex) { - const track = patternState.tracks.find((t) => t.id == trackId); +export function toggleStepState(trackIndex, stepIndex) { + const track = appState.pattern.tracks[trackIndex]; if (track && track.patterns && track.patterns.length > 0) { const activePattern = track.patterns[track.activePatternIndex]; if (activePattern && activePattern.steps.length > stepIndex) { activePattern.steps[stepIndex] = !activePattern.steps[stepIndex]; } } -} \ No newline at end of file +} diff --git a/assets/js/creations/pattern/pattern_ui.js b/assets/js/creations/pattern/pattern_ui.js index 746e474..caf54f0 100644 --- a/assets/js/creations/pattern/pattern_ui.js +++ b/assets/js/creations/pattern/pattern_ui.js @@ -1,171 +1,218 @@ // js/pattern_ui.js import { appState } from "../state.js"; import { - toggleStepState, - updateTrackSample +    updateTrackSample } from "./pattern_state.js"; -import { playSample, stopPlayback } from "./pattern_audio.js"; // Será criado no próximo passo +import { playSample, stopPlayback } from "./pattern_audio.js"; import { getTotalSteps } from "../utils.js"; +import { sendAction } from '../socket.js'; +import { initializeAudioContext } from '../audio.js'; // Função principal de renderização para o editor de patterns export function renderPatternEditor() { - const trackContainer = document.getElementById("track-container"); - trackContainer.innerHTML = ""; +  const trackContainer = document.getElementById("track-container"); +  trackContainer.innerHTML = ""; - appState.pattern.tracks.forEach((trackData) => { - const trackLane = document.createElement("div"); - trackLane.className = "track-lane"; - trackLane.dataset.trackId = trackData.id; + // (V7) Adicionado 'trackIndex' +  appState.pattern.tracks.forEach((trackData, trackIndex) => { +    const trackLane = document.createElement("div"); +    trackLane.className = "track-lane"; +    trackLane.dataset.trackIndex = trackIndex; // (V7) Usando índice - if (trackData.id === appState.pattern.activeTrackId) { - trackLane.classList.add('active-track'); - } +    if (trackData.id === appState.pattern.activeTrackId) { +        trackLane.classList.add('active-track'); +    } - trackLane.innerHTML = ` -
- -
- ${trackData.name} -
-
-
-
- VOL -
-
-
- PAN -
-
-
- `; +    trackLane.innerHTML = ` +     
+        +       
+        ${trackData.name} +     
+     
+       
+         
+          VOL +       
+       
+         
+          PAN +       
+     
+     
+    `; - trackLane.addEventListener('click', () => { - if (appState.pattern.activeTrackId === trackData.id) return; - stopPlayback(); - appState.pattern.activeTrackId = trackData.id; - document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track')); - trackLane.classList.add('active-track'); - updateGlobalPatternSelector(); - redrawSequencer(); - }); + // (Listener de clique da track é local, sem mudanças) +    trackLane.addEventListener('click', () => { +        if (appState.pattern.activeTrackId === trackData.id) return; +        stopPlayback(); +        appState.pattern.activeTrackId = trackData.id; +        document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track')); +        trackLane.classList.add('active-track'); +        updateGlobalPatternSelector(); +        redrawSequencer(); +    }); - trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); }); - trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over")); - trackLane.addEventListener("drop", (e) => { - e.preventDefault(); - trackLane.classList.remove("drag-over"); - const filePath = e.dataTransfer.getData("text/plain"); - if (filePath) { - updateTrackSample(trackData.id, filePath); - } - }); +    trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); }); +    trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over")); - trackContainer.appendChild(trackLane); - // A lógica dos knobs precisará ser reimplementada ou movida para um arquivo de componentes - }); - - updateGlobalPatternSelector(); - redrawSequencer(); + // (V9) Listener de "drop" (arrastar) agora usa 'sendAction' +    trackLane.addEventListener("drop", (e) => { +      e.preventDefault(); +      trackLane.classList.remove("drag-over"); +      const filePath = e.dataTransfer.getData("text/plain"); +      +      if (filePath) { + sendAction({ + type: 'SET_TRACK_SAMPLE', + trackIndex: trackIndex, + filePath: filePath + }); +      } +    }); + +    trackContainer.appendChild(trackLane); +  }); +  +  updateGlobalPatternSelector(); +  redrawSequencer(); } export function redrawSequencer() { - const totalGridSteps = getTotalSteps(); - document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => { - let sequencerContainer = wrapper.querySelector(".step-sequencer"); - if (!sequencerContainer) { - sequencerContainer = document.createElement("div"); - sequencerContainer.className = "step-sequencer"; - wrapper.appendChild(sequencerContainer); - } - - const parentTrackElement = wrapper.closest(".track-lane"); - const trackId = parentTrackElement.dataset.trackId; - const trackData = appState.pattern.tracks.find((t) => t.id == trackId); +  const totalGridSteps = getTotalSteps(); +  document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => { +    let sequencerContainer = wrapper.querySelector(".step-sequencer"); +    if (!sequencerContainer) { +      sequencerContainer = document.createElement("div"); +      sequencerContainer.className = "step-sequencer"; +      wrapper.appendChild(sequencerContainer); +    } +    +    const parentTrackElement = wrapper.closest(".track-lane"); +    const trackIndex = parseInt(parentTrackElement.dataset.trackIndex, 10); // (V7) +    // ... dentro da função redrawSequencer() ... - if (!trackData || !trackData.patterns || trackData.patterns.length === 0) { - sequencerContainer.innerHTML = ""; return; - } +    const trackData = appState.pattern.tracks[trackIndex]; - const activePattern = trackData.patterns[appState.pattern.activePatternIndex]; - if (!activePattern) { - sequencerContainer.innerHTML = ""; return; - } - const patternSteps = activePattern.steps; +    if (!trackData || !trackData.patterns || trackData.patterns.length === 0) { +      sequencerContainer.innerHTML = ""; return; +    } - sequencerContainer.innerHTML = ""; - for (let i = 0; i < totalGridSteps; i++) { - const stepWrapper = document.createElement("div"); - stepWrapper.className = "step-wrapper"; - const stepElement = document.createElement("div"); - stepElement.className = "step"; - - if (patternSteps[i] === true) { - stepElement.classList.add("active"); - } + // --- CORRIJA ESTAS DUAS LINHAS --- + // ANTES: + // const activePatternIndex = appState.pattern.activePatternIndex; + // const activePattern = trackData.patterns[activePatternIndex]; + // + // DEPOIS: +    const activePatternIndex = trackData.activePatternIndex; +    const activePattern = trackData.patterns[activePatternIndex]; - stepElement.addEventListener("click", () => { - toggleStepState(trackData.id, i); - stepElement.classList.toggle("active"); - if (trackData && trackData.samplePath) { - playSample(trackData.samplePath, trackData.id); - } - }); +    if (!activePattern) { +        sequencerContainer.innerHTML = ""; return; +    } +// ... resto da função ... +    const patternSteps = activePattern.steps; - const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4; - const groupIndex = Math.floor(i / beatsPerBar); - if (groupIndex % 2 === 0) { - stepElement.classList.add("step-dark"); - } +    sequencerContainer.innerHTML = ""; +    for (let i = 0; i < totalGridSteps; i++) { +      const stepWrapper = document.createElement("div"); +      stepWrapper.className = "step-wrapper"; +      const stepElement = document.createElement("div"); +      stepElement.className = "step"; +      +      if (patternSteps[i] === true) { +        stepElement.classList.add("active"); +      } - const stepsPerBar = 16; - if (i > 0 && i % stepsPerBar === 0) { - const marker = document.createElement("div"); - marker.className = "step-marker"; - marker.textContent = Math.floor(i / stepsPerBar) + 1; - stepWrapper.appendChild(marker); - } - - stepWrapper.appendChild(stepElement); - sequencerContainer.appendChild(stepWrapper); - } - }); +      stepElement.addEventListener("click", () => { + initializeAudioContext(); // (V8) + + const currentState = activePattern.steps[i] || false; + const isActive = !currentState; + + sendAction({ // (V7) + type: 'TOGGLE_NOTE', + trackIndex: trackIndex, + patternIndex: activePatternIndex, + stepIndex: i, + isActive: isActive + }); + +        if (isActive && trackData && trackData.samplePath) { +          playSample(trackData.samplePath, trackData.id); +        } +      }); + +      const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4; +      const groupIndex = Math.floor(i / beatsPerBar); +      if (groupIndex % 2 === 0) { +        stepElement.classList.add("step-dark"); +      } + +      const stepsPerBar = 16; +      if (i > 0 && i % stepsPerBar === 0) { +        const marker = document.createElement("div"); +        marker.className = "step-marker"; +        marker.textContent = Math.floor(i / stepsPerBar) + 1; +        stepWrapper.appendChild(marker); +      } +      +      stepWrapper.appendChild(stepElement); +      sequencerContainer.appendChild(stepWrapper); +    } +  }); } export function updateGlobalPatternSelector() { - const globalPatternSelector = document.getElementById('global-pattern-selector'); - if (!globalPatternSelector) return; +    const globalPatternSelector = document.getElementById('global-pattern-selector'); +    if (!globalPatternSelector) return; - const referenceTrack = appState.pattern.tracks[0]; - globalPatternSelector.innerHTML = ''; - if (referenceTrack && referenceTrack.patterns.length > 0) { - referenceTrack.patterns.forEach((pattern, index) => { - const option = document.createElement('option'); - option.value = index; - option.textContent = pattern.name; - globalPatternSelector.appendChild(option); - }); - globalPatternSelector.selectedIndex = appState.pattern.activePatternIndex; - globalPatternSelector.disabled = false; - } else { - const option = document.createElement('option'); - option.textContent = 'Sem patterns'; - globalPatternSelector.appendChild(option); - globalPatternSelector.disabled = true; - } +    const referenceTrack = appState.pattern.tracks[0]; +    globalPatternSelector.innerHTML = ''; +    if (referenceTrack && referenceTrack.patterns.length > 0) { +        referenceTrack.patterns.forEach((pattern, index) => { +            const option = document.createElement('option'); +            option.value = index; +            option.textContent = pattern.name; +            globalPatternSelector.appendChild(option); +        }); +        globalPatternSelector.selectedIndex = appState.pattern.activePatternIndex; +        globalPatternSelector.disabled = false; +    } else { +        const option = document.createElement('option'); +        option.textContent = 'Sem patterns'; +        globalPatternSelector.appendChild(option); +        globalPatternSelector.disabled = true; +    } } export function highlightStep(stepIndex, isActive) { - if (stepIndex < 0) return; - document.querySelectorAll(".track-lane").forEach((track) => { - const stepWrapper = track.querySelector( - `.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})` - ); - if (stepWrapper) { - const stepElement = stepWrapper.querySelector(".step"); - if (stepElement) { - stepElement.classList.toggle("playing", isActive); - } - } - }); +  if (stepIndex < 0) return; +  document.querySelectorAll(".track-lane").forEach((track) => { +    const stepWrapper = track.querySelector( +      `.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})` +    ); +    if (stepWrapper) { +      const stepElement = stepWrapper.querySelector(".step"); +      if (stepElement) { +        stepElement.classList.toggle("playing", isActive); +      } +    } +  }); +} + +// (V7) Função de UI "cirúrgica" +export function updateStepUI(trackIndex, patternIndex, stepIndex, isActive) { + if (patternIndex !== appState.pattern.activePatternIndex) { + return; + } + const trackElement = document.querySelector(`.track-lane[data-track-index="${trackIndex}"]`); + if (!trackElement) return; + const stepWrapper = trackElement.querySelector( + `.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})` + ); + if (!stepWrapper) return; + const stepElement = stepWrapper.querySelector(".step"); + if (!stepElement) return; + stepElement.classList.toggle("active", isActive); } \ No newline at end of file diff --git a/assets/js/creations/recording.js b/assets/js/creations/recording.js index 83868c7..04a31d6 100644 --- a/assets/js/creations/recording.js +++ b/assets/js/creations/recording.js @@ -145,7 +145,7 @@ async function _startRecording() { } catch (err) { console.error("Erro ao iniciar a gravação:", err); appState.global.isRecording = false; // - _updateRecordButtonUI(false); // + _updateRecordButtonUI(false); } } @@ -188,7 +188,7 @@ async function _stopRecording() { } catch (err) { console.error("Erro ao parar a gravação:", err); appState.global.isRecording = false; // - _updateRecordButtonUI(false); // + _updateRecordButtonUI(false); } } @@ -215,12 +215,9 @@ function _processRecording(blob) { const clipName = `Rec_${new Date().toISOString().slice(11, 19).replace(/:/g, '-')}`; // Adiciona o clipe à pista que já criamos - addAudioClipToTimeline(blobUrl, targetTrackId, 0, clipName); // + addAudioClipToTimeline(blobUrl, targetTrackId, 0, clipName, null); // - // addAudioClipToTimeline já chama o render, mas como o estado mudou - // (o clipe foi adicionado), renderizar de novo garante que o - // waveform *final* (do blob) seja desenhado corretamente. - renderAudioEditor(); // + // addAudioClipToTimeline já chama o render } /** diff --git a/assets/js/creations/server/package-lock.json b/assets/js/creations/server/package-lock.json new file mode 100644 index 0000000..8cffb3b --- /dev/null +++ b/assets/js/creations/server/package-lock.json @@ -0,0 +1,1330 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^5.1.0", + "pino": "^8.21.0", + "socket.io": "^4.8.1", + "tone": "^15.1.22" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/automation-events": { + "version": "7.1.13", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-7.1.13.tgz", + "integrity": "sha512-1Hay5TQPzxsskSqPTH3YXyzE9Iirz82zZDse2vr3+kOR7Sc7om17qIEPsESchlNX0EgKxANwR40i2g/O3GM1Tw==", + "dependencies": { + "@babel/runtime": "^7.28.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standardized-audio-context": { + "version": "25.3.77", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.77.tgz", + "integrity": "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "automation-events": "^7.0.9", + "tslib": "^2.7.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tone": { + "version": "15.1.22", + "resolved": "https://registry.npmjs.org/tone/-/tone-15.1.22.tgz", + "integrity": "sha512-TCScAGD4sLsama5DjvTUXlLDXSqPealhL64nsdV1hhr6frPWve0DeSo63AKnSJwgfg55fhvxj0iPPRwPN5o0ag==", + "dependencies": { + "standardized-audio-context": "^25.3.70", + "tslib": "^2.3.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/assets/js/creations/server/package.json b/assets/js/creations/server/package.json new file mode 100644 index 0000000..bd8b8b7 --- /dev/null +++ b/assets/js/creations/server/package.json @@ -0,0 +1,19 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^5.1.0", + "pino": "^8.21.0", + "socket.io": "^4.8.1", + "tone": "^15.1.22" + } +} diff --git a/assets/js/creations/server/server.js b/assets/js/creations/server/server.js new file mode 100644 index 0000000..d8e62cf --- /dev/null +++ b/assets/js/creations/server/server.js @@ -0,0 +1,514 @@ +// server.js - VERSÃO 6 (ACK + Clock Sync + Logs Dinâmicos + Persistência Autoritativa) + +// ========================= +// IMPORTS PRINCIPAIS +// ========================= +const express = require("express"); +const { createServer } = require("http"); +const { Server } = require("socket.io"); +const fs = require("fs"); +const path = require("path"); +const pino = require("pino"); + +// ------------------------- +// LOGGER DINÂMICO (GERENCIADOR) +// ------------------------- + +// Vamos manter um mapa de loggers ativos, um para cada sala +const loggersByRoom = new Map(); + +// Função para gerar um timestamp formatado para nome de arquivo +function getTimestampForFile() { + const d = new Date(); + const date = d.toISOString().split("T")[0]; // YYYY-MM-DD + // HH-MM-SS (formato 24h, fuso local) + const time = d.toTimeString().split(" ")[0].replace(/:/g, "-"); + return `${date}_${time}`; // ex: 2025-10-26_12-30-05 +} + +// Função principal: Pega o logger de uma sala ou cria um novo +function getActionLoggerForRoom(roomName) { + // 1. Se já criamos um logger para esta sala, reutilize-o + if (loggersByRoom.has(roomName)) { + return loggersByRoom.get(roomName); + } + + // 2. Se for a primeira vez, crie um novo logger + const timestamp = getTimestampForFile(); + + // Limpa o nome da sala para evitar caracteres inválidos no nome do arquivo + const safeRoomName = roomName.replace(/[^a-z0-9_-]/gi, "_"); + + const fileName = `${timestamp}_${safeRoomName}.log`; + const filePath = path.join(process.cwd(), "data", fileName); + + console.log(`[Logger] Novo log de sessão iniciado: ${filePath}`); + + // Garante que a pasta 'data' exista + fs.mkdirSync(path.join(process.cwd(), "data"), { recursive: true }); + + // Cria a instância do pino para este arquivo específico + const newLogger = pino(pino.destination(filePath)); + + // Armazena no mapa para reutilização + loggersByRoom.set(roomName, newLogger); + + return newLogger; +} + +// ------------------------- +// Configuração do Servidor +// ------------------------- +const app = express(); +const httpServer = createServer(app); +const PORT = process.env.PORT || 33007; + +// ------------------------- +// Persistência por sala +// ------------------------- +/* + Estrutura na memória: + roomStates = { + [roomName]: { + projectXml: '' | undefined, + audio: { tracks: [], clips: [] }, + seq: number, + tokensSeen: Set // idempotência + } + } +*/ +const roomStates = {}; // compat com V4 (já existia), ampliado para incluir audio/seq + +function dataFile(roomName) { + return path.join(process.cwd(), "data", `${roomName}.json`); +} + +function ensureRoom(roomName) { + if (roomStates[roomName]) return roomStates[roomName]; + + // tenta carregar do disco + try { + const p = dataFile(roomName); + if (fs.existsSync(p)) { + const j = JSON.parse(fs.readFileSync(p, "utf8")); + roomStates[roomName] = { + projectXml: j.projectXml || null, + audio: j.audio || { tracks: [], clips: [] }, + seq: j.seq || 0, + tokensSeen: new Set(), + }; + return roomStates[roomName]; + } + } catch (e) { + console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e); + } + + roomStates[roomName] = { + projectXml: null, + audio: { tracks: [], clips: [] }, + seq: 0, + tokensSeen: new Set(), + }; + return roomStates[roomName]; +} + +function saveRoom(roomName) { + const r = ensureRoom(roomName); + try { + fs.mkdirSync(path.join(process.cwd(), "data"), { recursive: true }); + fs.writeFileSync( + dataFile(roomName), + JSON.stringify({ projectXml: r.projectXml, audio: r.audio, seq: r.seq }) + ); + } catch (e) { + console.warn(`[persist] falha ao salvar estado da sala ${roomName}:`, e); + } +} + +// ------------------------- +// Server / IO +// ------------------------- +const io = new Server(httpServer, { + cors: { + origin: [ + "https://alice.ufsj.edu.br", + "http://127.0.0.1:33007", + "http://localhost:33007", + "http://localhost:5173", + "http://127.0.0.1:5173", + ], + methods: ["GET", "POST"], + credentials: true, + }, +}); + +console.log("Backend V6 (Logs Dinâmicos) iniciado. Aguardando conexões..."); + +// util de log das salas deste socket +function logMyRooms(socket, prefix = "SALAS") { + const myRooms = Array.from(socket.rooms || []); + console.log(`[${prefix}] socket ${socket.id} está em:`, myRooms); +} + +// ------------------------- +// REDUCER autoritativo do editor de áudio +// ------------------------- +function basenameNoExt(p) { + if (!p) return ""; + const base = String(p).split(/[\\/]/).pop(); + return base.replace(/\.[^/.]+$/, ""); +} + +function applyAuthoritativeAction(roomName, action) { + const room = ensureRoom(roomName); + const state = room.audio; + + // idempotência por token (se fornecido) + if (action.__token && room.tokensSeen.has(action.__token)) { + return null; + } + if (action.__token) room.tokensSeen.add(action.__token); + + let mutated = false; + + switch (action.type) { + case "ADD_AUDIO_LANE": { + // Usa o trackId enviado pelo cliente, se existir + const id = action.trackId || generateUniqueId("track_server"); + if (!state.tracks.find((t) => t.id === id)) { + state.tracks.push({ + id, + name: `Pista de Áudio ${state.tracks.length + 1}`, + }); + mutated = true; + } + break; + } + + case "ADD_AUDIO_CLIP": { + const { clipId, filePath, trackId, startTimeInSeconds, name } = action; + + // Blindagem do Servidor (Evitar faixas fantasma) + if ( + !trackId || + clipId == null || + startTimeInSeconds == null || + isNaN(startTimeInSeconds) + ) { + console.warn( + `[Server] Ação ADD_AUDIO_CLIP rejeitada (dados inválidos):`, + action + ); + return null; // Rejeita a ação, não faz broadcast + } + + if (!state.tracks.find((t) => t.id === trackId)) { + state.tracks.push({ + id: trackId, + name: `Pista de Áudio ${state.tracks.length + 1}`, + }); + mutated = true; + } + + if (!state.clips.find((c) => String(c.id) === String(clipId))) { + state.clips.push({ + id: clipId, + trackId, + name: name || basenameNoExt(filePath) || "sample", + sourcePath: filePath || null, // ideal: URL pública + startTimeInSeconds, + durationInSeconds: action.durationInSeconds || 0, + offset: action.offset || 0, + pitch: action.pitch || 0, + volume: action.volume ?? 1, + pan: action.pan ?? 0, + originalDuration: action.originalDuration || 0, + }); + mutated = true; + } + break; + } + + case "UPDATE_AUDIO_CLIP": { + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Blindagem do Servidor para Bug 4) + // ================================================================= + if (!action.clipId || !action.props) { + console.warn( + `[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`, + action + ); + return null; + } + + const { trackId, startTimeInSeconds } = action.props; + if ( + trackId !== undefined && + (trackId == null || (typeof trackId === "number" && isNaN(trackId))) + ) { + console.warn( + `[Server] Ação UPDATE_AUDIO_CLIP rejeitada (trackId inválido):`, + action + ); + return null; + } + if ( + startTimeInSeconds !== undefined && + (startTimeInSeconds == null || isNaN(startTimeInSeconds)) + ) { + console.warn( + `[Server] Ação UPDATE_AUDIO_CLIP rejeitada (startTimeInSeconds inválido):`, + action + ); + return null; + } + // ================================================================= + // 👆 FIM DA CORREÇÃO + // ================================================================= + + const c = state.clips.find((x) => String(x.id) === String(action.clipId)); + if (c && action.props && typeof action.props === "object") { + Object.assign(c, action.props); + mutated = true; + } + break; + } + + case "REMOVE_AUDIO_CLIP": { + const i = state.clips.findIndex( + (x) => String(x.id) === String(action.clipId) + ); + if (i >= 0) { + state.clips.splice(i, 1); + mutated = true; + } + break; + } + + default: + // outras ações não são da persistência do editor de áudio + mutated = false; + } + + if (!mutated) return null; + + room.seq += 1; // numeração autoritativa + saveRoom(roomName); // persistência em disco + return { ...action, __seq: room.seq }; // devolve ação com seq para broadcast +} + +io.on("connection", (socket) => { + console.log(`Novo usuário conectado: ${socket.id}`); + + // ======================================= + // JOIN ROOM + // ======================================= + socket.on("join_room", async (data) => { + const { roomName, userName } = data || {}; + if (!roomName) { + console.warn(`join_room inválido de ${socket.id} (sem roomName)`); + return; + } + + await socket.join(roomName); + console.log( + `Usuário ${userName || "(sem nome)"} (${ + socket.id + }) entrou na sala: ${roomName}` + ); + logMyRooms(socket, "JOIN"); + + // envia estado salvo do projeto (XML) — compat V4 + const room = ensureRoom(roomName); + if (room.projectXml) { + socket.emit("load_project_state", room.projectXml); + console.log( + `Estado XML enviado para ${userName || socket.id} na sala ${roomName}` + ); + } else { + console.log(`Sala ${roomName} sem XML salvo ainda.`); + } + + // envia snapshot autoritativo do editor de áudio + socket.emit("action_broadcast", { + action: { + type: "AUDIO_SNAPSHOT", + snapshot: room.audio, + __seq: room.seq, + __target: socket.id, + }, + }); + + socket + .to(roomName) + .emit("feedback", `Usuário ${userName || socket.id} entrou na sala.`); + }); + + // ======================================= + // CLOCK SYNC + // ======================================= + socket.on("what_time_is_it", (_data, cb) => { + cb && cb({ serverNowMs: Date.now() }); + }); + + // ======================================= + // BROADCAST COM ACK + PERSISTÊNCIA + // ======================================= + socket.on("broadcast_action", async (payload, cb) => { + const { roomName, action } = payload || {}; + if (!roomName || !action) { + cb && cb({ ok: false, error: "invalid_payload" }); + console.warn( + `[broadcast_action] payload inválido de ${socket.id}:`, + payload + ); + return; + } + + // 👇 *** NOSSO NOVO LOG DINÂMICO *** 👇 + try { + // 1. Pega o logger específico para ESTA sala + const roomLogger = getActionLoggerForRoom(roomName); + + // 2. Loga a ação (não precisamos mais salvar roomName, já está no nome do arq.) + roomLogger.info( + { + timestamp: Date.now(), + socketId: socket.id, + action: action, + }, + "action_received" + ); + } catch (e) { + console.warn(`[Logger] Falha ao logar ação para sala ${roomName}:`, e); + } + // *** FIM DO LOG *** + + // Confirma recebimento imediatamente + cb && cb({ ok: true, token: action.__token }); + + // Garante que o emissor está na sala (caso o join tenha falhado/atrasado) + if (!socket.rooms.has(roomName)) { + console.warn( + `[broadcast_action] ${socket.id} NÃO estava na sala ${roomName}. Forçando join...` + ); + await socket.join(roomName); + logMyRooms(socket, "FORCED_JOIN"); + } + + // Carrega estado da sala + const room = ensureRoom(roomName); + + // Persiste estado se for LOAD_PROJECT (compat) + if (action.type === "LOAD_PROJECT" && action.xml) { + room.projectXml = action.xml; + saveRoom(roomName); + console.log( + `[broadcast_action] Estado da sala ${roomName} atualizado (LOAD_PROJECT).` + ); + } + + // Aplica ações autoritativas do editor de áudio + const maybeApplied = applyAuthoritativeAction(roomName, action); + + // Debug: quem está na sala neste momento? + const socketsInRoom = await io.in(roomName).fetchSockets(); + const idsInRoom = socketsInRoom.map((s) => s.id); + console.log( + `[broadcast_action] ${action.type} (token=${action.__token}) para sala "${roomName}" com ${idsInRoom.length} cliente(s):`, + idsInRoom + ); + + // Se foi uma ação do editor de áudio e foi aplicada, rebroadcast com __seq + if (maybeApplied) { + io.to(roomName).emit("action_broadcast", { action: maybeApplied }); + return; + } + + // Caso contrário, apenas repercute a ação como antes (sem __seq) + io.to(roomName).emit("action_broadcast", { action }); + }); + + // ======================================= + // Retrocompat (daw_action) + // ======================================= + socket.on("daw_action", async (data) => { + const { room, action } = data || {}; + if (!room || !action) return; + + console.warn( + "[DEPRECATION] Recebido daw_action. Migre para broadcast_action com ACK." + ); + + // (Opcional, mas recomendado) Adicionando log dinâmico aqui também + try { + const roomLogger = getActionLoggerForRoom(room); + roomLogger.info( + { + timestamp: Date.now(), + socketId: socket.id, + action: action, + }, + "action_received (deprecated daw_action)" + ); + } catch (e) { + console.warn(`[Logger] Falha ao logar daw_action para sala ${room}:`, e); + } + + if (!socket.rooms.has(room)) { + console.warn( + `[daw_action] ${socket.id} NÃO estava na sala ${room}. Forçando join...` + ); + await socket.join(room); + logMyRooms(socket, "FORCED_JOIN(daw)"); + } + + const r = ensureRoom(room); + if (action.type === "LOAD_PROJECT" && action.xml) { + r.projectXml = action.xml; + saveRoom(room); + console.log( + `[daw_action] Estado da sala ${room} atualizado (LOAD_PROJECT).` + ); + } + + const socketsInRoom = await io.in(room).fetchSockets(); + const idsInRoom = socketsInRoom.map((s) => s.id); + console.log( + `[daw_action] ${action.type} para sala "${room}" (${idsInRoom.length} cliente(s)):`, + idsInRoom + ); + + io.in(room).emit("action_broadcast", { action }); + }); + + // ======================================= + // Re-sync explícito (quando cliente detectar buraco de seq) + // ======================================= + socket.on("audio_resync", ({ roomName, lastSeq }) => { + const room = ensureRoom(roomName); + // caminho simples: envia snapshot atual (estado autoritativo) + socket.emit("action_broadcast", { + action: { + type: "AUDIO_SNAPSHOT", + snapshot: room.audio, + __seq: room.seq, + __target: socket.id, + }, + }); + }); + + socket.on("disconnect", () => { + console.log(`Usuário desconectado: ${socket.id}`); + }); +}); + +app.get("/", (req, res) => { + res.send( + "Servidor Backend V6 (Logs Dinâmicos) da DAW colaborativa está no ar!" + ); +}); + +httpServer.listen(PORT, () => { + console.log(`Servidor escutando na porta http://localhost:${PORT}`); +}); diff --git a/assets/js/creations/socket.js b/assets/js/creations/socket.js new file mode 100644 index 0000000..ea5b174 --- /dev/null +++ b/assets/js/creations/socket.js @@ -0,0 +1,805 @@ +// js/socket.js — V5.5 (Toast para Sync Mode) + +// -------------------relómm------------------------------------------------------ +// IMPORTS & STATE +// ----------------------------------------------------------------------------- +import { appState, resetProjectState } from "./state.js"; +import { + addTrackToState, + removeLastTrackFromState, + updateTrackSample, +} from "./pattern/pattern_state.js"; +import { + addAudioTrackLane, + removeAudioClip, + addAudioClipToTimeline, + updateAudioClipProperties, + sliceAudioClip, + getAudioSnapshot, // 👈 novo + applyAudioSnapshot, // 👈 novo +} from "./audio/audio_state.js"; + +import { + togglePlayback, + stopPlayback, + rewindPlayback, +} from "./pattern/pattern_audio.js"; + +import { + startAudioEditorPlayback, + stopAudioEditorPlayback, + updateTransportLoop, + seekAudioEditor, // 👈 Adicionado + restartAudioEditorIfPlaying, // 👈 Adicionado +} from "./audio/audio_audio.js"; + +import { parseMmpContent } from "./file.js"; +import { renderAll, showToast } from "./ui.js"; // showToast() +import { updateStepUI, renderPatternEditor } from "./pattern/pattern_ui.js"; + +// ----------------------------------------------------------------------------- +// Gera um ID único otimista (ex: "track_1678886401000_abc123") +// ----------------------------------------------------------------------------- +function generateUniqueId(prefix = "item") { + return `${prefix}_${Date.now()}_${Math.random() + .toString(36) + .substring(2, 9)}`; +} + +// ----------------------------------------------------------------------------- +// CONFIGURAÇÃO DO SOCKET.IO +// ----------------------------------------------------------------------------- +const socket = io("http://localhost:33007", { + transports: ["websocket", "polling"], + withCredentials: true, +}); + +let USER_NAME = `Alicer-${Math.floor(Math.random() * 9999)}`; +let currentRoom = null; + +// ----------------------------------------------------------------------------- +// CLOCK SYNC — Sincroniza relógio cliente-servidor +// ----------------------------------------------------------------------------- +let serverOffsetMs = 0; +let rttMs = 0; + +function sampleServerTime() { + return new Promise((resolve) => { + const t0 = Date.now(); + socket.emit("what_time_is_it", null, (reply) => { + const t1 = Date.now(); + const rtt = t1 - t0; + const oneWay = rtt / 2; + const serverNow = + reply && typeof reply === "object" ? reply.serverNowMs : reply; + const offset = serverNow - (t0 + oneWay); + resolve({ offset, rtt }); + }); + }); +} + +function ewma(prev, next, alpha = 0.3) { + if (prev === null || prev === undefined) return next; + return prev * (1 - alpha) + next * alpha; +} + +async function syncServerTime(iterations = 5) { + let off = null; + let rtt = null; + for (let i = 0; i < iterations; i++) { + try { + const s = await sampleServerTime(); + off = ewma(off, s.offset); + rtt = ewma(rtt, s.rtt); + } catch {} + } + if (off != null) serverOffsetMs = off; + if (rtt != null) rttMs = rtt; +} + +function delayFromServerTimeMs(scheduleAtServerMs) { + const localNow = Date.now(); + const serverNowEst = localNow + serverOffsetMs; + return Math.max(0, scheduleAtServerMs - serverNowEst); +} + +// ----------------------------------------------------------------------------- +// ESTADO DE TOKENS / ACK / FALLBACK +// ----------------------------------------------------------------------------- +let lastActionTimeout = null; +let lastBroadcastTimeout = null; +let pendingToken = null; +let lastActionToken = 0; +const processedTokens = new Set(); + +// ----------------------------------------------------------------------------- +// FUNÇÕES AUXILIARES +// ----------------------------------------------------------------------------- +export function setUserName(name) { + USER_NAME = name; +} + +// ----------------------------------------------------------------------------- +// CONEXÃO / JOIN / LOGS +// ----------------------------------------------------------------------------- +socket.on("connect", () => { + console.log(`Conectado ao servidor com ID: ${socket.id}`); + showToast("✅ Conectado ao servidor", "success"); + if (USER_NAME.startsWith("Alicer-")) { + USER_NAME = `Alicer-${socket.id.substring(0, 4)}`; + } + const urlRoom = new URLSearchParams(window.location.search).get("room"); + currentRoom = urlRoom || null; + + if (currentRoom) { + console.log(`Modo Online. Sala detectada: ${currentRoom}`); + showToast(`🎧 Conectado à sala ${currentRoom}`, "info"); + // Mostra o botão se estiver online + const syncModeBtn = document.getElementById("sync-mode-btn"); + //if (syncModeBtn) syncModeBtn.style.display = ""; // Garante visibilidade + } else { + console.log("Modo Local. Conectado, mas não em uma sala."); + showToast("🔌 Modo local (fora de sala)", "warning"); + // Esconde se offline + const syncModeBtn = document.getElementById("sync-mode-btn"); + //if (syncModeBtn) syncModeBtn.style.display = "none"; + } + + syncServerTime(); + setInterval(syncServerTime, 10000); +}); + +// ----------------------------------------------------------------------------- +// DETECÇÃO DE AD BLOCK +// ----------------------------------------------------------------------------- +socket.on("connect_error", (err) => { + console.error("Falha crítica na conexão do Socket:", err.message); + alert( + "🚧 FALHA NA CONEXÃO 🚧\n\n❌Não foi possível conectar ao servidor em tempo real. ❌\n\n😅 Causa provável: Um Ad Blocker ou Firewall está bloqueando a conexão.\n\n😎 Por favor, desative seu Ad Blocker para este site e recarregue a página." + ); + showToast( + "❌ Falha grave de conexão. Desativa o Ad Blocker? 😅", + "error", + 10000 + ); +}); + +// ----------------------------------------------------------------------------- +// RECEBER ESTADO SALVO DA SALA +// ----------------------------------------------------------------------------- +socket.on("load_project_state", async (projectXml) => { + console.log("Recebendo estado salvo da sala..."); + showToast("🔄 Recebendo estado atual da sala...", "info", 4000); + if (isLoadingProject) return; + isLoadingProject = true; + try { + await parseMmpContent(projectXml); + renderAll(); + showToast("🎵 Projeto carregado com sucesso", "success"); + } catch (e) { + console.error("Erro ao carregar projeto:", e); + showToast("❌ Erro ao carregar projeto", "error"); + } + isLoadingProject = false; +}); + +// ----------------------------------------------------------------------------- +// ENTRAR NA SALA (Join) +// ----------------------------------------------------------------------------- +export function joinRoom() { + const urlRoom = new URLSearchParams(window.location.search).get("room"); + currentRoom = urlRoom || currentRoom; + if (currentRoom) { + console.log(`Entrando na sala: ${currentRoom} como ${USER_NAME}`); + showToast(`🚪 Entrando na sala ${currentRoom}`, "info"); + socket.emit("join_room", { roomName: currentRoom, userName: USER_NAME }); + setTimeout(() => { + try { + const hasAudio = + (appState.audio?.clips?.length || 0) > 0 || + (appState.audio?.tracks?.length || 0) > 0; + if (!hasAudio && currentRoom) { + sendAction({ type: "AUDIO_SNAPSHOT_REQUEST" }); + } + } catch {} + }, 800); + } else { + console.warn("joinRoom() chamado, mas nenhuma sala encontrada na URL."); + showToast("⚠️ Nenhuma sala encontrada na URL", "warning"); + } +} + +// ----------------------------------------------------------------------------- +// ENVIAR AÇÃO COM ACK + AGENDAMENTO DE TRANSPORTE +// ----------------------------------------------------------------------------- +export function sendAction(action) { + const inRoom = Boolean(currentRoom); + + // (Blindagem de ID/Validação) + if (action.type === "ADD_AUDIO_LANE" && !action.trackId) { + action.trackId = generateUniqueId("track"); + console.log(`[SOCKET] ADD_AUDIO_LANE ID gerado: ${action.trackId}`); + } + if (action.type === "ADD_AUDIO_CLIP") { + if (!action.clipId) { + action.clipId = generateUniqueId("clip"); + } + if ( + !action.trackId || + action.startTimeInSeconds == null || + isNaN(action.startTimeInSeconds) + ) { + console.error("[SOCKET] ADD_AUDIO_CLIP bloqueada:", action); + showToast("❌ Erro clip (inválido)", "error"); + return; + } + } + if (action.type === "UPDATE_AUDIO_CLIP") { + if (!action.clipId || !action.props) { + console.error("[SOCKET] UPDATE_AUDIO_CLIP bloqueada (base):", action); + return; + } + const { trackId, startTimeInSeconds } = action.props; + if ( + trackId !== undefined && + (trackId == null || (typeof trackId === "number" && isNaN(trackId))) + ) { + console.error("[SOCKET] UPDATE_AUDIO_CLIP bloqueada (trackId):", action); + return; + } + if ( + startTimeInSeconds !== undefined && + (startTimeInSeconds == null || isNaN(startTimeInSeconds)) + ) { + console.error( + "[SOCKET] UPDATE_AUDIO_CLIP bloqueada (startTime):", + action + ); + return; + } + } + + const token = (++lastActionToken).toString(); + action.__token = token; + action.__senderId = socket.id; + action.__senderName = USER_NAME; + + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Expandir `isTransport` para SET_SYNC_MODE) + // ================================================================= + const isTransport = + action.type === "TOGGLE_PLAYBACK" || + action.type === "STOP_PLAYBACK" || + action.type === "REWIND_PLAYBACK" || + action.type === "START_AUDIO_PLAYBACK" || + action.type === "STOP_AUDIO_PLAYBACK" || + action.type === "SET_LOOP_STATE" || + action.type === "SET_SEEK_TIME" || + action.type === "SET_SYNC_MODE"; // 👈 Adicionado + // ================================================================= + // 👆 FIM DA CORREÇÃO + // ================================================================= + + if (inRoom && isTransport) { + const leadTimeMs = 200; + const serverNowEst = Date.now() + serverOffsetMs; + if ( + action.type === "TOGGLE_PLAYBACK" || + action.type === "START_AUDIO_PLAYBACK" + ) { + action.scheduleAtServerMs = Math.round(serverNowEst + leadTimeMs); + } + } + + // (Lógica Global/Local) + if (!inRoom || (isTransport && appState.global.syncMode === "local")) { + console.log("[SOCKET] (local) executando ação:", action.type); + handleActionBroadcast(action); + return; + } + if (isTransport && appState.global.syncMode === "global") { + action.__syncMode = "global"; + } + + console.log( + "[SOCKET] Enviando broadcast_action:", + action.type, + "para", + currentRoom, + "token=", + token + ); + + socket + .compress(false) + .emit("broadcast_action", { roomName: currentRoom, action }, (ack) => { + if (ack && ack.ok && ack.token === token) { + if (lastActionTimeout) { + clearTimeout(lastActionTimeout); + lastActionTimeout = null; + } + } + }); + + if (lastActionTimeout) clearTimeout(lastActionTimeout); + lastActionTimeout = setTimeout(() => { + console.warn("[SOCKET] ACK não recebido:", action.type, token); + showToast(`⚠️ ACK (${action.type})`, "warning"); + }, 500); + + if (lastBroadcastTimeout) clearTimeout(lastBroadcastTimeout); + pendingToken = token; + lastBroadcastTimeout = setTimeout(() => { + if (processedTokens.has(token)) return; + console.warn("[SOCKET] Eco não recebido, fallback:", action.type, token); + showToast(`⚠️ Eco (${action.type}), fallback`, "warning"); + processedTokens.add(token); + handleActionBroadcast(action); + }, 900); +} + +// HELPERS POP-UPS +function basenameNoExt(path) { + if (!path) return ""; + const base = String(path).split(/[\\/]/).pop(); + return base.replace(/\.[^/.]+$/, ""); +} +function trackLabel(track, idx) { + const name = + track?.name || + track?.label || + track?.sampleName || + track?.sample?.displayName; + return name ? `(Faixa ${idx + 1})` : `Faixa ${idx + 1}`; +} +function actorOf(action) { + const n = action.__senderName || action.userName; + if (n && typeof n === "string") return n; + const sid = action.__senderId ? String(action.__senderId) : ""; + return `Alicer-${sid.slice(0, 4) || "????"}`; +} +function instrumentLabel(track, tIdx) { + const name = + track?.name || + track?.label || + track?.sampleName || + track?.sample?.displayName || + track?.filePath || + track?.sampleFile || + track?.samplePath; + if (name && typeof name === "string") { + const base = name.split(/[\\/]/).pop(); + return base.replace(/\.[^/.]+$/, ""); + } + return `Faixa ${tIdx + 1}`; +} + +let rerenderScheduled = false; +function schedulePatternRerender() { + if (rerenderScheduled) return; + rerenderScheduled = true; + requestAnimationFrame(() => { + renderPatternEditor(); + rerenderScheduled = false; + }); +} + +// RECEBER BROADCAST +socket.on("feedback", (msg) => { + console.log("[Servidor]", msg); + showToast(msg, "info"); +}); + +socket.on("action_broadcast", (payload) => { + const action = payload && payload.type ? payload : payload?.action || payload; + if (action && action.__token) { + processedTokens.add(action.__token); + if (lastBroadcastTimeout && pendingToken === action.__token) { + clearTimeout(lastBroadcastTimeout); + lastBroadcastTimeout = null; + pendingToken = null; + } + if (lastActionTimeout) { + clearTimeout(lastActionTimeout); + lastActionTimeout = null; + } + } + handleActionBroadcast(action); +}); + +// PROCESSAR AÇÕES RECEBIDAS +let isLoadingProject = false; +async function handleActionBroadcast(action) { + if (!action || !action.type) return; + if ( + action.type !== "LOAD_PROJECT" && + action.type !== "RESET_PROJECT" && + isLoadingProject + ) { + console.warn(`[AÇÃO IGNORADA] ${action.type} (carregando).`); + return; + } + + // (Filtro Global/Local) + const isTransport = + action.type === "TOGGLE_PLAYBACK" || + action.type === "STOP_PLAYBACK" || + action.type === "REWIND_PLAYBACK" || + action.type === "START_AUDIO_PLAYBACK" || + action.type === "STOP_AUDIO_PLAYBACK" || + action.type === "SET_LOOP_STATE" || + action.type === "SET_SEEK_TIME" || + action.type === "SET_SYNC_MODE"; // 👈 Adicionado + const isFromSelf = action.__senderId === socket.id; + if (isTransport && !isFromSelf) { + if (appState.global.syncMode === "local") { + console.log(`[SOCKET] (ignorado) ${action.type}, modo local.`); + return; + } + if (action.__syncMode !== "global") { + console.log(`[SOCKET] (ignorado) ${action.type}, remetente não global.`); + return; + } + } + + const scheduleAtServerMs = action.scheduleAtServerMs ?? null; + const delayMs = scheduleAtServerMs + ? delayFromServerTimeMs(scheduleAtServerMs) + : 0; + + switch (action.type) { + // Transporte Principal + case "TOGGLE_PLAYBACK": { + setTimeout(togglePlayback, delayMs); + const who = actorOf(action); + showToast(`▶ ${who} Play bases`, "info"); + break; + } + case "STOP_PLAYBACK": { + setTimeout(stopPlayback, delayMs); + const who = actorOf(action); + showToast(`⏹ ${who} Stop bases`, "info"); + break; + } + case "REWIND_PLAYBACK": + setTimeout(rewindPlayback, delayMs); + break; + + // Transporte Áudio Editor + case "START_AUDIO_PLAYBACK": + if (action.loopState) { + appState.global.isLoopActive = action.loopState.isLoopActive; + appState.global.loopStartTime = action.loopState.loopStartTime; + appState.global.loopEndTime = action.loopState.loopEndTime; + const btn = document.getElementById("audio-editor-loop-btn"); + if (btn) { + btn.classList.toggle("active", appState.global.isLoopActive); + } + updateTransportLoop(); + try { + const area = document.getElementById("loop-region"); + if (area) + area.classList.toggle("visible", appState.global.isLoopActive); + } catch (e) {} + } + const seekTime = action.seekTime ?? appState.audio.audioEditorSeekTime; + setTimeout(() => startAudioEditorPlayback(seekTime), delayMs); + break; + case "STOP_AUDIO_PLAYBACK": + setTimeout( + () => stopAudioEditorPlayback(action.rewind || false), + delayMs + ); + break; + + // ================================================================= + // 👇 INÍCIO DA CORREÇÃO (Handlers Sincronia de Loop/Seek/SyncMode) + // ================================================================= + case "SET_LOOP_STATE": { + const changed = + appState.global.isLoopActive !== !!action.isLoopActive || + appState.global.loopStartTime !== action.loopStartTime || + appState.global.loopEndTime !== action.loopEndTime; + + if (changed) { + appState.global.isLoopActive = !!action.isLoopActive; + appState.global.loopStartTime = action.loopStartTime; + appState.global.loopEndTime = action.loopEndTime; + + const btn = document.getElementById("audio-editor-loop-btn"); + if (btn) { + btn.classList.toggle("active", appState.global.isLoopActive); + } + + updateTransportLoop(); + restartAudioEditorIfPlaying(); + renderAll(); + + if (!isFromSelf) { + const who = actorOf(action); + showToast(`🔁 ${who} alterou o loop.`, "info"); + } + } + break; + } + case "SET_SEEK_TIME": { + // Evita aplicar seek se o tempo for muito próximo para não causar "tremidas" + if ( + Math.abs((appState.audio.audioEditorSeekTime || 0) - action.seekTime) > + 0.05 + ) { + seekAudioEditor(action.seekTime); + if (!isFromSelf) { + const who = actorOf(action); + showToast(`⏯️ ${who} moveu a agulha.`, "info"); + } + } + break; + } + case "SET_SYNC_MODE": { + const newMode = action.mode === "local" ? "local" : "global"; + const changed = appState.global.syncMode !== newMode; + if (changed) { + appState.global.syncMode = newMode; + const btn = document.getElementById("sync-mode-btn"); + if (btn) { + btn.classList.toggle("active", newMode === "global"); + btn.textContent = newMode === "global" ? "Global" : "Local"; + } + // Mostra o Toast AQUI, após a mudança ser aplicada + const who = actorOf(action); + const modeText = newMode === "global" ? "Global 🌐" : "Local 🏠"; + showToast(`${who} mudou modo para ${modeText}`, "info"); + } + break; + } + // ================================================================= + // 👆 FIM DA CORREÇÃO + // ================================================================= + + // Estado Global + case "LOAD_PROJECT": + isLoadingProject = true; + showToast("📂 Carregando...", "info"); + try { + await parseMmpContent(action.xml); + renderAll(); + showToast("🎶 Projeto sync", "success"); + } catch (e) { + console.error("Erro LOAD_PROJECT:", e); + showToast("❌ Erro projeto", "error"); + } + isLoadingProject = false; + break; + case "RESET_PROJECT": + resetProjectState(); + document.getElementById("bpm-input").value = 140; + document.getElementById("bars-input").value = 1; + document.getElementById("compasso-a-input").value = 4; + document.getElementById("compasso-b-input").value = 4; + renderAll(); + const who = actorOf(action); + showToast(`🧹 Reset por ${who}`, "warning"); + break; + + // Configs + case "SET_BPM": { + document.getElementById("bpm-input").value = action.value; + renderAll(); + const who = actorOf(action); + showToast(`🕰 ${who} BPM ${action.value}`, "info"); + break; + } + case "SET_BARS": { + document.getElementById("bars-input").value = action.value; + renderAll(); + const who = actorOf(action); + showToast(`🕰 ${who} Compasso add`, "info"); + break; + } + case "SET_TIMESIG_A": + document.getElementById("compasso-a-input").value = action.value; + renderAll(); + showToast("Compasso alt", "info"); + break; + case "SET_TIMESIG_B": + document.getElementById("compasso-b-input").value = action.value; + renderAll(); + showToast("Compasso alt", "info"); + break; + + // Tracks + case "ADD_TRACK": { + addTrackToState(); + renderPatternEditor(); + const who = actorOf(action); + showToast(`🥁 Faixa add por ${who}`, "info"); + break; + } + case "REMOVE_LAST_TRACK": { + removeLastTrackFromState(); + renderPatternEditor(); + const who = actorOf(action); + showToast(`❌ Faixa remov. por ${who}`, "warning"); + break; + } + case "ADD_AUDIO_LANE": { + const id = action.trackId; + if (!id) { + console.warn("ADD_AUDIO_LANE sem ID."); + break; + } + const exists = appState.audio.tracks.some((t) => t.id === id); + if (exists) { + console.log(`ADD_AUDIO_LANE ${id} já existe.`); + break; + } + appState.audio.tracks.push({ + id: id, + name: `Pista ${appState.audio.tracks.length + 1}`, + }); + renderAll(); + const who = actorOf(action); + showToast(`🎧 Pista add por ${who}`, "info"); + break; + } + case "REMOVE_AUDIO_CLIP": + if (removeAudioClip(action.clipId)) { + appState.global.selectedClipId = null; + renderAll(); + const who = actorOf(action); + showToast(`🎚️ Clip remov. por ${who}`, "info"); + } + break; + + // Notes + case "TOGGLE_NOTE": { + const { + trackIndex: ti, + patternIndex: pi, + stepIndex: si, + isActive, + } = action; + const t = appState.pattern.tracks[ti]; + if (t) { + t.patterns[pi] = t.patterns[pi] || { steps: [] }; + t.patterns[pi].steps[si] = isActive; + try { + updateStepUI(ti, pi, si, isActive); + } catch {} + if (!isFromSelf) { + schedulePatternRerender(); + } + } + const who = actorOf(action); + const v = isActive ? "+" : "-"; + showToast(`🎯 ${who} ${v} nota ${ti + 1}.${pi + 1}.${si + 1}`, "info"); + break; + } + + // Samples + case "SET_TRACK_SAMPLE": { + const ti = action.trackIndex; + const t = appState.pattern.tracks[ti]; + if (t) { + try { + await updateTrackSample(ti, action.filePath); + renderPatternEditor(); + const who = actorOf(action); + const sn = basenameNoExt(action.filePath) || "?"; + const tl = trackLabel(t, ti); + showToast(`🔊 ${who} trocou ${sn} em ${tl}`, "success"); + } catch (err) { + console.error("Erro SET_TRACK_SAMPLE:", err); + showToast("❌ Erro sample", "error"); + } + } + break; + } + + // Snapshots + case "AUDIO_SNAPSHOT_REQUEST": { + const clips = appState.audio?.clips?.length || 0, + tracks = appState.audio?.tracks?.length || 0; + const iHave = clips > 0 || tracks > 0; + if (!iHave || isFromSelf) break; + if (!window.__lastSnapshotSentAt) window.__lastSnapshotSentAt = 0; + const now = Date.now(); + if (now - window.__lastSnapshotSentAt < 1500) break; + window.__lastSnapshotSentAt = now; + try { + const snap = getAudioSnapshot(); + sendAction({ + type: "AUDIO_SNAPSHOT", + snapshot: snap, + __target: action.__senderId, + }); + } catch (e) { + console.warn("Erro AUDIO_SNAPSHOT_REQUEST:", e); + } + break; + } + case "AUDIO_SNAPSHOT": { + if (action.__target && action.__target !== socket.id) break; + const hasClips = (appState.audio?.clips?.length || 0) > 0; + if (hasClips) break; + try { + await applyAudioSnapshot(action.snapshot); + renderAll(); + const who = actorOf(action); + showToast(`🔁 Sync áudio por ${who}`, "success"); + } catch (e) { + console.error("Erro AUDIO_SNAPSHOT:", e); + showToast("❌ Erro sync áudio", "error"); + } + break; + } + + // Clip Sync + case "ADD_AUDIO_CLIP": { + try { + const { filePath, trackId, startTimeInSeconds, clipId, name } = action; + if ( + appState.audio?.clips?.some((c) => String(c.id) === String(clipId)) + ) { + break; + } + const trackExists = appState.audio?.tracks?.some( + (t) => t.id === trackId + ); + if (!trackExists) { + console.warn( + `ADD_AUDIO_CLIP Pista ${trackId} não existe, criando...` + ); + appState.audio.tracks.push({ + id: trackId, + name: `Pista ${appState.audio.tracks.length + 1}`, + }); + } + addAudioClipToTimeline( + filePath, + trackId, + startTimeInSeconds, + clipId, + name + ); + renderAll(); + const who = actorOf(action); + const track = appState.audio?.tracks?.find((t) => t.id === trackId); + const pista = track?.name || `Pista ${trackId}`; + showToast( + `🎧 ${who} add “${(name || filePath || "") + .split(/[\\/]/) + .pop()}” em ${pista}`, + "success" + ); + } catch (e) { + console.error("Erro ADD_AUDIO_CLIP:", e); + showToast("❌ Erro add clip", "error"); + } + break; + } + case "UPDATE_AUDIO_CLIP": { + try { + if (action.props?.__operation === "slice") { + sliceAudioClip(action.clipId, action.props.sliceTimeInTimeline); + } else { + updateAudioClipProperties(action.clipId, action.props || {}); + } + renderAll(); + const who = actorOf(action); + showToast(`✂️ Clip ${action.clipId} att por ${who}`, "info"); + } catch (e) { + console.error("Erro UPDATE_AUDIO_CLIP:", e); + showToast("❌ Erro att clip", "error"); + } + break; + } + + default: + console.warn("Ação desconhecida:", action.type); + } +} + +// EXPORTS +export { socket }; diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 0ec3a24..090d4de 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -1,66 +1,71 @@ // js/state.js -import { patternState, initializePatternState } from './pattern/pattern_state.js'; +import { initializePatternState } from './pattern/pattern_state.js'; import { audioState, initializeAudioState } from './audio/audio_state.js'; import { DEFAULT_VOLUME, DEFAULT_PAN } from "./config.js"; // Estado global da aplicação const globalState = { - sliceToolActive: false, - isPlaying: false, - isAudioEditorPlaying: false, - playbackIntervalId: null, - currentStep: 0, - metronomeEnabled: false, - originalXmlDoc: null, - currentBeatBasslineName: 'Novo Projeto', - masterVolume: DEFAULT_VOLUME, - masterPan: DEFAULT_PAN, - zoomLevelIndex: 2, - - // --- ADICIONADO PARA A ÁREA DE LOOP --- - isLoopActive: false, - loopStartTime: 0, - loopEndTime: 8, - - // --- ADICIONADO PARA O MODO DE REDIMENSIONAMENTO --- - resizeMode: 'trim', // Pode ser 'trim' (Modo 2) ou 'stretch' (Modo 1) - selectedClipId: null, - - // --- RECORDING --- - isRecording: false, +  sliceToolActive: false, +  isPlaying: false, +  isAudioEditorPlaying: false, +  playbackIntervalId: null, +  currentStep: 0, +  metronomeEnabled: false, +  originalXmlDoc: null, +  currentBeatBasslineName: 'Novo Projeto', +  masterVolume: DEFAULT_VOLUME, +  masterPan: DEFAULT_PAN, +  zoomLevelIndex: 2, +  isLoopActive: false, +  loopStartTime: 0, +  loopEndTime: 8, +  resizeMode: 'trim', +  selectedClipId: null, +  isRecording: false, +  clipboard: null, +  lastRulerClickTime: 0, }; +// --- ADICIONE ESTE BLOCO --- +// Define o ESTADO INICIAL para o pattern module +const patternState = { +    tracks: [], +    activeTrackId: null, +    activePatternIndex: 0, +}; +// --- FIM DA ADIÇÃO --- + // Combina todos os estados em um único objeto namespaced export let appState = { - global: globalState, - pattern: patternState, - audio: audioState, +  global: globalState, +  pattern: patternState, // <-- AGORA 'patternState' está definido +  audio: audioState, }; // Função para resetar o projeto para o estado inicial export function resetProjectState() { - initializePatternState(); - initializeAudioState(); +    initializePatternState(); // Esta função vai MODIFICAR appState.pattern +    initializeAudioState(); - Object.assign(globalState, { - sliceToolActive: false, - isPlaying: false, - isAudioEditorPlaying: false, - playbackIntervalId: null, - currentStep: 0, - metronomeEnabled: false, - originalXmlDoc: null, - currentBeatBasslineName: 'Novo Projeto', - masterVolume: DEFAULT_VOLUME, - masterPan: DEFAULT_PAN, - zoomLevelIndex: 2, - isLoopActive: false, - loopStartTime: 0, - loopEndTime: 8, - resizeMode: 'trim', // Reseta para o modo 'trim' - selectedClipId: null, - - // --- RECORDING --- - isRecording: false, - }); +    Object.assign(globalState, { +        sliceToolActive: false, +        isPlaying: false, +        isAudioEditorPlaying: false, +        playbackIntervalId: null, +        currentStep: 0, +        metronomeEnabled: false, +        originalXmlDoc: null, +        currentBeatBasslineName: 'Novo Projeto', +        masterVolume: DEFAULT_VOLUME, +        masterPan: DEFAULT_PAN, +        zoomLevelIndex: 2, +        isLoopActive: false, +        loopStartTime: 0, +        loopEndTime: 8, +        resizeMode: 'trim', +        selectedClipId: null, +        isRecording: false, +        clipboard: null, +        lastRulerClickTime: 0, +    }); } \ No newline at end of file diff --git a/assets/js/creations/ui.js b/assets/js/creations/ui.js index 4533a60..8acc632 100644 --- a/assets/js/creations/ui.js +++ b/assets/js/creations/ui.js @@ -153,4 +153,68 @@ export async function showOpenProjectModal() { export function closeOpenProjectModal() { const openProjectModal = document.getElementById("open-project-modal"); openProjectModal.classList.remove("visible"); -} \ No newline at end of file +} + +// ui.js — sistema global de avisos / toasts +export function showToast(message, type = "info", duration = 3000) { + // cria o contêiner se não existir + let container = document.getElementById("toast-container"); + if (!container) { + container = document.createElement("div"); + container.id = "toast-container"; + Object.assign(container.style, { + position: "fixed", + bottom: "20px", + right: "20px", + zIndex: 9999, + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + gap: "8px", + }); + document.body.appendChild(container); + } + + // cria o toast + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + Object.assign(toast.style, { + padding: "10px 16px", + borderRadius: "6px", + color: "#fff", + fontFamily: "sans-serif", + fontSize: "14px", + boxShadow: "0 2px 8px rgba(0,0,0,0.25)", + opacity: "0", + transform: "translateY(10px)", + transition: "all 0.3s ease", + maxWidth: "260px", + textAlign: "left", + wordWrap: "break-word", + }); + + // cores por tipo + const colors = { + info: "#3498db", + success: "#2ecc71", + warning: "#f1c40f", + error: "#e74c3c", + }; + toast.style.backgroundColor = colors[type] || colors.info; + + container.appendChild(toast); + + // animação de entrada + setTimeout(() => { + toast.style.opacity = "1"; + toast.style.transform = "translateY(0)"; + }, 50); + + // remover após tempo + setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateY(10px)"; + setTimeout(() => toast.remove(), 300); + }, duration); +} diff --git a/creation.html b/creation.html index 60c0fd6..d342b57 100644 --- a/creation.html +++ b/creation.html @@ -68,7 +68,7 @@ background-color: transparent; } - +