Skip to content

michaelprosario/ng-music-maker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NG-Music-Maker

Collection of typescript and Angular components for generating music

For a project overview, check the following blog post: http://innovativeteams.net/music-maker-using-nodejs-to-create-songs/

I do want to give a shout out to David Ingram of Google for putting together jsmidgen. David’s library handled all the low-level concerns for generating MIDI files, adding tracks, and notes. Please keep in mind that MIDI is a music protocol and file format that focuses on the idea of turning notes and off like switches over time. Make sure to check out his work here: https://github.com/dingram/jsmidgen

Features

  • express server to connect MIDI file services to APIs
  • Angular project to demo features
  • Angular project includes a drum pattern maker
  • TypeScript API for expressing ideas like chords, chord progressions, scales, and players
  • Players iterate over chord progressions using a style

To explore how the MIDI services work.. please inspect the examples here. (see examples project)

let midiServices = new MidiServices();
let scaleServices = new ScaleService(midiServices);
let chordServices = new ChordServices(midiServices);

function makeScale() {
    var file = new Midi.File();

    // Build a track
    var track = new Midi.Track();
    track.setTempo(80);
    file.addTrack(track);

    // Make a scale
    var scale = scaleServices.MakeScale("c4", ScaleType.MajorPentatonic, 2);
    for (var i = 0; i < scale.length; i++) {
        track.addNote(0, scale[i], 50);
    }

    // Write a MIDI file
    fs.writeFileSync('test.mid', file.toBytes(), 'binary');
}

function threeNotes() {
    var file = new Midi.File();

    // Build a track
    var track = new Midi.Track();
    track.setTempo(80);
    file.addTrack(track);

    let mm = midiServices;
    let beat = 50;
    track.addNote(0, mm.GetNoteNumber("c4"), beat);
    track.addNote(0, mm.GetNoteNumber("d4"), beat);
    track.addNote(0, mm.GetNoteNumber("e4"), beat);

    // Write a MIDI file
    fs.writeFileSync('test2.mid', file.toBytes(), 'binary');
}

function drumTest()
{
    var file = new Midi.File();

    // Build a track
    var track = new Midi.Track();
    track.setTempo(80);
    file.addTrack(track);

    let mm = midiServices;

    var addRhythmPattern = mm.AddRhythmPattern;
    addRhythmPattern(track, "x-x-|x-x-|xxx-|x-xx",DrumNotes.ClosedHighHat);
    fs.writeFileSync('drumTest.mid', file.toBytes(), 'binary');
}

function chordProgressions(){
    var file = new Midi.File();

    // Build a track
    var track = new Midi.Track();
    track.setTempo(80);
    file.addTrack(track);

    var chordList = new Array();
    chordList.push(new ChordChange(chordServices.MakeChord("e4", ChordType.Minor),4));
    chordList.push(new ChordChange(chordServices.MakeChord("c4", ChordType.Major),4));
    chordList.push(new ChordChange(chordServices.MakeChord("d4", ChordType.Major),4));
    chordList.push(new ChordChange(chordServices.MakeChord("c4", ChordType.Major),4));

    var chordPlayer = new SimplePlayer()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    chordPlayer = new Arpeggio1()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    chordPlayer = new RandomPlayer()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    chordPlayer = new BassPlayer1()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    chordPlayer = new BassPlayer2()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    chordPlayer = new BassPlayer3()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    chordPlayer = new OffBeatPlayer()
    chordPlayer.PlayFromChordChanges(track, chordList, 0);

    fs.writeFileSync('chordProgressions.mid', file.toBytes(), 'binary');
}


makeScale();
threeNotes();
drumTest();
chordProgressions();

API Reference

To learn more JSMIDGEN, please visit https://github.com/dingram/jsmidgen

addNote

var track = new Midi.Track();
track.addNote(0, ‘c4’, 64);

addChord

var track = new Midi.Track();
var c = mm.MakeChord(“c5”, mm.ChordType.Major);
track.addChord(0, c, beat*4);

setTempo

track.setTempo(bpm[, time])

You can find many of these functions in the MusicMaker services.

GetNoteNumber(aNote)

  • Converts a note string to a number. Returns an integer.
  • aNote: note to convert

MakeChord(root,type: ChordType)

  • Creates a chord based on note and chord type.
  • root: This can be a note like the string “c4” or an integer.
  • type: Type of chord. Please refer to the ‘ChordType’ enumeration.

MakeScale(note, type: ScaleType, octaves: number)

  • Creates a scale based on note and type.
  • root: This can be a note like the string “c4” or an integer.
  • type: Type of scale. Please refer to the ‘ScaleType’ enumeration.

SelectRandom(myArray)

  • Selects a random element from ‘myArray’ and returns it.
  • myArray: is an array of stuff.

