├── saved-audio └── .gitkeep ├── g4l-ui ├── saved-audio │ └── .gitkeep ├── .erb │ ├── mocks │ │ └── fileMock.js │ ├── img │ │ ├── erb-logo.png │ │ └── erb-banner.svg │ ├── configs │ │ ├── .eslintrc │ │ ├── webpack.config.eslint.ts │ │ ├── webpack.paths.ts │ │ ├── webpack.config.base.ts │ │ ├── webpack.config.renderer.dev.dll.ts │ │ ├── webpack.config.preload.dev.ts │ │ ├── webpack.config.main.prod.ts │ │ ├── webpack.config.renderer.prod.ts │ │ └── webpack.config.renderer.dev.ts │ └── scripts │ │ ├── .eslintrc │ │ ├── link-modules.ts │ │ ├── clean.js │ │ ├── check-node-env.js │ │ ├── check-port-in-use.js │ │ ├── delete-source-maps.js │ │ ├── electron-rebuild.js │ │ ├── check-build-exists.ts │ │ ├── notarize.js │ │ └── check-native-dep.js ├── .vs │ ├── ProjectSettings.json │ ├── slnx.sqlite │ ├── g4l-ui │ │ ├── v17 │ │ │ └── .wsuo │ │ └── FileContentIndex │ │ │ ├── 74cca680-690e-4bdc-bf30-d9b022aef20a.vsidx │ │ │ ├── 0566ba45-b611-454b-94d1-4c7906388fb4.vsidx │ │ │ ├── 63ef41b1-ad84-47f2-ade4-53c1bce2aeba.vsidx │ │ │ ├── 78fc0749-c947-4ca3-8ef0-98cf7aaeb034.vsidx │ │ │ └── 91a97c04-0a45-4402-b1a6-ad9ac6958337.vsidx │ └── VSWorkspaceState.json ├── assets │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icons │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 64x64.png │ │ ├── 96x96.png │ │ ├── 128x128.png │ │ ├── 256x256.png │ │ ├── 512x512.png │ │ └── 1024x1024.png │ ├── entitlements.mac.plist │ ├── assets.d.ts │ └── icon.svg ├── .vscode │ ├── extensions.json │ ├── settings.json │ └── launch.json ├── .github │ ├── config.yml │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── 3-Feature_request.md │ │ ├── 2-Question.md │ │ └── 1-Bug_report.md │ ├── stale.yml │ └── workflows │ │ ├── test.yml │ │ ├── publish.yml │ │ └── codeql-analysis.yml ├── src │ ├── renderer │ │ ├── preload.d.ts │ │ ├── index.ejs │ │ ├── index.tsx │ │ ├── LoadingIndicator.tsx │ │ ├── NotificationOverlay.tsx │ │ ├── GuideNumber.tsx │ │ └── App.css │ ├── __tests__ │ │ └── App.test.tsx │ ├── main │ │ ├── util.ts │ │ ├── preload.ts │ │ ├── main.ts │ │ └── menu.ts │ └── global.d.ts ├── .editorconfig ├── .gitattributes ├── .gitignore ├── tsconfig.json ├── .eslintignore ├── release │ └── app │ │ └── package.json ├── LICENSE ├── .eslintrc.js ├── CODE_OF_CONDUCT.md ├── package.json └── CHANGELOG.md ├── GARY.amxd ├── gary4live_screenshot.png ├── .gitignore ├── launch_electron.js ├── package.json ├── README.md ├── electron-communication.js └── commentedout.js /saved-audio/.gitkeep: -------------------------------------------------------------------------------- 1 | ECHO is on. 2 | -------------------------------------------------------------------------------- /g4l-ui/saved-audio/.gitkeep: -------------------------------------------------------------------------------- 1 | ECHO is on. 2 | -------------------------------------------------------------------------------- /g4l-ui/.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /g4l-ui/.vs/ProjectSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentProjectSetting": null 3 | } -------------------------------------------------------------------------------- /GARY.amxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/GARY.amxd -------------------------------------------------------------------------------- /g4l-ui/.vs/slnx.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.vs/slnx.sqlite -------------------------------------------------------------------------------- /g4l-ui/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icon.icns -------------------------------------------------------------------------------- /g4l-ui/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icon.ico -------------------------------------------------------------------------------- /g4l-ui/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icon.png -------------------------------------------------------------------------------- /gary4live_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/gary4live_screenshot.png -------------------------------------------------------------------------------- /g4l-ui/.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /g4l-ui/.vs/g4l-ui/v17/.wsuo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.vs/g4l-ui/v17/.wsuo -------------------------------------------------------------------------------- /g4l-ui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /g4l-ui/assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/16x16.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/24x24.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/32x32.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/48x48.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/64x64.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/96x96.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/128x128.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/256x256.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/512x512.png -------------------------------------------------------------------------------- /g4l-ui/assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /g4l-ui/.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /g4l-ui/.vs/g4l-ui/FileContentIndex/74cca680-690e-4bdc-bf30-d9b022aef20a.vsidx: -------------------------------------------------------------------------------- 1 | CDGG '3 -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /g4l-ui/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [electron-react-boilerplate, amilajack] 4 | patreon: amilajack 5 | open_collective: electron-react-boilerplate-594 6 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /g4l-ui/.vs/g4l-ui/FileContentIndex/0566ba45-b611-454b-94d1-4c7906388fb4.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.vs/g4l-ui/FileContentIndex/0566ba45-b611-454b-94d1-4c7906388fb4.vsidx -------------------------------------------------------------------------------- /g4l-ui/.vs/g4l-ui/FileContentIndex/63ef41b1-ad84-47f2-ade4-53c1bce2aeba.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.vs/g4l-ui/FileContentIndex/63ef41b1-ad84-47f2-ade4-53c1bce2aeba.vsidx -------------------------------------------------------------------------------- /g4l-ui/.vs/g4l-ui/FileContentIndex/78fc0749-c947-4ca3-8ef0-98cf7aaeb034.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.vs/g4l-ui/FileContentIndex/78fc0749-c947-4ca3-8ef0-98cf7aaeb034.vsidx -------------------------------------------------------------------------------- /g4l-ui/.vs/g4l-ui/FileContentIndex/91a97c04-0a45-4402-b1a6-ad9ac6958337.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betweentwomidnights/gary4live/HEAD/g4l-ui/.vs/g4l-ui/FileContentIndex/91a97c04-0a45-4402-b1a6-ad9ac6958337.vsidx -------------------------------------------------------------------------------- /g4l-ui/src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronHandler } from '../main/preload'; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-unused-vars 5 | interface Window { 6 | electron: ElectronHandler; 7 | } 8 | } 9 | 10 | export {}; 11 | -------------------------------------------------------------------------------- /g4l-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /g4l-ui/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /g4l-ui/src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../renderer/App'; 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | expect(render()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /g4l-ui/.vs/VSWorkspaceState.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExpandedNodes": [ 3 | "", 4 | "\\assets", 5 | "\\release", 6 | "\\release\\app", 7 | "\\src", 8 | "\\src\\main", 9 | "\\src\\renderer", 10 | "\\src\\__tests__" 11 | ], 12 | "SelectedNode": "\\package.json", 13 | "PreviewInSolutionExplorer": false 14 | } -------------------------------------------------------------------------------- /g4l-ui/src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | gary4live 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import { rimrafSync } from 'rimraf'; 2 | import fs from 'fs'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | const foldersToRemove = [ 6 | webpackPaths.distPath, 7 | webpackPaths.buildPath, 8 | webpackPaths.dllPath, 9 | ]; 10 | 11 | foldersToRemove.forEach((folder) => { 12 | if (fs.existsSync(folder)) rimrafSync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /g4l-ui/assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ffmpeg-master-latest-win64-gpl/ 2 | node_modules/ 3 | g4l-ui/node_modules/ 4 | *.logg4l-ui/saved-audio/ 5 | *.wav 6 | *.wav.asd 7 | *.log 8 | node-v20.13.1-x64.msi 9 | commentedout_backup.js 10 | commentedout_websockets_backup.js 11 | myBufferUI.js 12 | update_path.bat 13 | waveform.js 14 | recordtoggle.js 15 | package-lock.json 16 | commentedout_test.js 17 | g4l-ui/package-lock.json 18 | g4l-ui/release/build/ 19 | .vs/ 20 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`, 12 | ), 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /g4l-ui/.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the boilerplate. 🎉 4 | labels: 'enhancement' 5 | --- 6 | 7 | 16 | -------------------------------------------------------------------------------- /g4l-ui/.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /g4l-ui/src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212; 8 | const url = new URL(`http://localhost:${port}`); 9 | url.pathname = htmlFileName; 10 | return url.href; 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 13 | } 14 | -------------------------------------------------------------------------------- /g4l-ui/src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | 4 | const container = document.getElementById('root') as HTMLElement; 5 | const root = createRoot(container); 6 | root.render(); 7 | 8 | // calling IPC exposed from preload script 9 | window.electron.ipcRenderer.once('ipc-example', (arg) => { 10 | // eslint-disable-next-line no-console 11 | console.log(arg); 12 | }); 13 | window.electron.ipcRenderer.sendMessage('ipc-example', ['ping']); 14 | -------------------------------------------------------------------------------- /g4l-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (_err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`, 11 | ), 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { rimrafSync } from 'rimraf'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { 9 | glob: true, 10 | }); 11 | if (fs.existsSync(webpackPaths.distRendererPath)) 12 | rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { 13 | glob: true, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /g4l-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2022", 5 | "module": "commonjs", 6 | "lib": [ "dom", "es2022" ], 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "sourceMap": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "resolveJsonModule": true, 14 | "allowJs": true, 15 | "outDir": ".erb/dll", 16 | "typeRoots": [ "./node_modules/@types", "./src" ] 17 | }, 18 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 19 | } 20 | -------------------------------------------------------------------------------- /g4l-ui/.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /g4l-ui/release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gary4live", 3 | "version": "1.0.0", 4 | "description": "ya know, for music", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Kevin Griffing", 8 | "email": "kev@thecollabagepatch.com", 9 | "url": "https://github.com/betweentwomidnights" 10 | }, 11 | "main": "./dist/main/main.js", 12 | "scripts": { 13 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 14 | "postinstall": "npm run rebuild && npm run link-modules", 15 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /g4l-ui/.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /g4l-ui/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /g4l-ui/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["run", "start"], 11 | "env": { 12 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" 13 | } 14 | }, 15 | { 16 | "name": "Electron: Renderer", 17 | "type": "chrome", 18 | "request": "attach", 19 | "port": 9223, 20 | "webRoot": "${workspaceFolder}", 21 | "timeout": 15000 22 | } 23 | ], 24 | "compounds": [ 25 | { 26 | "name": "Electron: All", 27 | "configurations": ["Electron: Main", "Electron: Renderer"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /g4l-ui/assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | import React = require('react'); 5 | 6 | export const ReactComponent: React.FC>; 7 | 8 | const content: string; 9 | export default content; 10 | } 11 | 12 | declare module '*.png' { 13 | const content: string; 14 | export default content; 15 | } 16 | 17 | declare module '*.jpg' { 18 | const content: string; 19 | export default content; 20 | } 21 | 22 | declare module '*.scss' { 23 | const content: Styles; 24 | export default content; 25 | } 26 | 27 | declare module '*.sass' { 28 | const content: Styles; 29 | export default content; 30 | } 31 | 32 | declare module '*.css' { 33 | const content: Styles; 34 | export default content; 35 | } 36 | -------------------------------------------------------------------------------- /g4l-ui/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [macos-latest, windows-latest, ubuntu-latest] 12 | 13 | steps: 14 | - name: Check out Git repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Install Node.js and NPM 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | cache: npm 22 | 23 | - name: npm install 24 | run: | 25 | npm install 26 | 27 | - name: npm test 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | npm run package 32 | npm run lint 33 | npm exec tsc 34 | npm test 35 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"', 14 | ), 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"', 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /launch_electron.js: -------------------------------------------------------------------------------- 1 | const Max = require('max-api'); 2 | const { exec } = require('child_process'); 3 | const path = require('path') 4 | 5 | Max.post("Node script loaded."); 6 | 7 | // Function to launch the Electron app 8 | function launchElectronApp() { 9 | const path = "C:\\g4l\\g4l-ui\\release\\build\\Gary4Live Setup 1.0.0.exe"; 10 | exec(`"${path}"`, (error, stdout, stderr) => { 11 | if (error) { 12 | Max.post(`exec error: ${error}`); 13 | return; 14 | } 15 | Max.post(`stdout: ${stdout}`); 16 | if (stderr) { 17 | Max.post(`stderr: ${stderr}`); 18 | } 19 | }); 20 | } 21 | 22 | // Ensure Node script is ready 23 | Max.addHandler('launch', () => { 24 | launchElectronApp(); 25 | }); 26 | 27 | // Optional: add a ready check 28 | Max.addHandler('ready', () => { 29 | Max.outlet('ready'); 30 | }); 31 | 32 | // Listen for bang from Max 33 | Max.addHandler('bang', () => { 34 | launchElectronApp(); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "g4l", 3 | "version": "1.0.0", 4 | "description": "this is gary4live. gary iterates on input audio using max for live and musicgen.", 5 | "main": "commentedout.js", 6 | "dependencies": { 7 | "async": "^0.2.10", 8 | "asynckit": "^0.4.0", 9 | "combined-stream": "^1.0.8", 10 | "debug": "^4.3.4", 11 | "delayed-stream": "^1.0.0", 12 | "engine.io-client": "^6.5.3", 13 | "engine.io-parser": "^5.2.2", 14 | "fluent-ffmpeg": "^2.1.3", 15 | "form-data": "^4.0.0", 16 | "isexe": "^2.0.0", 17 | "mime-db": "^1.52.0", 18 | "mime-types": "^2.1.35", 19 | "ms": "^2.1.2", 20 | "nvm": "^0.0.4", 21 | "python-shell": "^5.0.0", 22 | "socket.io-client": "^4.7.4", 23 | "socket.io-parser": "^4.2.4", 24 | "which": "^1.3.1", 25 | "ws": "^8.11.0", 26 | "xmlhttprequest-ssl": "^2.0.0" 27 | }, 28 | "scripts": { 29 | "test": "echo \"Error: no test specified\" && exit 1" 30 | }, 31 | "keywords": [], 32 | "author": "", 33 | "license": "ISC" 34 | } 35 | -------------------------------------------------------------------------------- /g4l-ui/src/global.d.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | 3 | declare global { 4 | interface Window { 5 | api: { 6 | send: (channel: string, data: any) => void; 7 | receive: (channel: string, func: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => void; 8 | remove: (channel: string, func: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => void; 9 | startDrag: (filePath: string) => void; 10 | saveAudioFile: (base64Data: string) => void; 11 | onAudioFileSaved: (callback: (filePath: string) => void) => void; 12 | }; 13 | electron: { 14 | ipcRenderer: { 15 | sendMessage(channel: string, ...args: unknown[]): void; 16 | saveAudioFile: (base64Data: string) => void; 17 | onAudioFileSaved: (callback: (filePath: string) => void) => void; 18 | on(channel: string, func: (...args: unknown[]) => void): () => void; 19 | once(channel: string, func: (...args: unknown[]) => void): void; 20 | startDrag: (filePath: string) => void; 21 | }; 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if ( 16 | !('APPLE_ID' in process.env && 'APPLE_APP_SPECIFIC_PASSWORD' in process.env) 17 | ) { 18 | console.warn( 19 | 'Skipping notarizing step. APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD env variables must be set', 20 | ); 21 | return; 22 | } 23 | 24 | const appName = context.packager.appInfo.productFilename; 25 | 26 | await notarize({ 27 | appBundleId: build.appId, 28 | appPath: `${appOutDir}/${appName}.app`, 29 | appleId: process.env.APPLE_ID, 30 | appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /g4l-ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 thebigdookie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /g4l-ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | plugins: ['@typescript-eslint'], 4 | rules: { 5 | // A temporary hack related to IDE not resolving correct package.json 6 | 'import/no-extraneous-dependencies': 'off', 7 | 'react/react-in-jsx-scope': 'off', 8 | 'react/jsx-filename-extension': 'off', 9 | 'import/extensions': 'off', 10 | 'import/no-unresolved': 'off', 11 | 'import/no-import-module-exports': 'off', 12 | 'no-shadow': 'off', 13 | '@typescript-eslint/no-shadow': 'error', 14 | 'no-unused-vars': 'off', 15 | '@typescript-eslint/no-unused-vars': 'error', 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 2022, 19 | sourceType: 'module', 20 | }, 21 | settings: { 22 | 'import/resolver': { 23 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 24 | node: {}, 25 | webpack: { 26 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 27 | }, 28 | typescript: {}, 29 | }, 30 | 'import/parsers': { 31 | '@typescript-eslint/parser': ['.ts', '.tsx'], 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /g4l-ui/.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | # To enable auto publishing to github, update your electron publisher 11 | # config in package.json > "build" and remove the conditional below 12 | if: ${{ github.repository_owner == 'electron-react-boilerplate' }} 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [macos-latest] 19 | 20 | steps: 21 | - name: Checkout git repo 22 | uses: actions/checkout@v3 23 | 24 | - name: Install Node and NPM 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 18 28 | cache: npm 29 | 30 | - name: Install and build 31 | run: | 32 | npm install 33 | npm run postinstall 34 | npm run build 35 | 36 | - name: Publish releases 37 | env: 38 | # These values are used for auto updates signing 39 | APPLE_ID: ${{ secrets.APPLE_ID }} 40 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASS }} 41 | CSC_LINK: ${{ secrets.CSC_LINK }} 42 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 43 | # This is used for uploading release assets to github 44 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | run: | 46 | npm exec electron-builder -- --publish always --win --mac --linux 47 | -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; 7 | import webpackPaths from './webpack.paths'; 8 | import { dependencies as externals } from '../../release/app/package.json'; 9 | 10 | const configuration: webpack.Configuration = { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | stats: 'errors-only', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.[jt]sx?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'ts-loader', 22 | options: { 23 | // Remove this line to enable type checking in webpack builds 24 | transpileOnly: true, 25 | compilerOptions: { 26 | module: 'esnext', 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | output: { 35 | path: webpackPaths.srcPath, 36 | // https://github.com/webpack/webpack/issues/1114 37 | library: { 38 | type: 'commonjs2', 39 | }, 40 | }, 41 | 42 | /** 43 | * Determine the array of extensions that should be used to resolve modules. 44 | */ 45 | resolve: { 46 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 47 | modules: [webpackPaths.srcPath, 'node_modules'], 48 | // There is no need to add aliases here, the paths in tsconfig get mirrored 49 | plugins: [new TsconfigPathsPlugins()], 50 | }, 51 | 52 | plugins: [ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production', 55 | }), 56 | ], 57 | }; 58 | 59 | export default configuration; 60 | -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /g4l-ui/.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 4 | labels: 'bug' 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] Using npm 14 | - [ ] Using an up-to-date [`main` branch](https://github.com/electron-react-boilerplate/electron-react-boilerplate/tree/main) 15 | - [ ] Using latest version of devtools. [Check the docs for how to update](https://electron-react-boilerplate.js.org/docs/dev-tools/) 16 | - [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) 17 | - [ ] For issue in production release, add devtools output of `DEBUG_PROD=true npm run build && npm start` 18 | 19 | ## Expected Behavior 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Steps to Reproduce 28 | 29 | 30 | 31 | 32 | 1. 33 | 34 | 2. 35 | 36 | 3. 37 | 38 | 4. 39 | 40 | ## Possible Solution (Not obligatory) 41 | 42 | 43 | 44 | ## Context 45 | 46 | 47 | 48 | 49 | 50 | ## Your Environment 51 | 52 | 53 | 54 | - Node version : 55 | - electron-react-boilerplate version or branch : 56 | - Operating System and version : 57 | - Link to your project : 58 | 59 | 68 | -------------------------------------------------------------------------------- /g4l-ui/src/renderer/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loader2 } from 'lucide-react'; 3 | 4 | interface LoadingIndicatorProps { 5 | action: string | null; 6 | } 7 | 8 | const LoadingIndicator: React.FC = ({ action }) => { 9 | if (!action) return null; 10 | 11 | // Map actions to user-friendly messages 12 | const actionMessages: { [key: string]: string } = { 13 | bang: 'generating initial audio...', 14 | continue: 'generating continuation...', 15 | retry: 'regenerating audio...', 16 | load_output: 'loading output...', 17 | crop: 'cropping audio...', 18 | transform: 'transforming audio...', 19 | reset_transform: 'clearing session state... you\'ll now go back to transforming the top waveform' 20 | }; 21 | 22 | const message = actionMessages[action] || 'processing...'; 23 | 24 | return ( 25 |
43 | 48 | {message} 49 |
50 | ); 51 | }; 52 | 53 | // Add the keyframes for the spin animation 54 | const styleSheet = document.createElement('style'); 55 | styleSheet.textContent = ` 56 | @keyframes spin { 57 | from { 58 | transform: rotate(0deg); 59 | } 60 | to { 61 | transform: rotate(360deg); 62 | } 63 | }`; 64 | document.head.appendChild(styleSheet); 65 | 66 | export default LoadingIndicator; -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /g4l-ui/.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString(), 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency), 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.', 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":', 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package', 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure', 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const configuration: webpack.Configuration = { 19 | devtool: 'source-map', 20 | 21 | mode: 'production', 22 | 23 | target: 'electron-main', 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 27 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: '[name].js', 33 | library: { 34 | type: 'umd', 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 49 | analyzerPort: 8888, 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'production', 63 | DEBUG_PROD: false, 64 | START_MINIMIZED: false, 65 | }), 66 | 67 | new webpack.DefinePlugin({ 68 | 'process.type': '"browser"', 69 | }), 70 | ], 71 | 72 | /** 73 | * Disables webpack processing of __dirname and __filename. 74 | * If you run the bundle in node.js it falls back to these values of node.js. 75 | * https://github.com/webpack/webpack/issues/2010 76 | */ 77 | node: { 78 | __dirname: false, 79 | __filename: false, 80 | }, 81 | }; 82 | 83 | export default merge(baseConfig, configuration); 84 | -------------------------------------------------------------------------------- /g4l-ui/src/main/preload.ts: -------------------------------------------------------------------------------- 1 | // Disable no-unused-vars, broken for spread args 2 | /* eslint no-unused-vars: off */ 3 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 4 | 5 | export type Channels = 6 | | 'ipc-example' 7 | | 'save-audio-file' 8 | | 'audio-file-saved' 9 | | 'ondragstart' 10 | | 'toggle-guide'; // Add this line 11 | 12 | const electronHandler = { 13 | ipcRenderer: { 14 | sendMessage(channel: Channels, ...args: unknown[]) { 15 | ipcRenderer.send(channel, ...args); 16 | }, 17 | saveAudioFile: (base64Data: string) => { 18 | console.log('Preload sending save-audio-file with data:', base64Data); 19 | ipcRenderer.send('save-audio-file', base64Data); 20 | }, 21 | onAudioFileSaved: (callback: (filePath: string) => void) => { 22 | console.log('Preload setting listener for audio-file-saved'); 23 | ipcRenderer.on('audio-file-saved', (_event, filePath) => 24 | callback(filePath), 25 | ); 26 | }, 27 | startDrag: (fileName: string) => { 28 | ipcRenderer.send('ondragstart', fileName); 29 | }, 30 | on(channel: Channels, func: (...args: unknown[]) => void) { 31 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 32 | func(...args); 33 | ipcRenderer.on(channel, subscription); 34 | 35 | return () => { 36 | ipcRenderer.removeListener(channel, subscription); 37 | }; 38 | }, 39 | once(channel: Channels, func: (...args: unknown[]) => void) { 40 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 41 | }, 42 | }, 43 | }; 44 | 45 | const api = { 46 | send: (channel: Channels, data: any) => { 47 | console.log(`Preload sending ${channel} with data:`, data); 48 | ipcRenderer.send(channel, data); 49 | }, 50 | receive: (channel: Channels, func: (data: any) => void) => { 51 | console.log(`Preload receiving on channel: ${channel}`); 52 | ipcRenderer.on(channel, (_, data) => { 53 | console.log(`Data received on channel ${channel}:`, data); 54 | func(data); 55 | }); 56 | }, 57 | remove: (channel: Channels, func: (...args: any[]) => void) => { 58 | console.log(`Preload removing listener on channel: ${channel}`); 59 | ipcRenderer.removeListener(channel, func); 60 | }, 61 | }; 62 | 63 | contextBridge.exposeInMainWorld('electron', electronHandler); 64 | contextBridge.exposeInMainWorld('api', api); 65 | 66 | export type ElectronHandler = typeof electronHandler; 67 | export type Api = typeof api; 68 | -------------------------------------------------------------------------------- /g4l-ui/.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '44 16 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /g4l-ui/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /g4l-ui/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.renderer.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 11 | import { merge } from 'webpack-merge'; 12 | import TerserPlugin from 'terser-webpack-plugin'; 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | import checkNodeEnv from '../scripts/check-node-env'; 16 | import deleteSourceMaps from '../scripts/delete-source-maps'; 17 | 18 | checkNodeEnv('production'); 19 | deleteSourceMaps(); 20 | 21 | const configuration: webpack.Configuration = { 22 | devtool: 'source-map', 23 | 24 | mode: 'production', 25 | 26 | target: ['web', 'electron-renderer'], 27 | 28 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], 29 | 30 | output: { 31 | path: webpackPaths.distRendererPath, 32 | publicPath: './', 33 | filename: 'renderer.js', 34 | library: { 35 | type: 'umd', 36 | }, 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.s?(a|c)ss$/, 43 | use: [ 44 | MiniCssExtractPlugin.loader, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | sourceMap: true, 50 | importLoaders: 1, 51 | }, 52 | }, 53 | 'sass-loader', 54 | ], 55 | include: /\.module\.s?(c|a)ss$/, 56 | }, 57 | { 58 | test: /\.s?(a|c)ss$/, 59 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 60 | exclude: /\.module\.s?(c|a)ss$/, 61 | }, 62 | // Fonts 63 | { 64 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 65 | type: 'asset/resource', 66 | }, 67 | // Images 68 | { 69 | test: /\.(png|jpg|jpeg|gif)$/i, 70 | type: 'asset/resource', 71 | }, 72 | // SVG 73 | { 74 | test: /\.svg$/, 75 | use: [ 76 | { 77 | loader: '@svgr/webpack', 78 | options: { 79 | prettier: false, 80 | svgo: false, 81 | svgoConfig: { 82 | plugins: [{ removeViewBox: false }], 83 | }, 84 | titleProp: true, 85 | ref: true, 86 | }, 87 | }, 88 | 'file-loader', 89 | ], 90 | }, 91 | ], 92 | }, 93 | 94 | optimization: { 95 | minimize: true, 96 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], 97 | }, 98 | 99 | plugins: [ 100 | /** 101 | * Create global constants which can be configured at compile time. 102 | * 103 | * Useful for allowing different behaviour between development builds and 104 | * release builds 105 | * 106 | * NODE_ENV should be production so that modules do not perform certain 107 | * development checks 108 | */ 109 | new webpack.EnvironmentPlugin({ 110 | NODE_ENV: 'production', 111 | DEBUG_PROD: false, 112 | }), 113 | 114 | new MiniCssExtractPlugin({ 115 | filename: 'style.css', 116 | }), 117 | 118 | new BundleAnalyzerPlugin({ 119 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 120 | analyzerPort: 8889, 121 | }), 122 | 123 | new HtmlWebpackPlugin({ 124 | filename: 'index.html', 125 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 126 | minify: { 127 | collapseWhitespace: true, 128 | removeAttributeQuotes: true, 129 | removeComments: true, 130 | }, 131 | isBrowser: false, 132 | isDevelopment: false, 133 | }), 134 | 135 | new webpack.DefinePlugin({ 136 | 'process.type': '"renderer"', 137 | }), 138 | ], 139 | }; 140 | 141 | export default merge(baseConfig, configuration); 142 | -------------------------------------------------------------------------------- /g4l-ui/src/renderer/NotificationOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { X } from 'lucide-react'; 3 | 4 | interface NotificationOverlayProps { 5 | maxConnection: boolean; 6 | backendConnection: boolean; 7 | errorMessage?: { 8 | type: string; 9 | message: string; 10 | } | null; 11 | } 12 | 13 | const NotificationOverlay: React.FC = ({ 14 | maxConnection, 15 | backendConnection, 16 | errorMessage 17 | }) => { 18 | const [isVisible, setIsVisible] = useState(false); 19 | const [message, setMessage] = useState(''); 20 | const [isError, setIsError] = useState(false); 21 | const [showBackendWarning, setShowBackendWarning] = useState(false); 22 | const [isPersistent, setIsPersistent] = useState(false); 23 | 24 | useEffect(() => { 25 | // Handle initial connection attempts 26 | if (maxConnection) { 27 | // Give backend some time to connect before showing warning 28 | const backendTimeout = setTimeout(() => { 29 | if (!backendConnection) { 30 | setMessage("still not connected to backend. is your wifi on homie? try pressing '3' in m4l again."); 31 | setIsError(true); 32 | setIsVisible(true); 33 | setShowBackendWarning(true); 34 | setIsPersistent(false); 35 | } 36 | }, 5000); 37 | 38 | // Show initial max connection message 39 | if (!showBackendWarning) { 40 | setMessage(backendConnection 41 | ? 'connected to max for live & backend 🎵' 42 | : 'connected to max for live 🎵' 43 | ); 44 | setIsError(false); 45 | setIsVisible(true); 46 | setIsPersistent(false); 47 | } 48 | 49 | return () => clearTimeout(backendTimeout); 50 | } 51 | }, [maxConnection, backendConnection]); 52 | 53 | // Handle error messages 54 | useEffect(() => { 55 | if (errorMessage) { 56 | // Check if this is an error that should show Discord link 57 | const shouldShowDiscord = ( 58 | (errorMessage.type === 'transform' && errorMessage.message.includes('discord.gg')) || 59 | (errorMessage.type === 'ffmpeg' && errorMessage.message.includes('discord')) 60 | ); 61 | 62 | setMessage(errorMessage.message); 63 | setIsError(true); 64 | setIsVisible(true); 65 | setIsPersistent(shouldShowDiscord); 66 | } 67 | }, [errorMessage]); 68 | 69 | // Auto-hide for non-persistent notifications 70 | useEffect(() => { 71 | if (isVisible && !isPersistent) { 72 | const hideTimer = setTimeout(() => { 73 | setIsVisible(false); 74 | if (isError) { 75 | setShowBackendWarning(false); 76 | } 77 | }, 3000); 78 | return () => clearTimeout(hideTimer); 79 | } 80 | }, [isVisible, isError, isPersistent]); 81 | 82 | if (!isVisible) return null; 83 | 84 | // Function to parse message and create elements with clickable link 85 | const renderMessage = (text: string) => { 86 | if (text.includes('discord.gg')) { 87 | const parts = text.split('https://discord.gg/'); 88 | const discordLink = `https://discord.gg/${parts[1]}`; 89 | 90 | return ( 91 | <> 92 | {parts[0]} 93 | 99 | join discord 100 | 101 | 102 | ); 103 | } 104 | return text; 105 | }; 106 | 107 | return ( 108 |
131 | {isPersistent && ( 132 | 147 | )} 148 |
149 | {renderMessage(message)} 150 |
151 |
152 | ); 153 | }; 154 | 155 | export default NotificationOverlay; -------------------------------------------------------------------------------- /g4l-ui/src/renderer/GuideNumber.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | 3 | interface GuideNumberProps { 4 | number: number; 5 | blurb: string; 6 | isTransformButton?: boolean; // Add this prop 7 | isBottomButton?: boolean; 8 | } 9 | 10 | const GuideNumber: React.FC = ({ number, blurb, isTransformButton, isBottomButton }) => { 11 | const [showTooltip, setShowTooltip] = useState(false); 12 | const tooltipRef = useRef(null); 13 | 14 | useEffect(() => { 15 | if (showTooltip && tooltipRef.current) { 16 | const tooltip = tooltipRef.current; 17 | tooltip.style.visibility = 'hidden'; 18 | tooltip.style.display = 'block'; 19 | 20 | setTimeout(() => { 21 | const tooltipRect = tooltip.getBoundingClientRect(); 22 | const guideNumberRect = tooltip.parentElement?.getBoundingClientRect(); 23 | 24 | // Reset styles 25 | tooltip.style.left = 'auto'; 26 | tooltip.style.right = 'auto'; 27 | tooltip.style.transform = 'none'; 28 | tooltip.style.top = ''; 29 | tooltip.style.maxHeight = ''; 30 | tooltip.style.overflowY = ''; 31 | 32 | // Special handling for transform buttons 33 | if (isTransformButton) { 34 | // Position the guide number to the left of the button 35 | tooltip.style.right = 'calc(100% + 10px)'; // Position left with spacing 36 | tooltip.style.left = 'auto'; 37 | tooltip.style.transform = 'none'; // Remove any previous transforms 38 | 39 | // Vertical centering 40 | if (guideNumberRect) { 41 | const buttonHeight = guideNumberRect.height; 42 | const tooltipHeight = tooltipRect.height; 43 | tooltip.style.top = `${(buttonHeight - tooltipHeight) / 2}px`; 44 | } 45 | 46 | // Ensure tooltip doesn't go off screen to the left 47 | const leftEdge = tooltipRect.left; 48 | if (leftEdge < 0) { 49 | tooltip.style.right = 'auto'; 50 | tooltip.style.left = '-10px'; // Small negative offset to avoid button overlap 51 | } 52 | 53 | } else if (isBottomButton) { 54 | // Force tooltip to appear above the guide number 55 | tooltip.style.bottom = '100%'; 56 | tooltip.style.top = 'auto'; 57 | tooltip.style.left = '50%'; 58 | tooltip.style.transform = 'translateX(-50%)'; 59 | 60 | // Check if tooltip would go off left/right edges 61 | const updatedTooltipRect = tooltip.getBoundingClientRect(); 62 | if (updatedTooltipRect.right > window.innerWidth) { 63 | tooltip.style.left = 'auto'; 64 | tooltip.style.right = '0'; 65 | tooltip.style.transform = 'none'; 66 | } 67 | if (updatedTooltipRect.left < 0) { 68 | tooltip.style.left = '0'; 69 | tooltip.style.right = 'auto'; 70 | tooltip.style.transform = 'none'; 71 | } 72 | 73 | } else { 74 | // Original positioning logic for other guide numbers 75 | tooltip.style.left = '50%'; 76 | tooltip.style.transform = 'translateX(-50%)'; 77 | 78 | const spaceAbove = guideNumberRect ? guideNumberRect.top : 0; 79 | const spaceBelow = guideNumberRect 80 | ? window.innerHeight - guideNumberRect.bottom 81 | : 0; 82 | 83 | if (spaceBelow >= tooltipRect.height) { 84 | tooltip.style.top = '100%'; 85 | } else if (spaceAbove >= tooltipRect.height) { 86 | tooltip.style.top = `-${tooltipRect.height}px`; 87 | } else { 88 | if (spaceBelow >= spaceAbove) { 89 | tooltip.style.top = '100%'; 90 | tooltip.style.maxHeight = `${spaceBelow}px`; 91 | tooltip.style.overflowY = 'auto'; 92 | } else { 93 | tooltip.style.top = `-${spaceAbove}px`; 94 | tooltip.style.maxHeight = `${spaceAbove}px`; 95 | tooltip.style.overflowY = 'auto'; 96 | } 97 | } 98 | 99 | // Edge detection for non-transform buttons 100 | const updatedTooltipRect = tooltip.getBoundingClientRect(); 101 | if (updatedTooltipRect.right > window.innerWidth) { 102 | tooltip.style.left = 'auto'; 103 | tooltip.style.right = '0'; 104 | tooltip.style.transform = 'none'; 105 | } 106 | if (updatedTooltipRect.left < 0) { 107 | tooltip.style.left = '0'; 108 | tooltip.style.right = 'auto'; 109 | tooltip.style.transform = 'none'; 110 | } 111 | } 112 | 113 | tooltip.style.visibility = 'visible'; 114 | }, 10); 115 | } 116 | }, [showTooltip, isTransformButton]); 117 | 118 | return ( 119 |
setShowTooltip(true)} 122 | onMouseLeave={() => setShowTooltip(false)} 123 | > 124 | {number} 125 | {showTooltip && ( 126 |
131 | {blurb} 132 |
133 | )} 134 |
135 | ); 136 | }; 137 | 138 | export default GuideNumber; -------------------------------------------------------------------------------- /g4l-ui/.erb/configs/webpack.config.renderer.dev.ts: -------------------------------------------------------------------------------- 1 | import 'webpack-dev-server'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import webpack from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import chalk from 'chalk'; 7 | import { merge } from 'webpack-merge'; 8 | import { execSync, spawn } from 'child_process'; 9 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | 14 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 15 | // at the dev webpack config is not accidentally run in a production environment 16 | if (process.env.NODE_ENV === 'production') { 17 | checkNodeEnv('development'); 18 | } 19 | 20 | const port = process.env.PORT || 1212; 21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 22 | const skipDLLs = 23 | module.parent?.filename.includes('webpack.config.renderer.dev.dll') || 24 | module.parent?.filename.includes('webpack.config.eslint'); 25 | 26 | /** 27 | * Warn if the DLL is not built 28 | */ 29 | if ( 30 | !skipDLLs && 31 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) 32 | ) { 33 | console.log( 34 | chalk.black.bgYellow.bold( 35 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"', 36 | ), 37 | ); 38 | execSync('npm run postinstall'); 39 | } 40 | 41 | const configuration: webpack.Configuration = { 42 | devtool: 'inline-source-map', 43 | 44 | mode: 'development', 45 | 46 | target: ['web', 'electron-renderer'], 47 | 48 | entry: [ 49 | `webpack-dev-server/client?http://localhost:${port}/dist`, 50 | 'webpack/hot/only-dev-server', 51 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 52 | ], 53 | 54 | output: { 55 | path: webpackPaths.distRendererPath, 56 | publicPath: '/', 57 | filename: 'renderer.dev.js', 58 | library: { 59 | type: 'umd', 60 | }, 61 | }, 62 | 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.s?(c|a)ss$/, 67 | use: [ 68 | 'style-loader', 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | modules: true, 73 | sourceMap: true, 74 | importLoaders: 1, 75 | }, 76 | }, 77 | 'sass-loader', 78 | ], 79 | include: /\.module\.s?(c|a)ss$/, 80 | }, 81 | { 82 | test: /\.s?css$/, 83 | use: ['style-loader', 'css-loader', 'sass-loader'], 84 | exclude: /\.module\.s?(c|a)ss$/, 85 | }, 86 | // Fonts 87 | { 88 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 89 | type: 'asset/resource', 90 | }, 91 | // Images 92 | { 93 | test: /\.(png|jpg|jpeg|gif)$/i, 94 | type: 'asset/resource', 95 | }, 96 | // SVG 97 | { 98 | test: /\.svg$/, 99 | use: [ 100 | { 101 | loader: '@svgr/webpack', 102 | options: { 103 | prettier: false, 104 | svgo: false, 105 | svgoConfig: { 106 | plugins: [{ removeViewBox: false }], 107 | }, 108 | titleProp: true, 109 | ref: true, 110 | }, 111 | }, 112 | 'file-loader', 113 | ], 114 | }, 115 | ], 116 | }, 117 | plugins: [ 118 | ...(skipDLLs 119 | ? [] 120 | : [ 121 | new webpack.DllReferencePlugin({ 122 | context: webpackPaths.dllPath, 123 | manifest: require(manifest), 124 | sourceType: 'var', 125 | }), 126 | ]), 127 | 128 | new webpack.NoEmitOnErrorsPlugin(), 129 | 130 | /** 131 | * Create global constants which can be configured at compile time. 132 | * 133 | * Useful for allowing different behaviour between development builds and 134 | * release builds 135 | * 136 | * NODE_ENV should be production so that modules do not perform certain 137 | * development checks 138 | * 139 | * By default, use 'development' as NODE_ENV. This can be overriden with 140 | * 'staging', for example, by changing the ENV variables in the npm scripts 141 | */ 142 | new webpack.EnvironmentPlugin({ 143 | NODE_ENV: 'development', 144 | }), 145 | 146 | new webpack.LoaderOptionsPlugin({ 147 | debug: true, 148 | }), 149 | 150 | new ReactRefreshWebpackPlugin(), 151 | 152 | new HtmlWebpackPlugin({ 153 | filename: path.join('index.html'), 154 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 155 | minify: { 156 | collapseWhitespace: true, 157 | removeAttributeQuotes: true, 158 | removeComments: true, 159 | }, 160 | isBrowser: false, 161 | env: process.env.NODE_ENV, 162 | isDevelopment: process.env.NODE_ENV !== 'production', 163 | nodeModules: webpackPaths.appNodeModulesPath, 164 | }), 165 | ], 166 | 167 | node: { 168 | __dirname: false, 169 | __filename: false, 170 | }, 171 | 172 | devServer: { 173 | port, 174 | compress: true, 175 | hot: true, 176 | headers: { 'Access-Control-Allow-Origin': '*' }, 177 | static: { 178 | publicPath: '/', 179 | }, 180 | historyApiFallback: { 181 | verbose: true, 182 | }, 183 | setupMiddlewares(middlewares) { 184 | console.log('Starting preload.js builder...'); 185 | const preloadProcess = spawn('npm', ['run', 'start:preload'], { 186 | shell: true, 187 | stdio: 'inherit', 188 | }) 189 | .on('close', (code: number) => process.exit(code!)) 190 | .on('error', (spawnError) => console.error(spawnError)); 191 | 192 | console.log('Starting Main Process...'); 193 | let args = ['run', 'start:main']; 194 | if (process.env.MAIN_ARGS) { 195 | args = args.concat( 196 | ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat(), 197 | ); 198 | } 199 | spawn('npm', args, { 200 | shell: true, 201 | stdio: 'inherit', 202 | }) 203 | .on('close', (code: number) => { 204 | preloadProcess.kill(); 205 | process.exit(code!); 206 | }) 207 | .on('error', (spawnError) => console.error(spawnError)); 208 | return middlewares; 209 | }, 210 | }, 211 | }; 212 | 213 | export default merge(baseConfig, configuration); 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gary4live 2 | 3 | this is gary4live. gary iterates on input audio using max for live and musicgen. 4 | 5 | there is an installer for pc as well as for mac, but if you would rather build it yourself, it's really easy. 6 | 7 | ## update july_2_2025 8 | ### gary's now got two lil buddies lol...jerry and terry 9 | 10 | ![gary4live](./gary4live_screenshot.png) 11 | 12 | jerry is just our nickname for the stable-audio-open-small model. it generates 12 seconds of audio in under a second when using a gpu. it makes great input audio for gary's continuations. 13 | 14 | https://huggingface.co/stabilityai/stable-audio-open-small 15 | 16 | meta's new melodyflow model (terry) can be used inside this device now to transform input audio. it generates audio the same length as the input. you can have terry transform your recorded buffer or the outputs gary gives back (up to ~40 secs). terry's got 'presets' you can choose from. 17 | 18 | https://huggingface.co/spaces/facebook/melodyflow (you can play with terry here too) 19 | 20 | ## installation 21 | 22 | this repo was designed for the root directory to be placed in `C:\g4l` for maximum simplicity. dynamic filepaths are a pain in the ass in the max for live gui. sry. 23 | 24 | these paths can be changed with some effort (more instructions at the bottom) 25 | 26 | ### prerequisites 27 | 28 | **node.js** - install first: 29 | https://nodejs.org/en/download/ 30 | 31 | **ffmpeg** - required for audio cropping: 32 | 33 | 1. download ffmpeg for windows: https://ffmpeg.org/download.html 34 | 2. extract the downloaded folder 35 | 3. **rename the extracted folder to `ffmpeg`** 36 | 4. **move the entire `ffmpeg` folder to `C:\g4l\ffmpeg`** 37 | 5. the final structure should be `C:\g4l\ffmpeg\bin\ffmpeg.exe` 38 | 39 | ### setup 40 | 41 | clone this repository: 42 | ```bash 43 | git clone https://github.com/betweentwomidnights/gary4live 44 | ``` 45 | 46 | rename the directory: 47 | ```bash 48 | mv gary4live g4l 49 | ``` 50 | 51 | **install backend dependencies** (required for max for live communication): 52 | ```bash 53 | cd C:\g4l 54 | npm install 55 | npm audit fix 56 | ``` 57 | 58 | navigate to the frontend directory: 59 | ```bash 60 | cd C:\g4l\g4l-ui 61 | ``` 62 | 63 | **install frontend dependencies:** 64 | ```bash 65 | npm install 66 | npm install @types/react@latest @types/react-dom@latest 67 | npm install sass-loader@latest 68 | ``` 69 | 70 | ## security note 71 | 72 | when running `npm audit`, you may see 2 moderate vulnerabilities: 73 | 74 | - **electron <28.3.2**: affects image processing (we don't process user images) 75 | - **webpack-dev-server**: affects development mode only (not production builds) 76 | 77 | **we intentionally don't fix these because:** 78 | 1. `npm audit fix --force` breaks the electron app (requires major refactoring) 79 | 2. our app only communicates locally with ableton live (no internet access) 80 | 3. the vulnerabilities don't apply to our specific use case 81 | 82 | for a local music production tool, these pose minimal security risk. 83 | 84 | ## running the app 85 | 86 | to start the application in development mode: 87 | ```bash 88 | npm run start 89 | ``` 90 | 91 | to create the executable that the '2(launch electron)' button triggers in gary4live: 92 | ```bash 93 | npm run package 94 | ``` 95 | 96 | ## using with ableton 97 | 98 | in the ableton browser, add the g4l folder. 99 | 100 | `GARY.amxd` is the device. 101 | 102 | ## backend configuration 103 | 104 | by default, gary4live connects to our free hosted backend at `g4l.thecollabagepatch.com`. we run this for free and collect zero user data cuz that would be too much work. 105 | 106 | ### using localhost backend 107 | 108 | if you want to run the backend locally (requires **6gb+ gpu ram**, preferably 12gb+), you can build it from: https://github.com/betweentwomidnights/gary-backend-combined 109 | 110 | **to switch to localhost:** 111 | 112 | 1. open `commentedout.js` in your `C:\g4l` directory 113 | 2. find this configuration section: 114 | 115 | ```javascript 116 | // ============================================================================= 117 | // CONFIGURATION - Change these URLs when building from source with localhost 118 | // ============================================================================= 119 | 120 | // For localhost development with our github repo https://github.com/betweentwomidnights/gary-backend-combined 121 | // Uncomment these lines and comment out the production URLs below: 122 | // const BACKEND_URL = 'http://localhost:8000'; 123 | // const STABLE_AUDIO_HOST = 'localhost'; 124 | // const STABLE_AUDIO_PORT = 8005; 125 | // const STABLE_AUDIO_PATH = '/generate'; // Direct to service 126 | // const USE_HTTPS = false; 127 | 128 | // Production URLs (default): 129 | const BACKEND_URL = 'https://g4l.thecollabagepatch.com'; 130 | const STABLE_AUDIO_HOST = 'g4l.thecollabagepatch.com'; 131 | const STABLE_AUDIO_PORT = 443; 132 | const STABLE_AUDIO_PATH = '/audio/generate'; // Through Caddy proxy 133 | const USE_HTTPS = true; 134 | ``` 135 | 136 | 3. **comment out the production urls and uncomment the localhost urls** 137 | 138 | ## troubleshooting 139 | 140 | **ffmpeg not found error:** 141 | - make sure ffmpeg is extracted to `C:\g4l\ffmpeg\bin\ffmpeg.exe` 142 | - alternatively, install ffmpeg system-wide via chocolatey: `choco install ffmpeg` 143 | 144 | **generations stuck at 80%:** 145 | - check file permissions on `myBuffer.wav` and `myOutput.wav` 146 | - avoid manually editing these files 147 | 148 | **electron app won't start:** 149 | - make sure you ran the updated npm install commands 150 | - don't run `npm audit fix --force` as it breaks the app 151 | 152 | **mac users:** 153 | - there's a separate repository for the mac front-end: https://github.com/betweentwomidnights/gary-mac 154 | 155 | ## advanced usage 156 | 157 | if you want to change filepaths, the two buttons in the m4l gui need to be changed, as well as all the filepaths for `myBuffer.wav` and `myOutput.wav` in 'electron-communication.js' and 'commentedout.js' 158 | 159 | warning: you are going to give yourself headaches. max rly likes to gaslight. 160 | 161 | the final remaining edge cases revolve around these two files: `myBuffer.wav` and `myOutput.wav`. if generations get stuck at 80%, or you do not see the 'write' button actually changing `myBuffer.wav`, you have hit this issue with permissions. 162 | 163 | **do NOT:** 164 | - drag them into the timeline (the 'drag me' option in electron is fine. that's a copy of myOutput.wav) 165 | - manually change `myBuffer.wav` to a 6 minute audio file. it should only be changed by our write function and should be a maximum of 30 seconds 166 | 167 | ## support 168 | 169 | if you rly wanna learn how to use this thing, head to discord https://discord.gg/VECkyXEnAd and yell at me @kev, or go to https://youtube.com/@thepatch_dev 170 | 171 | ## related repositories 172 | 173 | - backend: https://github.com/betweentwomidnights/gary-backend-combined 174 | - mac frontend: https://github.com/betweentwomidnights/gary-mac -------------------------------------------------------------------------------- /g4l-ui/src/main/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, no-console: off, promise/always-return: off */ 2 | 3 | /** 4 | * This module executes inside of electron's main process. You can start 5 | * electron renderer process from here and communicate with the other processes 6 | * through IPC. 7 | * 8 | * When running `npm run build` or `npm run build:main`, this file is compiled to 9 | * `./src/main.js` using webpack. This gives us some performance wins. 10 | */ 11 | import path from 'path'; 12 | import * as fs from 'fs'; 13 | import { app, BrowserWindow, shell, ipcMain } from 'electron'; 14 | import { autoUpdater } from 'electron-updater'; 15 | import log from 'electron-log'; 16 | import WebSocket, { WebSocketServer } from 'ws'; 17 | import MenuBuilder from './menu'; 18 | import { resolveHtmlPath } from './util'; 19 | 20 | class AppUpdater { 21 | constructor() { 22 | log.transports.file.level = 'info'; 23 | autoUpdater.logger = log; 24 | autoUpdater.checkForUpdatesAndNotify(); 25 | } 26 | } 27 | 28 | let mainWindow: BrowserWindow | null = null; 29 | const wss = new WebSocketServer({ port: 8080 }); 30 | let connectedClient: WebSocket | null = null; 31 | 32 | wss.on('connection', function connection(ws) { 33 | console.log('WebSocket server: Client connected'); 34 | connectedClient = ws; 35 | 36 | // Convert string to Uint8Array before sending 37 | const connectionMessage = Buffer.from(JSON.stringify({ 38 | action: 'connection_status', 39 | data: true 40 | })); 41 | 42 | if (mainWindow) { 43 | mainWindow.webContents.send('fromNodeScript', connectionMessage); 44 | } 45 | 46 | ws.on('message', function incoming(message) { 47 | console.log('received: %s', message); 48 | if (mainWindow) { 49 | mainWindow.webContents.send('fromNodeScript', message); 50 | } 51 | }); 52 | 53 | ws.on('close', () => { 54 | console.log('WebSocket server: Client disconnected'); 55 | connectedClient = null; 56 | 57 | // Convert string to Uint8Array for disconnect message 58 | const disconnectionMessage = Buffer.from(JSON.stringify({ 59 | action: 'connection_status', 60 | data: false 61 | })); 62 | 63 | if (mainWindow) { 64 | mainWindow.webContents.send('fromNodeScript', disconnectionMessage); 65 | } 66 | }); 67 | }); 68 | 69 | function sendMessageToNodeScript(data: { action: string; data?: any }) { 70 | if (connectedClient && connectedClient.readyState === WebSocket.OPEN) { 71 | connectedClient.send(JSON.stringify(data)); // Send data as stringified JSON 72 | } 73 | } 74 | 75 | ipcMain.on('send-to-node-script', (_, payload) => { 76 | sendMessageToNodeScript(payload); // Ensure that `payload` is treated as an object within `sendMessageToNodeScript` 77 | }); 78 | 79 | ipcMain.on('ipc-example', async (event, arg) => { 80 | const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`; 81 | console.log(msgTemplate(arg)); 82 | event.reply('ipc-example', msgTemplate('pong')); 83 | }); 84 | 85 | // Function to ensure directory exists 86 | const ensureDirectoryExists = (filePath: string) => { 87 | const dir = path.dirname(filePath); 88 | if (!fs.existsSync(dir)) { 89 | fs.mkdirSync(dir, { recursive: true }); 90 | } 91 | }; 92 | 93 | // Function to convert base64 to a file 94 | const saveBase64AudioToFile = (base64Data: string, filePath: string) => { 95 | const buffer = Buffer.from(base64Data, 'base64'); 96 | ensureDirectoryExists(filePath); // Ensure directory exists 97 | try { 98 | fs.writeFileSync(filePath, buffer); 99 | console.log(`File saved to ${filePath}`); 100 | } catch (err) { 101 | console.error('Error saving file:', err); 102 | } 103 | }; 104 | 105 | // Generate a unique filename using a timestamp 106 | const generateUniqueFilename = (basePath: string, extension: string) => { 107 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // Format timestamp 108 | return path.join(basePath, `audio_${timestamp}.${extension}`); 109 | }; 110 | 111 | // Listen for the 'save-audio-file' IPC message 112 | ipcMain.on('save-audio-file', (event, base64Data) => { 113 | const projectPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '../../'); 114 | const filePath = generateUniqueFilename(path.join(projectPath, 'saved-audio'), 'wav'); // Generate unique filename 115 | console.log(`Saving file to ${filePath}`); 116 | saveBase64AudioToFile(base64Data, filePath); 117 | event.sender.send('audio-file-saved', filePath); 118 | }); 119 | 120 | if (process.env.NODE_ENV === 'production') { 121 | const sourceMapSupport = require('source-map-support'); 122 | sourceMapSupport.install(); 123 | } 124 | 125 | const isDebug = 126 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 127 | 128 | if (isDebug) { 129 | require('electron-debug')(); 130 | } 131 | 132 | const installExtensions = async () => { 133 | const installer = require('electron-devtools-installer'); 134 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 135 | const extensions = ['REACT_DEVELOPER_TOOLS']; 136 | 137 | return installer 138 | .default( 139 | extensions.map((name) => installer[name]), 140 | forceDownload, 141 | ) 142 | .catch(console.log); 143 | }; 144 | 145 | const createWindow = async () => { 146 | if (isDebug) { 147 | await installExtensions(); 148 | } 149 | 150 | const RESOURCES_PATH = app.isPackaged 151 | ? path.join(process.resourcesPath, 'assets') 152 | : path.join(__dirname, '../../assets'); 153 | 154 | const getAssetPath = (...paths: string[]): string => { 155 | return path.join(RESOURCES_PATH, ...paths); 156 | }; 157 | 158 | mainWindow = new BrowserWindow({ 159 | show: false, 160 | width: 670, 161 | height: 480, 162 | icon: getAssetPath('icon.png'), 163 | webPreferences: { 164 | nodeIntegration: true, // Enable Node.js integration 165 | preload: app.isPackaged 166 | ? path.join(__dirname, 'preload.js') 167 | : path.join(__dirname, '../../.erb/dll/preload.js'), 168 | }, 169 | alwaysOnTop: true, 170 | }); 171 | 172 | mainWindow.loadURL(resolveHtmlPath('index.html')); 173 | 174 | mainWindow.on('ready-to-show', () => { 175 | if (!mainWindow) { 176 | throw new Error('"mainWindow" is not defined'); 177 | } 178 | if (process.env.START_MINIMIZED) { 179 | mainWindow.minimize(); 180 | } else { 181 | mainWindow.show(); 182 | } 183 | }); 184 | 185 | mainWindow.on('closed', () => { 186 | mainWindow = null; 187 | }); 188 | 189 | ipcMain.on('ondragstart', (event, filePath) => { 190 | console.log(`Dragging file from ${filePath}`); 191 | const iconPath = getAssetPath('icon.ico'); 192 | console.log(`Using icon at ${iconPath}`); 193 | 194 | if (!fs.existsSync(filePath)) { 195 | console.error(`File does not exist at ${filePath}`); 196 | return; 197 | } 198 | 199 | if (!fs.existsSync(iconPath)) { 200 | console.error(`Icon does not exist at ${iconPath}`); 201 | return; 202 | } 203 | 204 | try { 205 | event.sender.startDrag({ 206 | file: filePath, 207 | icon: iconPath, 208 | }); 209 | console.log('Drag operation started successfully.'); 210 | } catch (error) { 211 | console.error('Error starting drag operation:', error); 212 | } 213 | }); 214 | 215 | const menuBuilder = new MenuBuilder(mainWindow); 216 | menuBuilder.buildMenu(); 217 | 218 | mainWindow.webContents.setWindowOpenHandler((edata) => { 219 | shell.openExternal(edata.url); 220 | return { action: 'deny' }; 221 | }); 222 | // eslint-disable-next-line no-new 223 | new AppUpdater(); 224 | }; 225 | 226 | app.on('window-all-closed', () => { 227 | if (process.platform !== 'darwin') { 228 | app.quit(); 229 | } 230 | }); 231 | 232 | app 233 | .whenReady() 234 | .then(() => { 235 | createWindow(); 236 | app.on('activate', () => { 237 | if (mainWindow === null) createWindow(); 238 | }); 239 | }) 240 | .catch(console.log); 241 | -------------------------------------------------------------------------------- /g4l-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "gary4live - musicgen continuations inside ableton", 3 | "keywords": [ 4 | "electron", 5 | "max-for-live", 6 | "music production", 7 | "ableton", 8 | "interactive" 9 | ], 10 | "homepage": "https://thecollabagepatch.com", 11 | "bugs": { 12 | "url": "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/electron-react-boilerplate/electron-react-boilerplate.git" 17 | }, 18 | "license": "MIT", 19 | "author": { 20 | "name": "Kevin Griffing", 21 | "email": "kev@thecollabagepatch.com", 22 | "url": "https://thecollabagepatch.com" 23 | }, 24 | "contributors": [ 25 | { 26 | "name": "Amila Welihinda", 27 | "email": "amilajack@gmail.com", 28 | "url": "https://github.com/amilajack" 29 | } 30 | ], 31 | "main": "./src/main/main.ts", 32 | "scripts": { 33 | "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", 34 | "build:dll": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts", 35 | "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts", 36 | "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts", 37 | "postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll", 38 | "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx", 39 | "package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll", 40 | "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", 41 | "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer", 42 | "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", 43 | "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", 44 | "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", 45 | "test": "jest" 46 | }, 47 | "browserslist": [], 48 | "prettier": { 49 | "singleQuote": true, 50 | "overrides": [ 51 | { 52 | "files": [ 53 | ".prettierrc", 54 | ".eslintrc" 55 | ], 56 | "options": { 57 | "parser": "json" 58 | } 59 | } 60 | ] 61 | }, 62 | "jest": { 63 | "moduleDirectories": [ 64 | "node_modules", 65 | "release/app/node_modules", 66 | "src" 67 | ], 68 | "moduleFileExtensions": [ 69 | "js", 70 | "jsx", 71 | "ts", 72 | "tsx", 73 | "json" 74 | ], 75 | "moduleNameMapper": { 76 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js", 77 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 78 | }, 79 | "setupFiles": [ 80 | "./.erb/scripts/check-build-exists.ts" 81 | ], 82 | "testEnvironment": "jsdom", 83 | "testEnvironmentOptions": { 84 | "url": "http://localhost/" 85 | }, 86 | "testPathIgnorePatterns": [ 87 | "release/app/dist", 88 | ".erb/dll" 89 | ], 90 | "transform": { 91 | "\\.(ts|tsx|js|jsx)$": "ts-jest" 92 | } 93 | }, 94 | "dependencies": { 95 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 96 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 97 | "@fortawesome/react-fontawesome": "^0.2.1", 98 | "electron-debug": "^3.2.0", 99 | "electron-log": "^4.4.8", 100 | "electron-updater": "^6.1.4", 101 | "lucide-react": "^0.471.0", 102 | "react": "^18.2.0", 103 | "react-dom": "^18.2.0", 104 | "react-router-dom": "^6.16.0", 105 | "wavesurfer.js": "^7.7.14", 106 | "ws": "^8.17.0" 107 | }, 108 | "devDependencies": { 109 | "@electron/notarize": "^2.1.0", 110 | "@electron/rebuild": "^3.3.0", 111 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", 112 | "@svgr/webpack": "^8.1.0", 113 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.2", 114 | "@testing-library/jest-dom": "^6.1.3", 115 | "@testing-library/react": "^14.0.0", 116 | "@types/electron": "^1.6.10", 117 | "@types/fluent-ffmpeg": "^2.1.24", 118 | "@types/jest": "^29.5.5", 119 | "@types/node": "20.6.2", 120 | "@types/react": "^19.1.8", 121 | "@types/react-dom": "^19.1.6", 122 | "@types/react-test-renderer": "^18.0.1", 123 | "@types/terser-webpack-plugin": "^5.0.4", 124 | "@types/webpack-bundle-analyzer": "^4.6.0", 125 | "@typescript-eslint/eslint-plugin": "^6.7.0", 126 | "@typescript-eslint/parser": "^6.7.0", 127 | "browserslist-config-erb": "^0.0.3", 128 | "chalk": "^4.1.2", 129 | "concurrently": "^8.2.1", 130 | "core-js": "^3.32.2", 131 | "cross-env": "^7.0.3", 132 | "css-loader": "^6.8.1", 133 | "css-minimizer-webpack-plugin": "^5.0.1", 134 | "detect-port": "^1.5.1", 135 | "electron": "^26.2.1", 136 | "electron-builder": "^24.13.3", 137 | "electron-devtools-installer": "^3.2.0", 138 | "electronmon": "^2.0.2", 139 | "eslint": "^8.49.0", 140 | "eslint-config-airbnb-base": "^15.0.0", 141 | "eslint-config-erb": "^4.1.0-0", 142 | "eslint-import-resolver-typescript": "^3.6.0", 143 | "eslint-import-resolver-webpack": "^0.13.7", 144 | "eslint-plugin-compat": "^4.2.0", 145 | "eslint-plugin-import": "^2.28.1", 146 | "eslint-plugin-jest": "^27.4.0", 147 | "eslint-plugin-jsx-a11y": "^6.7.1", 148 | "eslint-plugin-promise": "^6.1.1", 149 | "eslint-plugin-react": "^7.33.2", 150 | "eslint-plugin-react-hooks": "^4.6.0", 151 | "file-loader": "^6.2.0", 152 | "html-webpack-plugin": "^5.5.3", 153 | "identity-obj-proxy": "^3.0.0", 154 | "jest": "^29.7.0", 155 | "jest-environment-jsdom": "^29.7.0", 156 | "mini-css-extract-plugin": "^2.7.6", 157 | "prettier": "^3.0.3", 158 | "react-refresh": "^0.14.0", 159 | "react-test-renderer": "^18.2.0", 160 | "rimraf": "^5.0.1", 161 | "sass": "^1.67.0", 162 | "sass-loader": "^16.0.5", 163 | "style-loader": "^3.3.3", 164 | "terser-webpack-plugin": "^5.3.9", 165 | "ts-jest": "^29.1.1", 166 | "ts-loader": "^9.4.4", 167 | "ts-node": "^10.9.1", 168 | "tsconfig-paths-webpack-plugin": "^4.1.0", 169 | "typescript": "^5.2.2", 170 | "url-loader": "^4.1.1", 171 | "webpack": "^5.88.2", 172 | "webpack-bundle-analyzer": "^4.9.1", 173 | "webpack-cli": "^5.1.4", 174 | "webpack-dev-server": "^4.15.1", 175 | "webpack-merge": "^5.9.0" 176 | }, 177 | "build": { 178 | "productName": "gary4live", 179 | "appId": "com.thecollabagepatch.gary4live", 180 | "asar": true, 181 | "asarUnpack": "**\\*.{node,dll}", 182 | "files": [ 183 | "dist", 184 | "node_modules", 185 | "package.json" 186 | ], 187 | "afterSign": ".erb/scripts/notarize.js", 188 | "mac": { 189 | "target": { 190 | "target": "default", 191 | "arch": [ 192 | "arm64", 193 | "x64" 194 | ] 195 | }, 196 | "type": "distribution", 197 | "hardenedRuntime": true, 198 | "entitlements": "assets/entitlements.mac.plist", 199 | "entitlementsInherit": "assets/entitlements.mac.plist", 200 | "gatekeeperAssess": false 201 | }, 202 | "dmg": { 203 | "contents": [ 204 | { 205 | "x": 130, 206 | "y": 220 207 | }, 208 | { 209 | "x": 410, 210 | "y": 220, 211 | "type": "link", 212 | "path": "/Applications" 213 | } 214 | ] 215 | }, 216 | "win": { 217 | "target": [ 218 | "nsis" 219 | ] 220 | }, 221 | "linux": { 222 | "target": [ 223 | "AppImage" 224 | ], 225 | "category": "Development" 226 | }, 227 | "directories": { 228 | "app": "release/app", 229 | "buildResources": "assets", 230 | "output": "release/build" 231 | }, 232 | "extraResources": [ 233 | "./assets/**" 234 | ], 235 | "publish": { 236 | "provider": "github", 237 | "owner": "betweentwomidnights", 238 | "repo": "gary4live-frontend" 239 | } 240 | }, 241 | "collective": { 242 | "url": "https://thecollabagepatch.com" 243 | }, 244 | "devEngines": { 245 | "node": ">=14.x", 246 | "npm": ">=7.x" 247 | }, 248 | "electronmon": { 249 | "patterns": [ 250 | "!**/**", 251 | "src/main/**" 252 | ], 253 | "logLevel": "quiet" 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /g4l-ui/src/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | Menu, 4 | shell, 5 | BrowserWindow, 6 | MenuItemConstructorOptions, 7 | MenuItem, 8 | } from 'electron'; 9 | 10 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { 11 | selector?: string; 12 | submenu?: DarwinMenuItemConstructorOptions[] | Menu; 13 | } 14 | 15 | export default class MenuBuilder { 16 | mainWindow: BrowserWindow; 17 | 18 | constructor(mainWindow: BrowserWindow) { 19 | this.mainWindow = mainWindow; 20 | } 21 | 22 | buildMenu(): Menu { 23 | if ( 24 | process.env.NODE_ENV === 'development' || 25 | process.env.DEBUG_PROD === 'true' 26 | ) { 27 | this.setupDevelopmentEnvironment(); 28 | } 29 | 30 | const template: MenuItemConstructorOptions[] = 31 | process.platform === 'darwin' 32 | ? this.buildDarwinTemplate() 33 | : this.buildDefaultTemplate(); 34 | 35 | const menu = Menu.buildFromTemplate(template); 36 | Menu.setApplicationMenu(menu); 37 | 38 | return menu; 39 | } 40 | 41 | setupDevelopmentEnvironment(): void { 42 | this.mainWindow.webContents.on('context-menu', (_, props) => { 43 | const { x, y } = props; 44 | 45 | Menu.buildFromTemplate([ 46 | { 47 | label: 'Inspect element', 48 | click: () => { 49 | this.mainWindow.webContents.inspectElement(x, y); 50 | }, 51 | }, 52 | ]).popup({ window: this.mainWindow }); 53 | }); 54 | } 55 | 56 | buildDarwinTemplate(): MenuItemConstructorOptions[] { 57 | const subMenuAbout: DarwinMenuItemConstructorOptions = { 58 | label: 'Electron', 59 | submenu: [ 60 | { 61 | label: 'About ElectronReact', 62 | selector: 'orderFrontStandardAboutPanel:', 63 | }, 64 | { type: 'separator' }, 65 | { label: 'Services', submenu: [] }, 66 | { type: 'separator' }, 67 | { 68 | label: 'Hide ElectronReact', 69 | accelerator: 'Command+H', 70 | selector: 'hide:', 71 | }, 72 | { 73 | label: 'Hide Others', 74 | accelerator: 'Command+Shift+H', 75 | selector: 'hideOtherApplications:', 76 | }, 77 | { label: 'Show All', selector: 'unhideAllApplications:' }, 78 | { type: 'separator' }, 79 | { 80 | label: 'Quit', 81 | accelerator: 'Command+Q', 82 | click: () => { 83 | app.quit(); 84 | }, 85 | }, 86 | ], 87 | }; 88 | const subMenuEdit: DarwinMenuItemConstructorOptions = { 89 | label: 'Edit', 90 | submenu: [ 91 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 92 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 93 | { type: 'separator' }, 94 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 95 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 96 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 97 | { 98 | label: 'Select All', 99 | accelerator: 'Command+A', 100 | selector: 'selectAll:', 101 | }, 102 | ], 103 | }; 104 | const subMenuViewDev: MenuItemConstructorOptions = { 105 | label: 'View', 106 | submenu: [ 107 | { 108 | label: 'Reload', 109 | accelerator: 'Command+R', 110 | click: () => { 111 | this.mainWindow.webContents.reload(); 112 | }, 113 | }, 114 | { 115 | label: 'Toggle Full Screen', 116 | accelerator: 'Ctrl+Command+F', 117 | click: () => { 118 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 119 | }, 120 | }, 121 | { 122 | label: 'Toggle Developer Tools', 123 | accelerator: 'Alt+Command+I', 124 | click: () => { 125 | this.mainWindow.webContents.toggleDevTools(); 126 | }, 127 | }, 128 | ], 129 | }; 130 | const subMenuViewProd: MenuItemConstructorOptions = { 131 | label: 'View', 132 | submenu: [ 133 | { 134 | label: 'Toggle Full Screen', 135 | accelerator: 'Ctrl+Command+F', 136 | click: () => { 137 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 138 | }, 139 | }, 140 | ], 141 | }; 142 | const subMenuWindow: DarwinMenuItemConstructorOptions = { 143 | label: 'Window', 144 | submenu: [ 145 | { 146 | label: 'Minimize', 147 | accelerator: 'Command+M', 148 | selector: 'performMiniaturize:', 149 | }, 150 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 151 | { type: 'separator' }, 152 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 153 | ], 154 | }; 155 | const subMenuHelp: MenuItemConstructorOptions = { 156 | label: 'Help', 157 | submenu: [ 158 | { 159 | label: 'show guide', 160 | type: 'checkbox', 161 | checked: false, 162 | click: (menuItem) => { 163 | const isChecked = menuItem.checked; 164 | this.mainWindow.webContents.send('toggle-guide', isChecked); 165 | }, 166 | }, 167 | { 168 | label: 'hear stuff made using this', 169 | click() { 170 | shell.openExternal('https://youtube.com/@thecollabagepatch'); 171 | }, 172 | }, 173 | { 174 | label: 'github', 175 | click() { 176 | shell.openExternal( 177 | 'https://github.com/betweentwomidnights/gary4live', 178 | ); 179 | }, 180 | }, 181 | { 182 | label: 'community - discord', 183 | click() { 184 | shell.openExternal('https://discord.gg/VECkyXEnAd'); 185 | }, 186 | }, 187 | // { 188 | // label: 'Search Issues', 189 | // click() { 190 | // shell.openExternal('https://github.com/electron/electron/issues'); 191 | // }, 192 | // }, 193 | ], 194 | }; 195 | 196 | const subMenuView = 197 | process.env.NODE_ENV === 'development' || 198 | process.env.DEBUG_PROD === 'true' 199 | ? subMenuViewDev 200 | : subMenuViewProd; 201 | 202 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 203 | } 204 | 205 | buildDefaultTemplate() { 206 | const templateDefault = [ 207 | { 208 | label: '&File', 209 | submenu: [ 210 | { 211 | label: '&Open', 212 | accelerator: 'Ctrl+O', 213 | }, 214 | { 215 | label: '&Close', 216 | accelerator: 'Ctrl+W', 217 | click: () => { 218 | this.mainWindow.close(); 219 | }, 220 | }, 221 | ], 222 | }, 223 | { 224 | label: '&View', 225 | submenu: 226 | process.env.NODE_ENV === 'development' || 227 | process.env.DEBUG_PROD === 'true' 228 | ? [ 229 | { 230 | label: '&Reload', 231 | accelerator: 'Ctrl+R', 232 | click: () => { 233 | this.mainWindow.webContents.reload(); 234 | }, 235 | }, 236 | { 237 | label: 'Toggle &Full Screen', 238 | accelerator: 'F11', 239 | click: () => { 240 | this.mainWindow.setFullScreen( 241 | !this.mainWindow.isFullScreen(), 242 | ); 243 | }, 244 | }, 245 | { 246 | label: 'Toggle &Developer Tools', 247 | accelerator: 'Alt+Ctrl+I', 248 | click: () => { 249 | this.mainWindow.webContents.toggleDevTools(); 250 | }, 251 | }, 252 | ] 253 | : [ 254 | { 255 | label: 'Toggle &Full Screen', 256 | accelerator: 'F11', 257 | click: () => { 258 | this.mainWindow.setFullScreen( 259 | !this.mainWindow.isFullScreen(), 260 | ); 261 | }, 262 | }, 263 | ], 264 | }, 265 | { 266 | label: 'Help', 267 | submenu: [ 268 | { 269 | label: 'show guide', 270 | type: 'checkbox', 271 | checked: false, 272 | click: ( 273 | menuItem: MenuItem, 274 | _browserWindow: BrowserWindow, 275 | _event: KeyboardEvent 276 | ) => { 277 | const isChecked = menuItem.checked; 278 | this.mainWindow.webContents.send('toggle-guide', isChecked); 279 | }, 280 | }, 281 | { 282 | label: 'hear stuff made using this', 283 | click() { 284 | shell.openExternal('https://youtube.com/@thecollabagepatch'); 285 | }, 286 | }, 287 | { 288 | label: 'github', 289 | click() { 290 | shell.openExternal( 291 | 'https://github.com/betweentwomidnights/gary4live', 292 | ); 293 | }, 294 | }, 295 | { 296 | label: 'community - discord', 297 | click() { 298 | shell.openExternal('https://discord.gg/VECkyXEnAd'); 299 | }, 300 | }, 301 | // { 302 | // label: 'Search Issues', 303 | // click() { 304 | // shell.openExternal('https://github.com/electron/electron/issues'); 305 | // }, 306 | // }, 307 | ], 308 | }, 309 | ]; 310 | 311 | return templateDefault; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /g4l-ui/src/renderer/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | 6 | .logo.spin img { 7 | animation: spin 10s linear infinite; /* Adjust the duration as needed */ 8 | } 9 | 10 | @keyframes spin { 11 | from { 12 | transform: rotate(0deg); 13 | } 14 | to { 15 | transform: rotate(360deg); 16 | } 17 | } 18 | 19 | body { 20 | position: relative; 21 | color: white; 22 | height: 95vh; 23 | background: black; 24 | font-family: 'Arial Rounded MT'; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | overflow-x: hidden; /* Prevent horizontal scrolling */ 29 | overflow-y: hidden; 30 | } 31 | 32 | button { 33 | background-color: black; 34 | color: white; 35 | padding: 0px 2px; 36 | border-radius: 5px; 37 | border-color: red; 38 | border: none; 39 | appearance: none; 40 | font-size: 1.3rem; 41 | font-family: 'Agency FB'; 42 | box-shadow: none; 43 | transition: all ease-in 0.1s; 44 | cursor: pointer; 45 | opacity: 0.9; 46 | } 47 | 48 | button:hover { 49 | transform: scale(1.05); 50 | opacity: 1; 51 | } 52 | 53 | /* Add this CSS to your App.css for disabled button styling */ 54 | button:disabled { 55 | opacity: 0.4; 56 | cursor: not-allowed; 57 | transform: none !important; /* Prevent hover scale effect */ 58 | } 59 | 60 | button:disabled:hover { 61 | opacity: 0.4; /* Keep same opacity on hover */ 62 | transform: none !important; 63 | } 64 | 65 | li { 66 | list-style: none; 67 | } 68 | 69 | a { 70 | text-decoration: none; 71 | height: fit-content; 72 | width: fit-content; 73 | margin: 2px; 74 | } 75 | 76 | a:hover { 77 | opacity: 1; 78 | text-decoration: none; 79 | } 80 | 81 | .Hello { 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | margin: 1px 0; 86 | } 87 | 88 | .indicator { 89 | margin-top: 10px; 90 | } 91 | 92 | .recording-indicator { 93 | width: 21px; 94 | height: 21px; 95 | background-color: red; 96 | border-radius: 50%; 97 | } 98 | 99 | .idle-indicator { 100 | width: 21px; 101 | height: 21px; 102 | background-color: grey; 103 | border-radius: 50%; 104 | } 105 | 106 | /* Container styling for centering */ 107 | .logo { 108 | position: absolute; /* Take it out of the normal flow */ 109 | left: 50%; /* Center horizontally */ 110 | transform: translateX(-50%); /* Offset by its own width */ 111 | top: 30px; /* Position from top */ 112 | margin: 0; 113 | z-index: -1; /* Place behind other elements */ 114 | } 115 | 116 | .logo img { 117 | width: 150px; /* Set fixed width */ 118 | height: auto; 119 | display: block; 120 | animation: rotate 20s linear infinite; 121 | } 122 | 123 | .timeline { 124 | display: flex; 125 | justify-content: space-between; 126 | align-items: center; 127 | margin-top: 0px; 128 | margin-bottom: 5px; 129 | position: relative; 130 | height: 15px; /* Adjust height for better visibility of ticks */ 131 | } 132 | 133 | .timeline-tick { 134 | position: relative; 135 | flex-grow: 1; 136 | text-align: center; 137 | } 138 | 139 | .timeline-label { 140 | font-size: 12px; 141 | color: white; /* Change label color to white */ 142 | position: absolute; 143 | bottom: -15px; 144 | left: 50%; 145 | transform: translateX(-50%); 146 | } 147 | 148 | .timeline-mark { 149 | height: 10px; 150 | width: 1px; 151 | background-color: white; /* Change tick mark color to white */ 152 | display: inline-block; 153 | position: absolute; 154 | bottom: 0; 155 | left: 50%; 156 | transform: translateX(-50%); 157 | } 158 | 159 | .timeline-tick::before { 160 | content: ''; 161 | position: absolute; 162 | bottom: 0; 163 | left: 50%; 164 | transform: translateX(-50%); 165 | height: 5px; /* Height for tick marks for seconds */ 166 | width: 1px; 167 | background-color: white; /* Change tick mark color to white */ 168 | } 169 | 170 | .timeline-tick:nth-child(5n + 1)::before { 171 | height: 10px; /* Longer tick marks for every 5 seconds */ 172 | } 173 | 174 | 175 | .waveform { 176 | margin-bottom: 5px; /* Adjust spacing between first waveform and timeline */ 177 | } 178 | 179 | .waveform-out { 180 | margin-top: 5px; /* Adjust spacing between timeline and second waveform */ 181 | } 182 | 183 | /* Control Wrapper */ 184 | .control-wrapper { 185 | position: relative; 186 | display: inline-block; 187 | margin: 2px; 188 | } 189 | 190 | /* Indicator Wrapper */ 191 | .indicator-wrapper { 192 | position: relative; 193 | display: inline-block; 194 | margin-top: 70px; 195 | z-index: 12000; 196 | } 197 | 198 | /* Guide Number */ 199 | .guide-number { 200 | position: absolute; 201 | top: -10px; 202 | right: -10px; 203 | background-color: rgba(0, 0, 0, 0.479); 204 | color: white; 205 | border: 2px solid rgb(255, 0, 0); 206 | padding: 5px; 207 | cursor: pointer; 208 | font-weight: bold; 209 | z-index: 1000; 210 | } 211 | 212 | /* Tooltip */ 213 | .tooltip { 214 | position: absolute; 215 | background-color: #000000; 216 | color: white; 217 | padding: 5px; 218 | border-radius: 3px; 219 | white-space: pre-wrap; 220 | width: 250px; 221 | z-index: 4020; 222 | font-size: 12px; 223 | 224 | max-height: 400px; /* Optional default max height */ 225 | } 226 | 227 | 228 | .backend-status { 229 | margin-top: 20px; 230 | text-align: center; 231 | font-size: 14px; 232 | } 233 | 234 | .backend-status a { 235 | color: #b42020; 236 | text-decoration: none; 237 | } 238 | 239 | .backend-status a:hover { 240 | text-decoration: underline; 241 | } 242 | 243 | .gary-toggle-wrapper { 244 | position: relative; 245 | display: inline-block; 246 | margin: 2px; 247 | margin-left: 2px; /* Keep the reduced margin from before */ 248 | margin-right: 0px; /* Add some space to the right but not too much */ 249 | margin-bottom: -80px; 250 | } 251 | 252 | /* Style for the toggle button itself */ 253 | .gary-toggle { 254 | border: 1px solid red; /* Match your existing button style */ 255 | background-color: black; 256 | color: white; 257 | transition: all 0.2s ease; 258 | } 259 | 260 | /* Style for when Gary is tamed */ 261 | .gary-toggle.active { 262 | background-color: #b42020; /* Match your link hover color */ 263 | color: white; 264 | } 265 | 266 | .top-row { 267 | display: flex; 268 | justify-content: flex-start; /* Changed from space-between to align left */ 269 | align-items: center; 270 | width: 100%; 271 | margin-bottom: 10px; 272 | margin-top: 40px; 273 | } 274 | 275 | .left-controls { 276 | display: flex; 277 | align-items: center; 278 | gap: 0px; /* Reduced gap to bring fix_toggle closer to indicator */ 279 | } 280 | 281 | .gary-toggle-wrapper { 282 | margin-left: 8px; /* Reduced margin to bring tame gary closer */ 283 | } 284 | 285 | /* Transform buttons group */ 286 | .transform-controls { 287 | display: flex; 288 | align-items: center; 289 | gap: 8px; 290 | margin-left: auto; /* This pushes it to the right */ 291 | position: relative; /* Add this */ 292 | } 293 | 294 | /* Add these new classes */ 295 | .transform-button, 296 | .undo-transform-button, 297 | .reset-transform-button { 298 | position: relative; /* Add this */ 299 | } 300 | 301 | .transform-button .guide-number, 302 | .undo-transform-button .guide-number, 303 | .reset-transform-button .guide-number { 304 | top: -25px; /* Adjust as needed */ 305 | right: -20px; 306 | margin-right: 20px; 307 | z-index: 9000; 308 | } 309 | 310 | /* Progress and variation select row */ 311 | .progress-row { 312 | display: flex; 313 | align-items: center; 314 | justify-content: space-between; 315 | width: 100%; 316 | margin-top: 5px; /* Pull it up closer to the top controls */ 317 | margin-bottom: 10px; 318 | position: relative; 319 | z-index: 1; 320 | } 321 | 322 | /* Style the dropdown differently */ 323 | .variation-select { 324 | background-color: #b42020; /* Solid red background */ 325 | color: white; 326 | border: 1px solid red; 327 | padding: 4px 4px; 328 | border-radius: 5px; 329 | font-family: 'Agency FB'; 330 | font-size: 1.3rem; 331 | cursor: pointer; 332 | z-index: 1; 333 | margin-top: -20px; 334 | } 335 | 336 | .variation-select:hover { 337 | background-color: #d42525; /* Slightly lighter red on hover */ 338 | } 339 | 340 | .transform-button { 341 | border: 1px solid red; /* Match your existing button style */ 342 | } 343 | 344 | .flowstep-control { 345 | display: flex; 346 | flex-direction: column; 347 | align-items: center; 348 | margin-bottom: 0rem; 349 | width: 100%; 350 | } 351 | 352 | .flowstep-slider { 353 | width: 100%; 354 | height: 4px; 355 | -webkit-appearance: none; 356 | background: #444; 357 | border-radius: 2px; 358 | outline: none; 359 | margin: 5px 0; 360 | } 361 | 362 | .flowstep-slider::-webkit-slider-thumb { 363 | -webkit-appearance: none; 364 | appearance: none; 365 | width: 10px; 366 | height: 10px; 367 | border-radius: 50%; 368 | background: #ff0000; 369 | cursor: pointer; 370 | transition: background 0.15s ease; 371 | } 372 | 373 | .flowstep-slider::-webkit-slider-thumb:hover { 374 | background: #cc0000; 375 | } 376 | 377 | .flowstep-label { 378 | font-size: 0.7rem; 379 | color: #666; 380 | margin-top: 4px; 381 | } 382 | 383 | /* Ensure transform section maintains position */ 384 | .transform-section { 385 | display: flex; 386 | flex-direction: column; 387 | align-items: flex-end; 388 | margin-left: auto; 389 | gap: 10px; 390 | position: relative; /* Ensure it stays above the logo */ 391 | z-index: 10000; 392 | } 393 | 394 | .prompt-wrapper { 395 | display: flex; 396 | flex-direction: column; 397 | gap: 5px; 398 | margin-bottom: 50px; 399 | position: absolute; 400 | } 401 | 402 | .prompt-label { 403 | font-size: 10px; 404 | color: #a5a5a5; 405 | font-weight: bold; 406 | } 407 | 408 | .prompt-input { 409 | padding: 6px 8px; 410 | border: 1px solid #ccc; 411 | border-radius: 4px; 412 | font-size: 12px; 413 | width: 200px; 414 | background: #fff; 415 | } 416 | 417 | .prompt-input:focus { 418 | outline: none; 419 | border-color: #007bff; 420 | box-shadow: 0 0 3px rgba(0, 123, 255, 0.3); 421 | } 422 | 423 | .generate-button { 424 | padding: 6px 12px; 425 | background: #ec0000; 426 | color: white; 427 | border: none; 428 | border-radius: 4px; 429 | font-size: 12px; 430 | cursor: pointer; 431 | font-weight: bold; 432 | } 433 | 434 | .generate-button:hover { 435 | background: #d40000; 436 | } 437 | 438 | .generate-button:active { 439 | background: #eb0800; 440 | } 441 | 442 | .waveform-highlighted { 443 | border: 3px solid #ffffff !important; 444 | border-radius: 4px; 445 | box-shadow: 0 0 10px rgb(255, 255, 255); 446 | background-color: rgba(0, 123, 255, 0.1); 447 | transition: all 0.2s ease-in-out; 448 | } 449 | 450 | /* Optional: Make the transition smoother */ 451 | .waveform, .waveform-out { 452 | transition: all 0.2s ease-in-out; 453 | border: 3px solid transparent; /* Invisible border to prevent layout shift */ 454 | } 455 | 456 | /* Optional: Different colors for top vs bottom */ 457 | .waveform.waveform-highlighted { 458 | border-color: #ffbebe; /* Green for input/top */ 459 | box-shadow: 0 0 10px rgba(40, 167, 69, 0.5); 460 | background-color: rgba(40, 167, 69, 0.1); 461 | } 462 | 463 | .waveform-out.waveform-highlighted { 464 | border-color: #f8b9b9; /* Blue for output/bottom */ 465 | box-shadow: 0 0 10px rgba(0, 123, 255, 0.5); 466 | background-color: rgba(0, 123, 255, 0.1); 467 | } 468 | 469 | /* Add to your App.css */ 470 | 471 | .waveform-container { 472 | position: relative; 473 | /* inherits size from existing waveform */ 474 | } 475 | 476 | .recording-progress-overlay { 477 | position: absolute; 478 | top: 0; 479 | left: 0; 480 | right: 0; 481 | bottom: 0; 482 | pointer-events: none; /* Don't interfere with waveform interaction */ 483 | z-index: 2; 484 | } 485 | 486 | .recording-progress-outline { 487 | position: absolute; 488 | top: 0; 489 | left: 0; 490 | right: 0; 491 | bottom: 0; 492 | border: 2px dashed rgba(255, 0, 0, 0.4); /* Red dotted outline */ 493 | border-radius: 4px; 494 | background: 495 | /* Fake waveform pattern using CSS gradients */ 496 | repeating-linear-gradient( 497 | 90deg, 498 | transparent 0px, 499 | transparent 8px, 500 | rgba(255, 0, 0, 0.1) 8px, 501 | rgba(255, 0, 0, 0.1) 12px, 502 | transparent 12px, 503 | transparent 20px, 504 | rgba(255, 0, 0, 0.2) 20px, 505 | rgba(255, 0, 0, 0.2) 24px, 506 | transparent 24px, 507 | transparent 32px, 508 | rgba(255, 0, 0, 0.15) 32px, 509 | rgba(255, 0, 0, 0.15) 36px, 510 | transparent 36px, 511 | transparent 44px 512 | ); 513 | } 514 | 515 | .recording-progress-fill { 516 | position: absolute; 517 | top: 0; 518 | left: 0; 519 | bottom: 0; 520 | background: 521 | /* Filled version of the same pattern */ 522 | repeating-linear-gradient( 523 | 90deg, 524 | transparent 0px, 525 | transparent 8px, 526 | rgba(255, 0, 0, 0.3) 8px, 527 | rgba(255, 0, 0, 0.3) 12px, 528 | transparent 12px, 529 | transparent 20px, 530 | rgba(255, 0, 0, 0.5) 20px, 531 | rgba(255, 0, 0, 0.5) 24px, 532 | transparent 24px, 533 | transparent 32px, 534 | rgba(255, 0, 0, 0.4) 32px, 535 | rgba(255, 0, 0, 0.4) 36px, 536 | transparent 36px, 537 | transparent 44px 538 | ); 539 | border-right: 2px solid rgba(255, 0, 0, 0.8); /* Progress line */ 540 | transition: width 0.05s linear; /* Smooth fill animation */ 541 | border-radius: 2px 0 0 2px; 542 | } 543 | 544 | /* Alternative: More waveform-like pattern */ 545 | .recording-progress-fill.waveform-style { 546 | background: 547 | /* Simulated waveform peaks and valleys */ 548 | linear-gradient(180deg, 549 | transparent 0%, 550 | transparent 20%, 551 | rgba(255, 0, 0, 0.2) 20%, 552 | rgba(255, 0, 0, 0.4) 30%, 553 | rgba(255, 0, 0, 0.6) 40%, 554 | rgba(255, 0, 0, 0.4) 50%, 555 | rgba(255, 0, 0, 0.6) 60%, 556 | rgba(255, 0, 0, 0.4) 70%, 557 | rgba(255, 0, 0, 0.2) 80%, 558 | transparent 80%, 559 | transparent 100% 560 | ), 561 | /* Add horizontal bars to simulate waveform lines */ 562 | repeating-linear-gradient( 563 | 90deg, 564 | rgba(255, 0, 0, 0.1) 0px, 565 | rgba(255, 0, 0, 0.1) 1px, 566 | transparent 1px, 567 | transparent 4px 568 | ); 569 | } 570 | 571 | /* Pulse animation when recording */ 572 | .recording-progress-overlay.recording { 573 | animation: recordingPulse 2s ease-in-out infinite; 574 | } 575 | 576 | @keyframes recordingPulse { 577 | 0%, 100% { 578 | opacity: 1; 579 | } 580 | 50% { 581 | opacity: 0.7; 582 | } 583 | } 584 | 585 | /* Alternative: Steady glow without pulse (if pulse is too distracting) */ 586 | .save-buffer-button { 587 | /* Your existing button styles */ 588 | transition: all 0.3s ease; 589 | } 590 | 591 | .save-buffer-button.recording-highlight { 592 | /* White highlight during recording - STEADY VERSION */ 593 | border: 2px solid white !important; 594 | box-shadow: 595 | 0 0 12px rgba(255, 255, 255, 0.7), 596 | 0 0 24px rgba(255, 255, 255, 0.5); 597 | background-color: rgba(255, 255, 255, 0.15); 598 | /* No animation for steady glow */ 599 | } 600 | 601 | @keyframes saveBufferPulse { 602 | 0%, 100% { 603 | box-shadow: 604 | 0 0 10px rgba(255, 255, 255, 0.6), 605 | 0 0 20px rgba(255, 255, 255, 0.4), 606 | inset 0 0 5px rgba(255, 255, 255, 0.2); 607 | } 608 | 50% { 609 | box-shadow: 610 | 0 0 15px rgba(255, 255, 255, 0.8), 611 | 0 0 30px rgba(255, 255, 255, 0.6), 612 | inset 0 0 10px rgba(255, 255, 255, 0.3); 613 | } 614 | } 615 | 616 | /* Alternative: Steady glow without pulse (if pulse is too distracting) */ 617 | .save-buffer-button.recording-highlight.steady { 618 | border: 2px solid white !important; 619 | box-shadow: 620 | 0 0 1px rgba(255, 255, 255, 0.7), 621 | 0 0 2px rgba(255, 255, 255, 0.5); 622 | background-color: rgba(255, 255, 255, 0.15); 623 | animation: none; /* Remove pulse for steady glow */ 624 | } 625 | 626 | .jerry-control-wrapper { 627 | position: absolute; 628 | display: inline-block; 629 | margin: 2px; 630 | margin-bottom: 137px; 631 | margin-left: 112px; 632 | z-index: 9000; 633 | } 634 | 635 | .jerry-control-wrapper-2 { 636 | position: absolute; 637 | display: inline-block; 638 | margin: 2px; 639 | margin-bottom: 137px; 640 | margin-left: 168px; 641 | z-index: 9000; 642 | } 643 | 644 | .jerry-input { 645 | width: 32px; /* Make them narrow - adjust as needed */ 646 | padding: 6px 8px; 647 | border: 1px solid #ccc; 648 | height: 6px; 649 | font-size: 12px; 650 | background: #fff; 651 | text-align: center; /* Center the numbers */ 652 | } 653 | 654 | .jerry-input:focus { 655 | outline: none; 656 | border-color: #ff0000; 657 | box-shadow: 0 0 3px rgba(110, 0, 0, 0.3); 658 | } 659 | 660 | .jerry-label { 661 | font-size: 10px; 662 | color: #adacac; 663 | font-weight: bold; 664 | text-align: center; 665 | margin-bottom: 4px; 666 | width: 32px; /* Match the input width */ 667 | } 668 | 669 | .backend-connection-status { 670 | margin-top: 20px; 671 | text-align: center; 672 | font-size: 14px; 673 | display: flex; 674 | align-items: center; 675 | justify-content: center; 676 | gap: 8px; 677 | transition: all 0.3s ease; 678 | } 679 | 680 | .connection-dot { 681 | width: 8px; 682 | height: 8px; 683 | border-radius: 50%; 684 | transition: background-color 0.3s ease; 685 | } 686 | 687 | .backend-connection-status.connected { 688 | color: #4ade80; /* Green text */ 689 | } 690 | 691 | .backend-connection-status.connected .connection-dot { 692 | background-color: #4ade80; /* Green dot */ 693 | box-shadow: 0 0 6px rgba(74, 222, 128, 0.6); 694 | } 695 | 696 | .backend-connection-status.disconnected { 697 | color: #ef4444; /* Red text */ 698 | } 699 | 700 | .backend-connection-status.disconnected .connection-dot { 701 | background-color: #ef4444; /* Red dot */ 702 | box-shadow: 0 0 6px rgba(239, 68, 68, 0.6); 703 | } 704 | -------------------------------------------------------------------------------- /electron-communication.js: -------------------------------------------------------------------------------- 1 | const Max = require('max-api'); 2 | const WebSocket = require('ws'); 3 | const fs = require('fs'); 4 | const ffmpeg = require('fluent-ffmpeg'); 5 | const path = require('path'); 6 | const { execSync } = require('child_process'); 7 | 8 | function checkFfmpegInPath() { 9 | try { 10 | // Try to execute ffmpeg -version 11 | execSync('ffmpeg -version', { stdio: 'ignore' }); 12 | return true; 13 | } catch (error) { 14 | return false; 15 | } 16 | } 17 | 18 | function findFfmpegPath() { 19 | // Define all possible FFmpeg locations in order of preference 20 | const possiblePaths = [ 21 | // 1. Natural extraction location (where ffmpeg usually ends up) 22 | 'C:\\g4l\\ffmpeg\\bin\\ffmpeg.exe', 23 | 24 | // 2. Manual copy location (current expectation) 25 | 'C:\\g4l\\ffmpeg\\ffmpeg.exe', 26 | 27 | // 3. Relative to this script (if bundled with app) 28 | path.join(__dirname, 'ffmpeg', 'bin', 'ffmpeg.exe'), 29 | path.join(__dirname, 'ffmpeg', 'ffmpeg.exe'), 30 | path.join(__dirname, '..', 'ffmpeg', 'bin', 'ffmpeg.exe'), 31 | 32 | // 4. Common user installation locations 33 | 'C:\\ffmpeg\\bin\\ffmpeg.exe', 34 | 'C:\\tools\\ffmpeg\\bin\\ffmpeg.exe', 35 | 36 | // 5. Program Files locations 37 | 'C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe', 38 | 'C:\\Program Files (x86)\\ffmpeg\\bin\\ffmpeg.exe', 39 | 40 | // 6. Chocolatey installation 41 | 'C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe', 42 | 'C:\\tools\\ffmpeg\\ffmpeg.exe', 43 | 44 | // 7. User AppData (winget/scoop installations) 45 | path.join(process.env.LOCALAPPDATA || '', 'Microsoft', 'winget', 'Packages', 'Gyan.FFmpeg_Microsoft.Winget.Source*', 'ffmpeg-*', 'bin', 'ffmpeg.exe'), 46 | ]; 47 | 48 | // Check each path 49 | for (const testPath of possiblePaths) { 50 | Max.post(`Checking FFmpeg at: ${testPath}`); 51 | 52 | if (checkFfmpegAtPath(testPath)) { 53 | Max.post(`✅ Found working FFmpeg at: ${testPath}`); 54 | return testPath; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | function setupFfmpegPath() { 62 | Max.post('🔍 Searching for FFmpeg...'); 63 | 64 | // First check if ffmpeg is already accessible in PATH 65 | if (checkFfmpegInPath()) { 66 | Max.post('✅ FFmpeg found in system PATH, using system FFmpeg'); 67 | return true; 68 | } 69 | 70 | // If not in PATH, search common installation locations 71 | const ffmpegPath = findFfmpegPath(); 72 | 73 | if (ffmpegPath) { 74 | try { 75 | ffmpeg.setFfmpegPath(ffmpegPath); 76 | Max.post(`✅ Successfully configured FFmpeg: ${ffmpegPath}`); 77 | return true; 78 | } catch (error) { 79 | Max.post(`❌ Error setting FFmpeg path: ${error.message}`); 80 | } 81 | } 82 | 83 | // If we get here, FFmpeg wasn't found anywhere 84 | Max.post('❌ FFmpeg not found in any expected location!'); 85 | Max.post('📋 To fix this, you can:'); 86 | Max.post(' 1. Install FFmpeg via chocolatey: choco install ffmpeg'); 87 | Max.post(' 2. Install FFmpeg via winget: winget install ffmpeg'); 88 | Max.post(' 3. Download from https://ffmpeg.org and extract to C:\\g4l\\ffmpeg\\'); 89 | Max.post(' 4. Add FFmpeg to your system PATH'); 90 | 91 | return false; 92 | } 93 | 94 | // Call this function when initializing your application 95 | setupFfmpegPath(); 96 | 97 | const ws = new WebSocket('ws://localhost:8080'); 98 | 99 | let lastErrorMessage = null; // Track the last error message 100 | let isProcessing = false; 101 | 102 | // Define our variations array in the same order as the Max umenu 103 | const VARIATIONS = [ 104 | 'accordion_folk', 105 | 'banjo_bluegrass', 106 | 'piano_classical', 107 | 'celtic', 108 | 'strings_quartet', 109 | 'synth_retro', 110 | 'synth_modern', 111 | 'synth_edm', 112 | 'lofi_chill', 113 | 'synth_bass', 114 | 'rock_band', 115 | 'cinematic_epic', 116 | 'retro_rpg', 117 | 'chiptune', 118 | 'steel_drums', 119 | 'gamelan_fusion', 120 | 'music_box', 121 | 'trap_808', 122 | 'lo_fi_drums', 123 | 'boom_bap', 124 | 'percussion_ensemble', 125 | 'future_bass', 126 | 'synthwave_retro', 127 | 'melodic_techno', 128 | 'dubstep_wobble', 129 | 'glitch_hop', 130 | 'digital_disruption', 131 | 'circuit_bent', 132 | 'orchestral_glitch', 133 | 'vapor_drums', 134 | 'industrial_textures', 135 | 'jungle_breaks', 136 | 137 | 138 | 139 | // ... all other variations in the same order as Max umenu 140 | ]; 141 | 142 | ws.on('open', function open() { 143 | Max.post("Connected to Electron WebSocket server"); 144 | }); 145 | 146 | ws.on('close', () => Max.post("Disconnected from Electron WebSocket server")); 147 | 148 | Max.addHandler('set_flowstep', (value) => { 149 | if (ws.readyState === WebSocket.OPEN) { 150 | ws.send(JSON.stringify({ 151 | action: 'update_flowstep', 152 | data: value 153 | })); 154 | Max.post(`Sent flowstep update to WebSocket server: ${value}`); 155 | } 156 | }); 157 | 158 | Max.addHandler('reconnect', () => { 159 | lastErrorMessage = null; // Clear any stored error message 160 | if (ws.readyState === WebSocket.OPEN) { 161 | // Only send the connection status 162 | ws.send(JSON.stringify({ 163 | action: 'backend_connection_status', 164 | data: true 165 | })); 166 | Max.post('Sent reconnection status to WebSocket server'); 167 | } 168 | }); 169 | 170 | Max.addHandler('backend_connection', (...args) => { 171 | 172 | Max.post('Full backend_connection message data:'); 173 | Max.post(JSON.stringify({ 174 | args: args, 175 | firstArg: args[0], 176 | type: typeof args[0] 177 | })); 178 | 179 | if (ws.readyState === WebSocket.OPEN) { 180 | // Now we can properly check the value we receive 181 | const status = args[0] === 1; 182 | 183 | ws.send(JSON.stringify({ 184 | action: 'backend_connection_status', 185 | data: status 186 | })); 187 | Max.post(`Sent backend connection status to WebSocket server: ${status}`); 188 | } 189 | }); 190 | 191 | Max.addHandler('error_message', (data) => { 192 | // Store this error message 193 | lastErrorMessage = data; 194 | 195 | if (ws.readyState === WebSocket.OPEN) { 196 | const [type, message] = data.split('|'); 197 | 198 | ws.send(JSON.stringify({ 199 | action: 'error_message', 200 | data: { 201 | type, 202 | message 203 | } 204 | })); 205 | Max.post(`Sent error message to WebSocket server - Type: ${type}, Message: ${message}`); 206 | } 207 | }); 208 | 209 | // Handler for receiving variation changes from Max 210 | Max.addHandler('set_variation', (value) => { 211 | if (ws.readyState === WebSocket.OPEN) { 212 | // If value is a number (index) 213 | if (typeof value === 'number') { 214 | const variationName = VARIATIONS[value]; 215 | if (variationName) { 216 | ws.send(JSON.stringify({ 217 | action: 'update_variation', 218 | data: variationName 219 | })); 220 | Max.post(`Sent variation update to WebSocket server: ${variationName}`); 221 | } else { 222 | Max.post(`Invalid variation index: ${value}`); 223 | } 224 | } 225 | // If value is a string (variation name) 226 | else if (typeof value === 'string') { 227 | if (VARIATIONS.includes(value)) { 228 | ws.send(JSON.stringify({ 229 | action: 'update_variation', 230 | data: value 231 | })); 232 | Max.post(`Sent variation update to WebSocket server: ${value}`); 233 | } else { 234 | Max.post(`Invalid variation name: ${value}`); 235 | } 236 | } 237 | } 238 | }); 239 | 240 | Max.addHandler('tame_gary', (value) => { 241 | if (ws.readyState === WebSocket.OPEN) { 242 | ws.send(JSON.stringify({ garyToggle: value })); 243 | Max.post(`Sent Gary state to WebSocket server: ${value}`); 244 | } 245 | }); 246 | 247 | // Max handler to forward progress updates to the Electron app 248 | Max.addHandler('progress_update', (progress) => { 249 | if (ws.readyState === WebSocket.OPEN) { 250 | ws.send(JSON.stringify({ 251 | action: 'progress_update', 252 | data: progress 253 | })); 254 | Max.post(`Sent progress update to WebSocket server: ${progress}`); 255 | } 256 | }); 257 | 258 | Max.addHandler('number', (value) => { 259 | if (ws.readyState === WebSocket.OPEN) { 260 | ws.send(JSON.stringify({ toggle: value })); 261 | Max.post(`Sent to WebSocket server: { toggle: ${value} }`); 262 | } 263 | }); 264 | 265 | const sendAudioData = (filePath, actionType) => { 266 | fs.readFile(filePath, { encoding: 'base64' }, (err, base64Data) => { 267 | if (err) { 268 | Max.post(`Error reading file: ${err.message}`); 269 | return; 270 | } 271 | if (ws.readyState === WebSocket.OPEN) { 272 | ws.send(JSON.stringify({ action: actionType, data: base64Data })); 273 | Max.post(`Sent ${actionType} back to Electron from ${filePath}.`); 274 | } 275 | }); 276 | }; 277 | 278 | // Function to handle the crop_audio action from Electron 279 | const handleCropAudio = (end) => { 280 | const tempFilePath = 'C:/g4l/tempAudio.wav'; 281 | const croppedFilePath = 'C:/g4l/myOutput.wav'; 282 | 283 | 284 | 285 | try { 286 | fs.copyFileSync('C:/g4l/myOutput.wav', tempFilePath); // Copy the original file to tempFilePath 287 | 288 | ffmpeg(tempFilePath) 289 | .setStartTime(0) 290 | .setDuration(Number(end)) 291 | .output(croppedFilePath) 292 | .on('start', (cmdline) => { 293 | Max.post(`Started ffmpeg with command: ${cmdline}`); 294 | }) 295 | .on('end', () => { 296 | Max.post('Audio cropping successful.'); 297 | fs.unlinkSync(tempFilePath); 298 | 299 | // First, notify Max that crop was successful 300 | Max.outlet('crop_audio', 'success'); 301 | 302 | // Then trigger the replace_output 303 | Max.outlet('replace_output'); 304 | 305 | // Finally, send the new audio data back to Electron 306 | sendAudioData(croppedFilePath, 'audio_data_output'); 307 | 308 | // Reset processing flag 309 | isProcessing = false; 310 | }) 311 | .on('error', (err) => { 312 | Max.post('Error cropping audio: ' + err.message); 313 | fs.unlinkSync(tempFilePath); 314 | Max.outlet('crop_audio', 'error'); 315 | isProcessing = false; 316 | 317 | // Notify Electron of the error 318 | if (ws.readyState === WebSocket.OPEN) { 319 | ws.send(JSON.stringify({ 320 | action: 'error_message', 321 | data: { 322 | type: 'crop', 323 | message: 'Failed to crop audio: ' + err.message 324 | } 325 | })); 326 | } 327 | }) 328 | .run(); 329 | } catch (error) { 330 | Max.post('Error in crop operation: ' + error.message); 331 | isProcessing = false; 332 | Max.outlet('crop_audio', 'error'); 333 | } 334 | }; 335 | 336 | // Function to handle 'crop' message from Max 337 | Max.addHandler('crop', (end) => { 338 | if (!isProcessing) { 339 | isProcessing = true; 340 | ws.send(JSON.stringify({ action: 'crop', data: end })); 341 | Max.post(`Sent crop action with end: ${end}`); 342 | } else { 343 | Max.post('Processing already in progress.'); 344 | } 345 | }); 346 | 347 | Max.addHandler('audio_processed', () => { 348 | if (ws.readyState === WebSocket.OPEN) { 349 | ws.send(JSON.stringify({ 350 | action: 'audio_processed' 351 | })); 352 | Max.post('Sent audio_processed to WebSocket server'); 353 | } 354 | }); 355 | 356 | Max.addHandler('music_continued', () => { 357 | if (ws.readyState === WebSocket.OPEN) { 358 | ws.send(JSON.stringify({ 359 | action: 'music_continued' 360 | })); 361 | Max.post('Sent music_continued to WebSocket server'); 362 | } 363 | }); 364 | 365 | Max.addHandler('music_retried', () => { 366 | if (ws.readyState === WebSocket.OPEN) { 367 | ws.send(JSON.stringify({ 368 | action: 'music_retried' 369 | })); 370 | Max.post('Sent music_retried to WebSocket server'); 371 | } 372 | }); 373 | 374 | Max.addHandler('audio_transformed', () => { 375 | if (ws.readyState === WebSocket.OPEN) { 376 | ws.send(JSON.stringify({ 377 | action: 'audio_transformed' 378 | })); 379 | Max.post('Sent audio_transformed to WebSocket server'); 380 | } 381 | }); 382 | 383 | Max.addHandler('update_cropped_audio_complete', () => { 384 | if (ws.readyState === WebSocket.OPEN) { 385 | ws.send(JSON.stringify({ 386 | action: 'update_cropped_audio_complete' 387 | })); 388 | Max.post('Sent update_cropped_audio_complete to WebSocket server'); 389 | } 390 | }); 391 | 392 | Max.addHandler('stable_audio_generated', () => { 393 | if (ws.readyState === WebSocket.OPEN) { 394 | ws.send(JSON.stringify({ 395 | action: 'stable_audio_generated' 396 | })); 397 | Max.post('Sent stable_audio_generated to WebSocket server'); 398 | } 399 | }); 400 | 401 | ws.on('message', function incoming(data) { 402 | try { 403 | const command = JSON.parse(data); 404 | Max.post(`Received command: ${JSON.stringify(command)}`); // Debugging log 405 | switch (command.action) { 406 | case 'write_buffer': 407 | Max.outlet('write_buffer'); 408 | setTimeout(() => sendAudioData('C:\\g4l\\myBuffer.wav', 'audio_data_buffer'), 1000); 409 | break; 410 | case 'load_output': 411 | // First pause any ongoing playback 412 | Max.outlet('pause'); 413 | // Wait a brief moment for pause to take effect 414 | setTimeout(() => { 415 | // Reset the playback position 416 | Max.outlet('reset'); 417 | // Replace the audio file 418 | Max.outlet('replace_output'); 419 | // Send the audio data back to Electron 420 | sendAudioData('C:\\g4l\\myOutput.wav', 'audio_data_output'); 421 | // Notify Electron that we've reset the playback state 422 | ws.send(JSON.stringify({ 423 | action: 'playback_state_update', 424 | data: { 425 | isPlaying: false, 426 | position: 0 427 | } 428 | })); 429 | }, 100); 430 | break; 431 | case 'play': 432 | Max.outlet('play'); // Assumes there's a Max outlet configured to handle this 433 | break; 434 | case 'fix_toggle': 435 | Max.outlet('fix_toggle'); // Assumes there's a Max outlet configured to handle this 436 | break; 437 | case 'pause': 438 | Max.outlet('pause'); // Assumes there's a Max outlet configured to handle this 439 | break; 440 | case 'reset': 441 | Max.outlet('reset'); // Assumes there's a Max outlet configured to handle this 442 | break; 443 | case 'bang': 444 | Max.outlet('bang'); // Assumes there's a Max outlet configured to handle this 445 | break; 446 | case 'continue': 447 | Max.outlet('continue'); // Assumes there's a Max outlet configured to handle this 448 | break; 449 | case 'retry': 450 | Max.outlet('retry'); // Assumes there's a Max outlet configured to handle this 451 | break; 452 | case 'update_model_path': 453 | console.log('Updating model path with:', command.data); 454 | Max.outlet('forward_model_path', command.data); 455 | break; 456 | case 'update_prompt_duration': 457 | console.log('Updating prompt_duration with:', command.data); 458 | Max.outlet('update_prompt_duration', command.data); 459 | break; 460 | case 'crop': 461 | const { data } = command; 462 | handleCropAudio(data); 463 | break; 464 | case 'update_gary_state': 465 | const garyState = command.data.garyToggle; 466 | Max.outlet('update_gary_state', garyState); // This will connect to a route object in Max 467 | break; 468 | // When receiving variation updates from Electron 469 | case 'update_variation': 470 | // Find the index of the variation in our array 471 | const index = VARIATIONS.indexOf(command.data); 472 | if (index !== -1) { 473 | Max.outlet('update_variation', index); // Send the index instead of the string 474 | } 475 | break; 476 | case 'transform': 477 | Max.outlet('transform'); 478 | break; 479 | case 'undo_transform': 480 | Max.outlet('undo_transform'); 481 | break; 482 | case 'reset_transform': 483 | Max.outlet('reset_transform'); 484 | // Then handle the syncing of current state 485 | if (command.data && typeof command.data === 'object') { 486 | // Update model path if provided 487 | if (command.data.modelPath) { 488 | Max.outlet('forward_model_path', command.data.modelPath); 489 | } 490 | // Update prompt duration if provided 491 | if (command.data.promptDuration) { 492 | Max.outlet('update_prompt_duration', command.data.promptDuration); 493 | } 494 | // Update variation if provided 495 | if (command.data.variation) { 496 | const index = VARIATIONS.indexOf(command.data.variation); 497 | if (index !== -1) { 498 | Max.outlet('update_variation', index); 499 | } 500 | } 501 | } 502 | break; 503 | case 'backend_connection_status': 504 | Max.outlet('backend_connection_status', command.data); 505 | break; 506 | case 'update_flowstep': 507 | console.log('Updating flowstep with:', command.data); 508 | Max.outlet('update_flowstep', command.data); // This will connect to our live.dial 509 | break; 510 | case 'update_prompt': 511 | let cleanData = command.data; 512 | if (typeof cleanData === 'string' && cleanData.startsWith('"') && cleanData.endsWith('"')) { 513 | cleanData = cleanData.slice(1, -1); // Remove first and last characters 514 | } 515 | Max.outlet('forward_prompt', cleanData); 516 | break; 517 | case 'generate_stable_audio': 518 | console.log('Triggering stable audio generation'); 519 | Max.outlet('generate_stable_audio'); 520 | break; 521 | case 'update_jerry_cfg': 522 | console.log('Updating Jerry CFG with:', command.data); 523 | Max.outlet('forward_jerry_cfg', command.data); 524 | break; 525 | 526 | case 'update_jerry_steps': 527 | console.log('Updating Jerry steps with:', command.data); 528 | Max.outlet('forward_jerry_steps', command.data); 529 | break; 530 | default: 531 | Max.post(`Unhandled action: ${command.action}`); 532 | } 533 | } catch (error) { 534 | console.error('Error parsing incoming data:', error); 535 | Max.post('Error parsing incoming data: ' + error.message); 536 | } 537 | }); 538 | 539 | function initSocketConnection() { 540 | ws.on('open', function open() { 541 | Max.post('Connected to Electron WebSocket server.'); 542 | }); 543 | 544 | ws.on('close', () => { 545 | Max.post('Disconnected from Electron WebSocket server.'); 546 | }); 547 | 548 | ws.on('error', (error) => { 549 | Max.post('WebSocket error: ' + error.message); 550 | }); 551 | } 552 | 553 | // Initialize WebSocket connection and setup event listeners 554 | initSocketConnection(); 555 | -------------------------------------------------------------------------------- /g4l-ui/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.1.0 2 | 3 | - Migrate to `css-minifier-webpack-plugin` 4 | 5 | # 2.0.1 6 | 7 | ## Fixes 8 | 9 | - Fix broken css linking in production build 10 | 11 | # 2.0.0 12 | 13 | ## Breaking Changes 14 | 15 | - drop redux 16 | - remove counter example app 17 | - simplify directory structure 18 | - move `dll` dir to `.erb` dir 19 | - fix icon/font import paths 20 | - migrate to `react-refresh` from `react-hot-loader` 21 | - migrate to webpack@5 22 | - migrate to electron@11 23 | - remove e2e tests and testcafe integration 24 | - rename `app` dir to more conventional `src` dir 25 | - rename `resources` dir to `assets` 26 | - simplify npm scripts 27 | - drop stylelint 28 | - simplify styling of boilerplate app 29 | - remove `START_HOT` env variable 30 | - notarize support 31 | - landing page boilerplate 32 | - docs updates 33 | - restore removed debugging support 34 | 35 | # 1.4.0 36 | 37 | - Migrate to `eslint-config-erb@2` 38 | - Rename `dev` npm script to `start` 39 | - GitHub Actions: only publish GitHub releases when on master branch 40 | 41 | # 1.3.1 42 | 43 | - Fix sass building bug ([#2540](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2540)) 44 | - Fix CI bug related to E2E tests and network timeouts 45 | - Move automated dependency PRs to `next` ([#2554](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2554)) 46 | - Bump dependencies to patch semver 47 | 48 | # 1.3.0 49 | 50 | - Fixes E2E tests ([#2516](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2516)) 51 | - Fixes preload entrypoint ([#2503](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2503)) 52 | - Downgrade to `electron@8` 53 | - Bump dependencies to latest semver 54 | 55 | # 1.2.0 56 | 57 | - Migrate to redux toolkit 58 | - Lazy load routes with react suspense 59 | - Drop support for azure-pipelines and use only github actions 60 | - Bump all deps to latest semver 61 | - Remove `test-e2e` script from tests (blocked on release of https://github.com/DevExpress/testcafe-browser-provider-electron/pull/65) 62 | - Swap `typed-css-modules-webpack-plugin` for `typings-for-css-modules-loader` 63 | - Use latest version of `eslint-config-erb` 64 | - Remove unnecessary file extensions from ts exclude 65 | - Add experimental support for vscode debugging 66 | - Revert https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2365 as default for users, provide as opt in option 67 | 68 | # 1.1.0 69 | 70 | - Fix #2402 71 | - Simplify configs (https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2406) 72 | 73 | # 1.0.0 74 | 75 | - Migrate to TypeScript from Flow ([#2363](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2363)) 76 | - Use browserslist for `@babel/preset-env` targets ([#2368](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2368)) 77 | - Use preload script, disable `nodeIntegration` in renderer process for [improved security](https://www.electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content) ([#2365](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2365)) 78 | - Add support for azure pipelines ([#2369](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2369)) 79 | - Disable sourcemaps in production 80 | 81 | # 0.18.1 (2019.12.12) 82 | 83 | - Fix HMR env bug ([#2343](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2343)) 84 | - Bump all deps to latest semver 85 | - Bump to `electron@7` 86 | 87 | # 0.18.0 (2019.11.19) 88 | 89 | - Bump electron to `electron@6` (`electron@7` introduces breaking changes to testcafe end to end tests) 90 | - Revert back to [two `package.json` structure](https://www.electron.build/tutorials/two-package-structure) 91 | - Bump all deps to latest semver 92 | 93 | # 0.17.1 (2018.11.20) 94 | 95 | - Fix `yarn test-e2e` and testcafe for single package.json structure 96 | - Fixes incorrect path in `yarn start` script 97 | - Bumped deps 98 | - Bump g++ in travis 99 | - Change clone arguments to clone only master 100 | - Change babel config to target current electron version 101 | 102 | For full change list, see https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/2021 103 | 104 | # 0.17.0 (2018.10.30) 105 | 106 | - upgraded to `babel@7` (thanks to @vikr01 🎉🎉🎉) 107 | - migrated from [two `package.json` structure](https://www.electron.build/tutorials/two-package-structure) (thanks to @HyperSprite!) 108 | - initial auto update support (experimental) 109 | - migrate from greenkeeper to [renovate](https://renovatebot.com) 110 | - added issue template 111 | - use `babel-preset-env` to target current electron version 112 | - add [opencollective](https://opencollective.com/electron-react-boilerplate-594) banner message display in postinstall script (help support ERB 🙏) 113 | - fix failing ci issues 114 | 115 | # 0.16.0 (2018.10.3) 116 | 117 | - removed unused dependencies 118 | - migrate from `react-redux-router` to `connect-react-router` 119 | - move webpack configs to `./webpack` dir 120 | - use `g++` on travis when testing linux 121 | - migrate from `spectron` to `testcafe` for e2e tests 122 | - add linting support for config styles 123 | - changed stylelint config 124 | - temporarily disabled flow in appveyor to make ci pass 125 | - added necessary infra to publish releases from ci 126 | 127 | # 0.15.0 (2018.8.25) 128 | 129 | - Performance: cache webpack uglify results 130 | - Feature: add start minimized feature 131 | - Feature: lint and fix styles with prettier and stylelint 132 | - Feature: add greenkeeper support 133 | 134 | # 0.14.0 (2018.5.24) 135 | 136 | - Improved CI timings 137 | - Migrated README commands to yarn from npm 138 | - Improved vscode config 139 | - Updated all dependencies to latest semver 140 | - Fix `electron-rebuild` script bug 141 | - Migrated to `mini-css-extract-plugin` from `extract-text-plugin` 142 | - Added `optimize-css-assets-webpack-plugin` 143 | - Run `prettier` on json, css, scss, and more filetypes 144 | 145 | # 0.13.3 (2018.5.24) 146 | 147 | - Add git precommit hook, when git commit will use `prettier` to format git add code 148 | - Add format code function in `lint-fix` npm script which can use `prettier` to format project js code 149 | 150 | # 0.13.2 (2018.1.31) 151 | 152 | - Hot Module Reload (HMR) fixes 153 | - Bumped all dependencies to latest semver 154 | - Prevent error propagation of `CheckNativeDeps` script 155 | 156 | # 0.13.1 (2018.1.13) 157 | 158 | - Hot Module Reload (HMR) fixes 159 | - Bumped all dependencies to latest semver 160 | - Fixed electron-rebuild script 161 | - Fixed tests scripts to run on all platforms 162 | - Skip redux logs in console in test ENV 163 | 164 | # 0.13.0 (2018.1.6) 165 | 166 | #### Additions 167 | 168 | - Add native dependencies check on postinstall 169 | - Updated all dependencies to latest semver 170 | 171 | # 0.12.0 (2017.7.8) 172 | 173 | #### Misc 174 | 175 | - Removed `babel-polyfill` 176 | - Renamed and alphabetized npm scripts 177 | 178 | #### Breaking 179 | 180 | - Changed node dev `__dirname` and `__filename` to node built in fn's (https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/1035) 181 | - Renamed `src/bundle.js` to `src/renderer.prod.js` for consistency 182 | - Renamed `dll/vendor.js` to `dll/renderer.dev.dll.js` for consistency 183 | 184 | #### Additions 185 | 186 | - Enable node_modules cache on CI 187 | 188 | # 0.11.2 (2017.5.1) 189 | 190 | Yay! Another patch release. This release mostly includes refactorings and router bug fixes. Huge thanks to @anthonyraymond! 191 | 192 | ⚠️ Windows electron builds are failing because of [this issue](https://github.com/electron/electron/issues/9321). This is not an issue with the boilerplate ⚠️ 193 | 194 | #### Breaking 195 | 196 | - **Renamed `./src/main.development.js` => `./src/main.{dev,prod}.js`:** [#963](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/963) 197 | 198 | #### Fixes 199 | 200 | - **Fixed reloading when not on `/` path:** [#958](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/958) [#949](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/949) 201 | 202 | #### Additions 203 | 204 | - **Added support for stylefmt:** [#960](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/960) 205 | 206 | # 0.11.1 (2017.4.23) 207 | 208 | You can now debug the production build with devtools like so: 209 | 210 | ``` 211 | DEBUG_PROD=true npm run package 212 | ``` 213 | 214 | 🎉🎉🎉 215 | 216 | #### Additions 217 | 218 | - **Added support for debugging production build:** [#fab245a](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/941/commits/fab245a077d02a09630f74270806c0c534a4ff95) 219 | 220 | #### Bug Fixes 221 | 222 | - **Fixed bug related to importing native dependencies:** [#933](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/933) 223 | 224 | #### Improvements 225 | 226 | - **Updated all deps to latest semver** 227 | 228 | # 0.11.0 (2017.4.19) 229 | 230 | Here's the most notable changes since `v0.10.0`. Its been about a year since a release has been pushed. Expect a new release to be published every 3-4 weeks. 231 | 232 | #### Breaking Changes 233 | 234 | - **Dropped support for node < 6** 235 | - **Refactored webpack config files** 236 | - **Migrate to two-package.json project structure** 237 | - **Updated all devDeps to latest semver** 238 | - **Migrated to Jest:** [#768](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/768) 239 | - **Migrated to `react-router@4`** 240 | - **Migrated to `electron-builder@4`** 241 | - **Migrated to `webpack@2`** 242 | - **Migrated to `react-hot-loader@3`** 243 | - **Changed default live reload server PORT to `1212` from `3000`** 244 | 245 | #### Additions 246 | 247 | - **Added support for Yarn:** [#451](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/451) 248 | - **Added support for Flow:** [#425](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/425) 249 | - **Added support for stylelint:** [#911](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/911) 250 | - **Added support for electron-builder:** [#876](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/876) 251 | - **Added optional support for SASS:** [#880](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/880) 252 | - **Added support for eslint-plugin-flowtype:** [#911](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/911) 253 | - **Added support for appveyor:** [#280](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/280) 254 | - **Added support for webpack dlls:** [#860](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/860) 255 | - **Route based code splitting:** [#884](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/884) 256 | - **Added support for Webpack Bundle Analyzer:** [#922](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/922) 257 | 258 | #### Improvements 259 | 260 | - **Parallelize renderer and main build processes when running `npm run build`** 261 | - **Dynamically generate electron app menu** 262 | - **Improved vscode integration:** [#856](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/856) 263 | 264 | #### Bug Fixes 265 | 266 | - **Fixed hot module replacement race condition bug:** [#917](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/917) [#920](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/920) 267 | 268 | # 0.10.0 (2016.4.18) 269 | 270 | #### Improvements 271 | 272 | - **Use Babel in main process with Webpack build:** [#201](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/201) 273 | - **Change targets to built-in support by webpack:** [#197](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/197) 274 | - **use es2015 syntax for webpack configs:** [#195](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/195) 275 | - **Open application when webcontent is loaded:** [#192](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/192) 276 | - **Upgraded dependencies** 277 | 278 | #### Bug fixed 279 | 280 | - **Fix `npm list electron-prebuilt` in package.js:** [#188](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/188) 281 | 282 | # 0.9.0 (2016.3.23) 283 | 284 | #### Improvements 285 | 286 | - **Added [redux-logger](https://github.com/fcomb/redux-logger)** 287 | - **Upgraded [react-router-redux](https://github.com/reactjs/react-router-redux) to v4** 288 | - **Upgraded dependencies** 289 | - **Added `npm run dev` command:** [#162](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/162) 290 | - **electron to v0.37.2** 291 | 292 | #### Breaking Changes 293 | 294 | - **css module as default:** [#154](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/154). 295 | - **set default NODE_ENV to production:** [#140](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/140) 296 | 297 | # 0.8.0 (2016.2.17) 298 | 299 | #### Bug fixed 300 | 301 | - **Fix lint errors** 302 | - **Fix Webpack publicPath for production builds**: [#119](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/119). 303 | - **package script now chooses correct OS icon extension** 304 | 305 | #### Improvements 306 | 307 | - **babel 6** 308 | - **Upgrade Dependencies** 309 | - **Enable CSS source maps** 310 | - **Add json-loader**: [#128](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/128). 311 | - **react-router 2.0 and react-router-redux 3.0** 312 | 313 | # 0.7.1 (2015.12.27) 314 | 315 | #### Bug fixed 316 | 317 | - **Fixed npm script on windows 10:** [#103](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/103). 318 | - **history and react-router version bump**: [#109](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/109), [#110](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/110). 319 | 320 | #### Improvements 321 | 322 | - **electron 0.36** 323 | 324 | # 0.7.0 (2015.12.16) 325 | 326 | #### Bug fixed 327 | 328 | - **Fixed process.env.NODE_ENV variable in webpack:** [#74](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/74). 329 | - **add missing object-assign**: [#76](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/76). 330 | - **packaging in npm@3:** [#77](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/77). 331 | - **compatibility in windows:** [#100](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/100). 332 | - **disable chrome debugger in production env:** [#102](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/102). 333 | 334 | #### Improvements 335 | 336 | - **redux** 337 | - **css-modules** 338 | - **upgrade to react-router 1.x** 339 | - **unit tests** 340 | - **e2e tests** 341 | - **travis-ci** 342 | - **upgrade to electron 0.35.x** 343 | - **use es2015** 344 | - **check dev engine for node and npm** 345 | 346 | # 0.6.5 (2015.11.7) 347 | 348 | #### Improvements 349 | 350 | - **Bump style-loader to 0.13** 351 | - **Bump css-loader to 0.22** 352 | 353 | # 0.6.4 (2015.10.27) 354 | 355 | #### Improvements 356 | 357 | - **Bump electron-debug to 0.3** 358 | 359 | # 0.6.3 (2015.10.26) 360 | 361 | #### Improvements 362 | 363 | - **Initialize ExtractTextPlugin once:** [#64](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/64). 364 | 365 | # 0.6.2 (2015.10.18) 366 | 367 | #### Bug fixed 368 | 369 | - **Babel plugins production env not be set properly:** [#57](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/57). 370 | 371 | # 0.6.1 (2015.10.17) 372 | 373 | #### Improvements 374 | 375 | - **Bump electron to v0.34.0** 376 | 377 | # 0.6.0 (2015.10.16) 378 | 379 | #### Breaking Changes 380 | 381 | - **From react-hot-loader to react-transform** 382 | 383 | # 0.5.2 (2015.10.15) 384 | 385 | #### Improvements 386 | 387 | - **Run tests with babel-register:** [#29](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/29). 388 | 389 | # 0.5.1 (2015.10.12) 390 | 391 | #### Bug fixed 392 | 393 | - **Fix #51:** use `path.join(__dirname` instead of `./`. 394 | 395 | # 0.5.0 (2015.10.11) 396 | 397 | #### Improvements 398 | 399 | - **Simplify webpack config** see [#50](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/50). 400 | 401 | #### Breaking Changes 402 | 403 | - **webpack configs** 404 | - **port changed:** changed default port from 2992 to 3000. 405 | - **npm scripts:** remove `start-dev` and `dev-server`. rename `hot-dev-server` to `hot-server`. 406 | 407 | # 0.4.3 (2015.9.22) 408 | 409 | #### Bug fixed 410 | 411 | - **Fix #45 zeromq crash:** bump version of `electron-prebuilt`. 412 | 413 | # 0.4.2 (2015.9.15) 414 | 415 | #### Bug fixed 416 | 417 | - **run start-hot breaks chrome refresh(CTRL+R) (#42)**: bump `electron-debug` to `0.2.1` 418 | 419 | # 0.4.1 (2015.9.11) 420 | 421 | #### Improvements 422 | 423 | - **use electron-prebuilt version for packaging (#33)** 424 | 425 | # 0.4.0 (2015.9.5) 426 | 427 | #### Improvements 428 | 429 | - **update dependencies** 430 | 431 | # 0.3.0 (2015.8.31) 432 | 433 | #### Improvements 434 | 435 | - **eslint-config-airbnb** 436 | 437 | # 0.2.10 (2015.8.27) 438 | 439 | #### Features 440 | 441 | - **custom placeholder icon** 442 | 443 | #### Improvements 444 | 445 | - **electron-renderer as target:** via [webpack-target-electron-renderer](https://github.com/chentsulin/webpack-target-electron-renderer) 446 | 447 | # 0.2.9 (2015.8.18) 448 | 449 | #### Bug fixed 450 | 451 | - **Fix hot-reload** 452 | 453 | # 0.2.8 (2015.8.13) 454 | 455 | #### Improvements 456 | 457 | - **bump electron-debug** 458 | - **babelrc** 459 | - **organize webpack scripts** 460 | 461 | # 0.2.7 (2015.7.9) 462 | 463 | #### Bug fixed 464 | 465 | - **defaultProps:** fix typos. 466 | 467 | # 0.2.6 (2015.7.3) 468 | 469 | #### Features 470 | 471 | - **menu** 472 | 473 | #### Bug fixed 474 | 475 | - **package.js:** include webpack build. 476 | 477 | # 0.2.5 (2015.7.1) 478 | 479 | #### Features 480 | 481 | - **NPM Script:** support multi-platform 482 | - **package:** `--all` option 483 | 484 | # 0.2.4 (2015.6.9) 485 | 486 | #### Bug fixed 487 | 488 | - **Eslint:** typo, [#17](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/17) and improve `.eslintrc` 489 | 490 | # 0.2.3 (2015.6.3) 491 | 492 | #### Features 493 | 494 | - **Package Version:** use latest release electron version as default 495 | - **Ignore Large peerDependencies** 496 | 497 | #### Bug fixed 498 | 499 | - **Npm Script:** typo, [#6](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/6) 500 | - **Missing css:** [#7](https://github.com/electron-react-boilerplate/electron-react-boilerplate/pull/7) 501 | 502 | # 0.2.2 (2015.6.2) 503 | 504 | #### Features 505 | 506 | - **electron-debug** 507 | 508 | #### Bug fixed 509 | 510 | - **Webpack:** add `.json` and `.node` to extensions for imitating node require. 511 | - **Webpack:** set `node_modules` to externals for native module support. 512 | 513 | # 0.2.1 (2015.5.30) 514 | 515 | #### Bug fixed 516 | 517 | - **Webpack:** #1, change build target to `atom`. 518 | 519 | # 0.2.0 (2015.5.30) 520 | 521 | #### Features 522 | 523 | - **Ignore:** `test`, `tools`, `release` folder and devDependencies in `package.json`. 524 | - **Support asar** 525 | - **Support icon** 526 | 527 | # 0.1.0 (2015.5.27) 528 | 529 | #### Features 530 | 531 | - **Webpack:** babel, react-hot, ... 532 | - **Flux:** actions, api, components, containers, stores.. 533 | - **Package:** darwin (osx), linux and win32 (windows) platform. 534 | -------------------------------------------------------------------------------- /g4l-ui/.erb/img/erb-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /commentedout.js: -------------------------------------------------------------------------------- 1 | const Max = require('max-api'); 2 | const fs = require('fs'); 3 | const io = require('socket.io-client'); 4 | 5 | const https = require('https'); 6 | const http = require('http'); 7 | 8 | // ============================================================================= 9 | // CONFIGURATION - Change these URLs when building from source with localhost 10 | // ============================================================================= 11 | 12 | // For localhost development with our github repo https://github.com/betweentwomidnights/gary-backend-combined 13 | // Uncomment these lines and comment out the production URLs below: 14 | // const BACKEND_URL = 'http://localhost:8000'; 15 | // const STABLE_AUDIO_HOST = 'localhost'; 16 | // const STABLE_AUDIO_PORT = 8005; 17 | // const STABLE_AUDIO_PATH = '/generate'; // Direct to service 18 | // const USE_HTTPS = false; 19 | 20 | // Production URLs (default): 21 | const BACKEND_URL = 'https://g4l.thecollabagepatch.com'; 22 | const STABLE_AUDIO_HOST = 'g4l.thecollabagepatch.com'; 23 | const STABLE_AUDIO_PORT = 443; 24 | const STABLE_AUDIO_PATH = '/audio/generate'; // Through Caddy proxy 25 | const USE_HTTPS = true; 26 | 27 | // ============================================================================= 28 | 29 | const socket = io(BACKEND_URL, { 30 | transports: ['websocket'], // Force WebSocket usage 31 | reconnection: true, // Enable auto-reconnection 32 | reconnectionAttempts: Infinity, // Unlimited reconnection attempts 33 | reconnectionDelay: 1000, // Wait 1 second before attempting to reconnect 34 | reconnectionDelayMax: 5000, // Maximum delay between reconnections 35 | randomizationFactor: 0.5, 36 | timeout: 300000, // Connection timeout in milliseconds 37 | pingTimeout: 240000, // How many ms without a pong packet to consider the connection closed 38 | pingInterval: 120000 // How many ms before sending a new ping packet 39 | }); 40 | const path = require('path'); 41 | 42 | // New state variables for stable-audio 43 | let currentBPM = 120; // Default BPM 44 | let stableAudioPrompt = "aggressive techno"; // Default prompt 45 | let isStableAudioProcessing = false; 46 | 47 | let stableCFG = 1.0; // Default CFG 48 | let stableSteps = 8; // Default steps 49 | 50 | // Handler to receive BPM from Live's transport 51 | Max.addHandler('set_bpm', (bpm) => { 52 | if (typeof bpm === 'number' && bpm > 0) { 53 | currentBPM = Math.round(bpm); 54 | Max.post(`BPM updated to: ${currentBPM}`); 55 | } 56 | }); 57 | 58 | Max.addHandler('set_cfg', (cfg) => { 59 | if (typeof cfg === 'number' && cfg >= 0.5 && cfg <= 2.0) { 60 | // Quantize to nearest 0.1 increment 61 | stableCFG = Math.round(cfg * 10) / 10; 62 | Max.post(`Jerry CFG updated to: ${stableCFG} (from raw: ${cfg})`); 63 | } 64 | }); 65 | 66 | // Handler for steps changes 67 | Max.addHandler('set_steps', (steps) => { 68 | if (typeof steps === 'number' && steps >= 4 && steps <= 16) { 69 | stableSteps = Math.round(steps); // Ensure integer 70 | Max.post(`Jerry steps updated to: ${stableSteps}`); 71 | } 72 | }); 73 | 74 | // Handler to set the prompt text 75 | Max.addHandler('set_prompt', (prompt) => { 76 | if (typeof prompt === 'string') { 77 | // Remove 'text' prefix that textedit adds 78 | let cleanPrompt = prompt.trim(); 79 | if (cleanPrompt.startsWith('text ')) { 80 | cleanPrompt = cleanPrompt.substring(5); // Remove 'text ' (5 characters) 81 | } 82 | stableAudioPrompt = cleanPrompt; 83 | Max.post(`Prompt updated to: ${stableAudioPrompt}`); 84 | } 85 | }); 86 | 87 | // Handler for stable-audio generation 88 | Max.addHandler('generate_stable_audio', () => { 89 | if (isStableAudioProcessing) { 90 | Max.post('Stable audio generation already in progress...'); 91 | return; 92 | } 93 | 94 | isStableAudioProcessing = true; 95 | Max.post(`Generating stable audio: "${stableAudioPrompt}" at ${currentBPM} BPM (CFG: ${stableCFG}, Steps: ${stableSteps})`); 96 | 97 | // Construct the full prompt with BPM 98 | const fullPrompt = `${stableAudioPrompt} ${currentBPM}bpm`; 99 | 100 | // Prepare the request payload with new parameters 101 | const requestData = JSON.stringify({ 102 | prompt: fullPrompt, 103 | steps: stableSteps, // Use user-selected steps 104 | cfg_scale: stableCFG, // Use user-selected CFG 105 | return_format: "base64", 106 | seed: -1 // Random seed 107 | }); 108 | 109 | // Configure the HTTP request using the configuration variables 110 | const options = { 111 | hostname: STABLE_AUDIO_HOST, 112 | port: STABLE_AUDIO_PORT, 113 | path: STABLE_AUDIO_PATH, // Use the configurable path 114 | method: 'POST', 115 | headers: { 116 | 'Content-Type': 'application/json', 117 | 'Content-Length': Buffer.byteLength(requestData) 118 | } 119 | }; 120 | 121 | // Use the appropriate HTTP module based on configuration 122 | const httpModule = USE_HTTPS ? https : http; 123 | 124 | // Make the HTTP request 125 | const req = httpModule.request(options, (res) => { 126 | let responseData = ''; 127 | 128 | res.on('data', (chunk) => { 129 | responseData += chunk; 130 | }); 131 | 132 | res.on('end', () => { 133 | try { 134 | const response = JSON.parse(responseData); 135 | 136 | if (response.success && response.audio_base64) { 137 | // Convert base64 to buffer and save as myOutput.wav 138 | const audioBuffer = Buffer.from(response.audio_base64, 'base64'); 139 | fs.writeFileSync('C:/g4l/myOutput.wav', audioBuffer); 140 | 141 | Max.post('Stable audio generation successful!'); 142 | Max.post(`Generated: ${response.metadata.generation_time}s (${response.metadata.realtime_factor}x RT)`); 143 | 144 | // Notify Max that audio is ready 145 | Max.outlet('stable_audio_generated'); 146 | 147 | // Clear any existing session since this is new audio 148 | sessionID = null; 149 | 150 | } else { 151 | Max.post(`Stable audio generation failed: ${response.error || 'Unknown error'}`); 152 | Max.outlet('error_message', `stable_audio|${response.error || 'Generation failed'}`); 153 | } 154 | } catch (error) { 155 | Max.post(`Error parsing stable audio response: ${error.message}`); 156 | Max.outlet('error_message', `stable_audio|Failed to parse response`); 157 | } 158 | 159 | isStableAudioProcessing = false; 160 | }); 161 | }); 162 | 163 | req.on('error', (error) => { 164 | Max.post(`Stable audio request error: ${error.message}`); 165 | Max.outlet('error_message', `stable_audio|${error.message}`); 166 | isStableAudioProcessing = false; 167 | }); 168 | 169 | // Send the request 170 | req.write(requestData); 171 | req.end(); 172 | }); 173 | 174 | 175 | 176 | let modelPath = 'thepatch/vanya_ai_dnb_0.1'; // Default model path 177 | let sessionID = null; // Variable to store the session ID 178 | let isProcessing = false; 179 | let promptDuration = 6; // Default prompt duration 180 | let tameTheGaryEnabled = false; // New state variable for the toggle 181 | let variationName = 'accordion_folk'; // Default variation 182 | 183 | let isBackendConnected = false; 184 | 185 | let currentFlowstep = 0.13; // Default flowstep value 186 | let useMidpointSolver = false; // New state variable for the solver toggle 187 | 188 | let lastProgressUpdate = 0; 189 | let lastProgressTime = Date.now(); 190 | let progressTimeout = null; 191 | 192 | Max.addHandler('set_solver', (value) => { 193 | useMidpointSolver = value === 1; 194 | Max.post(`Solver set to: ${useMidpointSolver ? 'midpoint' : 'euler (default)'}`); 195 | }); 196 | 197 | Max.addHandler('set_flowstep', (value) => { 198 | // Convert the 0-127 range to 0.05-0.15 range 199 | if (typeof value === 'number') { 200 | currentFlowstep = 0.05 + (value / 127) * 0.10; 201 | currentFlowstep = Math.round(currentFlowstep * 1000) / 1000; // Round to 3 decimal places 202 | Max.post(`Flowstep updated to: ${currentFlowstep}`); 203 | } 204 | }); 205 | 206 | // Add handler for variation text input 207 | Max.addHandler('set_variation', (newVariation) => { 208 | if (typeof newVariation === 'string') { 209 | variationName = newVariation.trim(); 210 | Max.post(`Variation updated to: ${variationName}`); 211 | } 212 | }); 213 | 214 | // Update transform handler to use the variationName variable 215 | Max.addHandler('transform', () => { 216 | if (!isProcessing) { 217 | isProcessing = true; 218 | 219 | const audioPath = sessionID ? 'C:/g4l/myOutput.wav' : 'C:/g4l/myBuffer.wav'; 220 | 221 | // Add memory monitoring 222 | const memBefore = process.memoryUsage(); 223 | Max.post(`Memory before transform: ${Math.round(memBefore.heapUsed / 1024 / 1024)}MB`); 224 | 225 | fs.readFile(audioPath, (err, data) => { 226 | if (err) { 227 | Max.post(`Error reading audio file: ${err}`); 228 | isProcessing = false; 229 | return; 230 | } 231 | 232 | try { 233 | // Check file size before processing 234 | const fileSizeMB = data.length / 1024 / 1024; 235 | Max.post(`Audio file size: ${fileSizeMB.toFixed(2)}MB`); 236 | 237 | if (fileSizeMB > 50) { // 50MB limit 238 | Max.post('Audio file too large for transform'); 239 | isProcessing = false; 240 | return; 241 | } 242 | 243 | const audioData_base64 = data.toString('base64'); 244 | 245 | const request = { 246 | audio_data: audioData_base64, 247 | variation: variationName, 248 | session_id: sessionID, 249 | flowstep: currentFlowstep 250 | }; 251 | 252 | // Only add solver parameter if midpoint is selected 253 | if (useMidpointSolver) { 254 | request.solver = 'midpoint'; 255 | Max.post('Using midpoint solver (faster, 64 steps)'); 256 | } else { 257 | Max.post('Using default euler solver (higher quality, 25 steps)'); 258 | } 259 | 260 | socket.emit('transform_audio_request', request); 261 | 262 | // Force garbage collection hint 263 | if (global.gc) { 264 | global.gc(); 265 | } 266 | 267 | // Log memory after processing 268 | const memAfter = process.memoryUsage(); 269 | Max.post(`Memory after transform: ${Math.round(memAfter.heapUsed / 1024 / 1024)}MB`); 270 | 271 | } catch (error) { 272 | Max.post(`Error processing transform: ${error.message}`); 273 | isProcessing = false; 274 | } 275 | }); 276 | 277 | setTimeout(() => { 278 | isProcessing = false; 279 | }, timeoutDuration); 280 | } else { 281 | Max.post('Processing already in progress.'); 282 | } 283 | }); 284 | 285 | // Add handler for undo button 286 | Max.addHandler('undo_transform', () => { 287 | if (!isProcessing && sessionID) { 288 | isProcessing = true; 289 | 290 | const request = { 291 | session_id: sessionID 292 | }; 293 | 294 | socket.emit('undo_transform_request', request); 295 | 296 | setTimeout(() => { 297 | isProcessing = false; 298 | }, timeoutDuration); 299 | } else { 300 | Max.post('Either processing in progress or no session available for undo.'); 301 | } 302 | }); 303 | 304 | Max.addHandler('tame_gary', (value) => { 305 | tameTheGaryEnabled = value === 1; 306 | Max.post(`Tame the Gary ${tameTheGaryEnabled ? 'enabled' : 'disabled'}`); 307 | }); 308 | 309 | // Timeout duration in milliseconds 310 | const timeoutDuration = 500; // .5 seconds 311 | 312 | // Initialize WebSocket connection and setup event listeners 313 | function initSocketConnection() { 314 | socket.on('connect', () => { 315 | isBackendConnected = true; 316 | Max.post('Connected to WebSocket server.'); 317 | Max.outlet('backend_connection', true); 318 | if (sessionID) { 319 | socket.emit('verify_session', { session_id: sessionID }); 320 | } 321 | }); 322 | 323 | // Add these socket listeners to handle queue events 324 | socket.on('queue_status', (data) => { 325 | // Always post the message 326 | Max.post(data.message); 327 | 328 | // Send queue metrics to Max outlets 329 | Max.outlet('queue_status', data.position, data.total_queued); 330 | Max.outlet('time_estimate', data.estimated_seconds); 331 | 332 | // Additional debug logging 333 | Max.post(`Debug - Position: ${data.position}, Total: ${data.total_queued}, Wait: ${data.estimated_seconds}s`); 334 | }); 335 | 336 | socket.on('processing_started', (data) => { 337 | Max.post('Request is now being processed'); 338 | // No need to change isProcessing, it should stay true 339 | }); 340 | 341 | socket.on('queue_update', (data) => { 342 | Max.post(data.message); 343 | Max.outlet('queue_update', data.position, data.total_queued); 344 | if (data.estimated_seconds) { 345 | Max.outlet('time_update', data.estimated_seconds); 346 | } 347 | }); 348 | 349 | socket.on('reconnect_attempt', () => { 350 | Max.post('Attempting to reconnect to WebSocket server...'); 351 | }); 352 | 353 | Max.addHandler('reconnect', () => { 354 | // Only send the connection status 355 | Max.outlet('backend_connection', true); 356 | // Reset any error states if needed 357 | Max.post('Reconnected and reset states'); 358 | }); 359 | 360 | socket.on('reconnect_error', (error) => { 361 | Max.post('Reconnection error: ' + error.message); 362 | }); 363 | 364 | socket.on('reconnect_failed', () => { 365 | Max.post('Failed to reconnect to WebSocket server.'); 366 | }); 367 | 368 | socket.on('connect_error', (error) => { 369 | isBackendConnected = false; 370 | Max.post('Connection error: ' + error.message); 371 | Max.outlet('backend_connection', false); // Inform about lost backend connection 372 | }); 373 | 374 | socket.on('disconnect', (reason) => { 375 | isBackendConnected = false; 376 | Max.post('Disconnected from WebSocket server: ' + reason); 377 | Max.outlet('backend_connection', false); // Inform about lost backend connection 378 | if (reason === 'io server disconnect') { 379 | socket.connect(); 380 | } 381 | }); 382 | 383 | socket.on('audio_processed', (data) => { 384 | isProcessing = false; // Reset flag when audio processing is complete 385 | Max.post('Audio processing successful.'); 386 | sessionID = data.session_id; // Store the session ID 387 | const outputBuffer = Buffer.from(data.audio_data, 'base64'); 388 | fs.writeFileSync('C:/g4l/myOutput.wav', outputBuffer); 389 | Max.outlet('audio_processed'); 390 | Max.outlet('progress_update', 100); 391 | }); 392 | 393 | socket.on('music_continued', (data) => { 394 | isProcessing = false; // Reset flag when audio processing is complete 395 | Max.post('Music continuation successful.'); 396 | sessionID = data.session_id; // Update the session ID 397 | const outputBuffer = Buffer.from(data.audio_data, 'base64'); 398 | fs.writeFileSync('C:/g4l/myOutput.wav', outputBuffer); 399 | Max.outlet('music_continued'); 400 | Max.outlet('progress_update', 100); // Force the progress to 100% on completion 401 | }); 402 | 403 | socket.on('music_retried', (data) => { 404 | isProcessing = false; // Reset flag when audio processing is complete 405 | Max.post('Music retry successful.'); 406 | const outputBuffer = Buffer.from(data.audio_data, 'base64'); 407 | fs.writeFileSync('C:/g4l/myOutput.wav', outputBuffer); 408 | Max.outlet('music_retried'); 409 | Max.outlet('progress_update', 100); // Force the progress to 100% on completion 410 | }); 411 | 412 | socket.on('progress_update', (data) => { 413 | clearTimeout(progressTimeout); 414 | lastProgressUpdate = data.progress; 415 | lastProgressTime = Date.now(); 416 | Max.post(`progress update: ${data.progress}%`); 417 | Max.outlet('progress_update', data.progress); 418 | 419 | // Set timeout for next expected update 420 | progressTimeout = setTimeout(() => { 421 | if (Date.now() - lastProgressTime > 5000 && sessionID) { // Only if sessionID exists 422 | // Request progress status 423 | socket.emit('request_progress_status', { 424 | session_id: sessionID, 425 | last_progress: lastProgressUpdate 426 | }); 427 | } 428 | }, 5000); 429 | }); 430 | 431 | socket.on('error', (data) => { 432 | isProcessing = false; 433 | Max.post('Error from WebSocket server: ' + data.message); 434 | 435 | // Check if the error message includes the specific connection error 436 | if (data.message.includes('Connection refused') || 437 | data.message.includes('Failed to establish a new connection')) { 438 | Max.outlet('error_message', 'transform|terry is asleep right now. he uses alot of gpu ram. you can learn how to spin up your own terry container by bugging kevin in the discord tho: https://discord.gg/VECkyXEnAd'); 439 | } else { 440 | Max.outlet('error', data.message); 441 | } 442 | }); 443 | 444 | socket.on('update_cropped_audio_complete', (data) => { 445 | sessionID = data.session_id; // Update the session ID 446 | Max.post('Cropped audio updated successfully.'); 447 | 448 | // NEW: Forward session event to electron 449 | Max.outlet('update_cropped_audio_complete'); 450 | }); 451 | 452 | // Add new socket listener for transform response 453 | socket.on('audio_transformed', (data) => { 454 | isProcessing = false; 455 | Max.post('Audio transformation successful.'); 456 | sessionID = data.session_id; // Update session ID 457 | const outputBuffer = Buffer.from(data.audio_data, 'base64'); 458 | fs.writeFileSync('C:/g4l/myOutput.wav', outputBuffer); 459 | Max.outlet('audio_transformed'); 460 | Max.outlet('progress_update', 100); 461 | }); 462 | // Add socket listener for undo response 463 | socket.on('transform_undone', (data) => { 464 | isProcessing = false; 465 | Max.post('Transform undo successful.'); 466 | sessionID = data.session_id; 467 | const outputBuffer = Buffer.from(data.audio_data, 'base64'); 468 | fs.writeFileSync('C:/g4l/myOutput.wav', outputBuffer); 469 | Max.outlet('transform_undone'); 470 | }); 471 | } 472 | 473 | // Function to handle 'bang' message from Max 474 | Max.addHandler('bang', () => { 475 | if (!isProcessing) { 476 | isProcessing = true; 477 | // Optional: Send a cleanup request if sessionID exists 478 | if (sessionID) { 479 | socket.emit('cleanup_session_request', { session_id: sessionID }); 480 | } 481 | 482 | Max.post('Sending audio processing request to WebSocket server with a new session.'); 483 | processAudio('C:/g4l/myBuffer.wav'); 484 | sessionID = null; // Reset the session ID after sending the cleanup request 485 | 486 | // Add timeout to reset isProcessing 487 | setTimeout(() => { 488 | isProcessing = false; 489 | }, timeoutDuration); 490 | } else { 491 | Max.post('Processing already in progress.'); 492 | } 493 | }); 494 | 495 | // Function to handle 'continue' message from Max 496 | Max.addHandler('continue', () => { 497 | if (isProcessing) { 498 | Max.outlet('error_message', 'processing|Still processing previous request, wait a sec...'); 499 | Max.post('Processing already in progress.'); 500 | return; 501 | } 502 | 503 | if (!sessionID) { 504 | // NEW: Check if we have output audio to continue from (Jerry case) 505 | const outputAudioPath = 'C:/g4l/myOutput.wav'; 506 | 507 | if (fs.existsSync(outputAudioPath)) { 508 | Max.post('No session found, but output audio exists. Creating session and continuing...'); 509 | 510 | // Read the output audio and send as new session 511 | fs.readFile(outputAudioPath, (err, data) => { 512 | if (err) { 513 | Max.post(`Error reading output file for continue: ${err}`); 514 | Max.outlet('error_message', 'no_session|no audio available to continue from.'); 515 | return; 516 | } 517 | 518 | const audioData_base64 = data.toString('base64'); 519 | 520 | const request = { 521 | audio_data: audioData_base64, // Send the Jerry audio 522 | model_name: modelPath, 523 | prompt_duration: promptDuration 524 | // No session_id - will be auto-created 525 | }; 526 | 527 | // Add Gary-taming parameters if enabled 528 | if (tameTheGaryEnabled) { 529 | request.top_k = 150; 530 | request.cfg_coef = 5; 531 | request.description = "drums, percussion"; 532 | } 533 | 534 | socket.emit('continue_music_request', request); 535 | isProcessing = true; 536 | 537 | setTimeout(() => { 538 | isProcessing = false; 539 | }, timeoutDuration); 540 | }); 541 | 542 | return; 543 | } else { 544 | // No session and no output audio - show original error 545 | Max.outlet('error_message', 'no_session|no audio in the session to continue yet, homie. press bang first.'); 546 | Max.post('No session available - need to press bang first.'); 547 | return; 548 | } 549 | } 550 | 551 | // Existing logic for when session exists 552 | isProcessing = true; 553 | continueMusic(); 554 | 555 | setTimeout(() => { 556 | isProcessing = false; 557 | }, timeoutDuration); 558 | }); 559 | 560 | // Function to handle 'retry' message from Max 561 | Max.addHandler('retry', () => { 562 | if (isProcessing) { 563 | Max.outlet('error_message', 'processing|Still processing previous request, wait a sec...'); 564 | Max.post('Processing already in progress.'); 565 | return; 566 | } 567 | 568 | if (!sessionID) { 569 | // Using the same delimiter format as continue 570 | Max.outlet('error_message', 'no_continuation|no continued audio to retry bro. press continue first.'); 571 | Max.post('No continued audio available - need to press continue first.'); 572 | return; 573 | } 574 | 575 | isProcessing = true; 576 | 577 | const request = { 578 | session_id: sessionID, 579 | model_name: modelPath, 580 | prompt_duration: promptDuration 581 | }; 582 | 583 | // Add Gary-taming parameters if enabled 584 | if (tameTheGaryEnabled) { 585 | request.top_k = 150; 586 | request.cfg_coef = 5; 587 | request.description = "drums, percussion"; 588 | } 589 | 590 | socket.emit('retry_music_request', request); 591 | 592 | setTimeout(() => { 593 | isProcessing = false; 594 | }, timeoutDuration); 595 | }); 596 | 597 | // This handler will directly update the model path when text changes 598 | Max.addHandler('text', (newModelPath) => { 599 | if (typeof newModelPath === 'string') { 600 | modelPath = newModelPath.trim(); // Ensure to trim any extra whitespace 601 | Max.post(`Model path updated directly to: ${modelPath}`); 602 | } 603 | }); 604 | 605 | // Handler for receiving the prompt duration value 606 | Max.addHandler('prompt_duration', (value) => { 607 | if (typeof value === 'number' && value >= 1 && value <= 15) { 608 | promptDuration = value; 609 | Max.post(`Prompt duration set to: ${promptDuration}`); 610 | } else { 611 | Max.post('Invalid prompt duration value. It should be between 1 and 15.'); 612 | } 613 | }); 614 | 615 | // Handler for the 'crop_audio' event 616 | Max.addHandler('crop_audio', () => { 617 | const outputAudioPath = 'C:/g4l/myOutput.wav'; 618 | fs.readFile(outputAudioPath, (err, data) => { 619 | if (err) { 620 | Max.post(`Error reading output file for cropping: ${err}`); 621 | return; 622 | } 623 | const audioData_base64 = data.toString('base64'); 624 | socket.emit('update_cropped_audio', { session_id: sessionID, audio_data: audioData_base64 }); 625 | Max.post('Sent cropped audio data to backend'); 626 | }); 627 | }); 628 | 629 | // Add handler for reset_transform 630 | Max.addHandler('reset_transform', () => { 631 | Max.post('[commentedout] Reset transform handler triggered'); 632 | 633 | // If we have an active session, clean it up 634 | if (sessionID) { 635 | Max.post('[commentedout] Cleaning up session: ' + sessionID); 636 | socket.emit('cleanup_session_request', { session_id: sessionID }); 637 | sessionID = null; 638 | } else { 639 | Max.post('[commentedout] No active session to clean up'); 640 | } 641 | 642 | Max.post('[commentedout] Reset complete'); 643 | }); 644 | 645 | // Function to process audio 646 | function processAudio(inputAudioPath) { 647 | fs.readFile(inputAudioPath, (err, data) => { 648 | if (err) { 649 | Max.post(`Error reading audio file: ${err}`); 650 | isProcessing = false; 651 | Max.outlet('error', err.toString()); 652 | return; 653 | } 654 | const audioData_base64 = data.toString('base64'); 655 | 656 | // Base request object 657 | const request = { 658 | audio_data: audioData_base64, 659 | model_name: modelPath, 660 | prompt_duration: promptDuration 661 | }; 662 | 663 | // Add Gary-taming parameters if enabled 664 | if (tameTheGaryEnabled) { 665 | request.top_k = 100; 666 | request.cfg_coef = 5; 667 | request.description = "drums, percussion"; 668 | } 669 | 670 | socket.emit('process_audio_request', request); 671 | }); 672 | } 673 | 674 | // Function to continue music 675 | function continueMusic() { 676 | const outputAudioPath = 'C:/g4l/myOutput.wav'; 677 | fs.readFile(outputAudioPath, (err, data) => { 678 | if (err) { 679 | Max.post(`Error reading output file: ${err}`); 680 | isProcessing = false; 681 | Max.outlet('error', err.toString()); 682 | return; 683 | } 684 | const audioData_base64 = data.toString('base64'); 685 | 686 | // Base request object 687 | const request = { 688 | audio_data: audioData_base64, 689 | model_name: modelPath, 690 | session_id: sessionID, 691 | prompt_duration: promptDuration 692 | }; 693 | 694 | // Add Gary-taming parameters if enabled 695 | if (tameTheGaryEnabled) { 696 | request.top_k = 150; 697 | request.cfg_coef = 5; 698 | request.description = "drums, percussion"; 699 | } 700 | 701 | socket.emit('continue_music_request', request); 702 | }); 703 | } 704 | 705 | // Start the WebSocket connection 706 | initSocketConnection(); 707 | --------------------------------------------------------------------------------