diff options
author | Profpatsch <mail@profpatsch.de> | 2024-09-27T23·30+0200 |
---|---|---|
committer | Profpatsch <mail@profpatsch.de> | 2024-10-05T13·49+0000 |
commit | 9bec21ea1cb9137d8c5dae6ee2cb78aac1b5601a (patch) | |
tree | ec649db1716bc7b0a7a5d42b8adb4f0c81410659 /users | |
parent | 970dcaa04f3c4bda473674f7e7bb2d2d87ab13e8 (diff) |
feat(users/Profpatsch/lyric): add vscode extension & helpers r/8759
* tap-bpm: simple CLI program that accepts key inputs and averages a BPM value * lyric-timing-mpv-script: If you press Ctrl+l, mpv attaches the current timestamp to a .lrc file named after the song. This is for manually timing missing songs for uploading them to https://lrclib.net/ * extension: vscode extension for `.lrc` files, currently with the following features: 1. A “jump to LRC position” command which reads an .lrc timestamp from the current line and expects mpv to listen on `~/tmp/mpv-socket` (via `--input-ipc-server`), and will seek to the exact timestamp (down to the ms) in the currently playing song. 2. Some initial linting warnings - A lint that warns if the difference to the next timestamp is more than 10s (which usually means there’s an instrumental and the previous line is stuck) - A lint that checks that timestamps are monotonically increasing Change-Id: I32a4ac0e2c5bbe3d94e45ffcf647f81bc7c08aa0 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12537 Tested-by: BuildkiteCI Reviewed-by: Profpatsch <mail@profpatsch.de>
Diffstat (limited to 'users')
-rw-r--r-- | users/Profpatsch/.vscode/launch.json | 26 | ||||
-rw-r--r-- | users/Profpatsch/lyric/.gitignore | 6 | ||||
-rw-r--r-- | users/Profpatsch/lyric/.prettierrc | 8 | ||||
-rw-r--r-- | users/Profpatsch/lyric/build.ninja | 17 | ||||
-rw-r--r-- | users/Profpatsch/lyric/eslint.config.mjs | 42 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/.gitignore | 5 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/.prettierrc | 8 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/LICENSE | 1 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/eslint.config.mjs | 42 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/package.json | 52 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/src/extension.ts | 236 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/tsconfig.json | 22 | ||||
-rw-r--r-- | users/Profpatsch/lyric/lyric-timing-mpv-script.lua | 43 | ||||
-rw-r--r-- | users/Profpatsch/lyric/package.json | 23 | ||||
-rw-r--r-- | users/Profpatsch/lyric/src/index.ts | 16 | ||||
-rw-r--r-- | users/Profpatsch/lyric/src/tap-bpm.ts | 79 | ||||
-rw-r--r-- | users/Profpatsch/lyric/tsconfig.json | 17 |
17 files changed, 643 insertions, 0 deletions
diff --git a/users/Profpatsch/.vscode/launch.json b/users/Profpatsch/.vscode/launch.json index baa087d437d1..54f510e091ec 100644 --- a/users/Profpatsch/.vscode/launch.json +++ b/users/Profpatsch/.vscode/launch.json @@ -13,6 +13,32 @@ "runtimeArgs": [ "run", ], + }, + { + "type": "node", + "name": "Run tap-bpm", + "skipFiles": [ + "<node_internals>/**" + ], + "request": "launch", + "program": "${workspaceFolder}/lyric/dist/index.js", + "preLaunchTask": "ninja build lyric", + "outFiles": [ + "${workspaceFolder}/lyric/dist/**/*.js" + ], + "args": [ + "tap-bpm" + ] + }, + { + "preLaunchTask": "npm build lyric extension", + "name": "Launch lyric vscode Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/lyric/extension", + "${workspaceFolder}/lyric/extension" + ] } ] } diff --git a/users/Profpatsch/lyric/.gitignore b/users/Profpatsch/lyric/.gitignore new file mode 100644 index 000000000000..226235d62b9d --- /dev/null +++ b/users/Profpatsch/lyric/.gitignore @@ -0,0 +1,6 @@ +/dist/ +/.ninja/ +/node_modules/ + +# ignore for now +/package-lock.json diff --git a/users/Profpatsch/lyric/.prettierrc b/users/Profpatsch/lyric/.prettierrc new file mode 100644 index 000000000000..e97f84abaf62 --- /dev/null +++ b/users/Profpatsch/lyric/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 90, + "arrowParens": "avoid" +} diff --git a/users/Profpatsch/lyric/build.ninja b/users/Profpatsch/lyric/build.ninja new file mode 100644 index 000000000000..a5752bef5002 --- /dev/null +++ b/users/Profpatsch/lyric/build.ninja @@ -0,0 +1,17 @@ +builddir = .ninja + +outdir = ./dist +jsdir = $outdir/js + +rule tsc + command = node_modules/.bin/tsc + +build $outdir/index.js: tsc | src/index.ts tsconfig.json + +rule run + command = node $in + +build run: run $outdir/index.js + +build run-tap-bpm: run $outdir/index.js tap-bpm + pool = console diff --git a/users/Profpatsch/lyric/eslint.config.mjs b/users/Profpatsch/lyric/eslint.config.mjs new file mode 100644 index 000000000000..189efbd9be4c --- /dev/null +++ b/users/Profpatsch/lyric/eslint.config.mjs @@ -0,0 +1,42 @@ +import tseslint from "typescript-eslint"; +import tsplugin from "@typescript-eslint/eslint-plugin"; +import parser from "@typescript-eslint/parser"; + +export default tseslint.config(tseslint.configs.eslintRecommended, { + languageOptions: { + parser: parser, + parserOptions: { + projectService: true, + }, + }, + ignores: ["node_modules/", "eslint.config.mjs"], + plugins: { "@typescript-eslint": tsplugin }, + rules: { + "prettier/prettier": "off", + "prefer-const": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "warn", + "@typescript-eslint/no-duplicate-enum-values": "warn", + "@typescript-eslint/no-empty-object-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-extra-non-null-assertion": "warn", + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "@typescript-eslint/no-require-imports": "warn", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-unnecessary-type-constraint": "warn", + "@typescript-eslint/no-unsafe-declaration-merging": "warn", + "@typescript-eslint/no-unsafe-function-type": "warn", + "@typescript-eslint/strict-boolean-expressions": ["warn"], + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-wrapper-object-types": "warn", + "@typescript-eslint/prefer-as-const": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/triple-slash-reference": "warn", + }, +}); diff --git a/users/Profpatsch/lyric/extension/.gitignore b/users/Profpatsch/lyric/extension/.gitignore new file mode 100644 index 000000000000..d2405ad640c3 --- /dev/null +++ b/users/Profpatsch/lyric/extension/.gitignore @@ -0,0 +1,5 @@ +/node_modules/ +/out/ + +# ignore for now +/package-lock.json diff --git a/users/Profpatsch/lyric/extension/.prettierrc b/users/Profpatsch/lyric/extension/.prettierrc new file mode 100644 index 000000000000..e97f84abaf62 --- /dev/null +++ b/users/Profpatsch/lyric/extension/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 90, + "arrowParens": "avoid" +} diff --git a/users/Profpatsch/lyric/extension/LICENSE b/users/Profpatsch/lyric/extension/LICENSE new file mode 100644 index 000000000000..4292032e6223 --- /dev/null +++ b/users/Profpatsch/lyric/extension/LICENSE @@ -0,0 +1 @@ +same as toplevel diff --git a/users/Profpatsch/lyric/extension/eslint.config.mjs b/users/Profpatsch/lyric/extension/eslint.config.mjs new file mode 100644 index 000000000000..996dd8ca58ee --- /dev/null +++ b/users/Profpatsch/lyric/extension/eslint.config.mjs @@ -0,0 +1,42 @@ +import tseslint from "typescript-eslint"; +import tsplugin from "@typescript-eslint/eslint-plugin"; +import parser from "@typescript-eslint/parser"; + +export default tseslint.config(tseslint.configs.eslintRecommended, { + languageOptions: { + parser: parser, + parserOptions: { + projectService: true, + }, + }, + ignores: ["node_modules/", "eslint.config.mjs"], + plugins: { "@typescript-eslint": tsplugin }, + rules: { + "prettier/prettier": "off", + "prefer-const": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "warn", + "@typescript-eslint/no-duplicate-enum-values": "warn", + "@typescript-eslint/no-empty-object-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-extra-non-null-assertion": "warn", + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "@typescript-eslint/no-require-imports": "warn", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-unnecessary-type-constraint": "warn", + "@typescript-eslint/no-unsafe-declaration-merging": "warn", + "@typescript-eslint/no-unsafe-function-type": "warn", + "@typescript-eslint/strict-boolean-expressions": ["warn"], + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-wrapper-object-types": "warn", + "@typescript-eslint/prefer-as-const": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/triple-slash-reference": "warn", + }, +}); diff --git a/users/Profpatsch/lyric/extension/package.json b/users/Profpatsch/lyric/extension/package.json new file mode 100644 index 000000000000..722da81b4719 --- /dev/null +++ b/users/Profpatsch/lyric/extension/package.json @@ -0,0 +1,52 @@ +{ + "name": "profpatsch-jump-to-lrc-position", + "displayName": "Jump to .lrc Position in mpv", + "description": "Reads a timestamp from the current file and pipes it to a mpv socket", + "version": "0.0.1", + "engines": { + "vscode": "^1.75.0" + }, + "categories": [ + "Other" + ], + "main": "./out/extension.js", + "activationEvents": [ + "onLanguage:lrc" + ], + "contributes": { + "commands": [ + { + "command": "extension.jumpToLrcPosition", + "title": "Jump to .lrc Position", + "category": "LRC" + } + ], + "languages": [ + { + "id": "lrc", + "extensions": [ + ".lrc" + ], + "aliases": [ + "Lyric file" + ] + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc", + "install-extension": "vsce package --allow-missing-repository --out ./jump-to-lrc-position.vsix && code --install-extension ./jump-to-lrc-position.vsix" + }, + "devDependencies": { + "vscode": "^1.1.37", + "@eslint/js": "^9.10.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.5.5", + "@typescript-eslint/parser": "^8.7.0", + "eslint": "^9.10.0", + "globals": "^15.9.0", + "typescript": "^5.6.2", + "typescript-eslint": "^8.6.0" + } +} diff --git a/users/Profpatsch/lyric/extension/src/extension.ts b/users/Profpatsch/lyric/extension/src/extension.ts new file mode 100644 index 000000000000..4f46e0a1ed7c --- /dev/null +++ b/users/Profpatsch/lyric/extension/src/extension.ts @@ -0,0 +1,236 @@ +import * as vscode from 'vscode'; +import * as net from 'net'; + +export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push(...registerCheckLineTimestamp(context)); + context.subscriptions.push( + vscode.commands.registerCommand('extension.jumpToLrcPosition', () => { + const editor = vscode.window.activeTextEditor; + + if (!editor) { + vscode.window.showInformationMessage('No active editor found.'); + return; + } + + const ext = new Ext(editor); + const position = editor.selection.active; + const res = ext.getTimestampFromLine(position); + + if (!res) { + return; + } + const { milliseconds, seconds } = res; + + // Prepare JSON command to send to the socket + const jsonCommand = { + command: ['seek', seconds, 'absolute'], + }; + + const socket = new net.Socket(); + + const socketPath = process.env.HOME + '/tmp/mpv-socket'; + socket.connect(socketPath, () => { + socket.write(JSON.stringify(jsonCommand)); + socket.write('\n'); + vscode.window.showInformationMessage( + `Sent command to jump to [${formatTimestamp(milliseconds)}].`, + ); + socket.end(); + }); + + socket.on('error', err => { + vscode.window.showErrorMessage(`Failed to send command: ${err.message}`); + }); + }), + ); +} + +// If the difference to the timestamp on the next line is larger than 10 seconds, underline the next line and show a warning message on hover +export function registerCheckLineTimestamp(_context: vscode.ExtensionContext) { + const changesToCheck: Set<vscode.TextDocument> = new Set(); + const everSeen = new Set<vscode.TextDocument>(); + + return [ + vscode.workspace.onDidChangeTextDocument(e => { + changesToCheck.add(e.document); + if (vscode.window.activeTextEditor?.document === e.document) { + doEditorChecks(vscode.window.activeTextEditor, everSeen, changesToCheck); + } + }), + vscode.workspace.onDidOpenTextDocument(e => { + changesToCheck.add(e); + everSeen.add(e); + if (vscode.window.activeTextEditor?.document === e) { + doEditorChecks(vscode.window.activeTextEditor, everSeen, changesToCheck); + } + }), + + vscode.window.onDidChangeActiveTextEditor(editor => { + if (editor) { + doEditorChecks(editor, everSeen, changesToCheck); + } + }), + vscode.window.onDidChangeVisibleTextEditors(editors => { + for (const editor of editors) { + doEditorChecks(editor, everSeen, changesToCheck); + } + }), + ]; +} + +function doEditorChecks( + editor: vscode.TextEditor, + everSeen: Set<vscode.TextDocument>, + changesToCheck: Set<vscode.TextDocument>, +) { + const ext = new Ext(editor); + const document = editor.document; + + if (!everSeen.has(editor.document)) { + changesToCheck.add(editor.document); + everSeen.add(editor.document); + } + + if (!changesToCheck.has(document)) { + return; + } + changesToCheck.delete(document); + + const from = 0; + const to = document.lineCount - 1; + for (let line = from; line <= to; line++) { + const warnings: string[] = []; + const timeDiff = timeDifferenceTooLarge(ext, line); + if (timeDiff !== undefined) { + warnings.push(timeDiff); + } + const nextTimestampSmaller = nextLineTimestampSmallerThanCurrent(ext, line); + if (nextTimestampSmaller !== undefined) { + warnings.push(nextTimestampSmaller); + } + for (const warning of warnings) { + global_manageWarnings.setWarning(document, line, warning); + } + // unset any warnings if this doesn’t apply anymore + if (warnings.length === 0) { + global_manageWarnings.setWarning(document, line); + } + } +} + +/** Warn if the difference to the timestamp on the next line is larger than 10 seconds */ +function timeDifferenceTooLarge(ext: Ext, line: number): string | undefined { + const timeDifference = ext.getTimeDifferenceToNextLineTimestamp( + new vscode.Position(line, 0), + ); + if ( + !timeDifference || + timeDifference.thisLineIsEmpty || + timeDifference.difference <= 10000 + ) { + return; + } + return `Time difference to next line is ${formatTimestamp(timeDifference.difference)}`; +} + +/** Warn if the timestamp on the next line is smaller or equal to the current timestamp */ +function nextLineTimestampSmallerThanCurrent(ext: Ext, line: number): string | undefined { + const timeDifference = ext.getTimeDifferenceToNextLineTimestamp( + new vscode.Position(line, 0), + ); + if (!timeDifference) { + return; + } + if (timeDifference.difference == 0) { + return `The timestamp to the next line is not increasing`; + } + if (timeDifference.difference < 0) { + return `The timestamp to the next line is decreasing`; + } +} + +class Ext { + constructor(public editor: vscode.TextEditor) {} + + getTimeDifferenceToNextLineTimestamp(position: vscode.Position) { + const thisLineTimestamp = this.getTimestampFromLine(position); + const nextLineTimestamp = this.getTimestampFromLine( + position.with({ line: position.line + 1 }), + ); + if (!thisLineTimestamp || !nextLineTimestamp) { + return; + } + return { + difference: nextLineTimestamp.milliseconds - thisLineTimestamp.milliseconds, + thisLineIsEmpty: thisLineTimestamp.text.trim() === '', + }; + } + + getTimestampFromLine(position: vscode.Position) { + const document = this.editor.document; + const lineText = document.lineAt(position.line).text; + + // Extract timestamp [mm:ss.ms] from the current line + const match = lineText.match(/\[(\d+:\d+\.\d+)\](.*)/); + if (!match) { + return; + } + const [, timestamp, text] = match!; + const milliseconds = parseTimestamp(timestamp); + const seconds = milliseconds / 1000; + return { milliseconds, seconds, text }; + } +} + +function parseTimestamp(timestamp: string): number { + // Parse [mm:ss.ms] format into milliseconds + const [min, sec] = timestamp.split(':'); + + const minutes = parseInt(min, 10); + const seconds = parseFloat(sec); + + return minutes * 60 * 1000 + seconds * 1000; +} + +function formatTimestamp(ms: number): string { + // Format milliseconds back into [mm:ss.ms] + const minutes = Math.floor(ms / 60000); + ms %= 60000; + const seconds = (ms / 1000).toFixed(2); + + return `${String(minutes).padStart(2, '0')}:${seconds}`; +} + +class ManageWarnings { + private warnings: Map<number, string> = new Map(); + private diagnostics: vscode.DiagnosticCollection; + + constructor() { + this.diagnostics = vscode.languages.createDiagnosticCollection(); + } + + /** Set a warning message on a line in a document, if null then unset */ + setWarning(document: vscode.TextDocument, line: number, message?: string) { + if (message !== undefined) { + this.warnings.set(line, message); + } else { + this.warnings.delete(line); + } + this.updateDiagnostics(document); + } + + private updateDiagnostics(document: vscode.TextDocument) { + const mkWarning = (line: number, message: string) => { + const lineRange = document.lineAt(line).range; + return new vscode.Diagnostic(lineRange, message, vscode.DiagnosticSeverity.Warning); + }; + const diagnostics: vscode.Diagnostic[] = []; + for (const [line, message] of this.warnings) { + diagnostics.push(mkWarning(line, message)); + } + this.diagnostics.delete(document.uri); + this.diagnostics.set(document.uri, diagnostics); + } +} + +const global_manageWarnings = new ManageWarnings(); diff --git a/users/Profpatsch/lyric/extension/tsconfig.json b/users/Profpatsch/lyric/extension/tsconfig.json new file mode 100644 index 000000000000..44a5195795da --- /dev/null +++ b/users/Profpatsch/lyric/extension/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "lib": [ + "ES6" + ], + "outDir": "./out", + "rootDir": "./src", + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/users/Profpatsch/lyric/lyric-timing-mpv-script.lua b/users/Profpatsch/lyric/lyric-timing-mpv-script.lua new file mode 100644 index 000000000000..6558422f1092 --- /dev/null +++ b/users/Profpatsch/lyric/lyric-timing-mpv-script.lua @@ -0,0 +1,43 @@ +-- This function formats the current timestamp in the [mm:ss.ms] format +function format_timestamp(seconds) + local minutes = math.floor(seconds / 60) + local seconds = seconds % 60 + return string.format("[%02d:%05.2f]", minutes, seconds) +end + +-- Get the user’s cache directory +local cache_dir = os.getenv("XDG_CACHE_HOME") or os.getenv("HOME") .. "/.cache" + +-- This function writes the timestamp to the LRC file +function write_timestamp_to_lrc() + local filename = mp.get_property("path") + if not filename then + mp.msg.warn("No file currently playing.") + return + end + + -- Extract metadata for artist and title + local artist = mp.get_property("metadata/by-key/ARTIST", "Unknown Artist") + local title = mp.get_property("metadata/by-key/TITLE", "Unknown Title") + + -- Construct the lrc dir + local dir = cache_dir .. "/lyric/timed" + local lrc_filename = string.format("%s/%s - %s.lrc", dir, artist, title) + + -- Get current playback time + local current_time = mp.get_property_number("time-pos", 0) + local formatted_time = format_timestamp(current_time) + + -- Append the timestamp to the LRC file + local file = io.open(lrc_filename, "a") + if file then + file:write(formatted_time .. "\n") + file:close() + mp.msg.info("Timestamp " .. formatted_time .. " added to " .. lrc_filename) + else + mp.msg.error("Failed to open " .. lrc_filename) + end +end + +-- Bind Ctrl+l to the function that writes the timestamp +mp.add_key_binding("Ctrl+l", "insert_timestamp", write_timestamp_to_lrc) diff --git a/users/Profpatsch/lyric/package.json b/users/Profpatsch/lyric/package.json new file mode 100644 index 000000000000..4621d087e0fd --- /dev/null +++ b/users/Profpatsch/lyric/package.json @@ -0,0 +1,23 @@ +{ + "name": "lyric", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "type": "module", + "license": "GPL-3.0-or-later", + "description": "", + "dependencies": {}, + "devDependencies": { + "@eslint/js": "^9.10.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.5.5", + "@typescript-eslint/parser": "^8.7.0", + "eslint": "^9.10.0", + "globals": "^15.9.0", + "typescript": "^5.6.2", + "typescript-eslint": "^8.6.0" + } +} diff --git a/users/Profpatsch/lyric/src/index.ts b/users/Profpatsch/lyric/src/index.ts new file mode 100644 index 000000000000..6bab1c4590cd --- /dev/null +++ b/users/Profpatsch/lyric/src/index.ts @@ -0,0 +1,16 @@ +import { tapBpm } from "./tap-bpm.js"; + +async function main() { + // subcommand for tap-bpm + if (process.argv[2] === "tap-bpm") { + await tapBpm(); + } +} + +await main(); + +// sleep in a loop to block nodejs +console.log("Blocking event loop..."); +while (true) { + await new Promise((resolve) => setTimeout(resolve, 1000)); +} diff --git a/users/Profpatsch/lyric/src/tap-bpm.ts b/users/Profpatsch/lyric/src/tap-bpm.ts new file mode 100644 index 000000000000..2062fb8bbca7 --- /dev/null +++ b/users/Profpatsch/lyric/src/tap-bpm.ts @@ -0,0 +1,79 @@ +// create a node command line listener that allows the user to press any key , and averages the distances between the key presses to determine the BPM (with a window of 4 key presses). If the user presses q, the program should exit and print the final BPM. + +// Import the necessary modules +import * as readline from "readline"; + +export function tapBpm() { + // Set up readline interface to listen for keypresses + readline.emitKeypressEvents(process.stdin); + process.stdin.setRawMode(true); + // accept SIGINT on stdin + + // Array to store the time differences between the last 4 key presses + const timeDifferences: number[] = []; + let lastPressTime: number | null = null; + + // Function to calculate BPM based on average time between keypresses + function calculateBPM() { + if (timeDifferences.length < 1) { + return 0; + } + const averageTimeDiff = + timeDifferences.reduce((acc, curr) => acc + curr, 0) / + timeDifferences.length; + return (60 * 1000) / averageTimeDiff; + } + + // Handle the SIGINT (Ctrl+C) event manually + process.on("SIGINT", () => { + console.log( + "\nExiting via SIGINT (Ctrl+C)... Final BPM:", + calculateBPM().toFixed(2) + ); + process.exit(); + }); + + // Listen for keypress events + process.stdin.on("keypress", (str, key) => { + // Exit if 'q' is pressed + if (key.name === "q") { + console.log("Exiting... Final BPM:", calculateBPM().toFixed(2)); + process.exit(); + } + + // Handle Ctrl+C (SIGINT) + if (key.sequence === "\u0003") { + // '\u0003' is the raw code for Ctrl+C + console.log( + "\nExiting via Ctrl+C... Final BPM:", + calculateBPM().toFixed(2) + ); + process.exit(); + } + + // Capture the current time of the keypress + const currentTime = Date.now(); + + // If it's not the first keypress, calculate the time difference + if (lastPressTime !== null) { + const timeDiff = currentTime - lastPressTime; + + // Add the time difference to the array (limit to last 10 key presses) + if (timeDifferences.length >= 10) { + timeDifferences.shift(); // Remove the oldest time difference + } + timeDifferences.push(timeDiff); + + // Calculate and display the BPM + const bpm = calculateBPM(); + console.log("Current BPM:", bpm.toFixed(2)); + } else { + console.log("Waiting for more key presses to calculate BPM..."); + } + + // Update the lastPressTime to the current time + lastPressTime = currentTime; + }); + + console.log('Press any key to calculate BPM, press "q" to quit.'); +} diff --git a/users/Profpatsch/lyric/tsconfig.json b/users/Profpatsch/lyric/tsconfig.json new file mode 100644 index 000000000000..f3c47a381e20 --- /dev/null +++ b/users/Profpatsch/lyric/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "ESNext", + "outDir": "dist", + "strict": true, + "skipLibCheck": true, + "sourceMap": true, + "esModuleInterop": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} |