AddRhythmPattern(track,strPattern: string, note: number)

  • Provides an easy way to express patterns of notes
  • track: represents a track objects from jsmidgen
  • strPattern: Provide a string to represent a pattern of sound. Each character represents a 16th note. x plays the note. – advances by a 16th note. All other characters are ignored.
  • note: Note to play.

ChordChange(chord,length:number)

  • Class represents a chord played over a number of beats.
  • chord: an array of notes or chord
  • length: number of beats to play chord.

Chord Players

  • You can use chord player classes to generate music patterns based on a sequence of chords or chord progression.
  • Some of the chord player classes include the following: Arpeggio1, BassPLayer1, BassPLayer2, BassPLayer3, OffBeatPlayer, RandomPlayer, and SimplePlayer
  • To use a chord player class, create an instance of the class and call the following: PlayFromChordChanges(track,chordList,channel). You need to pass a track, a list of chord changes, and a MIDI channel.

TypeScript + Yarn Workspace + Lerna + Jest Monorepo Boilerplate

license test code style:airbnb code style:prettier Conventional Commits Commitizen friendly pr welcome

An example monorepo boilerplate for nodejs.

This works well, and I've been carefully maintaining it.

Just clone this! Then read and compare with README.

You'd grasp how this repo works.

Feel free to make issues for any questions or suggestions.

Scenario

This project has two packages, foo(@jjangga0214/foo) and bar(@jjangga0214/bar). bar depends on foo.

Lerna + Yarn Workspace

Lerna respects and and delegates monorepo management to yarn workspace, by 'useWorkspaces': true in lerna.json. But Lerna is still useful, as it provides utility commands for monorepo workflow (e.g. selective subpackge script execution, versioning, or package publishing).

Module Aliases

There're' some cases module alias becomes useful.

  1. A package with a deep and wide directory tree: For example, let's say foo/src/your/very/deep/module/index.ts imports ../../../../another/deep/module/index. In this case, absolute path from the root(e.g. alias #foo -> foo/src) like #foo/another/deep/module/index can be more concise and maintainable.

  2. Dependency not located in node_modules: This can happen in monorepo. For instance, @jjangga0214/bar depends on @jjangga0214/foo, but the dependancy does not exist in node_modules, but in packages/foo directory. In this case, creating an alias(@jjangga0214/foo -> packages/foo/src(ts: Path Mapping)) is needed.

