├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── assets └── chart-hero-demo.gif ├── config ├── helpers.js ├── karma-test-shim.js ├── karma.conf.js ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── webpack.test.js ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── controller │ │ ├── bpm │ │ │ └── bpm.service.ts │ │ ├── controller.module.ts │ │ ├── event-controls │ │ │ ├── event-controls.component.css │ │ │ ├── event-controls.component.html │ │ │ └── event-controls.component.ts │ │ ├── lyric │ │ │ └── lyric.service.ts │ │ ├── note-controls │ │ │ ├── ghl │ │ │ │ ├── note-controls-ghl.component.css │ │ │ │ ├── note-controls-ghl.component.html │ │ │ │ └── note-controls-ghl.component.ts │ │ │ ├── guitar │ │ │ │ ├── note-controls-guitar.component.css │ │ │ │ ├── note-controls-guitar.component.html │ │ │ │ └── note-controls-guitar.component.ts │ │ │ ├── note-controls.component.css │ │ │ ├── note-controls.component.html │ │ │ └── note-controls.component.ts │ │ ├── parent-controls │ │ │ ├── parent-controls.component.css │ │ │ ├── parent-controls.component.html │ │ │ └── parent-controls.component.ts │ │ ├── practice-section │ │ │ └── practice-section.service.ts │ │ ├── selector │ │ │ └── selector.service.ts │ │ ├── step │ │ │ └── step.service.ts │ │ ├── time-signature │ │ │ └── time-signature.service.ts │ │ └── type │ │ │ └── type.service.ts │ ├── file │ │ ├── download │ │ │ ├── file-download.component.css │ │ │ ├── file-download.component.html │ │ │ └── file-download.component.ts │ │ ├── file.module.ts │ │ ├── file.service.spec.ts │ │ ├── file.service.ts │ │ └── select │ │ │ ├── file-select.component.css │ │ │ ├── file-select.component.html │ │ │ ├── file-select.component.spec.ts │ │ │ └── file-select.component.ts │ ├── fretboard │ │ ├── beat │ │ │ ├── beat.component.css │ │ │ ├── beat.component.html │ │ │ ├── beat.component.ts │ │ │ ├── beat.service.ts │ │ │ └── beat.ts │ │ ├── event-link │ │ │ ├── event-link.component.css │ │ │ ├── event-link.component.html │ │ │ ├── event-link.component.ts │ │ │ ├── event-link.service.ts │ │ │ └── event-link.ts │ │ ├── event │ │ │ ├── event.component.css │ │ │ ├── event.component.html │ │ │ ├── event.component.ts │ │ │ ├── event.service.ts │ │ │ └── event.ts │ │ ├── fretboard.module.ts │ │ ├── fretboard │ │ │ ├── fretboard.component.css │ │ │ ├── fretboard.component.html │ │ │ ├── fretboard.component.ts │ │ │ └── fretboard.ts │ │ ├── note │ │ │ ├── ghl │ │ │ │ ├── note-ghl.component.html │ │ │ │ └── note-ghl.component.ts │ │ │ ├── guitar │ │ │ │ ├── note-guitar.component.html │ │ │ │ └── note-guitar.component.ts │ │ │ ├── note.component.css │ │ │ ├── note.component.html │ │ │ ├── note.component.ts │ │ │ ├── note.service.ts │ │ │ ├── note.ts │ │ │ └── open │ │ │ │ ├── note-open.component.html │ │ │ │ └── note-open.component.ts │ │ ├── preparer │ │ │ ├── prepared.ts │ │ │ └── preparer.service.ts │ │ └── speed │ │ │ ├── speed-controls.component.css │ │ │ ├── speed-controls.component.html │ │ │ ├── speed-controls.component.ts │ │ │ └── speed.service.ts │ ├── global │ │ ├── actions │ │ │ ├── actions.component.css │ │ │ ├── actions.component.html │ │ │ └── actions.component.ts │ │ ├── global.module.ts │ │ ├── keybindings │ │ │ ├── keybindings-actions.service.ts │ │ │ ├── keybindings.component.html │ │ │ ├── keybindings.component.ts │ │ │ └── keybindings.service.ts │ │ ├── modals │ │ │ ├── keybindings │ │ │ │ ├── keybindings-modal.component.css │ │ │ │ ├── keybindings-modal.component.html │ │ │ │ └── keybindings-modal.component.ts │ │ │ ├── modals.component.css │ │ │ ├── modals.component.html │ │ │ └── modals.component.ts │ │ └── storage │ │ │ └── storage.service.ts │ ├── model │ │ ├── actions │ │ │ └── actions.service.ts │ │ ├── id-generator │ │ │ └── id-generator.service.ts │ │ ├── import-export │ │ │ ├── file-to-memory │ │ │ │ ├── file-to-memory.module.ts │ │ │ │ ├── file-to-memory.service.spec.ts │ │ │ │ ├── file-to-memory.service.ts │ │ │ │ ├── memory-to-file.service.spec.ts │ │ │ │ ├── memory-to-file.service.ts │ │ │ │ └── test-file-to-memory.ts │ │ │ ├── memory-to-model │ │ │ │ ├── common │ │ │ │ │ ├── metadata.service.ts │ │ │ │ │ ├── sync-track-exporter.service.ts │ │ │ │ │ ├── sync-track-importer.service.ts │ │ │ │ │ ├── unsupported-track-exporter.service.ts │ │ │ │ │ └── unsupported-track-importer.service.ts │ │ │ │ ├── generic │ │ │ │ │ ├── generic-track-exporter.service.ts │ │ │ │ │ └── generic-track-importer.service.ts │ │ │ │ ├── ghl │ │ │ │ │ ├── ghl-track-exporter.service.ts │ │ │ │ │ └── ghl-track-importer.service.ts │ │ │ │ ├── guitar │ │ │ │ │ ├── guitar-track-exporter.service.ts │ │ │ │ │ └── guitar-track-importer.service.ts │ │ │ │ ├── memory-to-model.module.ts │ │ │ │ ├── memory-to-model.service.spec.ts │ │ │ │ ├── memory-to-model.service.ts │ │ │ │ ├── model-to-memory.service.spec.ts │ │ │ │ ├── model-to-memory.service.ts │ │ │ │ ├── test-memory-to-model.ts │ │ │ │ └── util │ │ │ │ │ ├── midi-time.service.spec.ts │ │ │ │ │ └── midi-time.service.ts │ │ │ ├── memory.ts │ │ │ ├── model-exporter.service.ts │ │ │ ├── model-import-export.module.ts │ │ │ └── model-importer.service.ts │ │ ├── model.module.ts │ │ ├── model.service.ts │ │ └── model.ts │ ├── tap-input │ │ ├── display │ │ │ ├── tap-display.component.css │ │ │ ├── tap-display.component.html │ │ │ └── tap-display.component.ts │ │ ├── input │ │ │ ├── tap-input.component.css │ │ │ ├── tap-input.component.html │ │ │ └── tap-input.component.ts │ │ ├── tap-input.module.ts │ │ └── tap-input.service.ts │ ├── time │ │ ├── audio-player-controls │ │ │ ├── audio-player-controls.component.css │ │ │ ├── audio-player-controls.component.html │ │ │ └── audio-player-controls.component.ts │ │ ├── audio-player │ │ │ └── audio-player.service.ts │ │ ├── duration │ │ │ └── duration.service.ts │ │ ├── increment │ │ │ └── increment.service.ts │ │ ├── scrollbar │ │ │ ├── scrollbar.component.css │ │ │ ├── scrollbar.component.html │ │ │ └── scrollbar.component.ts │ │ ├── time.module.ts │ │ ├── time.service.ts │ │ └── volume │ │ │ ├── volume-controls.component.css │ │ │ ├── volume-controls.component.html │ │ │ ├── volume-controls.component.ts │ │ │ └── volume.service.ts │ └── track │ │ ├── converter │ │ ├── converter.component.css │ │ ├── converter.component.html │ │ └── converter.component.ts │ │ ├── guitar-to-ghl │ │ └── guitar-to-ghl-converter.service.ts │ │ ├── selector │ │ ├── selector.component.css │ │ ├── selector.component.html │ │ └── selector.component.ts │ │ ├── track.module.ts │ │ ├── track.service.ts │ │ └── track.ts ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── tsconfig.json ├── tslint.json └── vendor.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .vscode 3 | .idea 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017, Nathan Blades 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chart Hero 1.1.0 2 | 3 | Chart Hero is a web based chart editing application for Guitar Hero style rhythm games. With Chart Hero, you can create new custom charts in your browser on any desktop operating system. 4 | 5 | Head to https://nb48.github.io/chart-hero/ to check out the application. All you need is an audio file. (.mp3, .ogg, or anything else your browser supports) 6 | 7 | There are some examples here - https://drive.google.com/open?id=1T5JM1XR1tfY1WS6N4kgyKyeFU94zLd5Z 8 | 9 | 10 | 11 | ## Running 12 | 13 | ``` 14 | git clone git@github.com:nb48/chart-hero.git 15 | cd chart-hero 16 | yarn install 17 | yarn start 18 | ``` 19 | 20 | This starts the application at http://localhost:8080 21 | 22 | ## Testing 23 | 24 | ``` 25 | yarn test 26 | ``` 27 | 28 | ## Building 29 | 30 | ``` 31 | yarn build 32 | ``` 33 | 34 | ## Built With 35 | 36 | * [Angular](https://github.com/angular) 37 | * [TypeScript](https://github.com/Microsoft/TypeScript) 38 | * [webpack](https://github.com/webpack) 39 | * [howler.js](https://github.com/goldfire/howler.js) 40 | 41 | ## Styled With 42 | 43 | * [Angular Material](https://material.angular.io/) 44 | 45 | ## License 46 | 47 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 48 | 49 | ## Acknowledgments 50 | 51 | Thanks to the Guitar Hero community for helping to design, test, and improve Chart Hero 52 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nb48/chart-hero/315c9fdb0d191223320c17e93efb177c0547ffd4/TODO.md -------------------------------------------------------------------------------- /assets/chart-hero-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nb48/chart-hero/315c9fdb0d191223320c17e93efb177c0547ffd4/assets/chart-hero-demo.gif -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var _root = path.resolve(__dirname, '..'); 4 | 5 | function root(args) { 6 | args = Array.prototype.slice.call(arguments, 0); 7 | return path.join.apply(path, [_root].concat(args)); 8 | } 9 | 10 | exports.root = root; 11 | -------------------------------------------------------------------------------- /config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | Error.stackTraceLimit = Infinity; 2 | 3 | require('core-js/es6'); 4 | require('core-js/es7/reflect'); 5 | 6 | require('zone.js/dist/zone'); 7 | require('zone.js/dist/long-stack-trace-zone'); 8 | require('zone.js/dist/proxy'); 9 | require('zone.js/dist/sync-test'); 10 | require('zone.js/dist/jasmine-patch'); 11 | require('zone.js/dist/async-test'); 12 | require('zone.js/dist/fake-async-test'); 13 | 14 | require('hammerjs'); 15 | 16 | var appContext = require.context('../src', true, /\.spec\.ts/); 17 | 18 | appContext.keys().forEach(appContext); 19 | 20 | var testing = require('@angular/core/testing'); 21 | var browser = require('@angular/platform-browser-dynamic/testing'); 22 | 23 | testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting()); 24 | -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.test'); 2 | 3 | module.exports = function (config) { 4 | var _config = { 5 | basePath: '', 6 | frameworks: ['jasmine'], 7 | files: [{ 8 | pattern: './karma-test-shim.js', 9 | watched: false 10 | }, { 11 | pattern: '../node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css', 12 | watched: false 13 | }], 14 | preprocessors: { 15 | './karma-test-shim.js': ['webpack', 'sourcemap'] 16 | }, 17 | webpack: webpackConfig, 18 | webpackMiddleware: { 19 | quiet: true 20 | }, 21 | webpackServer: { 22 | quiet: true 23 | }, 24 | reporters: ['spec'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_WARN, 28 | autoWatch: false, 29 | browsers: ['ChromeHeadless'], 30 | singleRun: true 31 | }; 32 | config.set(_config); 33 | }; 34 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var helpers = require('./helpers'); 5 | 6 | module.exports = { 7 | entry: { 8 | 'polyfills': helpers.root('src', 'polyfills.ts'), 9 | 'vendor': helpers.root('src', 'vendor.ts'), 10 | 'main': helpers.root('src', 'main.ts'), 11 | 'styles': helpers.root('src', 'styles.css') 12 | }, 13 | resolve: { 14 | extensions: ['.ts', '.js'] 15 | }, 16 | output: { 17 | filename: 'app.js' 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.ts$/, 22 | enforce: 'pre', 23 | loader: 'tslint-loader', 24 | options: { 25 | configFile: helpers.root('src', 'tslint.json'), 26 | formatter: 'grouped', 27 | formattersDirectory: helpers.root('node_modules', 'custom-tslint-formatters', 'formatters'), 28 | tsConfigFile: helpers.root('src', 'tsconfig.json') 29 | } 30 | }, { 31 | test: /\.ts$/, 32 | loaders: [{ 33 | loader: 'awesome-typescript-loader', 34 | options: { 35 | configFileName: helpers.root('src', 'tsconfig.json') 36 | } 37 | }, { 38 | loader: 'angular2-template-loader' 39 | }] 40 | }, { 41 | test: /\.html$/, 42 | loader: 'html-loader' 43 | }, { 44 | test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, 45 | loader: 'file-loader?name=assets/[name].[hash].[ext]' 46 | }, { 47 | test: /\.css$/, 48 | exclude: helpers.root('src', 'app'), 49 | loader: ExtractTextPlugin.extract({ 50 | fallback: 'style-loader', 51 | use: 'css-loader?sourceMap' 52 | }) 53 | }, { 54 | test: /\.css$/, 55 | include: helpers.root('src', 'app'), 56 | loader: 'raw-loader' 57 | }] 58 | }, 59 | plugins: [ 60 | new webpack.ContextReplacementPlugin( 61 | /angular(\\|\/)core(\\|\/)/, 62 | helpers.root('src') 63 | ), 64 | new HtmlWebpackPlugin({ 65 | template: helpers.root('src', 'index.html') 66 | }) 67 | ], 68 | optimization: { 69 | splitChunks: { 70 | chunks: 'all' 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | var webpackMerge = require('webpack-merge'); 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | var commonConfig = require('./webpack.common'); 4 | 5 | module.exports = webpackMerge(commonConfig, { 6 | mode: 'development', 7 | devtool: 'cheap-module-eval-source-map', 8 | output: { 9 | publicPath: '/', 10 | filename: '[name].js', 11 | chunkFilename: '[id].chunk.js' 12 | }, 13 | plugins: [ 14 | new ExtractTextPlugin('[name].css') 15 | ], 16 | devServer: { 17 | historyApiFallback: true 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackMerge = require('webpack-merge'); 3 | var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin'); 4 | var BaseHrefWebpackPlugin = require('base-href-webpack-plugin').BaseHrefWebpackPlugin; 5 | var UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 6 | var commonConfig = require('./webpack.common'); 7 | var helpers = require('./helpers'); 8 | 9 | const ENV = process.env.NODE_ENV = process.env.ENV = 'production'; 10 | 11 | module.exports = webpackMerge(commonConfig, { 12 | mode: 'production', 13 | devtool: 'source-map', 14 | output: { 15 | path: helpers.root('dist'), 16 | publicPath: '/chart-hero/', 17 | filename: '[name].[hash].js', 18 | chunkFilename: '[id].[hash].chunk.js' 19 | }, 20 | plugins: [ 21 | new webpack.NoEmitOnErrorsPlugin(), 22 | new ExtractTextWebpackPlugin('[name].[hash].css'), 23 | new BaseHrefWebpackPlugin({ baseHref: '/chart-hero/' }), 24 | new webpack.DefinePlugin({ 25 | 'process.env': { 26 | 'ENV': JSON.stringify(ENV) 27 | } 28 | }) 29 | ], 30 | optimization: { 31 | minimizer: [ 32 | new UglifyJsPlugin() 33 | ] 34 | }, 35 | performance: { 36 | hints: false 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /config/webpack.test.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var helpers = require('./helpers'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | devtool: 'inline-source-map', 7 | resolve: { 8 | extensions: ['.ts', '.js'] 9 | }, 10 | module: { 11 | rules: [{ 12 | test: /\.ts$/, 13 | loaders: [{ 14 | loader: 'awesome-typescript-loader', 15 | options: { 16 | configFileName: helpers.root('src', 'tsconfig.json'), 17 | silent: true 18 | } 19 | }, { 20 | loader: 'angular2-template-loader' 21 | }] 22 | }, { 23 | test: /\.html$/, 24 | loader: 'html-loader' 25 | }, { 26 | test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, 27 | loader: 'null-loader' 28 | }, { 29 | test: /\.css$/, 30 | exclude: helpers.root('src', 'app'), 31 | loader: 'null-loader' 32 | }, { 33 | test: /\.css$/, 34 | include: helpers.root('src', 'app'), 35 | loader: 'raw-loader' 36 | }] 37 | }, 38 | plugins: [ 39 | new webpack.ContextReplacementPlugin( 40 | /angular(\\|\/)core(\\|\/)@angular/, 41 | helpers.root('src'), 42 | {} 43 | ) 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "chart-hero", 4 | "version": "1.1.0", 5 | "description": "A chart editor for clone hero.", 6 | "repository": "git@github.com:nb48/chart-hero.git", 7 | "author": "Nathan Blades ", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "webpack-dev-server --config config/webpack.dev.js --inline --progress --port 8080", 11 | "test": "karma start config/karma.conf.js", 12 | "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail", 13 | "serve": "http-server dist -s -p 8081" 14 | }, 15 | "dependencies": { 16 | "@angular/animations": "^7.0.0", 17 | "@angular/cdk": "^7.0.0", 18 | "@angular/common": "^7.0.0", 19 | "@angular/compiler": "^7.0.0", 20 | "@angular/core": "^7.0.0", 21 | "@angular/forms": "^7.0.0", 22 | "@angular/http": "^7.0.0", 23 | "@angular/material": "^7.0.0", 24 | "@angular/platform-browser": "^7.0.0", 25 | "@angular/platform-browser-dynamic": "^7.0.0", 26 | "@angular/router": "^7.0.0", 27 | "@types/howler": "^2.0.0", 28 | "@types/jasmine": "^3.0.0", 29 | "@types/node": "^10.0.0", 30 | "angular2-template-loader": "^0.6.0", 31 | "awesome-typescript-loader": "^5.0.0", 32 | "base-href-webpack-plugin": "^2.0.0", 33 | "core-js": "^2.0.0", 34 | "css-loader": "^2.0.0", 35 | "custom-tslint-formatters": "^2.0.0", 36 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 37 | "file-loader": "^3.0.0", 38 | "font-awesome": "^4.0.0", 39 | "hammerjs": "^2.0.0", 40 | "howler": "^2.0.0", 41 | "html-loader": "^0.5.0", 42 | "html-webpack-plugin": "^3.0.0", 43 | "http-server": "^0.11.0", 44 | "jasmine-core": "^3.0.0", 45 | "karma": "^4.0.0", 46 | "karma-chrome-launcher": "^2.0.0", 47 | "karma-jasmine": "^2.0.0", 48 | "karma-sourcemap-loader": "^0.3.0", 49 | "karma-spec-reporter": "^0.0.32", 50 | "karma-webpack": "^3.0.0", 51 | "null-loader": "^0.1.0", 52 | "raw-loader": "^1.0.0", 53 | "rimraf": "^2.0.0", 54 | "rxjs": "^6.0.0", 55 | "style-loader": "^0.23.0", 56 | "tslint": "^5.0.0", 57 | "tslint-config-airbnb": "^5.0.0", 58 | "tslint-loader": "^3.0.0", 59 | "typescript": "^3.0.0", 60 | "uglifyjs-webpack-plugin": "^2.0.0", 61 | "webpack": "^4.0.0", 62 | "webpack-cli": "^3.0.0", 63 | "webpack-dev-server": "^3.0.0", 64 | "webpack-merge": "^4.0.0", 65 | "zone.js": "^0.8.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | 2 | .app { 3 | display: grid; 4 | width: 100vw; 5 | height: 100vh; 6 | grid-template-areas: "title-bar title-bar file-select" 7 | ". view parent-controls" 8 | "track-selector view parent-controls" 9 | "audio-player-controls view parent-controls" 10 | "volume-controls view parent-controls" 11 | "speed-controls view parent-controls" 12 | "converter view parent-controls" 13 | "tap-input view parent-controls" 14 | "tap-display view parent-controls" 15 | "actions view parent-controls" 16 | ". view file-download"; 17 | grid-template-columns: 20% 2fr 1fr; 18 | grid-template-rows: 9.25em 1em 2.25em 4.25em 4.25em 4em 3.25em 4em 4fr 1fr 4.25em; 19 | } 20 | 21 | .app .title-bar { 22 | grid-area: title-bar; 23 | display: flex; 24 | } 25 | 26 | .app .title-bar .title { 27 | order: 1; 28 | flex: 1; 29 | } 30 | 31 | .app .title-bar .title h1 { 32 | margin-left: 2%; 33 | margin-top: 1%; 34 | color: white; 35 | } 36 | 37 | .app .title-bar app-modals { 38 | order: 2; 39 | } 40 | 41 | .app app-file-select { 42 | grid-area: file-select; 43 | } 44 | 45 | .app app-track-selector { 46 | grid-area: track-selector; 47 | } 48 | 49 | .app app-audio-player-controls { 50 | grid-area: audio-player-controls; 51 | } 52 | 53 | .app app-volume-controls { 54 | grid-area: volume-controls; 55 | } 56 | 57 | .app app-speed-controls { 58 | grid-area: speed-controls; 59 | } 60 | 61 | .app app-tap-input { 62 | grid-area: tap-input; 63 | } 64 | 65 | .app app-tap-display { 66 | grid-area: tap-display; 67 | } 68 | 69 | .app app-actions { 70 | grid-area: actions; 71 | } 72 | 73 | .app app-converter { 74 | grid-area: converter; 75 | } 76 | 77 | .app .view { 78 | grid-area: view; 79 | display: grid; 80 | width: 99%; 81 | height: 99.30%; 82 | margin-left: 1%; 83 | grid-template-areas: "fretboard . scrollbar"; 84 | grid-template-columns: 1fr 0.5em 1em; 85 | grid-template-rows: 99.30%; 86 | } 87 | 88 | .app .view app-fretboard { 89 | grid-area: fretboard; 90 | } 91 | 92 | .app .view app-scrollbar { 93 | grid-area: scrollbar; 94 | } 95 | 96 | .app app-parent-controls { 97 | grid-area: parent-controls; 98 | } 99 | 100 | .app app-file-download { 101 | grid-area: file-download; 102 | } 103 | 104 | .app app-keybindings { 105 | display: none; 106 | } 107 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Chart Hero