(There is another case (e.g. "exporting" alias), but I'd like to omit them as not needed in this context.)

There are several 3rd party solutions that resolves modules aliases.

  1. Runtime mapper: module-alias, etc.
  2. Symlink: link-module-alias, etc.
  3. Transpiler/bundler: Babel plugins, Rollup, Webpack, etc.
  4. Post-compile-processor: tsc-alias, etc.

However, from node v14.6.0 and v12.19.0, node introduced a new native support for it, named Subpath Imports. It enables specifying alias path in package.json. It requires prefixing an alias by #.

This repo uses Subpath Import.

foo's package.json:

{
  "name": "@jjangga0214/foo",
  "imports": {
    "#foo/*": {
      "default": "./dist/*.js"
    }
  }
}

There is engines restriction in package.json, as subpath imports is added from nodejs v14.6.0 and v12.19.0.

// package.json
{
  "engines": {
    "node": ">=14.6.0"
  }
}

If you nodejs version does not fit in, you can consider 3rd party options.

Typescript: Path Mapping

Though we can avoid a runtime error(Module Not Found) for module alias resolution, compiling typescript is stil a differenct matter. Fot tsc, tsconfig.json has this path mappings configuration.

{
  "baseUrl": "packages",
  "paths": {
    "@jjangga0214/*": ["*/src"], // e.g. `@jjangga0214/foo` -> `foo/src`
    "#foo/*": ["foo/src/*"], // e.g. `#foo/hello` -> `foo/src/hello.ts`
    "#bar/*": ["bar/src/*"],
    "#*": ["*/src"] // e.g. `#foo` -> `foo/src`
  }
}

@jjangga0214/foo and @jjangga0214/bar are only used for cross-package references. For example, bar imports @jjangga0214/foo in its src/index.ts.

However, #foo and #bar are only used for package's interal use. For example, foo/src/index.ts imports #foo/hello, which is same as ./hello.

Note that bar must NOT import #foo or #foo/hello, causing errors. I'm pretty sure there's no reason to do that as prefixing # is only for package's internal use, not for exporting in this scenario.

But importing @jjangga0214/foo/hello in bar makes sense in some cases. For that, you should explicitly add additaional configuration like this.

{
  "baseUrl": "packages",
  "paths": {
    "@jjangga0214/*": ["*/src"],
+   "@jjangga0214/foo/*": ["foo/src/*"], // => this one!
    // Other paths are ommitted for brevity
  }
}

Be careful of paths orders for precedence. If the order changes, like the below, #foo/hello will be resolved to foo/hello/src, not foo/src/hello.

{
  "baseUrl": "packages",
  "paths": {
    "#*": ["*/src"],
    "#foo/*": ["foo/src/*"], // => this will not work!
    "#bar/*": ["bar/src/*"] // => this will not work!
  }
}

Dev

ts-node-dev is used for yarn dev. You can replace it to ts-node if you don't need features of node-dev.

For ts-node-dev(or ts-node) to understand Path Mapping, tsconfig-paths is used.

tsconfig.json:

{
  "ts-node": {
    "require": ["tsconfig-paths/register"]
    // Other options are ommitted for brevity
  }
}

Until wclr/ts-node-dev#286 is resolved, "ts-node" field in tsconfig.json will be ignored by ts-node-dev. Thus it should be given by command options (e.g -r below.). This is not needed if you only use ts-node, not ts-node-dev.

Each packages' package.json:

{
  "scripts": {
    "dev": "ts-node-dev -r tsconfig-paths/register src/index.ts"
  }
  // Other options are ommitted for brevity.
}

Extraneous Configuration

In development environment, fast execution by rapid compilation is useful. ts-node is configured to use swc internally. (Refer to the official docs -> That's why @swc/core and @swc/helpers are installed.)

tsconfig.json:

{
  "ts-node": {
    "transpileOnly": true,
    "transpiler": "ts-node/transpilers/swc-experimental"
    // Other options are ommitted for brevity
  }
}

Typescript: tsconfig

  1. VSCode only respects tsconfig.json (which can be multiple files) as of writing (until microsoft/vscode#12463 is resolved.). Other IDEs may have similar policy/restriction. In monorepo, as build-specific configurations (e.g. include, exclude, rootDir, etc) are package-specific, they should be seperated from the tsconfig.json. Otherwise, VSCode would not help you by its feature, like type checking, or go to definition, etc. For instance, it'd be inconvenient to work on foo's API in bar's code. To resolve this, build-specific configuration options are located in tsconfig.build.json. (But keep in note that compilation would always work flawlessly even if you only have tsconfig.json and let build-related options in there. The only problem in that case would be IDE support.)

  2. yarn build in each package executes tsc -b tsconfig.build.json, not tsc -p tsconfig.build.json. This is to use typescript's Project References feature. For example, yarn build under bar builds itself and its dependancy, foo (More specifically, foo is compiled before bar is compiled). Look at packages/bar/tsconfig.build.json. It explicitly refers ../foo/tsconfig.build.json. Thus, tsc -b tsconfig.build.json under bar will use packages/foo/tsconfig.build.json to build foo. And this fits well with --incremental option specified in tsconfig.json, as build cache can be reused if foo (or even bar) was already compiled before.

  3. Each packages has their own tsconfig.json. That's because ts-node-dev --project ../../tsconfig.json -r tsconfig-paths/register src/index.ts would not find Paths Mapping, although ../../tsconfig.json is given to ts-node-dev (env var TS_NODE_PROJECT wouldn't work, either).

  4. Path Mapping should only be located in "project root tsconfig.json", even if certain some aliases are only for package's internal use. This is because tsconfig-paths does not fully respect Project References (dividab/tsconfig-paths#153). (If you do not use tsconfig-paths, this is not an issue.)

Jest

If you write test code in javascript, you can do what you used to do without additional configuration. However, if you write test code in typescript, there are several ways to execute test in general.

You can consider tsc, @babel/preset-typescript, ts-jest, @swc/jest, and so on. And there're pros/cons.

  • tsc and @babel/preset-typescript requires explict 2 steps (compilation + execution), while ts-jest and @swc/jest does not (compilation is done under the hood).

  • @babel/preset-typescript and @swc/jest do not type-check (do only transpilation), while tsc and ts-jest do. (Note that @swc/jest plans to implement type-check. Issue and status: swc-project/swc#571) (Note: By tsc, you can turn off transpilation (to save time) but force type-check (--noEmit) only when you want (e.g. git commit hook). By doing so, for instance, you can run test many times (e.g. test failure -> fix -> test failure -> fix -> test success) very fast without type-check (e.g. @swc/jest) on your local machine, and finally type-check before git commit, reducing total time cost. Since microsoft/TypeScript#39122, using --incremental and --noEmit simultaneously also became possible.)

  • @swc/jest is very fast, and tsc "can be" fast.

    • For example, ts-jest took 5.756 s while @swc/jest took 0.962 s for entire tests in this repo.
    • You can use incremental(--incremental) compilation if using tsc.
    • If it's possible to turn off tsc's type-check, tsc can become "much" faster. This behaviour is not implemented yet (issue: microsoft/TypeScript#29651).

In this article, I'd like to introduce ts-jest and @swc/jest. In this repo, @swc/jest is preconfigured (as it is very fast of course). However, you can change it as you want.

By ts-jest/utils, Jest respects Path Mapping automatically by reading tsconfig.json and moduleNameMapper(in jest.config.js), which are, in this repo, already configured as follows. See how moduleNameMapper is handeled in jest.config.js and refer to docs for more details.

jest.config.js:

const { pathsToModuleNameMapper } = require('ts-jest/utils')
// Note that json import does not work if it contains comments, which tsc just ignores for tsconfig.
const { compilerOptions } = require('./tsconfig')

module.exports = {
  moduleNameMapper: {
    ...pathsToModuleNameMapper(
      compilerOptions.paths /* , { prefix: '<rootDir>/' }, */,
    ),
  },
  // Other options are ommited for brevity.
}

To use ts-jest, follow the steps below.

jest.config.js:

{
  // UNCOMMENT THE LINE BELOW TO ENABLE ts-jest
  // preset: 'ts-jest',
  // DELETE THE LINE BELOW TO DISABLE @swc/jest in favor of ts-jest
  "transform": { "^.+\\.(t|j)sx?$": ["@swc/jest"] }
}

And

yarn remove -W @swc/jest

swc is very fast ts/js transpiler written in Rust, and @swc/jest uses it under the hood.

Jest respects Path Mapping by reading tsconfig.json and moduleNameMapper(in jest.config.js), which are, in this repo, already configured.

Do not remove(yarn remove -W ts-jest) ts-jest just because you use @swc/jest. Though @swc/jest replaces ts-jest completely, ts-jest/utils is used in jest.config.js.

jest.config.js:

const { pathsToModuleNameMapper } = require('ts-jest/utils')
// Note that json import does not work if it contains comments, which tsc just ignores for tsconfig.
const { compilerOptions } = require('./tsconfig')

module.exports = {
  moduleNameMapper: {
    ...pathsToModuleNameMapper(
      compilerOptions.paths /* , { prefix: '<rootDir>/' }, */,
    ),
  },
  // Other options are ommited for brevity.
}

If you want to configure moduleNameMapper manually, then you don't need ts-jest.

Additional Dependencies

Currently swc does not provide some features of babel plugins (REF). Thus additional dependencies might be needed. (You will be able to know what to install by reading error message if it appears.)

A list of already installed packages in this repo is:

Linter and Formatter

eslint and prettier is used along each other. You can run yarn lint <target>.

Airbnb Style

This repo accepted airbnb style. eslint-config-airbnb-base and eslint-config-airbnb-typescript is configured.

If you need jsx and tsx rules, you should install eslint-config-airbnb instead of eslint-config-airbnb-base.

yarn add -D -W eslint-config-airbnb

And reconfigure .eslintrc.js as follows.

.eslintrc.js:

// Other options are ommitted for brevity
const common = {
  extends: [
    'airbnb-base', // "-base" does not include tsx rules.
    // 'airbnb' // Uncomment this line and remove the above line if tsx rules are needed. (Also install eslint-config-airbnb pacakge)
  ],
}
// Other options are ommitted for brevity
module.exports = {
  overrides: [
    {
      files: [
        '**/*.ts',
        // '**/*.tsx' // Uncomment this line if tsx rules are needed.
      ],
      extends: [
        'airbnb-typescript/base', // "/base" does not include tsx rules.
        // 'airbnb-typescript' // Uncomment this line and remove the above line if tsx rules are needed.
      ],
    },
  ],
}

It is not for markdown itself, but for javascript code block snippet appeared in markdown.

It is needed to lint jest code.

Overrides

By configuring overrides in .eslintrc.js, both of typescript and javascript files are able to be linted by eslint. (e.g. So typescript rules are not applied to .js files. Otherwise, it would cause errors.)

markdownlint-cli uses markdownlint under the hood, and the cli respects .markdownlintignore. You can yarn lint:md <target>.

You can also install vscode extension vscode-markdownlint.

It is used as commit message linter. This repo follows Conventional Commits style for git commit message. @commitlint/config-conventional is configured as preset, and commitlint is executed by husky for git's commit-msg hook.

Git Hooks

Husky executes lint-staged and commitlint by git hooks. lint-staged makes sure staged files are to be formatted before committed. Refer to .husky/* for details.

Root commands

Introducing some of commands specified in package.json. Refer to package.json for the full list.

# remove compiled js folders, typescript build info, jest cache, *.log, and test coverage
yarn clean

# measure a single test coverage of entire packages
yarn coverage

# open web browser to show test coverage report.
# run this AFTER running `yarn coverage`,
# to make it sure there are reports before showing them.
yarn coverage:show

# lint code (ts, js, and js snippets on markdown)
# e.g. yarn lint .
yarn lint <path>

# lint markdown
yarn lint:md <path>

# test entire packages
yarn test

# build entire packages in parallel
yarn build

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published