5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { combineLatest } from 'rxjs'; 3 | 4 | import { BeatService } from './fretboard/beat/beat.service'; 5 | import { EventService } from './fretboard/event/event.service'; 6 | import { EventLinkService } from './fretboard/event-link/event-link.service'; 7 | import { Fretboard } from './fretboard/fretboard/fretboard'; 8 | import { NoteService } from './fretboard/note/note.service'; 9 | import { StorageService } from './global/storage/storage.service'; 10 | 11 | @Component({ 12 | selector: 'app', 13 | templateUrl: 'app.component.html', 14 | styleUrls: ['app.component.css'], 15 | }) 16 | export class AppComponent { 17 | 18 | fretboard: Fretboard; 19 | zeroPosition: number; 20 | 21 | constructor( 22 | private beatService: BeatService, 23 | private noteService: NoteService, 24 | private eventService: EventService, 25 | private eventLinkService: EventLinkService, 26 | private storageService: StorageService, 27 | ) { 28 | this.beatService.zeroPositions.subscribe((zeroPosition) => { 29 | this.zeroPosition = zeroPosition; 30 | }); 31 | combineLatest( 32 | this.beatService.beats, 33 | this.noteService.notes, 34 | this.eventService.events, 35 | this.eventLinkService.eventLinks, 36 | (beats, notes, events, eventLinks) => { 37 | return { 38 | beats, 39 | notes, 40 | events, 41 | eventLinks, 42 | zeroPosition: this.zeroPosition, 43 | }; 44 | }, 45 | ).subscribe((fretboard) => { 46 | this.fretboard = fretboard; 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | import { AppControllerModule } from './controller/controller.module'; 6 | import { AppFileModule } from './file/file.module'; 7 | import { AppFretboardModule } from './fretboard/fretboard.module'; 8 | import { AppGlobalModule } from './global/global.module'; 9 | import { AppModelModule } from './model/model.module'; 10 | import { AppTapInputModule } from './tap-input/tap-input.module'; 11 | import { AppTimeModule } from './time/time.module'; 12 | import { AppTrackModule } from './track/track.module'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | AppControllerModule, 17 | AppFileModule, 18 | AppFretboardModule, 19 | AppGlobalModule, 20 | AppModelModule, 21 | AppTapInputModule, 22 | AppTimeModule, 23 | AppTrackModule, 24 | ], 25 | declarations: [ 26 | AppComponent, 27 | ], 28 | bootstrap: [AppComponent], 29 | }) 30 | export class AppModule { 31 | } 32 | -------------------------------------------------------------------------------- /src/app/controller/bpm/bpm.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { ActionsService } from '../../model/actions/actions.service'; 5 | import { ModelTrackEvent, ModelTrackBPMChange } from '../../model/model'; 6 | import { SelectorService } from '../selector/selector.service'; 7 | 8 | @Injectable() 9 | export class BPMService { 10 | 11 | private event: ModelTrackEvent; 12 | 13 | constructor( 14 | private actionsService: ActionsService, 15 | private selectorService: SelectorService, 16 | ) { 17 | this.selectorService.selectedEvents.subscribe((event) => { 18 | this.event = event; 19 | }); 20 | } 21 | 22 | updateBPM(bpm: number): void { 23 | const newEvent = JSON.parse(JSON.stringify(this.event)); 24 | (newEvent as ModelTrackBPMChange).bpm = bpm; 25 | this.actionsService.syncTrackEventChanged(newEvent); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/controller/controller.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { 4 | MatButtonModule, 5 | MatFormFieldModule, 6 | MatInputModule, 7 | MatListModule, 8 | MatRadioModule, 9 | MatSlideToggleModule, 10 | MatTooltipModule, 11 | } from '@angular/material'; 12 | import { BrowserModule } from '@angular/platform-browser'; 13 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 14 | 15 | import { EventControlsComponent } from './event-controls/event-controls.component'; 16 | import { NoteControlsGHLComponent } from './note-controls/ghl/note-controls-ghl.component'; 17 | import { NoteControlsGuitarComponent } from './note-controls/guitar/note-controls-guitar.component'; 18 | import { NoteControlsComponent } from './note-controls/note-controls.component'; 19 | import { ParentControlsComponent } from './parent-controls/parent-controls.component'; 20 | 21 | import { BPMService } from './bpm/bpm.service'; 22 | import { LyricService } from './lyric/lyric.service'; 23 | import { PracticeSectionService } from './practice-section/practice-section.service'; 24 | import { SelectorService } from './selector/selector.service'; 25 | import { StepService } from './step/step.service'; 26 | import { TimeSignatureService } from './time-signature/time-signature.service'; 27 | import { TypeService } from './type/type.service'; 28 | 29 | @NgModule({ 30 | imports: [ 31 | BrowserModule, 32 | BrowserAnimationsModule, 33 | FormsModule, 34 | MatButtonModule, 35 | MatFormFieldModule, 36 | MatInputModule, 37 | MatListModule, 38 | MatRadioModule, 39 | MatSlideToggleModule, 40 | MatTooltipModule, 41 | ReactiveFormsModule, 42 | ], 43 | exports: [ 44 | ParentControlsComponent, 45 | ], 46 | declarations: [ 47 | EventControlsComponent, 48 | NoteControlsGHLComponent, 49 | NoteControlsGuitarComponent, 50 | NoteControlsComponent, 51 | ParentControlsComponent, 52 | ], 53 | providers: [ 54 | BPMService, 55 | LyricService, 56 | PracticeSectionService, 57 | SelectorService, 58 | StepService, 59 | TimeSignatureService, 60 | TypeService, 61 | ], 62 | }) 63 | export class AppControllerModule { 64 | } 65 | -------------------------------------------------------------------------------- /src/app/controller/event-controls/event-controls.component.css: -------------------------------------------------------------------------------- 1 | 2 | .event-controls { 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .event-controls .info { 10 | order: 1; 11 | } 12 | 13 | .event-controls .info label { 14 | width: 35%; 15 | margin-left: 5%; 16 | } 17 | 18 | .event-controls .info input { 19 | width: 60%; 20 | } 21 | 22 | .event-controls .actions { 23 | order: 2; 24 | width: calc(100% - 1em); 25 | height: 10%; 26 | margin-top: 2%; 27 | margin-left: 0.5em; 28 | margin-right: 0.5em; 29 | display: grid; 30 | grid-template-areas: ". . delete-event" 31 | ". . ."; 32 | grid-template-columns: 1fr 1fr 1fr; 33 | grid-template-rows: 1fr 1fr; 34 | } 35 | 36 | .event-controls .actions .delete-event { 37 | grid-area: delete-event; 38 | } 39 | 40 | .event-controls .actions button { 41 | margin: 0.5em; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/controller/event-controls/event-controls.component.html: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /src/app/controller/lyric/lyric.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { ActionsService } from '../../model/actions/actions.service'; 4 | import { ModelTrackEvent, ModelTrackLyric } from '../../model/model'; 5 | import { SelectorService } from '../selector/selector.service'; 6 | 7 | @Injectable() 8 | export class LyricService { 9 | 10 | private event: ModelTrackEvent; 11 | 12 | constructor( 13 | private actionsService: ActionsService, 14 | private selectorService: SelectorService, 15 | ) { 16 | this.selectorService.selectedEvents.subscribe((event) => { 17 | this.event = event; 18 | }); 19 | } 20 | 21 | updateWord(word: string): void { 22 | if (!this.event) { 23 | return; 24 | } 25 | const newEvent = JSON.parse(JSON.stringify(this.event)) as ModelTrackLyric; 26 | newEvent.word = word; 27 | this.actionsService.eventEventChanged(newEvent); 28 | } 29 | 30 | flipMultiSyllable(): void { 31 | if (!this.event) { 32 | return; 33 | } 34 | const newEvent = JSON.parse(JSON.stringify(this.event)) as ModelTrackLyric; 35 | newEvent.multiSyllable = !newEvent.multiSyllable; 36 | this.actionsService.eventEventChanged(newEvent); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/ghl/note-controls-ghl.component.css: -------------------------------------------------------------------------------- 1 | 2 | .note-controls-ghl { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .note-controls-ghl > svg { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/ghl/note-controls-ghl.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/ghl/note-controls-ghl.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { ModelTrackNoteType } from '../../../model/model'; 4 | import { TypeService } from '../../type/type.service'; 5 | 6 | @Component({ 7 | selector: 'app-note-controls-ghl', 8 | templateUrl: './note-controls-ghl.component.html', 9 | styleUrls: ['./note-controls-ghl.component.css'], 10 | }) 11 | export class NoteControlsGHLComponent { 12 | @Input() type: ModelTrackNoteType[]; 13 | 14 | constructor(private typeService: TypeService) { 15 | } 16 | 17 | get black1(): boolean { 18 | return this.type.indexOf(ModelTrackNoteType.GHLBlack1) !== -1; 19 | } 20 | 21 | get black2(): boolean { 22 | return this.type.indexOf(ModelTrackNoteType.GHLBlack2) !== -1; 23 | } 24 | 25 | get black3(): boolean { 26 | return this.type.indexOf(ModelTrackNoteType.GHLBlack3) !== -1; 27 | } 28 | 29 | get white1(): boolean { 30 | return this.type.indexOf(ModelTrackNoteType.GHLWhite1) !== -1; 31 | } 32 | 33 | get white2(): boolean { 34 | return this.type.indexOf(ModelTrackNoteType.GHLWhite2) !== -1; 35 | } 36 | 37 | get white3(): boolean { 38 | return this.type.indexOf(ModelTrackNoteType.GHLWhite3) !== -1; 39 | } 40 | 41 | flipBlack1(): void { 42 | this.typeService.flip1(); 43 | } 44 | 45 | flipBlack2(): void { 46 | this.typeService.flip2(); 47 | } 48 | 49 | flipBlack3(): void { 50 | this.typeService.flip3(); 51 | } 52 | 53 | flipWhite1(): void { 54 | this.typeService.flip4(); 55 | } 56 | 57 | flipWhite2(): void { 58 | this.typeService.flip5(); 59 | } 60 | 61 | flipWhite3(): void { 62 | this.typeService.flip6(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/guitar/note-controls-guitar.component.css: -------------------------------------------------------------------------------- 1 | 2 | .note-controls-guitar { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .note-controls-guitar > svg { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/guitar/note-controls-guitar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/guitar/note-controls-guitar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { ModelTrackNoteType } from '../../../model/model'; 4 | import { TypeService } from '../../type/type.service'; 5 | 6 | @Component({ 7 | selector: 'app-note-controls-guitar', 8 | templateUrl: './note-controls-guitar.component.html', 9 | styleUrls: ['./note-controls-guitar.component.css'], 10 | }) 11 | export class NoteControlsGuitarComponent { 12 | @Input() type: ModelTrackNoteType[]; 13 | 14 | constructor(private typeService: TypeService) { 15 | } 16 | 17 | get green(): boolean { 18 | return this.type.indexOf(ModelTrackNoteType.GuitarGreen) !== -1; 19 | } 20 | 21 | get red(): boolean { 22 | return this.type.indexOf(ModelTrackNoteType.GuitarRed) !== -1; 23 | } 24 | 25 | get yellow(): boolean { 26 | return this.type.indexOf(ModelTrackNoteType.GuitarYellow) !== -1; 27 | } 28 | 29 | get blue(): boolean { 30 | return this.type.indexOf(ModelTrackNoteType.GuitarBlue) !== -1; 31 | } 32 | 33 | get orange(): boolean { 34 | return this.type.indexOf(ModelTrackNoteType.GuitarOrange) !== -1; 35 | } 36 | 37 | flipGreen(): void { 38 | this.typeService.flip1(); 39 | } 40 | 41 | flipRed(): void { 42 | this.typeService.flip2(); 43 | } 44 | 45 | flipYellow(): void { 46 | this.typeService.flip3(); 47 | } 48 | 49 | flipBlue(): void { 50 | this.typeService.flip4(); 51 | } 52 | 53 | flipOrange(): void { 54 | this.typeService.flip5(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/note-controls.component.css: -------------------------------------------------------------------------------- 1 | 2 | .note-controls { 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .note-controls .info { 10 | order: 1; 11 | } 12 | 13 | .note-controls .info label { 14 | width: 35%; 15 | margin-left: 5%; 16 | } 17 | 18 | .note-controls .info input { 19 | width: 60%; 20 | } 21 | 22 | .note-controls .note-type-guitar { 23 | order: 2; 24 | height: 10%; 25 | margin-top: 2%; 26 | } 27 | 28 | .note-controls .note-type-ghl { 29 | order: 2; 30 | height: 20%; 31 | margin-top: 2%; 32 | } 33 | 34 | .note-controls .step-controls { 35 | order: 3; 36 | width: 80%; 37 | height: auto; 38 | margin-top: 3%; 39 | margin-bottom: 3%; 40 | margin-left: 10%; 41 | margin-right: 10%; 42 | } 43 | 44 | .note-controls .step-controls .group { 45 | width: 100%; 46 | display: inline-flex; 47 | justify-content: space-around; 48 | } 49 | 50 | .note-controls .step-controls .group .button { 51 | color: white; 52 | } 53 | 54 | .note-controls .step-controls .group .down { 55 | margin-top: 0.65em; 56 | } 57 | 58 | .note-controls .step-controls .group .up { 59 | margin-top: -0.60em; 60 | } 61 | 62 | .note-controls .step-controls .group mat-form-field { 63 | width: 3em; 64 | } 65 | 66 | .note-controls .time-controls { 67 | order: 4; 68 | width: 80%; 69 | height: auto; 70 | margin-top: 3%; 71 | margin-bottom: 5%; 72 | margin-left: 10%; 73 | margin-right: 10%; 74 | display: flex; 75 | justify-content: space-around; 76 | } 77 | 78 | .note-controls .actions { 79 | order: 5; 80 | width: calc(100% - 1em); 81 | height: auto; 82 | margin-left: 0.5em; 83 | margin-right: 0.5em; 84 | display: grid; 85 | grid-template-areas: ". increase-sustain . decrease-sustain . delete-note ."; 86 | grid-template-columns: 30% auto 2% auto 1fr auto 2%; 87 | grid-template-rows: 1fr; 88 | } 89 | 90 | .note-controls .actions .increase-sustain { 91 | grid-area: increase-sustain; 92 | } 93 | 94 | .note-controls .actions .decrease-sustain { 95 | grid-area: decrease-sustain; 96 | } 97 | 98 | .note-controls .actions .delete-note { 99 | grid-area: delete-note; 100 | } 101 | -------------------------------------------------------------------------------- /src/app/controller/note-controls/note-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | 4 | import { ActionsService } from '../../model/actions/actions.service'; 5 | import { ModelTrackNoteType, ModelTrackEventType } from '../../model/model'; 6 | import { showTime } from '../../time/audio-player-controls/audio-player-controls.component'; 7 | import { StepService } from '../step/step.service'; 8 | import { TypeService } from '../type/type.service'; 9 | import { SelectorService } from '../selector/selector.service'; 10 | 11 | @Component({ 12 | selector: 'app-note-controls', 13 | templateUrl: './note-controls.component.html', 14 | styleUrls: ['./note-controls.component.css'], 15 | }) 16 | export class NoteControlsComponent implements OnDestroy { 17 | 18 | subscription: Subscription; 19 | selected: boolean; 20 | id: number; 21 | time: number; 22 | formattedTime: string; 23 | length: number; 24 | formattedLength: string; 25 | forceHopo: boolean; 26 | tap: boolean; 27 | isGuitarNote: boolean; 28 | isGHLNote: boolean; 29 | type: ModelTrackNoteType[]; 30 | stepControl: string; 31 | customStepTop: number; 32 | customStepBottom: number; 33 | 34 | constructor( 35 | private actionsService: ActionsService, 36 | private stepService: StepService, 37 | private typeService: TypeService, 38 | private selectorService: SelectorService, 39 | ) { 40 | this.selected = false; 41 | this.stepControl = 42 | this.stepService.stepControl ? this.stepService.stepControl : 'one'; 43 | this.customStepTop = 44 | this.stepService.customStepTop ? this.stepService.customStepTop : 1; 45 | this.customStepBottom = 46 | this.stepService.customStepBottom ? this.stepService.customStepBottom : 1; 47 | this.subscription = this.selectorService.selectedNotes.subscribe((note) => { 48 | if (!note) { 49 | this.selected = false; 50 | return; 51 | } 52 | this.selected = true; 53 | this.id = note.id; 54 | this.time = note.time; 55 | this.formattedTime = showTime(note.time); 56 | this.length = note.length; 57 | this.formattedLength = showTime(note.length); 58 | this.forceHopo = note.forceHopo; 59 | this.tap = note.tap; 60 | this.isGuitarNote = note.event === ModelTrackEventType.GuitarNote; 61 | this.isGHLNote = note.event === ModelTrackEventType.GHLNote; 62 | this.type = note.type; 63 | }); 64 | this.newStep(); 65 | } 66 | 67 | ngOnDestroy(): void { 68 | this.subscription.unsubscribe(); 69 | } 70 | 71 | forceHopoChanged(): void { 72 | this.typeService.flipForceHOPO(); 73 | } 74 | 75 | tapChanged(): void { 76 | this.typeService.flipTap(); 77 | } 78 | 79 | newStep(): void { 80 | this.stepService.newStep(this.stepControl, this.customStepTop, this.customStepBottom); 81 | } 82 | 83 | moveForwards(): void { 84 | this.stepService.moveForwardsTime(); 85 | } 86 | 87 | moveBackwards(): void { 88 | this.stepService.moveBackwardsTime(); 89 | } 90 | 91 | snapForwards(): void { 92 | this.stepService.snapForwardsTime(); 93 | } 94 | 95 | snapBackwards(): void { 96 | this.stepService.snapBackwardsTime(); 97 | } 98 | 99 | increaseSustain(): void { 100 | this.stepService.increaseSustain(); 101 | } 102 | 103 | decreaseSustain(): void { 104 | this.stepService.decreaseSustain(); 105 | } 106 | 107 | delete(): void { 108 | this.actionsService.deleteEvent(); 109 | } 110 | 111 | clickStepControl(): void { 112 | this.stepControl = 'custom'; 113 | this.newStep(); 114 | } 115 | 116 | keyDown(event: Event) { 117 | event.stopPropagation(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/app/controller/parent-controls/parent-controls.component.css: -------------------------------------------------------------------------------- 1 | 2 | .parent-controls { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/controller/parent-controls/parent-controls.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/app/controller/parent-controls/parent-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { SelectorService } from '../selector/selector.service'; 4 | 5 | @Component({ 6 | selector: 'app-parent-controls', 7 | templateUrl: './parent-controls.component.html', 8 | styleUrls: ['./parent-controls.component.css'], 9 | }) 10 | export class ParentControlsComponent { 11 | 12 | noteSelected: boolean; 13 | eventSelected: boolean; 14 | 15 | constructor(private selectorService: SelectorService) { 16 | this.eventSelected = false; 17 | this.selectorService.selectedEvents.subscribe((event) => { 18 | if (!event) { 19 | this.eventSelected = false; 20 | return; 21 | } 22 | this.eventSelected = true; 23 | }); 24 | this.noteSelected = false; 25 | this.selectorService.selectedNotes.subscribe((note) => { 26 | if (!note) { 27 | this.noteSelected = false; 28 | return; 29 | } 30 | this.noteSelected = true; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/controller/practice-section/practice-section.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { ActionsService } from '../../model/actions/actions.service'; 4 | import { ModelTrackEvent, ModelTrackPracticeSection } from '../../model/model'; 5 | import { SelectorService } from '../selector/selector.service'; 6 | 7 | @Injectable() 8 | export class PracticeSectionService { 9 | 10 | private event: ModelTrackEvent; 11 | 12 | constructor( 13 | private actionsService: ActionsService, 14 | private selectorService: SelectorService, 15 | ) { 16 | this.selectorService.selectedEvents.subscribe((event) => { 17 | this.event = event; 18 | }); 19 | } 20 | 21 | updatePracticeSection(name: string): void { 22 | const newEvent = JSON.parse(JSON.stringify(this.event)); 23 | (newEvent as ModelTrackPracticeSection).name = name; 24 | this.actionsService.eventEventChanged(newEvent); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/controller/time-signature/time-signature.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { ActionsService } from '../../model/actions/actions.service'; 5 | import { ModelTrackEvent, ModelTrackTSChange } from '../../model/model'; 6 | import { SelectorService } from '../selector/selector.service'; 7 | 8 | @Injectable() 9 | export class TimeSignatureService { 10 | 11 | private event: ModelTrackEvent; 12 | 13 | constructor( 14 | private actionsService: ActionsService, 15 | private selectorService: SelectorService, 16 | ) { 17 | this.selectorService.selectedEvents.subscribe((event) => { 18 | this.event = event; 19 | }); 20 | } 21 | 22 | updateTimeSignature(ts: number): void { 23 | const newEvent = JSON.parse(JSON.stringify(this.event)); 24 | (newEvent as ModelTrackTSChange).ts = ts; 25 | this.actionsService.syncTrackEventChanged(newEvent); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/controller/type/type.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { ActionsService } from '../../model/actions/actions.service'; 5 | import { ModelTrackNote, ModelTrackEventType, ModelTrackNoteType } from '../../model/model'; 6 | import { SelectorService } from '../selector/selector.service'; 7 | 8 | @Injectable() 9 | export class TypeService { 10 | 11 | private note: ModelTrackNote; 12 | 13 | constructor( 14 | private actionsService: ActionsService, 15 | private selectorService: SelectorService, 16 | ) { 17 | this.selectorService.selectedNotes.subscribe((note) => { 18 | this.note = note; 19 | }); 20 | } 21 | 22 | flip1(): void { 23 | if (!this.note) { 24 | return; 25 | } 26 | const type = this.isGhl() ? ModelTrackNoteType.GHLBlack1 : ModelTrackNoteType.GuitarGreen; 27 | this.flip(type); 28 | } 29 | 30 | flip2(): void { 31 | if (!this.note) { 32 | return; 33 | } 34 | const type = this.isGhl() ? ModelTrackNoteType.GHLBlack2 : ModelTrackNoteType.GuitarRed; 35 | this.flip(type); 36 | } 37 | 38 | flip3(): void { 39 | if (!this.note) { 40 | return; 41 | } 42 | const type = this.isGhl() ? ModelTrackNoteType.GHLBlack3 : ModelTrackNoteType.GuitarYellow; 43 | this.flip(type); 44 | } 45 | 46 | flip4(): void { 47 | if (!this.note) { 48 | return; 49 | } 50 | const type = this.isGhl() ? ModelTrackNoteType.GHLWhite1 : ModelTrackNoteType.GuitarBlue; 51 | this.flip(type); 52 | } 53 | 54 | flip5(): void { 55 | if (!this.note) { 56 | return; 57 | } 58 | const type = this.isGhl() ? ModelTrackNoteType.GHLWhite2 : ModelTrackNoteType.GuitarOrange; 59 | this.flip(type); 60 | } 61 | 62 | flip6(): void { 63 | if (!this.note) { 64 | return; 65 | } 66 | if (!this.isGhl()) { 67 | return; 68 | } 69 | const type = ModelTrackNoteType.GHLWhite3; 70 | this.flip(type); 71 | } 72 | 73 | flipForceHOPO(): void { 74 | if (!this.note) { 75 | return; 76 | } 77 | this.note.forceHopo = !this.note.forceHopo; 78 | this.updateNote(); 79 | } 80 | 81 | flipTap(): void { 82 | if (!this.note) { 83 | return; 84 | } 85 | this.note.tap = !this.note.tap; 86 | this.updateNote(); 87 | } 88 | 89 | private flip(type: ModelTrackNoteType): void { 90 | const index = this.note.type.indexOf(type); 91 | if (index === -1) { 92 | this.note.type = this.note.type.concat([type]); 93 | this.updateNote(); 94 | } else { 95 | this.note.type.splice(index, 1); 96 | this.updateNote(); 97 | } 98 | } 99 | 100 | private updateNote(): void { 101 | const newNote = JSON.parse(JSON.stringify(this.note)); 102 | this.actionsService.trackEventChanged(newNote); 103 | } 104 | 105 | private isGhl(): boolean { 106 | return this.note.event === ModelTrackEventType.GHLNote; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/file/download/file-download.component.css: -------------------------------------------------------------------------------- 1 | 2 | .file-download { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .file-download button { 8 | width: 100%; 9 | margin-bottom: 0.25em; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/file/download/file-download.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
-------------------------------------------------------------------------------- /src/app/file/download/file-download.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ModelExporterService } from '../../model/import-export/model-exporter.service'; 4 | 5 | @Component({ 6 | selector: 'app-file-download', 7 | templateUrl: './file-download.component.html', 8 | styleUrls: ['./file-download.component.css'], 9 | }) 10 | export class FileDownloadComponent { 11 | 12 | constructor(public modelExporter: ModelExporterService) { 13 | } 14 | 15 | exportChart() { 16 | const chartString = this.modelExporter.export(); 17 | const datetime = new Date() 18 | .toISOString() 19 | .replace(/:/g, '-') 20 | .split('.')[0]; 21 | const filename = `notes-${datetime}.chart`; 22 | const chart = new File([chartString], filename, { type: 'text/plain' }); 23 | const url = window.URL.createObjectURL(chart); 24 | const link = document.createElement('a'); 25 | link.setAttribute('style', 'display: none'); 26 | link.setAttribute('href', url); 27 | link.setAttribute('download', filename); 28 | document.body.appendChild(link); 29 | link.click(); 30 | window.URL.revokeObjectURL(url); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/file/file.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatButtonModule, MatListModule, MatTooltipModule } from '@angular/material'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { FileDownloadComponent } from './download/file-download.component'; 7 | import { FileSelectComponent } from './select/file-select.component'; 8 | import { FileService } from './file.service'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | BrowserModule, 13 | BrowserAnimationsModule, 14 | MatButtonModule, 15 | MatListModule, 16 | MatTooltipModule, 17 | ], 18 | exports: [ 19 | FileDownloadComponent, 20 | FileSelectComponent, 21 | ], 22 | declarations: [ 23 | FileDownloadComponent, 24 | FileSelectComponent, 25 | ], 26 | providers: [ 27 | FileService, 28 | ], 29 | }) 30 | export class AppFileModule { 31 | } 32 | -------------------------------------------------------------------------------- /src/app/file/file.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AudioPlayerService } from '../time/audio-player/audio-player.service'; 4 | import { ModelImporterService } from '../model/import-export/model-importer.service'; 5 | import { TimeService } from '../time/time.service'; 6 | import { FileService } from './file.service'; 7 | 8 | const testFile = new File(['testFileString'], 'testFileName'); 9 | 10 | describe('Service: FileService', () => { 11 | 12 | let service: FileService; 13 | 14 | beforeEach(() => { 15 | TestBed.configureTestingModule({ 16 | providers: [ 17 | { provide: AudioPlayerService, useClass: MockAudioPlayerService }, 18 | { provide: ModelImporterService, useClass: MockModelImporterService }, 19 | { provide: TimeService, useClass: MockTimeService }, 20 | FileService, 21 | ], 22 | }); 23 | service = TestBed.get(FileService); 24 | }); 25 | 26 | it('FileStore should update audio file name after setting audio file', () => { 27 | let audioFileName; 28 | service.audioFileNames.subscribe(afn => audioFileName = afn); 29 | service.audioFile = testFile; 30 | expect(audioFileName).toEqual('testFileName'); 31 | }); 32 | 33 | it('FileStore should pass url to audio player after setting audio file', () => { 34 | service.audioFile = testFile; 35 | const audioPlayer = TestBed.get(AudioPlayerService); 36 | expect((audioPlayer as any).url).toBeDefined; 37 | expect(typeof (audioPlayer as any).url === 'string'); 38 | }); 39 | 40 | it('FileStore should update chart file name after setting chart file', () => { 41 | let chartFileName; 42 | service.chartFileNames.subscribe(cfn => chartFileName = cfn); 43 | service.chartFile = testFile; 44 | expect(chartFileName).toEqual('testFileName'); 45 | }); 46 | 47 | it('FileStore should pass chart string to chart importer after setting chart file', (done) => { 48 | service.chartFile = testFile; 49 | setTimeout(() => { 50 | const modelImporter = TestBed.get(ModelImporterService); 51 | expect((modelImporter as any).file).toEqual('testFileString'); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | 57 | class MockAudioPlayerService { 58 | 59 | private $url: string; 60 | 61 | set audio(url: string) { 62 | this.$url = url; 63 | } 64 | 65 | get url(): string { 66 | return this.$url; 67 | } 68 | } 69 | 70 | class MockModelImporterService { 71 | 72 | public file: string; 73 | 74 | import(file: string) { 75 | this.file = file; 76 | } 77 | } 78 | 79 | class MockTimeService { 80 | 81 | playing: false; 82 | } 83 | -------------------------------------------------------------------------------- /src/app/file/file.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | 4 | import { AudioPlayerService } from '../time/audio-player/audio-player.service'; 5 | import { ModelImporterService } from '../model/import-export/model-importer.service'; 6 | import { TimeService } from '../time/time.service'; 7 | 8 | @Injectable() 9 | export class FileService { 10 | 11 | private audioFileNameSubject: BehaviorSubject; 12 | private chartFileNameSubject: BehaviorSubject; 13 | 14 | constructor( 15 | private audioPlayer: AudioPlayerService, 16 | private modelImporter: ModelImporterService, 17 | private timeService: TimeService, 18 | ) { 19 | this.audioFileNameSubject = new BehaviorSubject(''); 20 | this.chartFileNameSubject = new BehaviorSubject(''); 21 | } 22 | 23 | get audioFileNames(): Observable { 24 | return this.audioFileNameSubject.asObservable(); 25 | } 26 | 27 | set audioFile(file: File) { 28 | if (this.timeService.playing) { 29 | this.timeService.stop(); 30 | } 31 | this.audioFileNameSubject.next(file.name); 32 | const extension = file.name.split('.')[1]; 33 | if (!extension) { 34 | return; 35 | } 36 | this.audioPlayer.setAudio(URL.createObjectURL(file), extension); 37 | } 38 | 39 | get chartFileNames(): Observable { 40 | return this.chartFileNameSubject.asObservable(); 41 | } 42 | 43 | loadChartFileName(fileName: string): void { 44 | this.chartFileNameSubject.next(fileName); 45 | } 46 | 47 | set chartFile(file: File) { 48 | if (this.timeService.playing) { 49 | this.timeService.stop(); 50 | } else { 51 | this.timeService.time = 0; 52 | } 53 | this.chartFileNameSubject.next(file.name); 54 | const reader = new FileReader(); 55 | reader.onload = () => { 56 | this.modelImporter.import(reader.result.toString()); 57 | }; 58 | reader.readAsText(file); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/file/select/file-select.component.css: -------------------------------------------------------------------------------- 1 | 2 | .container { 3 | width: 100%; 4 | display: flex; 5 | } 6 | 7 | .file-select { 8 | order: 1; 9 | flex: 1; 10 | width: 80%; 11 | } 12 | 13 | .file-select .audio-input button, 14 | .file-select .chart-input button { 15 | width: 11em; 16 | } 17 | 18 | .file-select .audio-input label, 19 | .file-select .chart-input label { 20 | margin-left: 1em; 21 | } 22 | 23 | .reset { 24 | order: 2; 25 | margin-top: 2em; 26 | margin-right: 1em; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/file/select/file-select.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/file/select/file-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { of } from 'rxjs'; 4 | 5 | import { IdGeneratorService } from '../../model/id-generator/id-generator.service'; 6 | import { ModelImporterService } from '../../model/import-export/model-importer.service'; 7 | import { AppFileModule } from '../file.module'; 8 | import { FileService } from '../file.service'; 9 | import { FileSelectComponent } from './file-select.component'; 10 | 11 | describe('Component: FileSelectComponent', () => { 12 | 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [ 18 | AppFileModule, 19 | ], 20 | providers: [ 21 | { provide: FileService, useClass: MockFileService }, 22 | { provide: IdGeneratorService, useClass: MockIdGeneratorService }, 23 | { provide: ModelImporterService, useClass: MockModelImporterService }, 24 | ], 25 | }); 26 | fixture = TestBed.createComponent(FileSelectComponent); 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('FileSelect should display audio file name', () => { 31 | const label = fixture.debugElement.query(By.css('.audio-input label')); 32 | expect(label.nativeElement.textContent).toEqual('testAudioFile'); 33 | }); 34 | 35 | it('FileSelect should display clickable audio file input button', () => { 36 | const button = fixture.debugElement.query(By.css('.audio-input button')); 37 | button.nativeElement.click(); 38 | }); 39 | 40 | it('FileSelect should display chart file name', () => { 41 | const label = fixture.debugElement.query(By.css('.chart-input label')); 42 | expect(label.nativeElement.textContent).toEqual('testChartFile'); 43 | }); 44 | 45 | it('FileSelect should display clickable chart file input button', () => { 46 | const button = fixture.debugElement.query(By.css('.chart-input button')); 47 | button.nativeElement.click(); 48 | }); 49 | }); 50 | 51 | class MockIdGeneratorService { 52 | 53 | reset = (): void => undefined; 54 | } 55 | 56 | class MockModelImporterService { 57 | 58 | import = (): void => undefined; 59 | } 60 | 61 | class MockFileService { 62 | 63 | audioFileNames = of('testAudioFile'); 64 | chartFileNames = of('testChartFile'); 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/app/file/select/file-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ModelImporterService } from '../../model/import-export/model-importer.service'; 4 | import { FileService } from '../file.service'; 5 | 6 | @Component({ 7 | selector: 'app-file-select', 8 | templateUrl: './file-select.component.html', 9 | styleUrls: ['./file-select.component.css'], 10 | }) 11 | export class FileSelectComponent { 12 | 13 | constructor( 14 | private modelImporter: ModelImporterService, 15 | public service: FileService, 16 | ) { 17 | } 18 | 19 | reset() { 20 | this.modelImporter.import(''); 21 | this.service.loadChartFileName(''); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/fretboard/beat/beat.component.css: -------------------------------------------------------------------------------- 1 | 2 | .beat { 3 | outline: none; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/fretboard/beat/beat.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/fretboard/beat/beat.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { showTime } from '../../time/audio-player-controls/audio-player-controls.component'; 4 | import { TimeService } from '../../time/time.service'; 5 | import { Beat } from './beat'; 6 | 7 | @Component({ 8 | selector: '[app-beat]', 9 | templateUrl: './beat.component.html', 10 | styleUrls: ['./beat.component.css'], 11 | }) 12 | export class BeatComponent { 13 | @Input() beat: Beat; 14 | 15 | constructor(private timeService: TimeService) { 16 | } 17 | 18 | get playing(): boolean { 19 | return this.timeService.playing; 20 | } 21 | 22 | selectAndSnap(event: any): void { 23 | if (!this.timeService.playing) { 24 | this.timeService.time = this.beat.time; 25 | } 26 | event.stopPropagation(); 27 | } 28 | 29 | get tooltip(): string { 30 | return `Beat - ${showTime(this.beat.time)}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/fretboard/beat/beat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject, combineLatest } from 'rxjs'; 3 | 4 | import { TimeService } from '../../time/time.service'; 5 | import { Prepared } from '../preparer/prepared'; 6 | import { PreparerService } from '../preparer/preparer.service'; 7 | import { SpeedService } from '../speed/speed.service'; 8 | import { Beat } from './beat'; 9 | 10 | @Injectable() 11 | export class BeatService { 12 | 13 | private beatsSubject: ReplaySubject; 14 | private zeroPositionsSubject: ReplaySubject; 15 | private prepared: Prepared; 16 | private time: number; 17 | 18 | constructor( 19 | private timeService: TimeService, 20 | private preparerService: PreparerService, 21 | private speedService: SpeedService, 22 | ) { 23 | this.beatsSubject = new ReplaySubject(); 24 | this.zeroPositionsSubject = new ReplaySubject(); 25 | combineLatest( 26 | this.timeService.times, 27 | this.preparerService.prepareds, 28 | (time, prepared) => { 29 | this.prepared = prepared; 30 | this.time = time; 31 | }, 32 | ).subscribe(() => { 33 | this.beatsSubject.next(this.buildBeats()); 34 | }); 35 | this.zeroPositionsSubject.next(this.speedService.zeroPosition()); 36 | } 37 | 38 | get beats(): Observable { 39 | return this.beatsSubject.asObservable(); 40 | } 41 | 42 | get zeroPositions(): Observable { 43 | return this.zeroPositionsSubject.asObservable(); 44 | } 45 | 46 | private buildBeats(): Beat[] { 47 | return this.prepared.beats 48 | .filter(b => this.speedService.timeInView(b.time, this.time)) 49 | .map(b => ({ 50 | id: b.id, 51 | time: b.time, 52 | y: this.speedService.calculateYPos(b.time, this.time), 53 | })); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/fretboard/beat/beat.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Beat { 3 | id: number; 4 | time: number; 5 | y: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/fretboard/event-link/event-link.component.css: -------------------------------------------------------------------------------- 1 | 2 | .event-link { 3 | outline: none; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/fretboard/event-link/event-link.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/fretboard/event-link/event-link.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { ModelTrackEventType } from '../../model/model'; 4 | import { EventLink } from './event-link'; 5 | 6 | @Component({ 7 | selector: '[app-event-link]', 8 | templateUrl: './event-link.component.html', 9 | styleUrls: ['./event-link.component.css'], 10 | }) 11 | export class EventLinkComponent { 12 | @Input() eventLink: EventLink; 13 | 14 | get color(): string { 15 | switch (this.eventLink.type) { 16 | case ModelTrackEventType.SoloToggle: 17 | return 'blue'; 18 | case ModelTrackEventType.StarPowerToggle: 19 | return 'orange'; 20 | case ModelTrackEventType.LyricToggle: 21 | return 'black'; 22 | } 23 | throw new Error('Unsupported event link passed to event link component'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/fretboard/event-link/event-link.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject, combineLatest } from 'rxjs'; 3 | 4 | import { TimeService } from '../../time/time.service'; 5 | import { Prepared } from '../preparer/prepared'; 6 | import { ModelTrackEventType } from '../../model/model'; 7 | import { PreparerService } from '../preparer/preparer.service'; 8 | import { SpeedService } from '../speed/speed.service'; 9 | import { EventLink } from './event-link'; 10 | 11 | @Injectable() 12 | export class EventLinkService { 13 | 14 | private eventLinksSubject = new ReplaySubject(); 15 | private time: number; 16 | private prepared: Prepared; 17 | 18 | constructor( 19 | private timeService: TimeService, 20 | private preparerService: PreparerService, 21 | private speedService: SpeedService, 22 | ) { 23 | this.eventLinksSubject = new ReplaySubject(); 24 | combineLatest( 25 | this.timeService.times, 26 | this.preparerService.prepareds, 27 | (time, prepared) => { 28 | this.time = time; 29 | this.prepared = prepared; 30 | }, 31 | ).subscribe(() => { 32 | this.eventLinksSubject.next(this.buildEventLinks()); 33 | }); 34 | } 35 | 36 | get eventLinks(): Observable { 37 | return this.eventLinksSubject.asObservable(); 38 | } 39 | 40 | private buildEventLinks(): EventLink[] { 41 | return this.prepared.eventLinks 42 | .filter(el => this.speedService.timeInView(el.startTime, this.time) || 43 | this.speedService.timeInView(el.endTime, this.time) || 44 | this.time > el.startTime && this.time < el.endTime) 45 | .map(el => ({ 46 | id: el.id, 47 | type: el.type, 48 | x: this.buildEventX(el.type), 49 | y1: this.speedService.calculateYPos(el.startTime, this.time), 50 | y2: this.speedService.calculateYPos(el.endTime, this.time), 51 | })); 52 | } 53 | 54 | private buildEventX(type: ModelTrackEventType): number { 55 | const x = (n: number) => 5 + 10 * n / 6; 56 | switch (type) { 57 | case ModelTrackEventType.SoloToggle: 58 | return x(4); 59 | case ModelTrackEventType.StarPowerToggle: 60 | return x(5); 61 | case ModelTrackEventType.LyricToggle: 62 | return x(6); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/fretboard/event-link/event-link.ts: -------------------------------------------------------------------------------- 1 | import { ModelTrackEventType } from '../../model/model'; 2 | 3 | export interface EventLink { 4 | id: number; 5 | type: ModelTrackEventType; 6 | x: number; 7 | y1: number; 8 | y2: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/fretboard/event/event.component.css: -------------------------------------------------------------------------------- 1 | 2 | .event { 3 | outline: none; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/fretboard/event/event.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/fretboard/event/event.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { SelectorService } from '../../controller/selector/selector.service'; 4 | import { ModelTrackEventType } from '../../model/model'; 5 | import { showTime } from '../../time/audio-player-controls/audio-player-controls.component'; 6 | import { TimeService } from '../../time/time.service'; 7 | import { Event } from './event'; 8 | 9 | @Component({ 10 | selector: '[app-event]', 11 | templateUrl: './event.component.html', 12 | styleUrls: ['./event.component.css'], 13 | }) 14 | export class EventComponent { 15 | @Input() event: Event; 16 | 17 | constructor( 18 | private selectorService: SelectorService, 19 | private timeService: TimeService, 20 | ) { 21 | } 22 | 23 | get color(): string { 24 | switch (this.event.type) { 25 | case ModelTrackEventType.BPMChange: 26 | return 'green'; 27 | case ModelTrackEventType.TSChange: 28 | return 'red'; 29 | case ModelTrackEventType.PracticeSection: 30 | return 'yellow'; 31 | case ModelTrackEventType.Lyric: 32 | return 'white'; 33 | case ModelTrackEventType.SoloToggle: 34 | return 'blue'; 35 | case ModelTrackEventType.StarPowerToggle: 36 | return 'orange'; 37 | case ModelTrackEventType.LyricToggle: 38 | return 'black'; 39 | } 40 | throw new Error('Unsupported event passed to event component'); 41 | } 42 | 43 | get stroke(): string { 44 | return this.event.type === ModelTrackEventType.LyricToggle ? 'white' : 'black'; 45 | } 46 | 47 | get playing(): boolean { 48 | return this.timeService.playing; 49 | } 50 | 51 | select(event: any): void { 52 | if (!this.timeService.playing) { 53 | this.selectorService.selectEvent(this.event.id); 54 | } 55 | event.stopPropagation(); 56 | } 57 | 58 | selectAndSnap(event: any): void { 59 | if (!this.timeService.playing) { 60 | this.selectorService.selectEvent(this.event.id); 61 | this.timeService.time = this.event.time; 62 | } 63 | event.stopPropagation(); 64 | } 65 | 66 | get tooltip(): string { 67 | const type = this.type(); 68 | const defaultTitle = `${type} - ${showTime(this.event.time)}`; 69 | if (this.event.type === ModelTrackEventType.Lyric) { 70 | return `${defaultTitle} - ${this.event.word}`; 71 | } 72 | return defaultTitle; 73 | } 74 | 75 | private type(): string { 76 | switch (this.event.type) { 77 | case ModelTrackEventType.BPMChange: 78 | return 'BPM Change'; 79 | case ModelTrackEventType.TSChange: 80 | return 'Time Signature Change'; 81 | case ModelTrackEventType.PracticeSection: 82 | return 'Practice Section'; 83 | case ModelTrackEventType.Lyric: 84 | return 'Lyric'; 85 | case ModelTrackEventType.SoloToggle: 86 | return 'Solo Toggle'; 87 | case ModelTrackEventType.StarPowerToggle: 88 | return 'Star Power Toggle'; 89 | case ModelTrackEventType.LyricToggle: 90 | return 'Lyric Toggle'; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/fretboard/event/event.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject, combineLatest } from 'rxjs'; 3 | 4 | import { SelectorService } from '../../controller/selector/selector.service'; 5 | import { TimeService } from '../../time/time.service'; 6 | import { ModelTrackEventType } from '../../model/model'; 7 | import { Prepared } from '../preparer/prepared'; 8 | import { PreparerService } from '../preparer/preparer.service'; 9 | import { SpeedService } from '../speed/speed.service'; 10 | import { Event } from './event'; 11 | 12 | @Injectable() 13 | export class EventService { 14 | 15 | private eventsSubject = new ReplaySubject(); 16 | private time: number; 17 | private prepared: Prepared; 18 | private selectedId: number; 19 | 20 | constructor( 21 | private timeService: TimeService, 22 | private selectorService: SelectorService, 23 | private preparerService: PreparerService, 24 | private speedService: SpeedService, 25 | ) { 26 | this.eventsSubject = new ReplaySubject(); 27 | combineLatest( 28 | this.timeService.times, 29 | this.preparerService.prepareds, 30 | this.selectorService.selectedEvents, 31 | (time, prepared, selectedEvent) => { 32 | this.time = time; 33 | this.prepared = prepared; 34 | this.selectedId = selectedEvent ? selectedEvent.id : null; 35 | }, 36 | ).subscribe(() => { 37 | this.eventsSubject.next(this.buildEvents()); 38 | }); 39 | } 40 | 41 | get events(): Observable { 42 | return this.eventsSubject.asObservable(); 43 | } 44 | 45 | private buildEvents(): Event[] { 46 | return this.prepared.events 47 | .filter(b => this.speedService.timeInView(b.time, this.time)) 48 | .map(b => ({ 49 | id: b.id, 50 | time: b.time, 51 | x: this.buildEventX(b.type), 52 | y: this.speedService.calculateYPos(b.time, this.time), 53 | type: b.type, 54 | selected: b.id === this.selectedId, 55 | word: b.word, 56 | })) 57 | .sort((a, b) => a.type - b.type); 58 | } 59 | 60 | private buildEventX(type: ModelTrackEventType): number { 61 | const x = (n: number) => 5 + 10 * n / 6; 62 | switch (type) { 63 | case ModelTrackEventType.BPMChange: 64 | return x(0); 65 | case ModelTrackEventType.TSChange: 66 | return x(1); 67 | case ModelTrackEventType.PracticeSection: 68 | return x(2); 69 | case ModelTrackEventType.Lyric: 70 | return x(3); 71 | case ModelTrackEventType.SoloToggle: 72 | return x(4); 73 | case ModelTrackEventType.StarPowerToggle: 74 | return x(5); 75 | case ModelTrackEventType.LyricToggle: 76 | return x(6); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/fretboard/event/event.ts: -------------------------------------------------------------------------------- 1 | import { ModelTrackEventType } from '../../model/model'; 2 | 3 | export interface Event { 4 | id: number; 5 | time: number; 6 | type: ModelTrackEventType; 7 | x: number; 8 | y: number; 9 | selected: boolean; 10 | word?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/fretboard/fretboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatListModule, MatSliderModule, MatTooltipModule } from '@angular/material'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { BeatComponent } from './beat/beat.component'; 7 | import { EventComponent } from './event/event.component'; 8 | import { EventLinkComponent } from './event-link/event-link.component'; 9 | import { FretboardComponent } from './fretboard/fretboard.component'; 10 | import { NoteGHLComponent } from './note/ghl/note-ghl.component'; 11 | import { NoteGuitarComponent } from './note/guitar/note-guitar.component'; 12 | import { NoteOpenComponent } from './note/open/note-open.component'; 13 | import { NoteComponent } from './note/note.component'; 14 | import { SpeedControlsComponent } from './speed/speed-controls.component'; 15 | 16 | import { BeatService } from './beat/beat.service'; 17 | import { EventService } from './event/event.service'; 18 | import { EventLinkService } from './event-link/event-link.service'; 19 | import { NoteService } from './note/note.service'; 20 | import { PreparerService } from './preparer/preparer.service'; 21 | import { SpeedService } from './speed/speed.service'; 22 | 23 | @NgModule({ 24 | imports: [ 25 | BrowserModule, 26 | BrowserAnimationsModule, 27 | MatListModule, 28 | MatSliderModule, 29 | MatTooltipModule, 30 | ], 31 | exports: [ 32 | FretboardComponent, 33 | SpeedControlsComponent, 34 | ], 35 | declarations: [ 36 | BeatComponent, 37 | EventComponent, 38 | EventLinkComponent, 39 | FretboardComponent, 40 | NoteGHLComponent, 41 | NoteGuitarComponent, 42 | NoteOpenComponent, 43 | NoteComponent, 44 | SpeedControlsComponent, 45 | ], 46 | providers: [ 47 | BeatService, 48 | EventService, 49 | EventLinkService, 50 | NoteService, 51 | PreparerService, 52 | SpeedService, 53 | ], 54 | }) 55 | export class AppFretboardModule { 56 | } 57 | -------------------------------------------------------------------------------- /src/app/fretboard/fretboard/fretboard.component.css: -------------------------------------------------------------------------------- 1 | 2 | .fretboard { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .fretboard > svg { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/fretboard/fretboard/fretboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/app/fretboard/fretboard/fretboard.component.ts: -------------------------------------------------------------------------------- 1 | import { SelectorService } from './../../controller/selector/selector.service'; 2 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 3 | 4 | import { Beat } from '../beat/beat'; 5 | import { Event } from '../event/event'; 6 | import { EventLink } from '../event-link/event-link'; 7 | import { Note } from '../note/note'; 8 | import { Fretboard } from './fretboard'; 9 | 10 | @Component({ 11 | selector: 'app-fretboard', 12 | templateUrl: './fretboard.component.html', 13 | styleUrls: ['./fretboard.component.css'], 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class FretboardComponent { 17 | @Input() fretboard: Fretboard; 18 | 19 | constructor(private selectorService: SelectorService) { 20 | } 21 | 22 | clearSelection(): void { 23 | this.selectorService.clearSelection(); 24 | } 25 | 26 | trackBeat(index: number, item: Beat) { 27 | return item.id; 28 | } 29 | 30 | trackNoteSustain(index: number, item: Note) { 31 | return -item.id; 32 | } 33 | 34 | trackNote(index: number, item: Note) { 35 | return item.id; 36 | } 37 | 38 | trackEvent(index: number, item: Event) { 39 | return item.id; 40 | } 41 | 42 | trackEventLink(index: number, item: EventLink) { 43 | return item.id; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/fretboard/fretboard/fretboard.ts: -------------------------------------------------------------------------------- 1 | import { Beat } from '../beat/beat'; 2 | import { Note } from '../note/note'; 3 | import { Event } from '../event/event'; 4 | import { EventLink } from '../event-link/event-link'; 5 | 6 | export interface Fretboard { 7 | beats: Beat[]; 8 | zeroPosition: number; 9 | notes: Note[]; 10 | events: Event[]; 11 | eventLinks: EventLink[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/fretboard/note/ghl/note-ghl.component.html: -------------------------------------------------------------------------------- 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 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/app/fretboard/note/ghl/note-ghl.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { NoteGHL, NoteGHLColor } from '../note'; 4 | 5 | @Component({ 6 | selector: '[app-note-ghl]', 7 | templateUrl: './note-ghl.component.html', 8 | }) 9 | export class NoteGHLComponent { 10 | @Input() note: NoteGHL; 11 | @Input() drawSustain: boolean; 12 | 13 | get black(): boolean { 14 | return this.note.color === NoteGHLColor.Black; 15 | } 16 | 17 | get white(): boolean { 18 | return this.note.color === NoteGHLColor.White; 19 | } 20 | 21 | get chord(): boolean { 22 | return this.note.color === NoteGHLColor.Chord; 23 | } 24 | 25 | get blackNotePath(): string { 26 | return `M 1.5 -4 27 | l 2.8 4.7 28 | s 1.3 3 -1.82 3.22 29 | l -5.4 0 30 | s -3.28 -0.14 -1.74 -3.26 31 | l 2.76 -4.7 32 | s 1.7 -2.3 3.4 0 33 | z`; 34 | } 35 | 36 | get whiteNotePath(): string { 37 | return `M 1.5 4 38 | l 2.8 -4.7 39 | s 1.3 -3 -1.82 -3.22 40 | l -5.4 0 41 | s -3.28 0.14 -1.74 3.26 42 | l 2.76 4.7 43 | s 1.7 2.3 3.4 0 44 | Z`; 45 | } 46 | 47 | get halfWhiteNotePath(): string { 48 | return `M 0 0 49 | ${this.roundedCorner(-4.5, 4)} 50 | ${this.roundedCorner(4.5, 4)} 51 | Z`; 52 | } 53 | 54 | get halfBlackNotePath(): string { 55 | return `M 0 0 56 | ${this.roundedCorner(-4.5, -4)} 57 | ${this.roundedCorner(4.5, -4)} 58 | Z`; 59 | } 60 | 61 | noteTransform(scale: number, x: number, y: number): string { 62 | const translateX = (this.note.x + x) / scale; 63 | const translateY = (this.note.y + y) / scale; 64 | return `scale(${scale}) translate(${translateX},${translateY})`; 65 | } 66 | 67 | private roundedCorner(x: number, y: number): string { 68 | return `l ${x} 0 69 | l 0 ${0.5 * y} 70 | s 0 ${0.5 * y} ${-0.7 * x} ${0.5 * y} 71 | l ${-0.3 * x} 0 72 | l 0 ${-y}`; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/fretboard/note/guitar/note-guitar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/app/fretboard/note/guitar/note-guitar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { NoteGuitar, NoteGuitarColor } from '../note'; 4 | 5 | @Component({ 6 | selector: '[app-note-guitar]', 7 | templateUrl: './note-guitar.component.html', 8 | }) 9 | export class NoteGuitarComponent { 10 | @Input() note: NoteGuitar; 11 | @Input() drawSustain: boolean; 12 | 13 | get color(): string { 14 | switch (this.note.color) { 15 | case NoteGuitarColor.Green: 16 | return 'green'; 17 | case NoteGuitarColor.Red: 18 | return 'red'; 19 | case NoteGuitarColor.Yellow: 20 | return 'yellow'; 21 | case NoteGuitarColor.Blue: 22 | return 'blue'; 23 | case NoteGuitarColor.Orange: 24 | return 'orange'; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/fretboard/note/note.component.css: -------------------------------------------------------------------------------- 1 | 2 | .note { 3 | outline: none; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/fretboard/note/note.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/fretboard/note/note.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { SelectorService } from '../../controller/selector/selector.service'; 4 | import { showTime } from '../../time/audio-player-controls/audio-player-controls.component'; 5 | import { TimeService } from '../../time/time.service'; 6 | import { Note, NoteType } from './note'; 7 | 8 | @Component({ 9 | selector: '[app-note]', 10 | templateUrl: './note.component.html', 11 | styleUrls: ['./note.component.css'], 12 | }) 13 | export class NoteComponent { 14 | @Input() note: Note; 15 | @Input() drawSustain: boolean; 16 | 17 | constructor( 18 | private selectorService: SelectorService, 19 | private timeService: TimeService, 20 | ) { 21 | } 22 | 23 | get playing(): boolean { 24 | return this.timeService.playing; 25 | } 26 | 27 | select(event: any): void { 28 | if (!this.timeService.playing) { 29 | this.selectorService.selectNote(this.note.id); 30 | } 31 | event.stopPropagation(); 32 | } 33 | 34 | selectAndSnap(event: any): void { 35 | if (!this.timeService.playing) { 36 | this.selectorService.selectNote(this.note.id); 37 | this.timeService.time = this.note.time; 38 | } 39 | event.stopPropagation(); 40 | } 41 | 42 | get open(): boolean { 43 | return this.note.type === NoteType.Open; 44 | } 45 | 46 | get guitar(): boolean { 47 | return this.note.type === NoteType.Guitar; 48 | } 49 | 50 | get ghl(): boolean { 51 | return this.note.type === NoteType.GHL; 52 | } 53 | 54 | get tooltip(): string { 55 | return `Note - ${showTime(this.note.time)}`; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/fretboard/note/note.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Note = 3 | NoteOpen | 4 | NoteGuitar | 5 | NoteGHL; 6 | 7 | export enum NoteType { 8 | Open, 9 | Guitar, 10 | GHL, 11 | } 12 | 13 | export interface NoteOpen { 14 | id: number; 15 | time: number; 16 | type: NoteType.Open; 17 | selected: boolean; 18 | y: number; 19 | sustain: boolean; 20 | endY: number; 21 | hopo: boolean; 22 | tap: boolean; 23 | } 24 | 25 | export interface NoteGuitar { 26 | id: number; 27 | time: number; 28 | type: NoteType.Guitar; 29 | selected: boolean; 30 | x: number; 31 | y: number; 32 | color: NoteGuitarColor; 33 | sustain: boolean; 34 | endY: number; 35 | hopo: boolean; 36 | tap: boolean; 37 | } 38 | 39 | export enum NoteGuitarColor { 40 | Green, 41 | Red, 42 | Yellow, 43 | Blue, 44 | Orange, 45 | } 46 | 47 | export interface NoteGHL { 48 | id: number; 49 | time: number; 50 | type: NoteType.GHL; 51 | selected: boolean; 52 | x: number; 53 | y: number; 54 | color: NoteGHLColor; 55 | sustain: boolean; 56 | endY: number; 57 | hopo: boolean; 58 | tap: boolean; 59 | } 60 | 61 | export enum NoteGHLColor { 62 | Black, 63 | White, 64 | Chord, 65 | } 66 | -------------------------------------------------------------------------------- /src/app/fretboard/note/open/note-open.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/fretboard/note/open/note-open.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { NoteOpen } from '../note'; 4 | 5 | @Component({ 6 | selector: '[app-note-open]', 7 | templateUrl: './note-open.component.html', 8 | }) 9 | export class NoteOpenComponent { 10 | @Input() note: NoteOpen; 11 | @Input() drawSustain: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/fretboard/preparer/prepared.ts: -------------------------------------------------------------------------------- 1 | import { ModelTrackEventType } from '../../model/model'; 2 | 3 | export interface Prepared { 4 | beats: PreparedBeat[]; 5 | notes: PreparedNote[]; 6 | events: PreparedEvent[]; 7 | eventLinks: PreparedEventLink[]; 8 | } 9 | 10 | export interface PreparedBeat { 11 | id: number; 12 | time: number; 13 | } 14 | 15 | export interface PreparedNote { 16 | id: number; 17 | time: number; 18 | length: number; 19 | hopo: boolean; 20 | tap: boolean; 21 | open: boolean; 22 | guitarLane1: PreparedNoteGuitarColor; 23 | guitarLane2: PreparedNoteGuitarColor; 24 | guitarLane3: PreparedNoteGuitarColor; 25 | guitarLane4: PreparedNoteGuitarColor; 26 | guitarLane5: PreparedNoteGuitarColor; 27 | ghlLane1: PreparedNoteGHLColor; 28 | ghlLane2: PreparedNoteGHLColor; 29 | ghlLane3: PreparedNoteGHLColor; 30 | } 31 | 32 | export enum PreparedNoteGuitarColor { 33 | None, 34 | Green, 35 | Red, 36 | Yellow, 37 | Blue, 38 | Orange, 39 | } 40 | 41 | export enum PreparedNoteGHLColor { 42 | None, 43 | Black, 44 | White, 45 | Chord, 46 | } 47 | 48 | export interface PreparedEvent { 49 | id: number; 50 | time: number; 51 | type: ModelTrackEventType; 52 | word?: string; 53 | } 54 | 55 | export interface PreparedEventLink { 56 | id: number; 57 | startTime: number; 58 | endTime: number; 59 | type: ModelTrackEventType; 60 | } 61 | -------------------------------------------------------------------------------- /src/app/fretboard/speed/speed-controls.component.css: -------------------------------------------------------------------------------- 1 | 2 | .speed-controls { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .speed-controls .slider { 8 | display: flex; 9 | justify-content: flex-end; 10 | width: 100%; 11 | } 12 | 13 | .speed-controls .slider label { 14 | order: 1; 15 | padding-top: 0.5em; 16 | margin-right: 0.5em; 17 | } 18 | 19 | .speed-controls .slider mat-slider { 20 | order: 2; 21 | width: 63%; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/fretboard/speed/speed-controls.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/fretboard/speed/speed-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { SpeedService } from './speed.service'; 4 | 5 | @Component({ 6 | selector: 'app-speed-controls', 7 | templateUrl: './speed-controls.component.html', 8 | styleUrls: ['./speed-controls.component.css'], 9 | }) 10 | export class SpeedControlsComponent { 11 | 12 | constructor(public service: SpeedService) { 13 | } 14 | 15 | captureEvent(event: any) { 16 | event.stopPropagation(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/fretboard/speed/speed.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { TimeService } from '../../time/time.service'; 5 | 6 | const spacer = 0.2; 7 | 8 | const zeroPosition = 100 * 1.5 / 1.8; 9 | 10 | const speedValues = { 11 | 1: 0.4, 12 | 2: 0.55, 13 | 3: 0.7, 14 | 4: 0.85, 15 | 5: 1, 16 | 6: 1.2, 17 | 7: 1.4, 18 | 8: 1.6, 19 | 9: 1.8, 20 | 10: 2, 21 | 11: 2.2, 22 | 12: 2.4, 23 | 13: 2.6, 24 | 14: 2.8, 25 | 15: 3.0, 26 | 16: 3.2, 27 | 17: 3.4, 28 | 18: 3.6, 29 | 19: 3.8, 30 | 20: 4, 31 | }; 32 | 33 | @Injectable() 34 | export class SpeedService { 35 | 36 | private currentSpeed: number; 37 | private currentSpeedValue: number; 38 | private timeBefore: number; 39 | private timeAfter: number; 40 | private speedSubject: ReplaySubject; 41 | 42 | constructor(private timeService: TimeService) { 43 | this.speedSubject = new ReplaySubject(); 44 | this.speed = 5; 45 | } 46 | 47 | get speed(): number { 48 | return this.currentSpeedValue; 49 | } 50 | 51 | get speeds(): Observable { 52 | return this.speedSubject.asObservable(); 53 | } 54 | 55 | set speed(speed: number) { 56 | this.currentSpeed = speedValues[speed]; 57 | this.currentSpeedValue = speed; 58 | this.timeBefore = (1 / this.currentSpeed) * 1.5; 59 | this.timeAfter = (1 / this.currentSpeed) * -0.3; 60 | this.timeService.refresh(); 61 | this.speedSubject.next(this.currentSpeedValue); 62 | } 63 | 64 | get after(): number { 65 | return this.timeAfter; 66 | } 67 | 68 | get before(): number { 69 | return this.timeBefore; 70 | } 71 | 72 | zeroPosition() { 73 | return zeroPosition; 74 | } 75 | 76 | timeInView(eventTime: number, currentTime: number): boolean { 77 | return eventTime > (currentTime + this.timeAfter - spacer) && 78 | eventTime < (currentTime + this.timeBefore + spacer); 79 | } 80 | 81 | timeInTightView(eventTime: number, currentTime: number): boolean { 82 | return eventTime > (currentTime + this.timeAfter) && 83 | eventTime < (currentTime + this.timeBefore); 84 | } 85 | 86 | calculateYPos(eventTime: number, currentTime: number): number { 87 | const bottom = currentTime + this.timeAfter; 88 | const top = currentTime + this.timeBefore; 89 | return (1 - (eventTime - bottom) / (top - bottom)) * 100; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/app/global/actions/actions.component.css: -------------------------------------------------------------------------------- 1 | 2 | .actions { 3 | width: calc(100% - 0.5em); 4 | height: 100%; 5 | margin-left: 0.5em; 6 | display: grid; 7 | grid-template-areas: "add-note add-bpm-change add-ts-change" 8 | "add-practice-section add-solo-toggle add-star-power-toggle" 9 | "add-phrase add-lyric ."; 10 | grid-template-columns: 1fr 1fr 1fr; 11 | grid-template-rows: 1fr 1fr 1fr; 12 | } 13 | 14 | .actions .add-note { 15 | grid-area: add-note; 16 | } 17 | 18 | .actions .add-bpm-change { 19 | grid-area: add-bpm-change; 20 | } 21 | 22 | .actions .add-ts-change { 23 | grid-area: add-ts-change; 24 | } 25 | 26 | .actions .add-practice-section { 27 | grid-area: add-practice-section; 28 | } 29 | 30 | .actions .add-solo-toggle { 31 | grid-area: add-solo-toggle; 32 | } 33 | 34 | .actions .add-star-power-toggle { 35 | grid-area: add-star-power-toggle; 36 | } 37 | 38 | .actions .add-phrase { 39 | grid-area: add-phrase; 40 | } 41 | 42 | .actions .add-lyric { 43 | grid-area: add-lyric; 44 | } 45 | 46 | .actions button { 47 | margin: 0.5em; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/global/actions/actions.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/global/actions/actions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ActionsService } from '../../model/actions/actions.service'; 4 | 5 | @Component({ 6 | selector: 'app-actions', 7 | templateUrl: './actions.component.html', 8 | styleUrls: ['./actions.component.css'], 9 | }) 10 | export class ActionsComponent { 11 | 12 | constructor(public service: ActionsService) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/global/global.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | MatButtonModule, 4 | MatDialogModule, 5 | MatListModule, 6 | MatTooltipModule, 7 | } from '@angular/material'; 8 | import { BrowserModule } from '@angular/platform-browser'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | 11 | import { ActionsComponent } from './actions/actions.component'; 12 | import { KeybindingsComponent } from './keybindings/keybindings.component'; 13 | import { KeybindingsModalComponent } from './modals/keybindings/keybindings-modal.component'; 14 | import { ModalsComponent } from './modals/modals.component'; 15 | 16 | import { KeybindingsService } from './keybindings/keybindings.service'; 17 | import { KeybindingsActionsService } from './keybindings/keybindings-actions.service'; 18 | import { StorageService } from './storage/storage.service'; 19 | 20 | @NgModule({ 21 | imports: [ 22 | BrowserModule, 23 | BrowserAnimationsModule, 24 | MatButtonModule, 25 | MatListModule, 26 | MatTooltipModule, 27 | MatDialogModule, 28 | ], 29 | exports: [ 30 | ActionsComponent, 31 | KeybindingsComponent, 32 | ModalsComponent, 33 | ], 34 | declarations: [ 35 | ActionsComponent, 36 | KeybindingsComponent, 37 | KeybindingsModalComponent, 38 | ModalsComponent, 39 | ], 40 | providers: [ 41 | KeybindingsService, 42 | KeybindingsActionsService, 43 | StorageService, 44 | ], 45 | entryComponents: [ 46 | KeybindingsModalComponent, 47 | ], 48 | }) 49 | export class AppGlobalModule { 50 | } 51 | -------------------------------------------------------------------------------- /src/app/global/keybindings/keybindings-actions.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | 4 | import { SelectorService } from '../../controller/selector/selector.service'; 5 | import { StepService } from '../../controller/step/step.service'; 6 | import { TypeService } from '../../controller/type/type.service'; 7 | import { ActionsService } from '../../model/actions/actions.service'; 8 | import { TapInputService } from '../../tap-input/tap-input.service'; 9 | import { TimeService } from '../../time/time.service'; 10 | import { Action } from './keybindings.service'; 11 | 12 | @Injectable() 13 | export class KeybindingsActionsService { 14 | 15 | constructor( 16 | private selectorService: SelectorService, 17 | private stepService: StepService, 18 | private typeService: TypeService, 19 | private actionsService: ActionsService, 20 | private tapInputService: TapInputService, 21 | private timeService: TimeService, 22 | ) { 23 | } 24 | 25 | getAction(action: Action): () => void { 26 | switch (action) { 27 | case Action.SelectNext: 28 | return () => this.selectorService.selectNext(); 29 | case Action.SelectPrevious: 30 | return () => this.selectorService.selectPrevious(); 31 | case Action.AudioPlayOrPause: 32 | return () => this.timeService.playing 33 | ? this.timeService.pause() 34 | : this.timeService.play(); 35 | case Action.AudioStop: 36 | return () => this.timeService.stop(); 37 | case Action.AudioRepeat: 38 | return () => this.timeService.repeat(); 39 | case Action.AddNote: 40 | return () => this.actionsService.addNote(); 41 | case Action.AddBPMChange: 42 | return () => this.actionsService.addBPMChange(); 43 | case Action.AddTSChange: 44 | return () => this.actionsService.addTSChange(); 45 | case Action.AddPracticeSection: 46 | return () => this.actionsService.addPracticeSection(); 47 | case Action.AddSoloToggle: 48 | return () => this.actionsService.addSoloToggle(); 49 | case Action.AddStarPowerToggle: 50 | return () => this.actionsService.addStarPowerToggle(); 51 | case Action.AddPhrase: 52 | return () => this.actionsService.addLyricToggle(); 53 | case Action.AddLyric: 54 | return () => this.actionsService.addLyric(); 55 | case Action.ControlToggleNote1: 56 | return () => this.typeService.flip1(); 57 | case Action.ControlToggleNote2: 58 | return () => this.typeService.flip2(); 59 | case Action.ControlToggleNote3: 60 | return () => this.typeService.flip3(); 61 | case Action.ControlToggleNote4: 62 | return () => this.typeService.flip4(); 63 | case Action.ControlToggleNote5: 64 | return () => this.typeService.flip5(); 65 | case Action.ControlToggleNote6: 66 | return () => this.typeService.flip6(); 67 | case Action.ControlSnapForwards: 68 | return () => this.stepService.snapForwardsTime(); 69 | case Action.ControlMoveForwards: 70 | return () => this.stepService.moveForwardsTime(); 71 | case Action.ControlMoveBackwards: 72 | return () => this.stepService.moveBackwardsTime(); 73 | case Action.ControlSnapBackwards: 74 | return () => this.stepService.snapBackwardsTime(); 75 | case Action.ControlIncreaseLength: 76 | return () => this.stepService.increaseSustain(); 77 | case Action.ControlDecreaseLength: 78 | return () => this.stepService.decreaseSustain(); 79 | case Action.ControlToggleHOPO: 80 | return () => this.typeService.flipForceHOPO(); 81 | case Action.ControlToggleTap: 82 | return () => this.typeService.flipTap(); 83 | case Action.ControlDelete: 84 | return () => this.actionsService.deleteEvent(); 85 | case Action.TapInputSelectAll: 86 | return () => this.tapInputService.selectAll(); 87 | case Action.TapInputDeselectAll: 88 | return () => this.tapInputService.deselectAll(); 89 | case Action.TapInputCreateNotes: 90 | return () => this.tapInputService.createNotes(); 91 | case Action.TapInputDeleteTimes: 92 | return () => this.tapInputService.deleteTimes(); 93 | case Action.SnapToSelected: 94 | return () => this.actionsService.snapToSelected(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/app/global/keybindings/keybindings.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /src/app/global/keybindings/keybindings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { KeybindingsService } from './keybindings.service'; 4 | 5 | @Component({ 6 | selector: 'app-keybindings', 7 | templateUrl: './keybindings.component.html', 8 | }) 9 | export class KeybindingsComponent { 10 | 11 | constructor(public service: KeybindingsService) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/global/modals/keybindings/keybindings-modal.component.css: -------------------------------------------------------------------------------- 1 | 2 | .keybindings-modal { 3 | width: 100%; 4 | height: 100%; 5 | display: grid; 6 | grid-template-areas: "title reset close" 7 | "message message message" 8 | "keybindings keybindings ."; 9 | grid-template-columns: 1fr 3em 3em; 10 | grid-template-rows: 3em 2em 1fr; 11 | } 12 | 13 | .keybindings-modal .title { 14 | grid-area: title; 15 | } 16 | 17 | .keybindings-modal .reset { 18 | grid-area: reset; 19 | margin-left: 1em; 20 | } 21 | 22 | .keybindings-modal .close { 23 | grid-area: close; 24 | margin-left: 1em; 25 | } 26 | 27 | .keybindings-modal .message { 28 | grid-area: message; 29 | margin-left: 1em; 30 | } 31 | 32 | .keybindings-modal .keybindings { 33 | grid-area: keybindings; 34 | margin: 1em; 35 | max-height: 50vh; 36 | overflow: auto; 37 | overflow-x: hidden; 38 | } 39 | 40 | .keybindings-modal .keybindings .keybinding { 41 | width: 38em; 42 | margin-bottom: 1em; 43 | margin-right: 2em; 44 | padding: 1em; 45 | border-width: 2px; 46 | border-color: white; 47 | border-style: solid; 48 | cursor: pointer; 49 | } 50 | 51 | .keybindings-modal .keybindings .action { 52 | display: inline-block; 53 | width: 25em; 54 | margin-right: 1em; 55 | } 56 | 57 | .keybindings-modal .keybindings .key { 58 | display: inline-block; 59 | width: 8em; 60 | } 61 | -------------------------------------------------------------------------------- /src/app/global/modals/keybindings/keybindings-modal.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Edit Keybindings
3 |
{{message}}
4 | 7 | 10 |
11 |
12 |
{{keybinding.label}}
13 |
{{keybinding.key}}
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/global/modals/keybindings/keybindings-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { KeybindingsService, Action, Keybinding } from '../../keybindings/keybindings.service'; 6 | 7 | @Component({ 8 | selector: 'app-keybindings-modal', 9 | templateUrl: './keybindings-modal.component.html', 10 | styleUrls: ['./keybindings-modal.component.css'], 11 | }) 12 | export class KeybindingsModalComponent implements OnDestroy { 13 | 14 | keybindings: Keybinding[]; 15 | subscription: Subscription; 16 | currentAction: Action; 17 | 18 | constructor( 19 | private dialogRef: MatDialogRef, 20 | private keybindingsService: KeybindingsService, 21 | ) { 22 | this.subscription = this.keybindingsService.keybindings.subscribe((keybindings) => { 23 | this.keybindings = keybindings; 24 | }); 25 | this.keybindingsService.activateModal(); 26 | } 27 | 28 | ngOnDestroy(): void { 29 | this.subscription.unsubscribe(); 30 | this.keybindingsService.deactivateModal(); 31 | } 32 | 33 | get message(): string { 34 | if (this.currentAction === undefined) { 35 | return 'Click on a keybind to change it'; 36 | } 37 | return 'Enter the new keybind'; 38 | } 39 | 40 | keyPress(e: KeyboardEvent): void { 41 | e.stopPropagation(); 42 | if (this.currentAction === undefined) { 43 | return; 44 | } 45 | if (e.key !== 'Escape') { 46 | this.keybindingsService.updateBind(this.currentAction, e.key); 47 | } 48 | this.currentAction = undefined; 49 | } 50 | 51 | changeBind(action: Action): void { 52 | if (this.currentAction !== undefined) { 53 | this.currentAction = undefined; 54 | return; 55 | } 56 | this.currentAction = action; 57 | } 58 | 59 | reset(): void { 60 | this.keybindingsService.reset(); 61 | } 62 | 63 | close(): void { 64 | this.dialogRef.close(); 65 | } 66 | 67 | captureScroll(e: Event): void { 68 | e.stopPropagation(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/global/modals/modals.component.css: -------------------------------------------------------------------------------- 1 | 2 | .modals { 3 | width: 12em; 4 | height: 100%; 5 | } 6 | 7 | .modals button { 8 | width: 11em; 9 | margin-top: 1em; 10 | margin-left: 1em; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/global/modals/modals.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/global/modals/modals.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatDialog } from '@angular/material'; 3 | 4 | import { KeybindingsModalComponent } from './keybindings/keybindings-modal.component'; 5 | 6 | @Component({ 7 | selector: 'app-modals', 8 | templateUrl: './modals.component.html', 9 | styleUrls: ['./modals.component.css'], 10 | }) 11 | export class ModalsComponent { 12 | 13 | constructor(private dialog: MatDialog) { 14 | } 15 | 16 | keybindings(): void { 17 | this.dialog.open(KeybindingsModalComponent); 18 | } 19 | 20 | metadata(): void { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/model/id-generator/id-generator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | const increment = 10; 4 | 5 | @Injectable() 6 | export class IdGeneratorService { 7 | 8 | nextId: number; 9 | 10 | constructor() { 11 | this.reset(); 12 | } 13 | 14 | reset(): void { 15 | this.nextId = increment; 16 | } 17 | 18 | id(): number { 19 | const newId = this.nextId; 20 | this.nextId += increment; 21 | return newId; 22 | } 23 | 24 | catchUp(maxId: number): void { 25 | this.nextId = maxId + increment; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/model/import-export/file-to-memory/file-to-memory.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { FileToMemoryService } from './file-to-memory.service'; 4 | import { MemoryToFileService } from './memory-to-file.service'; 5 | 6 | @NgModule({ 7 | providers: [ 8 | FileToMemoryService, 9 | MemoryToFileService, 10 | ], 11 | }) 12 | export class AppModelFileToMemoryModule { 13 | } 14 | -------------------------------------------------------------------------------- /src/app/model/import-export/file-to-memory/file-to-memory.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AppModelFileToMemoryModule } from './file-to-memory.module'; 4 | import { FileToMemoryService } from './file-to-memory.service'; 5 | import { TEST_FILE, TEST_MEMORY } from './test-file-to-memory'; 6 | 7 | describe('Service: FileToMemoryService', () => { 8 | 9 | let service: FileToMemoryService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | AppModelFileToMemoryModule, 15 | ], 16 | }); 17 | service = TestBed.get(FileToMemoryService); 18 | }); 19 | 20 | it('FileToMemoryService should transform file to memory correctly', () => { 21 | const memory = service.import(TEST_FILE); 22 | expect(memory).toEqual(TEST_MEMORY); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/model/import-export/file-to-memory/memory-to-file.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AppModelFileToMemoryModule } from './file-to-memory.module'; 4 | import { MemoryToFileService } from './memory-to-file.service'; 5 | import { TEST_FILE, TEST_MEMORY } from './test-file-to-memory'; 6 | 7 | describe('Service: MemoryToFileService', () => { 8 | 9 | let service: MemoryToFileService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | AppModelFileToMemoryModule, 15 | ], 16 | }); 17 | service = TestBed.get(MemoryToFileService); 18 | }); 19 | 20 | it('MemoryToFileService should transform memory to file correctly', () => { 21 | const file = service.export(TEST_MEMORY); 22 | expect(file).toEqual(TEST_FILE); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/model/import-export/file-to-memory/memory-to-file.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, EventEmitter } from '@angular/core'; 2 | 3 | import { Memory, MemoryMetadata, MemorySyncTrack, MemoryTrack } from '../memory'; 4 | 5 | const formatFloat = (midiTime: number): string => { 6 | return `${midiTime}`.split('.')[0]; 7 | }; 8 | 9 | @Injectable() 10 | export class MemoryToFileService { 11 | 12 | export(memory: Memory): string { 13 | return this.exportChart(memory); 14 | } 15 | 16 | private exportChart(chart: Memory): string { 17 | let file = `[Song]\n{\n${this.exportMetadata(chart.metadata)}} 18 | [SyncTrack]\n{\n${this.exportSyncTrack(chart.syncTrack)}}\n`; 19 | const maybeAddTrack = (name: string, track: MemoryTrack[]) => { 20 | if (track) { 21 | file += `[${name}]\n{\n${this.exportTrack(track)}}\n`; 22 | } 23 | }; 24 | maybeAddTrack('ExpertSingle', chart.guitar.expert); 25 | maybeAddTrack('HardSingle', chart.guitar.hard); 26 | maybeAddTrack('MediumSingle', chart.guitar.medium); 27 | maybeAddTrack('EasySingle', chart.guitar.easy); 28 | maybeAddTrack('ExpertGHLGuitar', chart.ghlGuitar.expert); 29 | maybeAddTrack('HardGHLGuitar', chart.ghlGuitar.hard); 30 | maybeAddTrack('MediumGHLGuitar', chart.ghlGuitar.medium); 31 | maybeAddTrack('EasyGHLGuitar', chart.ghlGuitar.easy); 32 | maybeAddTrack('Events', chart.events); 33 | maybeAddTrack('PART VOCALS', chart.vocals); 34 | maybeAddTrack('VENUE', chart.venue); 35 | return file; 36 | } 37 | 38 | private exportMetadata(metadata: MemoryMetadata[]): string { 39 | return metadata.map(({ name, value }) => { 40 | return ` ${name} = ${value}\n`; 41 | }).join(''); 42 | } 43 | 44 | private exportSyncTrack(syncTrack: MemorySyncTrack[]): string { 45 | return syncTrack.map(({ midiTime, type, value, text }) => { 46 | if (type === 'B') { 47 | return ` ${formatFloat(midiTime)} = ${type} ${formatFloat(value)}\n`; 48 | } 49 | if (type === 'TS') { 50 | return ` ${formatFloat(midiTime)} = ${type} ${formatFloat(value)}\n`; 51 | } 52 | return ` ${formatFloat(midiTime)} = ${type} ${text}\n`; 53 | }).join(''); 54 | } 55 | 56 | private exportTrack(track: MemoryTrack[]): string { 57 | return track.map(({ midiTime, type, note, length, text }) => { 58 | if (type === 'N') { 59 | return ` ${formatFloat(midiTime)} = ${type} ${note} ${formatFloat(length)}\n`; 60 | } 61 | if (type === 'S') { 62 | return ` ${formatFloat(midiTime)} = ${type} ${note} ${formatFloat(length)}\n`; 63 | } 64 | return ` ${formatFloat(midiTime)} = ${type} ${text}\n`; 65 | }).join(''); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/model/import-export/file-to-memory/test-file-to-memory.ts: -------------------------------------------------------------------------------- 1 | import { Memory } from '../memory'; 2 | 3 | export const TEST_FILE = `[Song] 4 | { 5 | Name = Test Name 6 | Artist = Test Artist 7 | Charter = Test Charter 8 | Resolution = 200 9 | Offset = -0.1 10 | } 11 | [SyncTrack] 12 | { 13 | 0 = B 60000 14 | 0 = TS 4 15 | 0 = UNSUPPORTED UNSUPPORTED 16 | } 17 | [ExpertGHLGuitar] 18 | { 19 | 200 = N 0 0 20 | 400 = N 1 0 21 | 400 = S 2 300 22 | 400 = E solo 23 | 600 = N 2 0 24 | 800 = N 3 0 25 | 800 = E soloend 26 | 1000 = N 4 0 27 | 1200 = N 7 0 28 | 0 = UNSUPPORTED UNSUPPORTED 29 | } 30 | [Events] 31 | { 32 | 200 = E "section Section 1" 33 | 400 = E "phrase_start" 34 | 400 = E "lyric A" 35 | 600 = E "lyric B=" 36 | 800 = E "lyric C" 37 | 1000 = E "phrase_end" 38 | 0 = UNSUPPORTED UNSUPPORTED 39 | } 40 | `; 41 | 42 | export const TEST_MEMORY: Memory = { 43 | metadata: [{ 44 | name: 'Name', 45 | value: 'Test Name', 46 | }, { 47 | name: 'Artist', 48 | value: 'Test Artist', 49 | }, { 50 | name: 'Charter', 51 | value: 'Test Charter', 52 | }, { 53 | name: 'Resolution', 54 | value: '200', 55 | }, { 56 | name: 'Offset', 57 | value: '-0.1', 58 | }], 59 | syncTrack: [{ 60 | midiTime: 0, 61 | type: 'B', 62 | value: 60000, 63 | }, { 64 | midiTime: 0, 65 | type: 'TS', 66 | value: 4, 67 | }, { 68 | midiTime: 0, 69 | type: 'UNSUPPORTED', 70 | text: 'UNSUPPORTED', 71 | }], 72 | guitar: { 73 | expert: null, 74 | hard: null, 75 | medium: null, 76 | easy: null, 77 | }, 78 | ghlGuitar: { 79 | expert: [{ 80 | midiTime: 200, 81 | type: 'N', 82 | note: 0, 83 | length: 0, 84 | }, { 85 | midiTime: 400, 86 | type: 'N', 87 | note: 1, 88 | length: 0, 89 | }, { 90 | midiTime: 400, 91 | type: 'S', 92 | note: 2, 93 | length: 300, 94 | }, { 95 | midiTime: 400, 96 | type: 'E', 97 | text: 'solo', 98 | }, { 99 | midiTime: 600, 100 | type: 'N', 101 | note: 2, 102 | length: 0, 103 | }, { 104 | midiTime: 800, 105 | type: 'N', 106 | note: 3, 107 | length: 0, 108 | }, { 109 | midiTime: 800, 110 | type: 'E', 111 | text: 'soloend', 112 | }, { 113 | midiTime: 1000, 114 | type: 'N', 115 | note: 4, 116 | length: 0, 117 | }, { 118 | midiTime: 1200, 119 | type: 'N', 120 | note: 7, 121 | length: 0, 122 | }, { 123 | midiTime: 0, 124 | type: 'UNSUPPORTED', 125 | text: 'UNSUPPORTED', 126 | }], 127 | hard: null, 128 | medium: null, 129 | easy: null, 130 | }, 131 | events: [{ 132 | midiTime: 200, 133 | type: 'E', 134 | text: '"section Section 1"', 135 | }, { 136 | midiTime: 400, 137 | type: 'E', 138 | text: '"phrase_start"', 139 | }, { 140 | midiTime: 400, 141 | type: 'E', 142 | text: '"lyric A"', 143 | }, { 144 | midiTime: 600, 145 | type: 'E', 146 | text: '"lyric B="', 147 | }, { 148 | midiTime: 800, 149 | type: 'E', 150 | text: '"lyric C"', 151 | }, { 152 | midiTime: 1000, 153 | type: 'E', 154 | text: '"phrase_end"', 155 | }, { 156 | midiTime: 0, 157 | type: 'UNSUPPORTED', 158 | text: 'UNSUPPORTED', 159 | }], 160 | vocals: null, 161 | venue: null, 162 | }; 163 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/common/metadata.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { Model, ModelMetadata } from '../../../model'; 4 | import { MemoryMetadata } from '../../memory'; 5 | 6 | @Injectable() 7 | export class MetadataService { 8 | 9 | import(metadata: MemoryMetadata[]): ModelMetadata[] { 10 | if (!metadata) { 11 | return [this.defaultResolution(), this.defaultOffset()]; 12 | } 13 | if (!metadata.find(m => m.name === 'Resolution')) { 14 | metadata.push(this.defaultResolution()); 15 | } 16 | if (!metadata.find(m => m.name === 'Offset')) { 17 | metadata.push(this.defaultOffset()); 18 | } 19 | return metadata as ModelMetadata[]; 20 | } 21 | 22 | export(metadata: ModelMetadata[]): MemoryMetadata[] { 23 | return metadata as MemoryMetadata[]; 24 | } 25 | 26 | getResolution(metadata: ModelMetadata[]): number { 27 | return parseFloat(metadata.find(m => m.name === 'Resolution').value); 28 | } 29 | 30 | getOffset(metadata: ModelMetadata[]): number { 31 | return parseFloat(metadata.find(m => m.name === 'Offset').value); 32 | } 33 | 34 | private defaultResolution(): ModelMetadata { 35 | return { 36 | name: 'Resolution', 37 | value: '192', 38 | }; 39 | } 40 | 41 | private defaultOffset(): ModelMetadata { 42 | return { 43 | name: 'Offset', 44 | value: '0', 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/common/sync-track-exporter.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { 4 | ModelTrack, 5 | ModelTrackEventType, 6 | ModelTrackBPMChange, 7 | ModelTrackTSChange, 8 | } from '../../../model'; 9 | import { MemorySyncTrack } from '../../memory'; 10 | import { MidiTimeService } from '../util/midi-time.service'; 11 | 12 | @Injectable() 13 | export class SyncTrackExporterService { 14 | 15 | constructor(private midiTimeService: MidiTimeService) { 16 | } 17 | 18 | export(st: ModelTrack, resolution: number, offset: number): MemorySyncTrack[] { 19 | this.midiTimeService.clearCache(); 20 | const bpmChanges = this.filterBPMChanges(st, offset); 21 | const tsChanges = this.filterTSChanges(st, offset); 22 | return [ 23 | ...this.exportBPMChanges(st, bpmChanges, resolution), 24 | ...this.exportTSChanges(st, tsChanges, bpmChanges, resolution), 25 | ...st.unsupported, 26 | ].sort((a, b) => a.midiTime - b.midiTime); 27 | } 28 | 29 | private filterBPMChanges(st: ModelTrack, offset: number): ModelTrackBPMChange[] { 30 | return st.events 31 | .filter(e => e.event === ModelTrackEventType.BPMChange) 32 | .map(e => e as ModelTrackBPMChange) 33 | .map(e => ({ 34 | id: e.id, 35 | event: e.event, 36 | time: e.time - offset, 37 | bpm: e.bpm, 38 | } as ModelTrackBPMChange)); 39 | } 40 | 41 | private filterTSChanges(st: ModelTrack, offset: number): ModelTrackTSChange[] { 42 | return st.events 43 | .filter(e => e.event === ModelTrackEventType.TSChange) 44 | .map(e => e as ModelTrackTSChange) 45 | .map(e => ({ 46 | id: e.id, 47 | event: e.event, 48 | time: e.time - offset, 49 | ts: e.ts, 50 | } as ModelTrackTSChange)); 51 | } 52 | 53 | private exportBPMChanges( 54 | st: ModelTrack, 55 | bpmChanges: ModelTrackBPMChange[], 56 | resolution: number, 57 | ) : MemorySyncTrack[] { 58 | return bpmChanges.map((e) => { 59 | const time = e.time; 60 | const midiTime = this.midiTimeService.calculateMidiTime(time, resolution, bpmChanges); 61 | return { 62 | midiTime, 63 | type: 'B', 64 | value: e.bpm * 1000, 65 | }; 66 | }); 67 | } 68 | 69 | private exportTSChanges( 70 | st: ModelTrack, 71 | tsChanges: ModelTrackTSChange[], 72 | bpmChanges: ModelTrackBPMChange[], 73 | resolution: number, 74 | ) : MemorySyncTrack[] { 75 | return tsChanges.map((e) => { 76 | const time = e.time; 77 | const midiTime = this.midiTimeService.calculateMidiTime(time, resolution, bpmChanges); 78 | return { 79 | midiTime, 80 | type: 'TS', 81 | value: e.ts, 82 | }; 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/common/sync-track-importer.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { IdGeneratorService } from '../../../id-generator/id-generator.service'; 4 | import { ModelTrack, ModelTrackEvent, ModelTrackEventType } from '../../../model'; 5 | import { MemorySyncTrack } from '../../memory'; 6 | import { MidiTimeService } from '../util/midi-time.service'; 7 | 8 | export const defaultSyncTrack = (): MemorySyncTrack => ({ 9 | midiTime: 0, 10 | type: 'B', 11 | value: 120000, 12 | }); 13 | 14 | const supportedEvents = ['B', 'TS']; 15 | 16 | @Injectable() 17 | export class SyncTrackImporterService { 18 | 19 | constructor( 20 | private idGenerator: IdGeneratorService, 21 | private midiTimeService: MidiTimeService, 22 | ) { 23 | } 24 | 25 | import(st: MemorySyncTrack[], resolution: number, offset: number): ModelTrack { 26 | this.midiTimeService.clearCache(); 27 | return { 28 | events: [...this.importSyncTrack(st, resolution, offset)], 29 | unsupported: [...this.importUnsupportedSyncTrack(st)], 30 | }; 31 | } 32 | 33 | private importSyncTrack(st: MemorySyncTrack[], resolution: number, offset: number) 34 | : ModelTrackEvent[] { 35 | let syncTrack = st 36 | ? st.filter(e => supportedEvents.indexOf(e.type) !== -1) 37 | : [defaultSyncTrack()]; 38 | if (syncTrack.length === 0) { 39 | syncTrack = [defaultSyncTrack()]; 40 | } 41 | const bpmChanges = syncTrack.filter(e => e.type === 'B'); 42 | return syncTrack.map((e) => { 43 | const time = this.midiTimeService.calculateTime(e.midiTime, resolution, bpmChanges); 44 | if (e.type === 'B') { 45 | return { 46 | id: this.idGenerator.id(), 47 | event: ModelTrackEventType.BPMChange as 48 | ModelTrackEventType.BPMChange, 49 | time: time + offset, 50 | bpm: e.value / 1000, 51 | }; 52 | } 53 | if (e.type === 'TS') { 54 | return { 55 | id: this.idGenerator.id(), 56 | event: ModelTrackEventType.TSChange as 57 | ModelTrackEventType.TSChange, 58 | time: time + offset, 59 | ts: e.value, 60 | }; 61 | } 62 | }); 63 | } 64 | 65 | private importUnsupportedSyncTrack(st: MemorySyncTrack[]): MemorySyncTrack[] { 66 | return st ? st.filter(st => supportedEvents.indexOf(st.type) === -1) : []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/common/unsupported-track-exporter.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { ModelTrack } from '../../../model'; 4 | import { MemoryTrack } from '../../memory'; 5 | 6 | @Injectable() 7 | export class UnsupportedTrackExporterService { 8 | 9 | export(track: ModelTrack): MemoryTrack[] { 10 | return track.unsupported.length > 0 ? track.unsupported : null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/common/unsupported-track-importer.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { ModelTrack } from '../../../model'; 4 | import { MemoryTrack } from '../../memory'; 5 | 6 | @Injectable() 7 | export class UnsupportedTrackImporterService { 8 | 9 | import(track: MemoryTrack[]): ModelTrack { 10 | return { 11 | events: [], 12 | unsupported: track ? track : [], 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/ghl/ghl-track-exporter.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { ModelTrack, ModelTrackNoteType } from '../../../model'; 4 | import { MemoryTrack } from '../../memory'; 5 | import { 6 | GenericTrackExporterService, 7 | NoteExporter, 8 | } from '../generic/generic-track-exporter.service'; 9 | 10 | const ghlNoteExporter: NoteExporter = (note: ModelTrackNoteType): number => { 11 | switch (note) { 12 | case ModelTrackNoteType.GHLBlack1: 13 | return 3; 14 | case ModelTrackNoteType.GHLBlack2: 15 | return 4; 16 | case ModelTrackNoteType.GHLBlack3: 17 | return 8; 18 | case ModelTrackNoteType.GHLWhite1: 19 | return 0; 20 | case ModelTrackNoteType.GHLWhite2: 21 | return 1; 22 | case ModelTrackNoteType.GHLWhite3: 23 | return 2; 24 | } 25 | }; 26 | 27 | @Injectable() 28 | export class GHLTrackExporterService { 29 | 30 | constructor(private genericExporter: GenericTrackExporterService) { 31 | } 32 | 33 | export( 34 | track: ModelTrack, 35 | syncTrack: ModelTrack, 36 | resolution: number, 37 | offset: number, 38 | ): MemoryTrack[] { 39 | return this.genericExporter.export(track, syncTrack, resolution, offset, ghlNoteExporter); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/ghl/ghl-track-importer.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { ModelTrack, ModelTrackEventType, ModelTrackNoteType } from '../../../model'; 4 | import { MemorySyncTrack, MemoryTrack } from '../../memory'; 5 | import { 6 | GenericTrackImporterService, 7 | NoteImporter, 8 | SupportedNotes, 9 | } from '../generic/generic-track-importer.service'; 10 | 11 | const supportedGHLNotes: SupportedNotes = [0, 1, 2, 3, 4, 5, 6, 7, 8]; 12 | 13 | const ghlNoteImporter: NoteImporter = (notes: number[]): ModelTrackNoteType[] => { 14 | if (notes[0] === 7) { 15 | return []; 16 | } 17 | return notes.map((note) => { 18 | switch (note) { 19 | case 3: 20 | return ModelTrackNoteType.GHLBlack1; 21 | case 4: 22 | return ModelTrackNoteType.GHLBlack2; 23 | case 8: 24 | return ModelTrackNoteType.GHLBlack3; 25 | case 0: 26 | return ModelTrackNoteType.GHLWhite1; 27 | case 1: 28 | return ModelTrackNoteType.GHLWhite2; 29 | case 2: 30 | return ModelTrackNoteType.GHLWhite3; 31 | } 32 | }); 33 | }; 34 | 35 | @Injectable() 36 | export class GHLTrackImporterService { 37 | 38 | constructor(private genericImporter: GenericTrackImporterService) { 39 | } 40 | 41 | import( 42 | track: MemoryTrack[], 43 | syncTrack: MemorySyncTrack[], 44 | resolution: number, 45 | offset: number, 46 | ): ModelTrack { 47 | return this.genericImporter.import( 48 | track, 49 | syncTrack, 50 | resolution, 51 | offset, 52 | supportedGHLNotes, 53 | ghlNoteImporter, 54 | ModelTrackEventType.GHLNote, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/guitar/guitar-track-exporter.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { ModelTrack, ModelTrackNoteType } from '../../../model'; 4 | import { MemoryTrack } from '../../memory'; 5 | import { 6 | GenericTrackExporterService, 7 | NoteExporter, 8 | } from '../generic/generic-track-exporter.service'; 9 | 10 | const guitarNoteExporter: NoteExporter = (note: ModelTrackNoteType): number => { 11 | switch (note) { 12 | case ModelTrackNoteType.GuitarGreen: 13 | return 0; 14 | case ModelTrackNoteType.GuitarRed: 15 | return 1; 16 | case ModelTrackNoteType.GuitarYellow: 17 | return 2; 18 | case ModelTrackNoteType.GuitarBlue: 19 | return 3; 20 | case ModelTrackNoteType.GuitarOrange: 21 | return 4; 22 | } 23 | }; 24 | 25 | @Injectable() 26 | export class GuitarTrackExporterService { 27 | 28 | constructor(private genericExporter: GenericTrackExporterService) { 29 | } 30 | 31 | export( 32 | track: ModelTrack, 33 | syncTrack: ModelTrack, 34 | resolution: number, 35 | offset: number, 36 | ): MemoryTrack[] { 37 | return this.genericExporter 38 | .export(track, syncTrack, resolution, offset, guitarNoteExporter); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/guitar/guitar-track-importer.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { ModelTrack, ModelTrackEventType, ModelTrackNoteType } from '../../../model'; 4 | import { MemorySyncTrack, MemoryTrack } from '../../memory'; 5 | import { 6 | GenericTrackImporterService, 7 | NoteImporter, 8 | SupportedNotes, 9 | } from '../generic/generic-track-importer.service'; 10 | 11 | const supportedGuitarNotes: SupportedNotes = [0, 1, 2, 3, 4, 5, 6, 7]; 12 | 13 | const guitarNoteImporter: NoteImporter = (notes: number[]): ModelTrackNoteType[] => { 14 | if (notes[0] === 7) { 15 | return []; 16 | } 17 | return notes.map((note) => { 18 | switch (note) { 19 | case 0: 20 | return ModelTrackNoteType.GuitarGreen; 21 | case 1: 22 | return ModelTrackNoteType.GuitarRed; 23 | case 2: 24 | return ModelTrackNoteType.GuitarYellow; 25 | case 3: 26 | return ModelTrackNoteType.GuitarBlue; 27 | case 4: 28 | return ModelTrackNoteType.GuitarOrange; 29 | } 30 | }); 31 | }; 32 | 33 | @Injectable() 34 | export class GuitarTrackImporterService { 35 | 36 | constructor(private genericImporter: GenericTrackImporterService) { 37 | } 38 | 39 | import( 40 | track: MemoryTrack[], 41 | syncTrack: MemorySyncTrack[], 42 | resolution: number, 43 | offset: number, 44 | ): ModelTrack { 45 | return this.genericImporter.import( 46 | track, 47 | syncTrack, 48 | resolution, 49 | offset, 50 | supportedGuitarNotes, 51 | guitarNoteImporter, 52 | ModelTrackEventType.GuitarNote, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/memory-to-model.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { MetadataService } from './common/metadata.service'; 4 | import { SyncTrackExporterService } from './common/sync-track-exporter.service'; 5 | import { SyncTrackImporterService } from './common/sync-track-importer.service'; 6 | import { UnsupportedTrackExporterService } 7 | from './common/unsupported-track-exporter.service'; 8 | import { UnsupportedTrackImporterService } 9 | from './common/unsupported-track-importer.service'; 10 | import { GenericTrackExporterService } 11 | from './generic/generic-track-exporter.service'; 12 | import { GenericTrackImporterService } 13 | from './generic/generic-track-importer.service'; 14 | import { GHLTrackExporterService } from './ghl/ghl-track-exporter.service'; 15 | import { GHLTrackImporterService } from './ghl/ghl-track-importer.service'; 16 | import { GuitarTrackExporterService } from './guitar/guitar-track-exporter.service'; 17 | import { GuitarTrackImporterService } from './guitar/guitar-track-importer.service'; 18 | import { MidiTimeService } from './util/midi-time.service'; 19 | import { MemoryToModelService } from './memory-to-model.service'; 20 | import { ModelToMemoryService } from './model-to-memory.service'; 21 | 22 | @NgModule({ 23 | providers: [ 24 | MetadataService, 25 | SyncTrackExporterService, 26 | SyncTrackImporterService, 27 | UnsupportedTrackImporterService, 28 | UnsupportedTrackExporterService, 29 | GenericTrackExporterService, 30 | GenericTrackImporterService, 31 | GHLTrackExporterService, 32 | GHLTrackImporterService, 33 | GuitarTrackExporterService, 34 | GuitarTrackImporterService, 35 | MidiTimeService, 36 | MemoryToModelService, 37 | ModelToMemoryService, 38 | ], 39 | }) 40 | export class AppModelMemoryToModelModule { 41 | } 42 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/memory-to-model.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { IdGeneratorService } from '../../id-generator/id-generator.service'; 4 | import { AppModelMemoryToModelModule } from './memory-to-model.module'; 5 | import { MemoryToModelService } from './memory-to-model.service'; 6 | import { TEST_MEMORY, TEST_MODEL } from './test-memory-to-model'; 7 | 8 | describe('Service: MemoryToModelService', () => { 9 | 10 | let service: MemoryToModelService; 11 | 12 | beforeEach(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | AppModelMemoryToModelModule, 16 | ], 17 | providers: [ 18 | IdGeneratorService, 19 | ], 20 | }); 21 | service = TestBed.get(MemoryToModelService); 22 | }); 23 | 24 | it('MemoryToModelService should transform memory into model correctly', () => { 25 | const model = service.import(TEST_MEMORY); 26 | expect(model).toEqual(TEST_MODEL); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/memory-to-model.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { Model, ModelMetadata, ModelTrack } from '../../model'; 4 | import { Memory, MemoryMetadata } from '../memory'; 5 | import { MetadataService } from './common/metadata.service'; 6 | import { SyncTrackImporterService } from './common/sync-track-importer.service'; 7 | import { UnsupportedTrackImporterService } from './common/unsupported-track-importer.service'; 8 | import { GenericTrackImporterService } from './generic/generic-track-importer.service'; 9 | import { GHLTrackImporterService } from './ghl/ghl-track-importer.service'; 10 | import { GuitarTrackImporterService } from './guitar/guitar-track-importer.service'; 11 | 12 | @Injectable() 13 | export class MemoryToModelService { 14 | 15 | constructor( 16 | private genericImporter: GenericTrackImporterService, 17 | private ghlImporter: GHLTrackImporterService, 18 | private guitarImporter: GuitarTrackImporterService, 19 | private metadataService: MetadataService, 20 | private syncTrackImporter: SyncTrackImporterService, 21 | private unsupportedTrackImporter: UnsupportedTrackImporterService, 22 | ) { 23 | } 24 | 25 | import(cf: Memory): Model { 26 | const metadata = this.metadataService.import(cf.metadata); 27 | const resolution = this.metadataService.getResolution(metadata); 28 | const offset = this.metadataService.getOffset(metadata); 29 | return { 30 | metadata, 31 | syncTrack: this.syncTrackImporter 32 | .import(cf.syncTrack, resolution, offset), 33 | guitar: { 34 | expert: this.guitarImporter 35 | .import(cf.guitar.expert, cf.syncTrack, resolution, offset), 36 | hard: this.guitarImporter 37 | .import(cf.guitar.hard, cf.syncTrack, resolution, offset), 38 | medium: this.guitarImporter 39 | .import(cf.guitar.medium, cf.syncTrack, resolution, offset), 40 | easy: this.guitarImporter 41 | .import(cf.guitar.easy, cf.syncTrack, resolution, offset), 42 | }, 43 | ghlGuitar: { 44 | expert: this.ghlImporter 45 | .import(cf.ghlGuitar.expert, cf.syncTrack, resolution, offset), 46 | hard: this.ghlImporter 47 | .import(cf.ghlGuitar.hard, cf.syncTrack, resolution, offset), 48 | medium: this.ghlImporter 49 | .import(cf.ghlGuitar.medium, cf.syncTrack, resolution, offset), 50 | easy: this.ghlImporter 51 | .import(cf.ghlGuitar.easy, cf.syncTrack, resolution, offset), 52 | }, 53 | events: this.genericImporter 54 | .import(cf.events, cf.syncTrack, resolution, offset, [], () => undefined, null), 55 | vocals: this.unsupportedTrackImporter 56 | .import(cf.vocals), 57 | venue: this.unsupportedTrackImporter 58 | .import(cf.venue), 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/model-to-memory.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AppModelMemoryToModelModule } from './memory-to-model.module'; 4 | import { ModelToMemoryService } from './model-to-memory.service'; 5 | import { TEST_MEMORY, TEST_MODEL } from './test-memory-to-model'; 6 | 7 | describe('Service: ModelToMemoryService', () => { 8 | 9 | let service: ModelToMemoryService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ 14 | AppModelMemoryToModelModule, 15 | ], 16 | }); 17 | service = TestBed.get(ModelToMemoryService); 18 | }); 19 | 20 | it('ModelToMemoryService should transform model into memory correctly', () => { 21 | const memory = service.export(TEST_MODEL); 22 | expect(memory).toEqual(TEST_MEMORY); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/model-to-memory.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | import { Model } from '../../model'; 4 | import { Memory } from '../memory'; 5 | import { MetadataService } from './common/metadata.service'; 6 | import { SyncTrackExporterService } from './common/sync-track-exporter.service'; 7 | import { UnsupportedTrackExporterService } from './common/unsupported-track-exporter.service'; 8 | import { GenericTrackExporterService } from './generic/generic-track-exporter.service'; 9 | import { GHLTrackExporterService } from './ghl/ghl-track-exporter.service'; 10 | import { GuitarTrackExporterService } from './guitar/guitar-track-exporter.service'; 11 | 12 | @Injectable() 13 | export class ModelToMemoryService { 14 | 15 | constructor( 16 | private genericExporter: GenericTrackExporterService, 17 | private ghlExporter: GHLTrackExporterService, 18 | private guitarExporter: GuitarTrackExporterService, 19 | private metadataService: MetadataService, 20 | private syncTrackExporter: SyncTrackExporterService, 21 | private trackExporter: UnsupportedTrackExporterService, 22 | ) { 23 | } 24 | 25 | export(cs: Model): Memory { 26 | const resolution = this.metadataService.getResolution(cs.metadata); 27 | const offset = this.metadataService.getOffset(cs.metadata); 28 | return { 29 | metadata: this.metadataService 30 | .export(cs.metadata), 31 | syncTrack: this.syncTrackExporter 32 | .export(cs.syncTrack, resolution, offset), 33 | guitar: { 34 | expert: this.guitarExporter 35 | .export(cs.guitar.expert, cs.syncTrack, resolution, offset), 36 | hard: this.guitarExporter 37 | .export(cs.guitar.hard, cs.syncTrack, resolution, offset), 38 | medium: this.guitarExporter 39 | .export(cs.guitar.medium, cs.syncTrack, resolution, offset), 40 | easy: this.guitarExporter 41 | .export(cs.guitar.easy, cs.syncTrack, resolution, offset), 42 | }, 43 | ghlGuitar: { 44 | expert: this.ghlExporter 45 | .export(cs.ghlGuitar.expert, cs.syncTrack, resolution, offset), 46 | hard: this.ghlExporter 47 | .export(cs.ghlGuitar.hard, cs.syncTrack, resolution, offset), 48 | medium: this.ghlExporter 49 | .export(cs.ghlGuitar.medium, cs.syncTrack, resolution, offset), 50 | easy: this.ghlExporter 51 | .export(cs.ghlGuitar.easy, cs.syncTrack, resolution, offset), 52 | }, 53 | events: this.genericExporter 54 | .export(cs.events, cs.syncTrack, resolution, offset, () => undefined), 55 | vocals: this.trackExporter 56 | .export(cs.vocals), 57 | venue: this.trackExporter 58 | .export(cs.venue), 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/util/midi-time.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ModelTrackBPMChange, ModelTrackEventType } from '../../../model'; 4 | import { MemorySyncTrack } from '../../memory'; 5 | import { MidiTimeService } from './midi-time.service'; 6 | 7 | const syncTrack = (midiTime: number, value: number): MemorySyncTrack => ({ 8 | midiTime, 9 | value: value * 1000, 10 | type: 'B', 11 | }); 12 | 13 | const bpmChange = (time: number, bpm: number): ModelTrackBPMChange => ({ 14 | time, 15 | bpm, 16 | event: ModelTrackEventType.BPMChange, 17 | id: 1, 18 | }); 19 | 20 | describe('Service: MidiTimeService', () => { 21 | 22 | let service: MidiTimeService; 23 | 24 | beforeEach(() => { 25 | TestBed.configureTestingModule({ 26 | providers: [ 27 | MidiTimeService, 28 | ], 29 | }); 30 | service = TestBed.get(MidiTimeService); 31 | }); 32 | 33 | describe('MidiTimeService should calculate time correctly', () => { 34 | 35 | const testCase = ( 36 | id: number, 37 | midiTime: number, 38 | resolution: number, 39 | syncTrack: MemorySyncTrack[], 40 | expectedTime: number, 41 | ): void => { 42 | it(`Test case ${id}`, () => { 43 | const time = service.calculateTime(midiTime, resolution, syncTrack); 44 | expect(time).toEqual(expectedTime); 45 | }); 46 | }; 47 | 48 | testCase(1, 0, 100, [], 0); 49 | testCase(2, 0, 100, [syncTrack(0, 60)], 0); 50 | testCase(3, 100, 100, [syncTrack(0, 60)], 1); 51 | testCase(4, 100, 100, [syncTrack(0, 120)], 0.5); 52 | testCase(5, 100, 200, [syncTrack(0, 60)], 0.5); 53 | testCase(6, 200, 100, [syncTrack(0, 60), syncTrack(100, 30)], 3); 54 | testCase(7, 300, 100, [syncTrack(0, 60), syncTrack(100, 30), syncTrack(200, 60)], 4); 55 | testCase(8, 100, 100, [syncTrack(0, 60), syncTrack(100, 30)], 1); 56 | testCase(9, 50, 100, [syncTrack(0, 60), syncTrack(100, 30)], 0.5); 57 | testCase(10, 150, 100, [syncTrack(0, 60), syncTrack(100, 30), syncTrack(200, 60)], 2); 58 | }); 59 | 60 | describe('MidiTimeService should calculate midi time correctly', () => { 61 | 62 | const testCase = ( 63 | id: number, 64 | time: number, 65 | resolution: number, 66 | bpmChanges: ModelTrackBPMChange[], 67 | expectedMidiTime: number, 68 | ): void => { 69 | it(`Test case ${id}`, () => { 70 | const midiTime = service.calculateMidiTime(time, resolution, bpmChanges); 71 | expect(midiTime).toEqual(expectedMidiTime); 72 | }); 73 | }; 74 | 75 | testCase(1, 0, 100, [], 0); 76 | testCase(2, 0, 100, [bpmChange(0, 60)], 0); 77 | testCase(3, 1, 100, [bpmChange(0, 60)], 100); 78 | testCase(4, 0.5, 100, [bpmChange(0, 120)], 100); 79 | testCase(5, 0.5, 200, [bpmChange(0, 60)], 100); 80 | testCase(6, 3, 100, [bpmChange(0, 60), bpmChange(1, 30)], 200); 81 | testCase(7, 4, 100, [bpmChange(0, 60), bpmChange(1, 30), bpmChange(3, 60)], 300); 82 | testCase(8, 1, 100, [bpmChange(0, 60), bpmChange(1, 30)], 100); 83 | testCase(9, 0.5, 100, [bpmChange(0, 60), bpmChange(1, 30)], 50); 84 | testCase(10, 2, 100, [bpmChange(0, 60), bpmChange(1, 30), bpmChange(3, 60)], 150); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory-to-model/util/midi-time.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { roundTime } from '../../../actions/actions.service'; 4 | import { ModelTrackBPMChange } from '../../../model'; 5 | import { MemorySyncTrack } from '../../memory'; 6 | 7 | const conversionFactor = (bpm: number, resolution: number): number => { 8 | return bpm * resolution / 60; 9 | }; 10 | 11 | @Injectable() 12 | export class MidiTimeService { 13 | 14 | private timeCache: Map; 15 | private midiTimeCache: Map; 16 | 17 | constructor() { 18 | this.clearCache(); 19 | } 20 | 21 | clearCache(): void { 22 | this.timeCache = new Map(); 23 | this.midiTimeCache = new Map(); 24 | } 25 | 26 | calculateTime(midiTime: number, resolution: number, bpmChanges: MemorySyncTrack[]): number { 27 | if (this.timeCache.has(midiTime)) { 28 | return this.timeCache.get(midiTime); 29 | } 30 | const earlierChanges = bpmChanges.filter(e => e.midiTime < midiTime); 31 | if (earlierChanges.length > 1) { 32 | const latestChange = earlierChanges.pop(); 33 | const bpm = latestChange.value / 1000; 34 | const timeUntilLatestChange = 35 | this.calculateTime(latestChange.midiTime, resolution, earlierChanges); 36 | const timeAfterLatestChange = 37 | (midiTime - latestChange.midiTime) / conversionFactor(bpm, resolution); 38 | const result = roundTime(timeUntilLatestChange + timeAfterLatestChange); 39 | this.timeCache.set(midiTime, result); 40 | return result; 41 | } 42 | const bpm: number = earlierChanges.length === 1 ? earlierChanges[0].value / 1000 : 1; 43 | const result = roundTime(midiTime / conversionFactor(bpm, resolution)); 44 | this.timeCache.set(midiTime, result); 45 | return result; 46 | } 47 | 48 | calculateMidiTime(time: number, resolution: number, bpmChanges: ModelTrackBPMChange[]) 49 | : number { 50 | if (this.midiTimeCache.has(time)) { 51 | return this.midiTimeCache.get(time); 52 | } 53 | const earlierChanges = bpmChanges.filter(bc => bc.time < time); 54 | if (earlierChanges.length > 1) { 55 | const latestChange = earlierChanges.pop(); 56 | const bpm = latestChange.bpm; 57 | const midiTimeUntilLatestChange = 58 | this.calculateMidiTime(latestChange.time, resolution, earlierChanges); 59 | const midiTimeAfterLatestChange = 60 | (time - latestChange.time) * conversionFactor(bpm, resolution); 61 | const result = midiTimeUntilLatestChange + midiTimeAfterLatestChange; 62 | this.midiTimeCache.set(time, result); 63 | return result; 64 | } 65 | const bpm: number = earlierChanges.length === 1 ? earlierChanges[0].bpm : 1; 66 | const result = time * conversionFactor(bpm, resolution); 67 | this.midiTimeCache.set(time, result); 68 | return result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/model/import-export/memory.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Memory { 3 | metadata: MemoryMetadata[]; 4 | syncTrack: MemorySyncTrack[]; 5 | guitar: { 6 | expert: MemoryTrack[]; 7 | hard: MemoryTrack[]; 8 | medium: MemoryTrack[]; 9 | easy: MemoryTrack[]; 10 | }; 11 | ghlGuitar: { 12 | expert: MemoryTrack[]; 13 | hard: MemoryTrack[]; 14 | medium: MemoryTrack[]; 15 | easy: MemoryTrack[]; 16 | }; 17 | events: MemoryTrack[]; 18 | vocals: MemoryTrack[]; 19 | venue: MemoryTrack[]; 20 | } 21 | 22 | export interface MemoryMetadata { 23 | name: string; 24 | value: string; 25 | } 26 | 27 | export interface MemorySyncTrack { 28 | midiTime: number; 29 | type: string; 30 | value?: number; 31 | text?: string; 32 | } 33 | 34 | export interface MemoryTrack { 35 | midiTime: number; 36 | type: string; 37 | note?: number; 38 | length?: number; 39 | text?: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/model/import-export/model-exporter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { ModelService } from '../model.service'; 4 | import { MemoryToFileService } from './file-to-memory/memory-to-file.service'; 5 | import { ModelToMemoryService } from './memory-to-model/model-to-memory.service'; 6 | 7 | @Injectable() 8 | export class ModelExporterService { 9 | 10 | constructor( 11 | private model: ModelService, 12 | private memoryToFile: MemoryToFileService, 13 | private modelToMemory: ModelToMemoryService, 14 | ) { 15 | } 16 | 17 | export(): string { 18 | const model = this.model.model; 19 | const memory = this.modelToMemory.export(model); 20 | const file = this.memoryToFile.export(memory); 21 | return file; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/model/import-export/model-import-export.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppModelFileToMemoryModule } from './file-to-memory/file-to-memory.module'; 4 | import { AppModelMemoryToModelModule } from './memory-to-model/memory-to-model.module'; 5 | import { ModelExporterService } from './model-exporter.service'; 6 | import { ModelImporterService } from './model-importer.service'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | AppModelFileToMemoryModule, 11 | AppModelMemoryToModelModule, 12 | ], 13 | providers: [ 14 | ModelExporterService, 15 | ModelImporterService, 16 | ], 17 | }) 18 | export class AppModelImportExportModule { 19 | } 20 | -------------------------------------------------------------------------------- /src/app/model/import-export/model-importer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { SelectorService } from '../../controller/selector/selector.service'; 4 | import { TimeService } from '../../time/time.service'; 5 | import { TrackService } from '../../track/track.service'; 6 | import { IdGeneratorService } from '../id-generator/id-generator.service'; 7 | import { ModelService } from '../model.service'; 8 | import { FileToMemoryService } from './file-to-memory/file-to-memory.service'; 9 | import { MemoryToModelService } from './memory-to-model/memory-to-model.service'; 10 | 11 | @Injectable() 12 | export class ModelImporterService { 13 | 14 | constructor( 15 | private selectorService: SelectorService, 16 | private timeService: TimeService, 17 | private trackService: TrackService, 18 | private idGenerator: IdGeneratorService, 19 | private modelService: ModelService, 20 | private fileToMemory: FileToMemoryService, 21 | private memoryToModel: MemoryToModelService, 22 | ) { 23 | this.import(''); 24 | } 25 | 26 | import(file: string): void { 27 | this.idGenerator.reset(); 28 | const memory = this.fileToMemory.import(file); 29 | const model = this.memoryToModel.import(memory); 30 | this.timeService.time = 0; 31 | this.trackService.defaultTrack(model); 32 | this.modelService.model = model; 33 | this.selectorService.clearSelection(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/model/model.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { ActionsService } from './actions/actions.service'; 4 | import { IdGeneratorService } from './id-generator/id-generator.service'; 5 | import { AppModelImportExportModule } from './import-export/model-import-export.module'; 6 | import { ModelService } from './model.service'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | AppModelImportExportModule, 11 | ], 12 | providers: [ 13 | ActionsService, 14 | IdGeneratorService, 15 | ModelService, 16 | ], 17 | }) 18 | export class AppModelModule { 19 | } 20 | -------------------------------------------------------------------------------- /src/app/model/model.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { Model } from './model'; 5 | 6 | @Injectable() 7 | export class ModelService { 8 | 9 | private currentModelSubject: ReplaySubject; 10 | private currentModel: Model; 11 | 12 | constructor() { 13 | this.currentModelSubject = new ReplaySubject(); 14 | } 15 | 16 | set model(model: Model) { 17 | this.currentModel = model; 18 | this.currentModelSubject.next(model); 19 | } 20 | 21 | get model(): Model { 22 | return this.currentModel; 23 | } 24 | 25 | get models(): Observable { 26 | return this.currentModelSubject.asObservable(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/model/model.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Model { 3 | metadata: ModelMetadata[]; 4 | syncTrack: ModelTrack; 5 | guitar: { 6 | expert: ModelTrack; 7 | hard: ModelTrack; 8 | medium: ModelTrack; 9 | easy: ModelTrack; 10 | }; 11 | ghlGuitar: { 12 | expert: ModelTrack; 13 | hard: ModelTrack; 14 | medium: ModelTrack; 15 | easy: ModelTrack; 16 | }; 17 | events: ModelTrack; 18 | vocals: ModelTrack; 19 | venue: ModelTrack; 20 | } 21 | 22 | export interface ModelMetadata { 23 | name: string; 24 | value: string; 25 | } 26 | 27 | export interface ModelTrack { 28 | events: ModelTrackEvent[]; 29 | unsupported: any[]; 30 | } 31 | 32 | export type ModelTrackEvent = 33 | ModelTrackBPMChange | 34 | ModelTrackTSChange | 35 | ModelTrackNote | 36 | ModelTrackPracticeSection | 37 | ModelTrackStarPowerToggle | 38 | ModelTrackSoloToggle | 39 | ModelTrackLyricToggle | 40 | ModelTrackLyric; 41 | 42 | export enum ModelTrackEventType { 43 | BPMChange, 44 | TSChange, 45 | GuitarNote, 46 | GHLNote, 47 | PracticeSection, 48 | Lyric, 49 | StarPowerToggle, 50 | SoloToggle, 51 | LyricToggle, 52 | } 53 | 54 | export interface ModelTrackBPMChange { 55 | id: number; 56 | event: ModelTrackEventType.BPMChange; 57 | time: number; 58 | bpm: number; 59 | } 60 | 61 | export interface ModelTrackTSChange { 62 | id: number; 63 | event: ModelTrackEventType.TSChange; 64 | time: number; 65 | ts: number; 66 | } 67 | 68 | export interface ModelTrackNote { 69 | id: number; 70 | event: ModelTrackEventType.GuitarNote | ModelTrackEventType.GHLNote; 71 | time: number; 72 | type: ModelTrackNoteType[]; 73 | length: number; 74 | forceHopo: boolean; 75 | tap: boolean; 76 | } 77 | 78 | export enum ModelTrackNoteType { 79 | GuitarGreen, 80 | GuitarRed, 81 | GuitarYellow, 82 | GuitarBlue, 83 | GuitarOrange, 84 | GHLBlack1, 85 | GHLBlack2, 86 | GHLBlack3, 87 | GHLWhite1, 88 | GHLWhite2, 89 | GHLWhite3, 90 | } 91 | 92 | export interface ModelTrackPracticeSection { 93 | id: number; 94 | event: ModelTrackEventType.PracticeSection; 95 | time: number; 96 | name: string; 97 | } 98 | 99 | export interface ModelTrackSoloToggle { 100 | id: number; 101 | event: ModelTrackEventType.SoloToggle; 102 | time: number; 103 | } 104 | 105 | export interface ModelTrackStarPowerToggle { 106 | id: number; 107 | event: ModelTrackEventType.StarPowerToggle; 108 | time: number; 109 | } 110 | 111 | export interface ModelTrackLyricToggle { 112 | id: number; 113 | event: ModelTrackEventType.LyricToggle; 114 | time: number; 115 | } 116 | 117 | export interface ModelTrackLyric { 118 | id: number; 119 | event: ModelTrackEventType.Lyric; 120 | time: number; 121 | word: string; 122 | multiSyllable: boolean; 123 | } 124 | -------------------------------------------------------------------------------- /src/app/tap-input/display/tap-display.component.css: -------------------------------------------------------------------------------- 1 | 2 | .tap-display { 3 | width: 94%; 4 | height: 98%; 5 | max-height: 35vh; 6 | margin-left: 4%; 7 | margin-right: 2%; 8 | margin-bottom: 2%; 9 | display: grid; 10 | grid-template-areas: "times actions"; 11 | grid-template-columns: 1fr 4em; 12 | } 13 | 14 | .tap-display .times { 15 | grid-area: times; 16 | max-height: 100%; 17 | overflow: auto; 18 | display: flex; 19 | flex-wrap: wrap; 20 | align-content: flex-start; 21 | } 22 | 23 | .tap-display .times .time { 24 | width: 30%; 25 | margin-left: 1.5%; 26 | margin-right: 1.5%; 27 | color: white; 28 | margin-bottom: 2%; 29 | } 30 | 31 | .tap-display .times .selected { 32 | background-color: dimgray; 33 | } 34 | 35 | .tap-display .actions { 36 | grid-area: actions; 37 | } 38 | 39 | .tap-display .actions button { 40 | margin-left: 0.5em; 41 | margin-bottom: 2%; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/tap-input/display/tap-display.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{showTime(time)}} 5 |
6 |
7 |
8 | 11 | 14 | 17 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/tap-input/display/tap-display.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { showTime } from '../../time/audio-player-controls/audio-player-controls.component'; 4 | import { TapInputService, TapInputTime } from '../tap-input.service'; 5 | 6 | @Component({ 7 | selector: 'app-tap-display', 8 | templateUrl: './tap-display.component.html', 9 | styleUrls: ['./tap-display.component.css'], 10 | }) 11 | export class TapDisplayComponent { 12 | 13 | times: TapInputTime[]; 14 | 15 | constructor(private service: TapInputService) { 16 | this.service.times.subscribe((times) => { 17 | this.times = times; 18 | }); 19 | } 20 | 21 | captureScroll(event: Event): void { 22 | event.stopPropagation(); 23 | } 24 | 25 | showTime(time: TapInputTime): string { 26 | return showTime(time.time); 27 | } 28 | 29 | selectAll(): void { 30 | this.service.selectAll(); 31 | } 32 | 33 | deselectAll(): void { 34 | this.service.deselectAll(); 35 | } 36 | 37 | createNotes(): void { 38 | this.service.createNotes(); 39 | } 40 | 41 | deleteTimes(): void { 42 | this.service.deleteTimes(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/tap-input/input/tap-input.component.css: -------------------------------------------------------------------------------- 1 | 2 | .tap-input { 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | } 7 | 8 | .tap-input mat-list { 9 | flex: 1; 10 | } 11 | 12 | .tap-input mat-form-field { 13 | width: 98%; 14 | margin-left: 1%; 15 | margin-right: 1%; 16 | } 17 | 18 | .tap-input button { 19 | margin-top: 1em; 20 | margin-right: calc(1.5% + 0.75em); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/tap-input/input/tap-input.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/tap-input/input/tap-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { TapInputService } from '../tap-input.service'; 4 | 5 | @Component({ 6 | selector: 'app-tap-input', 7 | templateUrl: './tap-input.component.html', 8 | styleUrls: ['./tap-input.component.css'], 9 | }) 10 | export class TapInputComponent { 11 | 12 | text: string; 13 | 14 | constructor(private service: TapInputService) { 15 | this.clearInput(); 16 | } 17 | 18 | clearInput(): void { 19 | this.text = ''; 20 | } 21 | 22 | keyDown(e: Event): void { 23 | e.stopPropagation(); 24 | this.service.addTime(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/tap-input/tap-input.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { 6 | MatButtonModule, 7 | MatFormFieldModule, 8 | MatInputModule, 9 | MatListModule, 10 | MatTooltipModule, 11 | } from '@angular/material'; 12 | 13 | import { TapDisplayComponent } from './display/tap-display.component'; 14 | import { TapInputComponent } from './input/tap-input.component'; 15 | 16 | import { TapInputService } from './tap-input.service'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | BrowserModule, 21 | BrowserAnimationsModule, 22 | FormsModule, 23 | MatButtonModule, 24 | MatFormFieldModule, 25 | MatInputModule, 26 | MatListModule, 27 | MatTooltipModule, 28 | ], 29 | exports: [ 30 | TapDisplayComponent, 31 | TapInputComponent, 32 | ], 33 | declarations: [ 34 | TapDisplayComponent, 35 | TapInputComponent, 36 | ], 37 | providers: [ 38 | TapInputService, 39 | ], 40 | }) 41 | export class AppTapInputModule { 42 | } 43 | -------------------------------------------------------------------------------- /src/app/tap-input/tap-input.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, BehaviorSubject } from 'rxjs'; 3 | 4 | import { ActionsService } from '../model/actions/actions.service'; 5 | import { TimeService } from '../time/time.service'; 6 | 7 | export interface TapInputTime { 8 | time: number; 9 | selected: boolean; 10 | } 11 | 12 | @Injectable() 13 | export class TapInputService { 14 | 15 | private timesSubject: BehaviorSubject; 16 | private currentTime: number; 17 | 18 | constructor( 19 | private actionsService: ActionsService, 20 | private timeService: TimeService, 21 | ) { 22 | this.timesSubject = new BehaviorSubject([]); 23 | this.timeService.times.subscribe((time) => { 24 | this.currentTime = time; 25 | }); 26 | } 27 | 28 | get times(): Observable { 29 | return this.timesSubject.asObservable(); 30 | } 31 | 32 | addTime(): void { 33 | this.timesSubject.next([...this.timesSubject.value, { 34 | time: this.currentTime, 35 | selected: false, 36 | }]); 37 | } 38 | 39 | selectAll(): void { 40 | this.timesSubject.value.forEach(time => time.selected = true); 41 | this.timesSubject.next(this.timesSubject.value); 42 | } 43 | 44 | deselectAll(): void { 45 | this.timesSubject.value.forEach(time => time.selected = false); 46 | this.timesSubject.next(this.timesSubject.value); 47 | } 48 | 49 | createNotes(): void { 50 | const newNotes = this.timesSubject.value.filter(time => time.selected); 51 | const newNoteTimes = newNotes.map(time => time.time); 52 | const noDuplicates = Array.from(new Set(newNoteTimes)); 53 | this.actionsService.addNoteAtTimes(noDuplicates); 54 | const newTimes = this.timesSubject.value.filter(time => !time.selected); 55 | this.timesSubject.next(newTimes); 56 | } 57 | 58 | deleteTimes(): void { 59 | this.timesSubject.next([]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/time/audio-player-controls/audio-player-controls.component.css: -------------------------------------------------------------------------------- 1 | 2 | .audio-player-controls { 3 | margin-right: 2%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: flex-end; 7 | } 8 | 9 | .audio-player-controls mat-form-field { 10 | margin-left: 2%; 11 | margin-top: 0.2em; 12 | width: 7em; 13 | } 14 | 15 | .audio-player-controls input { 16 | color: white; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/time/audio-player-controls/audio-player-controls.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 | 11 | 14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/app/time/audio-player-controls/audio-player-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AudioPlayerService } from '../audio-player/audio-player.service'; 4 | import { TimeService } from '../time.service'; 5 | 6 | export const showTime = (time: number): string => { 7 | const minutes = Math.floor(time / 60); 8 | const seconds = time - (minutes * 60); 9 | return `${minutes}m${seconds.toFixed(3)}s`; 10 | }; 11 | 12 | const readTime = (time: string): number => { 13 | try { 14 | const minutes = time.split('m')[0]; 15 | const seconds = time.split('m')[1].split('s')[0]; 16 | const result = parseInt(minutes, 10) * 60 + parseFloat(seconds); 17 | return result; 18 | } catch (error) { 19 | return NaN; 20 | } 21 | }; 22 | 23 | @Component({ 24 | selector: 'app-audio-player-controls', 25 | templateUrl: './audio-player-controls.component.html', 26 | styleUrls: ['./audio-player-controls.component.css'], 27 | }) 28 | export class AudioPlayerControlsComponent { 29 | 30 | private time: number; 31 | private text: string; 32 | 33 | constructor ( 34 | private audioPlayer: AudioPlayerService, 35 | private timeService: TimeService, 36 | ) { 37 | this.time = 0; 38 | this.timeService.times.subscribe((time: number) => { 39 | this.time = time; 40 | }); 41 | } 42 | 43 | get loaded(): boolean { 44 | return this.audioPlayer.loaded; 45 | } 46 | 47 | get playing(): boolean { 48 | return this.timeService.playing; 49 | } 50 | 51 | get currentTime(): string { 52 | return showTime(this.time); 53 | } 54 | 55 | saveTime(newTime: string): void { 56 | this.text = newTime; 57 | } 58 | 59 | changeTime(): void { 60 | const time = readTime(this.text); 61 | if (!isNaN(time)) { 62 | this.timeService.time = time; 63 | } 64 | } 65 | 66 | play(): void { 67 | this.timeService.play(); 68 | } 69 | 70 | pause(): void { 71 | this.timeService.pause(); 72 | } 73 | 74 | stop(): void { 75 | this.timeService.stop(); 76 | } 77 | 78 | repeat(): void { 79 | this.timeService.repeat(); 80 | } 81 | 82 | keyDown(event: Event) { 83 | event.stopPropagation(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/time/audio-player/audio-player.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | import { Howl } from 'howler'; 3 | import { interval } from 'rxjs'; 4 | 5 | const frame = 1000 / 60; 6 | 7 | @Injectable() 8 | export class AudioPlayerService { 9 | 10 | private audioLoaded: boolean; 11 | private volume: number; 12 | private timeEmitter: EventEmitter; 13 | private endedEmitter: EventEmitter; 14 | private durationEmitter: EventEmitter; 15 | private howl: Howl; 16 | 17 | constructor() { 18 | this.audioLoaded = false; 19 | this.volume = undefined; 20 | this.timeEmitter = new EventEmitter(); 21 | this.endedEmitter = new EventEmitter(); 22 | this.durationEmitter = new EventEmitter(); 23 | interval(frame).subscribe(() => { 24 | if (this.audioLoaded) { 25 | this.timeEmitter.emit(this.howl.seek() as number); 26 | } 27 | }); 28 | } 29 | 30 | get loaded(): boolean { 31 | return this.audioLoaded; 32 | } 33 | 34 | get times(): EventEmitter { 35 | return this.timeEmitter; 36 | } 37 | 38 | get ended(): EventEmitter { 39 | return this.endedEmitter; 40 | } 41 | 42 | get durations(): EventEmitter { 43 | return this.durationEmitter; 44 | } 45 | 46 | setVolume(volume: number) { 47 | this.volume = volume; 48 | if (this.howl) { 49 | this.howl.volume(this.volume); 50 | } 51 | } 52 | 53 | setAudio(url: string, extension: string): void { 54 | this.audioLoaded = false; 55 | this.howl = new Howl({ 56 | src: [url], 57 | format: [extension], 58 | volume: this.volume, 59 | }); 60 | this.howl.once('load', () => { 61 | this.audioLoaded = true; 62 | this.durationEmitter.emit(this.howl.duration()); 63 | }); 64 | this.howl.on('end', () => { 65 | this.endedEmitter.emit(); 66 | }); 67 | } 68 | 69 | start(time: number): void { 70 | this.howl.seek(time); 71 | this.howl.play(); 72 | } 73 | 74 | stop(): void { 75 | this.howl.pause(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/time/duration/duration.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { Track, getTrack } from '../../track/track'; 5 | import { ModelService } from '../../model/model.service'; 6 | import { Model, ModelTrackEvent } from '../../model/model'; 7 | import { AudioPlayerService } from '../audio-player/audio-player.service'; 8 | 9 | const minDuration = 5; 10 | 11 | @Injectable() 12 | export class DurationService { 13 | 14 | private audioDuration: number; 15 | private modelDuration: number; 16 | private durationSubject: ReplaySubject; 17 | 18 | constructor(private model: ModelService, private audioPlayer: AudioPlayerService) { 19 | this.audioDuration = minDuration; 20 | this.durationSubject = new ReplaySubject(); 21 | this.audioPlayer.durations.subscribe((duration: number) => { 22 | this.buildAudioDuration(duration); 23 | this.newDuration(); 24 | }); 25 | this.model.models.subscribe((model) => { 26 | this.buildDuration(model); 27 | this.newDuration(); 28 | }); 29 | } 30 | 31 | get durations(): Observable { 32 | return this.durationSubject.asObservable(); 33 | } 34 | 35 | private newDuration(): void { 36 | const duration = Math.max(this.audioDuration, this.modelDuration); 37 | this.durationSubject.next(duration); 38 | } 39 | 40 | private buildAudioDuration(duration: number): void { 41 | this.audioDuration = Math.max(minDuration, duration); 42 | } 43 | 44 | private buildDuration(model: Model): void { 45 | const events: ModelTrackEvent[] = []; 46 | Object.keys(Track) 47 | .map(k => Track[k]) 48 | .filter(v => typeof v === 'number') 49 | .forEach((track: Track) => { 50 | events.push(...(getTrack(model, track).events)); 51 | }); 52 | const lastEvent = events.length === 0 53 | ? undefined 54 | : events.reduce((a, b) => { 55 | const aTime = this.buildEventTime(a); 56 | const bTime = this.buildEventTime(b); 57 | return aTime > bTime ? a : b; 58 | }); 59 | const duration = lastEvent ? this.buildEventTime(lastEvent) : 0; 60 | this.modelDuration = Math.max(minDuration, duration); 61 | } 62 | 63 | private buildEventTime(event: ModelTrackEvent): number { 64 | const length = (event as any).length ? (event as any).length : 0; 65 | return event.time + length; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/time/increment/increment.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ReplaySubject, Observable, combineLatest } from 'rxjs'; 3 | 4 | import { ModelService } from '../../model/model.service'; 5 | import { 6 | Model, 7 | ModelTrackEventType, 8 | ModelTrackBPMChange, 9 | } from '../../model/model'; 10 | import { TimeService } from '../time.service'; 11 | 12 | @Injectable() 13 | export class IncrementService { 14 | 15 | private incrementSubject: ReplaySubject; 16 | 17 | constructor(private model: ModelService, private time: TimeService) { 18 | this.incrementSubject = new ReplaySubject(); 19 | combineLatest(this.model.models, this.time.times, ((model, time) => { 20 | this.incrementSubject.next(this.buildCurrentIncrement(model, time)); 21 | })).subscribe(() => { 22 | }); 23 | } 24 | 25 | get increments(): Observable { 26 | return this.incrementSubject.asObservable(); 27 | } 28 | 29 | private buildCurrentIncrement(model: Model, time: number): number { 30 | if (this.time.playing) { 31 | return 0; 32 | } 33 | const reversedBPMChanges = model.syncTrack.events 34 | .filter(e => e.event === ModelTrackEventType.BPMChange) 35 | .map(e => e as ModelTrackBPMChange) 36 | .sort((a, b) => b.time - a.time); 37 | const firstBPMChange = reversedBPMChanges[reversedBPMChanges.length - 1]; 38 | if (time < firstBPMChange.time) { 39 | return 60 / firstBPMChange.bpm; 40 | } 41 | const currentBPM = reversedBPMChanges 42 | .find(e => e.time <= time + 0.005).bpm; 43 | return 60 / currentBPM; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/time/scrollbar/scrollbar.component.css: -------------------------------------------------------------------------------- 1 | 2 | .scrollbar { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .scrollbar > svg { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | .scrollbar > svg rect { 13 | outline: none; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/time/scrollbar/scrollbar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /src/app/time/time.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 3 | import { 4 | MatButtonModule, 5 | MatFormFieldModule, 6 | MatInputModule, 7 | MatListModule, 8 | MatSliderModule, 9 | MatTooltipModule, 10 | } from '@angular/material'; 11 | import { BrowserModule } from '@angular/platform-browser'; 12 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 13 | 14 | import { AudioPlayerControlsComponent } 15 | from './audio-player-controls/audio-player-controls.component'; 16 | import { ScrollbarComponent } from './scrollbar/scrollbar.component'; 17 | import { VolumeControlsComponent } from './volume/volume-controls.component'; 18 | 19 | import { AudioPlayerService } from './audio-player/audio-player.service'; 20 | import { DurationService } from './duration/duration.service'; 21 | import { IncrementService } from './increment/increment.service'; 22 | import { VolumeService } from './volume/volume.service'; 23 | import { TimeService } from './time.service'; 24 | 25 | @NgModule({ 26 | imports: [ 27 | BrowserModule, 28 | BrowserAnimationsModule, 29 | FormsModule, 30 | MatButtonModule, 31 | MatFormFieldModule, 32 | MatInputModule, 33 | MatListModule, 34 | MatSliderModule, 35 | MatTooltipModule, 36 | ReactiveFormsModule, 37 | ], 38 | exports: [ 39 | AudioPlayerControlsComponent, 40 | ScrollbarComponent, 41 | VolumeControlsComponent, 42 | ], 43 | declarations: [ 44 | AudioPlayerControlsComponent, 45 | ScrollbarComponent, 46 | VolumeControlsComponent, 47 | ], 48 | providers: [ 49 | AudioPlayerService, 50 | DurationService, 51 | IncrementService, 52 | TimeService, 53 | VolumeService, 54 | ], 55 | }) 56 | export class AppTimeModule { 57 | } 58 | -------------------------------------------------------------------------------- /src/app/time/time.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { AudioPlayerService } from './audio-player/audio-player.service'; 5 | 6 | @Injectable() 7 | export class TimeService { 8 | 9 | private currentlyPlaying: boolean; 10 | private currentlyRepeating: boolean; 11 | private currentTime: number; 12 | private lastPlayedTime: number; 13 | private timeSubject: ReplaySubject; 14 | 15 | constructor(private audioPlayer: AudioPlayerService) { 16 | this.timeSubject = new ReplaySubject(); 17 | this.time = 0; 18 | this.currentlyPlaying = false; 19 | this.lastPlayedTime = this.currentTime; 20 | this.audioPlayer.times.subscribe((time: number) => { 21 | if (this.playing) { 22 | this.time = time; 23 | } 24 | }); 25 | this.audioPlayer.ended.subscribe(() => { 26 | if (this.currentlyRepeating) { 27 | this.currentlyRepeating = false; 28 | return; 29 | } 30 | if (this.playing) { 31 | this.stop(); 32 | } 33 | }); 34 | } 35 | 36 | get times(): Observable { 37 | return this.timeSubject.asObservable(); 38 | } 39 | 40 | set time(time: number) { 41 | this.currentTime = time; 42 | this.refresh(); 43 | } 44 | 45 | get playing(): boolean { 46 | return this.currentlyPlaying; 47 | } 48 | 49 | refresh() { 50 | this.timeSubject.next(this.currentTime); 51 | } 52 | 53 | play() { 54 | if (!this.audioPlayer.loaded) { 55 | return; 56 | } 57 | this.currentlyPlaying = true; 58 | this.audioPlayer.start(this.currentTime); 59 | this.lastPlayedTime = this.currentTime; 60 | } 61 | 62 | pause() { 63 | if (!this.audioPlayer.loaded) { 64 | return; 65 | } 66 | this.currentlyPlaying = false; 67 | this.audioPlayer.stop(); 68 | } 69 | 70 | stop() { 71 | if (!this.audioPlayer.loaded) { 72 | return; 73 | } 74 | if (this.playing) { 75 | this.pause(); 76 | } 77 | this.time = 0; 78 | } 79 | 80 | repeat() { 81 | if (!this.audioPlayer.loaded) { 82 | return; 83 | } 84 | if (this.playing) { 85 | this.currentlyRepeating = true; 86 | this.audioPlayer.stop(); 87 | this.time = this.lastPlayedTime; 88 | this.audioPlayer.start(this.lastPlayedTime); 89 | } else { 90 | this.time = this.lastPlayedTime; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/time/volume/volume-controls.component.css: -------------------------------------------------------------------------------- 1 | 2 | .volume-controls { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .volume-controls .slider { 8 | display: flex; 9 | justify-content: flex-end; 10 | width: 100%; 11 | } 12 | 13 | .volume-controls .slider label { 14 | order: 1; 15 | padding-top: 0.5em; 16 | margin-right: 0.5em; 17 | } 18 | 19 | .volume-controls .slider mat-slider { 20 | order: 2; 21 | width: 63%; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/time/volume/volume-controls.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/time/volume/volume-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { VolumeService } from './volume.service'; 4 | 5 | @Component({ 6 | selector: 'app-volume-controls', 7 | templateUrl: './volume-controls.component.html', 8 | styleUrls: ['./volume-controls.component.css'], 9 | }) 10 | export class VolumeControlsComponent { 11 | 12 | constructor(public service: VolumeService) { 13 | } 14 | 15 | captureEvent(event: any) { 16 | event.stopPropagation(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/time/volume/volume.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | 4 | import { AudioPlayerService } from '../audio-player/audio-player.service'; 5 | 6 | const defaultVolume = 25; 7 | 8 | @Injectable() 9 | export class VolumeService { 10 | 11 | private volumeSubject: BehaviorSubject; 12 | 13 | constructor(private audioPlayer: AudioPlayerService) { 14 | this.volumeSubject = new BehaviorSubject(undefined); 15 | this.newVolume(defaultVolume); 16 | } 17 | 18 | get volumes(): Observable { 19 | return this.volumeSubject.asObservable(); 20 | } 21 | 22 | newVolume(volume: number) { 23 | this.audioPlayer.setVolume(volume / 100); 24 | this.volumeSubject.next(volume); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/track/converter/converter.component.css: -------------------------------------------------------------------------------- 1 | 2 | .converter { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .converter button { 8 | min-width: 60%; 9 | margin-right: 0.5em; 10 | float: right; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/track/converter/converter.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/track/converter/converter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { GuitarToGHLConverterService } from '../guitar-to-ghl/guitar-to-ghl-converter.service'; 4 | 5 | @Component({ 6 | selector: 'app-converter', 7 | templateUrl: './converter.component.html', 8 | styleUrls: ['./converter.component.css'], 9 | }) 10 | export class ConverterComponent { 11 | 12 | shouldConvertExpertGuitarToExpertGHL: boolean; 13 | 14 | constructor(private guitarToGHLConverter: GuitarToGHLConverterService) { 15 | this.guitarToGHLConverter.shouldConverts.subscribe((shouldConvert) => { 16 | this.shouldConvertExpertGuitarToExpertGHL = shouldConvert; 17 | }); 18 | } 19 | 20 | convertExpertGuitarToExpertGHLGuitar() { 21 | this.guitarToGHLConverter.convertExpert(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/track/guitar-to-ghl/guitar-to-ghl-converter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject, combineLatest } from 'rxjs'; 3 | 4 | import { IdGeneratorService } from '../../model/id-generator/id-generator.service'; 5 | import { Model, ModelTrackEventType, ModelTrackNote, ModelTrackNoteType } from '../../model/model'; 6 | import { ModelService } from '../../model/model.service'; 7 | import { Track } from '../track'; 8 | import { TrackService } from '../track.service'; 9 | 10 | @Injectable() 11 | export class GuitarToGHLConverterService { 12 | 13 | private shouldConvertsSubject: ReplaySubject; 14 | private model: Model; 15 | private track: Track; 16 | 17 | constructor( 18 | private modelService: ModelService, 19 | private trackService: TrackService, 20 | private idGenerator: IdGeneratorService, 21 | ) { 22 | this.shouldConvertsSubject = new ReplaySubject(); 23 | combineLatest( 24 | this.modelService.models, 25 | this.trackService.tracks, 26 | (model, track) => { 27 | this.model = model; 28 | this.track = track; 29 | }, 30 | ).subscribe(() => { 31 | this.decideToConvert(); 32 | }); 33 | } 34 | 35 | get shouldConverts(): Observable { 36 | return this.shouldConvertsSubject.asObservable(); 37 | } 38 | 39 | convertExpert(): void { 40 | this.model.ghlGuitar.expert = { 41 | events: this.model.guitar.expert.events 42 | .map(event => event as ModelTrackNote) 43 | .map(event => ({ 44 | id: this.idGenerator.id(), 45 | event: ModelTrackEventType.GHLNote as ModelTrackEventType.GHLNote, 46 | time: event.time, 47 | type: this.convertGuitarNotesToGHLNotes(event.type), 48 | length: event.length, 49 | forceHopo: event.forceHopo, 50 | tap: event.tap, 51 | })), 52 | unsupported: this.model.guitar.expert.unsupported.map((unsupported) => { 53 | return JSON.parse(JSON.stringify(unsupported)); 54 | }), 55 | }; 56 | this.modelService.model = this.model; 57 | } 58 | 59 | private decideToConvert() { 60 | const trackIsGHLExpert = this.track === Track.GHLGuitarExpert; 61 | const guitarExpertIsEmpty = this.model.guitar.expert.events.length === 0; 62 | const ghlExpertIsEmpty = this.model.ghlGuitar.expert.events.length === 0; 63 | const shouldConvert = trackIsGHLExpert && !guitarExpertIsEmpty && ghlExpertIsEmpty; 64 | this.shouldConvertsSubject.next(shouldConvert); 65 | } 66 | 67 | private convertGuitarNotesToGHLNotes(notes: ModelTrackNoteType[]) 68 | : ModelTrackNoteType[] { 69 | return notes.map((note) => { 70 | switch (note) { 71 | case ModelTrackNoteType.GuitarGreen: 72 | return ModelTrackNoteType.GHLBlack1; 73 | case ModelTrackNoteType.GuitarRed: 74 | return ModelTrackNoteType.GHLBlack2; 75 | case ModelTrackNoteType.GuitarYellow: 76 | return ModelTrackNoteType.GHLBlack3; 77 | case ModelTrackNoteType.GuitarBlue: 78 | return ModelTrackNoteType.GHLWhite1; 79 | case ModelTrackNoteType.GuitarOrange: 80 | return ModelTrackNoteType.GHLWhite2; 81 | } 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/track/selector/selector.component.css: -------------------------------------------------------------------------------- 1 | 2 | .track-select { 3 | width: 56%; 4 | height: 100%; 5 | margin-left: 42%; 6 | margin-right: 2%; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/track/selector/selector.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{track.view}} 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/app/track/selector/selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { SelectorService } from '../../controller/selector/selector.service'; 4 | import { Track } from '../track'; 5 | import { TrackService } from '../track.service'; 6 | 7 | const tracks = [{ 8 | model: Track.GuitarExpert, 9 | view: 'Guitar - Expert', 10 | }, { 11 | model: Track.GuitarHard, 12 | view: 'Guitar - Hard', 13 | }, { 14 | model: Track.GuitarMedium, 15 | view: 'Guitar - Medium', 16 | }, { 17 | model: Track.GuitarEasy, 18 | view: 'Guitar - Easy', 19 | }, { 20 | model: Track.GHLGuitarExpert, 21 | view: 'Guitar Hero Live - Expert', 22 | }, { 23 | model: Track.GHLGuitarHard, 24 | view: 'Guitar Hero Live - Hard', 25 | }, { 26 | model: Track.GHLGuitarMedium, 27 | view: 'Guitar Hero Live - Medium', 28 | }, { 29 | model: Track.GHLGuitarEasy, 30 | view: 'Guitar Hero Live - Easy', 31 | }]; 32 | 33 | @Component({ 34 | selector: 'app-track-selector', 35 | templateUrl: './selector.component.html', 36 | styleUrls: ['./selector.component.css'], 37 | }) 38 | export class TrackSelectorComponent { 39 | 40 | tracks = tracks; 41 | track: Track; 42 | 43 | constructor( 44 | public trackService: TrackService, 45 | private selectorService: SelectorService, 46 | ) { 47 | this.trackService.tracks.subscribe((track) => { 48 | this.track = track; 49 | }); 50 | } 51 | 52 | newTrack(track: Track) { 53 | this.trackService.newTrack(track); 54 | this.selectorService.clearSelection(); 55 | } 56 | 57 | captureScroll(e: any) { 58 | e.stopPropagation(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/track/track.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { MatButtonModule, MatTooltipModule, MatSelectModule } from '@angular/material'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | import { ConverterComponent } from './converter/converter.component'; 8 | import { TrackSelectorComponent } from './selector/selector.component'; 9 | 10 | import { GuitarToGHLConverterService } from './guitar-to-ghl/guitar-to-ghl-converter.service'; 11 | import { TrackService } from './track.service'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | BrowserModule, 16 | BrowserAnimationsModule, 17 | FormsModule, 18 | MatButtonModule, 19 | MatTooltipModule, 20 | MatSelectModule, 21 | ], 22 | exports: [ 23 | ConverterComponent, 24 | TrackSelectorComponent, 25 | ], 26 | declarations: [ 27 | ConverterComponent, 28 | TrackSelectorComponent, 29 | ], 30 | providers: [ 31 | GuitarToGHLConverterService, 32 | TrackService, 33 | ], 34 | }) 35 | export class AppTrackModule { 36 | } 37 | -------------------------------------------------------------------------------- /src/app/track/track.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | import { Track } from './track'; 5 | import { Model, ModelTrack } from '../model/model'; 6 | 7 | @Injectable() 8 | export class TrackService { 9 | 10 | private tracksSubject: ReplaySubject; 11 | 12 | constructor() { 13 | this.tracksSubject = new ReplaySubject(); 14 | } 15 | 16 | get tracks(): Observable { 17 | return this.tracksSubject.asObservable(); 18 | } 19 | 20 | newTrack(track: Track) { 21 | this.tracksSubject.next(track); 22 | } 23 | 24 | defaultTrack(cs: Model): void { 25 | let longestTrack = Track.GuitarExpert; 26 | let longestCount = 0; 27 | const checkTrack = (track: ModelTrack, view: Track): void => { 28 | if (track.events.length > longestCount) { 29 | longestTrack = view; 30 | longestCount = track.events.length; 31 | } 32 | }; 33 | checkTrack(cs.guitar.expert, Track.GuitarExpert); 34 | checkTrack(cs.guitar.hard, Track.GuitarHard); 35 | checkTrack(cs.guitar.medium, Track.GuitarMedium); 36 | checkTrack(cs.guitar.easy, Track.GuitarEasy); 37 | checkTrack(cs.ghlGuitar.expert, Track.GHLGuitarExpert); 38 | checkTrack(cs.ghlGuitar.hard, Track.GHLGuitarHard); 39 | checkTrack(cs.ghlGuitar.medium, Track.GHLGuitarMedium); 40 | checkTrack(cs.ghlGuitar.easy, Track.GHLGuitarEasy); 41 | this.newTrack(longestTrack); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/track/track.ts: -------------------------------------------------------------------------------- 1 | import { Model, ModelTrack } from '../model/model'; 2 | 3 | export enum Track { 4 | GuitarExpert, 5 | GuitarHard, 6 | GuitarMedium, 7 | GuitarEasy, 8 | GHLGuitarExpert, 9 | GHLGuitarHard, 10 | GHLGuitarMedium, 11 | GHLGuitarEasy, 12 | Events, 13 | Vocals, 14 | Venue, 15 | } 16 | 17 | export const getTrack = (cs: Model, track: Track): ModelTrack => { 18 | switch (track) { 19 | case Track.GuitarExpert: 20 | return cs.guitar.expert; 21 | case Track.GuitarHard: 22 | return cs.guitar.hard; 23 | case Track.GuitarMedium: 24 | return cs.guitar.medium; 25 | case Track.GuitarEasy: 26 | return cs.guitar.easy; 27 | case Track.GHLGuitarExpert: 28 | return cs.ghlGuitar.expert; 29 | case Track.GHLGuitarHard: 30 | return cs.ghlGuitar.hard; 31 | case Track.GHLGuitarMedium: 32 | return cs.ghlGuitar.medium; 33 | case Track.GHLGuitarEasy: 34 | return cs.ghlGuitar.easy; 35 | case Track.Events: 36 | return cs.events; 37 | case Track.Vocals: 38 | return cs.vocals; 39 | case Track.Venue: 40 | return cs.venue; 41 | } 42 | }; 43 | 44 | export const isGHLTrack = (track: Track): boolean => { 45 | switch (track) { 46 | case Track.GHLGuitarExpert: 47 | case Track.GHLGuitarHard: 48 | case Track.GHLGuitarMedium: 49 | case Track.GHLGuitarEasy: 50 | return true; 51 | default: 52 | return false; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chart Hero 6 | 7 | 8 | 9 | 10 | Loading... 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | 6 | if (process.env.ENV === 'production') { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic().bootstrapModule(AppModule); 11 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6'; 2 | import 'core-js/es7/reflect'; 3 | require('zone.js/dist/zone'); 4 | 5 | if (process.env.ENV === 'production') { 6 | } else { 7 | Error['stackTraceLimit'] = Infinity; 8 | require('zone.js/dist/long-stack-trace-zone'); 9 | } 10 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import '../node_modules/font-awesome/css/font-awesome.css'; 2 | @import '~@angular/material/prebuilt-themes/pink-bluegrey.css'; 3 | 4 | html, body { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | .app-tooltip { 10 | font-size: 1.5em; 11 | max-width: 100% !important; 12 | } 13 | 14 | .volume-controls .mat-list-item-content, 15 | .speed-controls .mat-list-item-content, 16 | .tap-input .mat-list-item-content, { 17 | padding-right: 0 !important; 18 | } 19 | 20 | input[type=number]::-webkit-outer-spin-button, 21 | input[type=number]::-webkit-inner-spin-button { 22 | -webkit-appearance: none; 23 | margin: 0; 24 | } 25 | 26 | input[type=number] { 27 | -moz-appearance:textfield; 28 | } 29 | 30 | .no-select { 31 | -webkit-touch-callout: none; 32 | -webkit-user-select: none; 33 | -khtml-user-select: none; 34 | -moz-user-select: none; 35 | -ms-user-select: none; 36 | user-select: none; 37 | } 38 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "lib": ["es2015", "dom"], 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-airbnb", 3 | "rules": { 4 | "ter-indent": [true, 4], 5 | "strict-boolean-expressions": false, 6 | "no-boolean-literal-compare": false 7 | } 8 | } -------------------------------------------------------------------------------- /src/vendor.ts: -------------------------------------------------------------------------------- 1 | 2 | // Angular 3 | import '@angular/common'; 4 | import '@angular/core'; 5 | import '@angular/platform-browser'; 6 | import '@angular/platform-browser-dynamic'; 7 | 8 | // RxJS 9 | import 'rxjs'; 10 | 11 | // HammerJS 12 | import 'hammerjs'; 13 | 14 | // Howler 15 | import 'howler'; 16 | --------------------------------------------------------------------------------