51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/UI/SelectBox.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2022 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | import styled from "styled-components";
25 |
26 | const Select = styled.select`
27 | width: 100%;
28 | height: 35px;
29 | background: white;
30 | color: gray;
31 | padding: 0 5px;
32 | font-size: 14px;
33 | border: none;
34 | margin-right: 10px;
35 | border-radius: 7px;
36 |
37 | option {
38 | color: black;
39 | background: white;
40 | display: flex;
41 | white-space: pre;
42 | min-height: 20px;
43 | padding: 0px 2px 1px;
44 | }
45 | `;
46 |
47 | type SelectBoxProps = {
48 | title: string,
49 | items: object,
50 | onChange: ( event: any ) => void,
51 | selected?: string
52 | };
53 |
54 | /**
55 | * @param {String} title of select box
56 | * @param {Object} items key value pairs of all options to display
57 | * @param {Function} onChange handler to fire when option is selected
58 | * @param {String=} selected name of the optionally selected option key
59 | */
60 | export default function SelectBox({ title, items, onChange, selected = "" }: SelectBoxProps ) {
61 |
62 | return (
63 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/src/model/Note.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2015-2022 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | export default class Note {
25 | note : string;
26 | octave : number;
27 | offset : number;
28 | duration : number;
29 | measure : number;
30 |
31 | constructor( note: string, octave: number, offset: number, duration: number, measure: number ) {
32 | this.note = note;
33 | this.octave = octave;
34 | this.offset = offset; // offset within the sequence
35 | this.duration = duration; // length of the note
36 | this.measure = measure; // measure the Note belongs to
37 | }
38 |
39 | clone(): Note {
40 | return new Note( this.note, this.octave, this.offset, this.duration, this.measure );
41 | }
42 |
43 | equals( compareNote: Note ): boolean {
44 | if ( compareNote === this ) {
45 | return true;
46 | }
47 |
48 | return compareNote.note === this.note &&
49 | compareNote.octave === this.octave &&
50 | compareNote.offset === this.offset &&
51 | compareNote.duration === this.duration &&
52 | compareNote.measure === this.measure;
53 | }
54 |
55 | overlaps( compareNote: Note ): boolean {
56 | if ( compareNote === this ) {
57 | return false;
58 | }
59 |
60 | return compareNote.note === this.note &&
61 | compareNote.octave === this.octave &&
62 | compareNote.offset === this.offset &&
63 | compareNote.measure === this.measure;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/model/Pattern.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2015 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | import Note from "./Note";
25 |
26 | export default class Pattern
27 | {
28 | name : string;
29 | notes : Array;
30 | patternNum : number;
31 | offset : number;
32 |
33 | constructor( name: string, notes: Array = [], patternNum: number = 0, offset: number = 0 ) {
34 | this.name = name;
35 | this.notes = notes; // all the notes within the pattern
36 | this.patternNum = patternNum; // the number of this pattern within the total sequence
37 | this.offset = offset; // the start offset of the pattern
38 | }
39 |
40 | offsetConflictsWithPattern( compareNoteOffset: number ): boolean {
41 | const noteOffsets = this.getNoteOffsets();
42 |
43 | for ( const noteOffset of noteOffsets ) {
44 | const actualOffset = noteOffset - this.offset;
45 | if ( actualOffset === 0 ) {
46 | if ( actualOffset === compareNoteOffset ) {
47 | return true;
48 | }
49 | } else if ( compareNoteOffset % actualOffset === 0 ) {
50 | return true;
51 | }
52 | }
53 | return false;
54 | }
55 |
56 | getNoteOffsets(): Array {
57 | return this.notes.map(({ offset }) => offset );
58 | }
59 |
60 | getRangeStartOffset(): number {
61 | return this.notes[ 0 ]?.offset ?? 0;
62 | }
63 |
64 | getRangeEndOffset(): number {
65 | if ( this.notes.length > 0 ) {
66 | const lastNote = this.notes[ this.notes.length - 1 ];
67 | return lastNote.offset + lastNote.duration;
68 | }
69 | return 0;
70 | }
71 |
72 | getRangeLength(): number {
73 | return this.getRangeEndOffset() - this.getRangeStartOffset();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | @import "./styles/_mixins";
2 | @import "./styles/typography";
3 |
4 | $titleHeight: 66px;
5 | $actionsHeight: 55px;
6 | $headerHeight: #{$titleHeight + $actionsHeight};
7 |
8 | .app {
9 | height: 100%;
10 | max-height: 100vh;
11 | font-size: 16px;
12 | color: white;
13 |
14 | &__header {
15 | position: fixed;
16 | width: 100%;
17 | height: $headerHeight;
18 | top: 0;
19 | left: 0;
20 | z-index: 1;
21 | background-color: $color-4;
22 | box-sizing: border-box;
23 | }
24 |
25 | &__title {
26 | max-width: $app-width;
27 | margin: 0 auto;
28 | padding: $spacing-small;
29 | display: flex;
30 | justify-content: space-between;
31 | align-items: center;
32 |
33 | @include lessThanIdeal() {
34 | justify-content: center;
35 | }
36 |
37 | &-logo {
38 | height: 50px;
39 | }
40 |
41 | &-text {
42 | @include titleFont();
43 | font-size: 150%;
44 | color: #F6F6F6;
45 | margin: $spacing-xxsmall 0 0;
46 | }
47 | }
48 |
49 | &__actions {
50 | height: $actionsHeight;
51 | border-bottom: 1px solid $color-5;
52 | background-color: $color-4;
53 | padding: $spacing-small $spacing-medium;
54 | box-sizing: border-box;
55 |
56 | &-container {
57 | max-width: $app-width;
58 | margin: 0 auto;
59 |
60 | @include lessThanIdeal() {
61 | text-align: center;
62 |
63 | .app__actions-descr {
64 | display: none;
65 | }
66 | }
67 |
68 | @include ideal() {
69 | display: flex;
70 | justify-content: space-between;
71 | align-items: center;
72 | }
73 | }
74 |
75 | &-descr {
76 | color: $color-3;
77 | }
78 |
79 | &-ui {
80 | justify-content: center;
81 | display: flex;
82 | align-items: center;
83 | }
84 |
85 | &-button {
86 | @include button();
87 | }
88 | }
89 |
90 | &__wrapper {
91 | max-width: $app-width;
92 | height: 100%;
93 | margin: 0 auto;
94 | padding-top: $headerHeight;
95 | }
96 |
97 | &__container {
98 | @include large() {
99 | display: flex;
100 | justify-content: center;
101 | margin-top: $spacing-medium;
102 | }
103 |
104 | @include mobile() {
105 | padding: 0 $spacing-medium 0;
106 | margin-top: $spacing-medium;
107 | }
108 | }
109 |
110 | &__footer {
111 | text-align: center;
112 | color: $color-text;
113 |
114 | @include mobile() {
115 | padding: 0 $spacing-small $spacing-small;
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/services/MidiService.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2022 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | import Composition from "../model/Composition";
25 | import { getMeasureDurationInSeconds } from "../utils/AudioMath";
26 |
27 | type MIDIWriterTrack = {
28 | addEvent: Function,
29 | setTempo: Function,
30 | addTrackName: Function,
31 | setTimeSignature: Function
32 | };
33 | type NoteEventDef = {
34 | pitch: string,
35 | duration: string,
36 | startTick: number
37 | };
38 | type MIDIWriter = {
39 | NoteEvent : new ( data: NoteEventDef ) => {},
40 | Track : new () => MIDIWriterTrack,
41 | Writer : new ( tracks: MIDIWriterTrack[] ) => { dataUri: () => string },
42 | };
43 |
44 | export const createMIDI = async ( composition: Composition ): Promise => {
45 |
46 | const midiWriter: MIDIWriter = ( await import( "midi-writer-js" ) as MIDIWriter );
47 | const midiTracks: Array = [];
48 |
49 | composition.patterns.forEach(({ name }) => {
50 | const track = new midiWriter.Track();
51 | track.setTempo( composition.tempo );
52 | track.addTrackName( name );
53 | track.setTimeSignature( composition.beatAmount, composition.beatUnit );
54 | midiTracks.push( track );
55 | });
56 |
57 | // all measures have the same duration
58 | const measureDuration = getMeasureDurationInSeconds( composition.tempo, composition.beatAmount );
59 | // we specify event ranges in ticks (128 ticks == 1 beat)
60 | const TICKS = ( 128 * composition.beatAmount ) / measureDuration; // ticks per measure
61 |
62 | // walk through all patterns
63 | composition.patterns.forEach(( track, trackIndex ) => {
64 | const midiTrack = midiTracks[ trackIndex ];
65 | track.notes.forEach(({ note, octave, offset, duration }) => {
66 | midiTrack.addEvent(
67 | new midiWriter.NoteEvent({
68 | pitch : `${note}${octave}`,
69 | duration : `T${Math.round( duration * TICKS )}`,
70 | startTick : Math.round( offset * TICKS )
71 | })
72 | );
73 | });
74 | });
75 | return ( new midiWriter.Writer( midiTracks )).dataUri();
76 | };
77 |
--------------------------------------------------------------------------------
/src/utils/PitchUtil.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Igor Zinken 2016-2022 - https://www.igorski.nl
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | * this software and associated documentation files (the "Software"), to deal in
8 | * the Software without restriction, including without limitation the rights to
9 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10 | * the Software, and to permit persons to whom the Software is furnished to do so,
11 | * subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | */
23 |
24 | /**
25 | * order of note names within a single octave
26 | */
27 | export const OCTAVE_SCALE: Array = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ];
28 |
29 | /**
30 | * @param {string} aNote - musical note to return ( A, B, C, D, E, F, G with
31 | * possible enharmonic notes ( 'b' meaning 'flat', '#' meaning 'sharp' )
32 | * NOTE: flats are CASE sensitive ( to prevent seeing the note 'B' instead of 'b' )
33 | * @param {number} aOctave - the octave to return ( accepted range 0 - 9 )
34 | * @return {number} containing exact frequency in Hz for requested note
35 | */
36 | export const getFrequency = ( aNote: string, aOctave: number ): number => {
37 | let freq;
38 | let enharmonic = 0;
39 |
40 | // detect flat enharmonic
41 | let i = aNote.indexOf( "b" );
42 | if ( i > -1 ) {
43 | aNote = aNote.substr( i - 1, 1 );
44 | enharmonic = -1;
45 | }
46 |
47 | // detect sharp enharmonic
48 | i = aNote.indexOf( "#" );
49 | if ( i > -1 ) {
50 | aNote = aNote.substr( i - 1, 1 );
51 | enharmonic = 1;
52 | }
53 |
54 | freq = getOctaveIndex( aNote, enharmonic );
55 |
56 | if ( aOctave === 4 ) {
57 | return freq;
58 | }
59 | else {
60 | // translate the pitches to the requested octave
61 | const d = aOctave - 4;
62 | let j = Math.abs( d );
63 |
64 | for ( i = 0; i < j; ++i ) {
65 | if ( d > 0 ) {
66 | freq *= 2;
67 | }
68 | else {
69 | freq *= 0.5;
70 | }
71 | }
72 | return freq;
73 | }
74 | };
75 |
76 | /* internal methods */
77 |
78 | /**
79 | * pitch table for all notes from C to B at octave 4
80 | * which is used for calculating all pitches at other octaves
81 | */
82 | const OCTAVE: Array = [
83 | 261.626, 277.183, 293.665, 311.127, 329.628, 349.228, 369.994, 391.995, 415.305, 440, 466.164, 493.883
84 | ];
85 |
86 | /**
87 | * retrieves the index in the octave array for a given note
88 | * modifier enharmonic returns the previous ( for a 'flat' note )
89 | * or next ( for a 'sharp' note ) index
90 | *
91 | * @param {string} aNote ( A, B, C, D, E, F, G )
92 | * @param {number=} aEnharmonic optional, defaults to 0 ( 0, -1 for flat, 1 for sharp )
93 | * @return {number}
94 | */
95 | function getOctaveIndex( aNote: string, aEnharmonic?: number ): number {
96 | if ( typeof aEnharmonic !== "number" ) {
97 | aEnharmonic = 0;
98 | }
99 |
100 | for ( let i = 0, j = OCTAVE.length; i < j; ++i ) {
101 | if ( OCTAVE_SCALE[ i ] === aNote ) {
102 | let k = i + aEnharmonic;
103 |
104 | if ( k > j ) {
105 | return OCTAVE[ 0 ];
106 | }
107 | if ( k < 0 ) {
108 | return OCTAVE[ OCTAVE.length - 1 ];
109 | }
110 | return OCTAVE[ k ];
111 | }
112 | }
113 | return NaN;
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/Info/Info.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2022 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | import { useState } from "react";
25 | import "./Info.scss";
26 |
27 | export default function Info( props?: any ) {
28 | const [ opened, setOpened ] = useState( false );
29 |
30 | return (
31 |
32 |
33 |
What's this?
34 |
35 |
36 | The Molecular Music Generator (MMG) uses a simple algorithm to generate
37 | musical patterns which can be fed to hardware or software instruments.
38 |
43 | The rules for the algorithm are as follows:
44 |
45 |
46 |
Two different note lengths need to be defined, e.g. "4" and "3" (in quarter notes)
47 |
a scale needs to be defined, e.g. C major (the white keys on a piano), let's say we start on the E note, the list of notes will then contain : E, F, G, A, B, C
48 | a pattern length needs to be defined, e.g. 4 bars
49 |
The algorithm will then function like so (keeping the above definitions in mind):
50 |
The first note of the scale (E) is played at the length of the first defined note length (4)
51 |
Each time the duration of the played note has ended, the next note in the scale (F) is played
52 |
Once the first pattern length has been reached (4 bars), a new pattern will start
53 |
The previously "recorded" pattern will loop its contents indefinitely while the new patterns are created / played
54 |
When a newly played note sounds simultaneously with another note from a PREVIOUS pattern, the note length will change (in above example from 4 to 3)
55 |
This will be the new note length to use for all subsequent added notes, until another simultaneously played note is found, leading it to switch back to the previous note length (in above example, back to 4)
56 |
As the pattern is now played over an existing one, it is likely that notes will be played in unison, leading to the switching of note length
57 |
As more patterns accumulate, a perfectly mathematical pattern of notes are weaving in and out of the notes of the other patterns
58 |
59 |
60 | Experimenting with different note lengths, scales or even time signatures can lead to interesting results!
61 |
62 |
63 |
64 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/definitions/samples.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2022 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | import { CompositionSource } from "../interfaces/CompositionSource";
25 |
26 | export const DEFAULT_COMPOSITION: CompositionSource = {
27 | name: "Default",
28 | description: "Something to set the mood. Ascending the C minor scale.",
29 | timeSigBeatAmount: 4,
30 | timeSigBeatUnit: 4,
31 | tempo: 120,
32 | scale: "C,D,D#,F,G,G#,A#",
33 | note1Length: 2,
34 | note2Length: 0.5,
35 | patternLength: 16,
36 | patternAmount: 8,
37 | octaveLower: 2,
38 | octaveUpper: 7,
39 | uniqueTrackPerPattern: false
40 | };
41 |
42 | export const COMPOSITIONS: Array = [
43 | DEFAULT_COMPOSITION,
44 | {
45 | ...DEFAULT_COMPOSITION,
46 | name: "4F1",
47 | description: "Waltz in the \"saddest of keys\", starting on F.",
48 | timeSigBeatAmount: 3,
49 | timeSigBeatUnit: 4,
50 | tempo: 136,
51 | scale: "F,G,A,Bb,C,D,E",
52 | note1Length: 4,
53 | note2Length: 1,
54 | patternLength: 6,
55 | patternAmount: 16,
56 | octaveLower: 3,
57 | octaveUpper: 6,
58 | },
59 | {
60 | ...DEFAULT_COMPOSITION,
61 | name: "4E3",
62 | description: "By Duncan Lockerby, as explained in his video. C major starting on E.",
63 | scale: "E,F,G,A,B,C,D",
64 | note1Length: 4,
65 | note2Length: 3,
66 | },
67 | {
68 | ...DEFAULT_COMPOSITION,
69 | name: "10 G 3.5",
70 | description: "By Duncan Lockerby. A slow piece in C major, starting on G.",
71 | scale: "G,A,B,C,D,E,F",
72 | note1Length: 10,
73 | note2Length: 3.5,
74 | },
75 | {
76 | ...DEFAULT_COMPOSITION,
77 | name: "9 C 14.5",
78 | description: "By Duncan Lockerby",
79 | tempo: 220,
80 | scale: "C,D,E,F,G,A,B",
81 | note1Length: 9,
82 | note2Length: 14.5,
83 | },
84 | {
85 | ...DEFAULT_COMPOSITION,
86 | name: "11.5 B 4",
87 | description: "By Duncan Lockerby. C major starting on B.",
88 | tempo: 240,
89 | scale: "B,C,D,E,F,G,A",
90 | note1Length: 11.5,
91 | note2Length: 4,
92 | },
93 | {
94 | ...DEFAULT_COMPOSITION,
95 | name: "0.5 C 3",
96 | description: "A frenetic piece, using a shuffled list of intervals in C Romanian / Ukranian Dorian",
97 | timeSigBeatAmount: 4,
98 | timeSigBeatUnit: 4,
99 | tempo: 110,
100 | scale: "A#,F#,D#,G,A,D,C",
101 | note1Length: 0.5,
102 | note2Length: 3,
103 | patternLength: 4,
104 | patternAmount: 8,
105 | octaveLower: 2,
106 | octaveUpper: 7,
107 | },
108 | {
109 | ...DEFAULT_COMPOSITION,
110 | name: "Enigmatic scale",
111 | description: "The input used by Drosophelia for the song \"6581\", where each pattern was played through a separate Commodore 64.",
112 | tempo: 96,
113 | scale: "C,C#,G#,E,F#,B,G#",
114 | note1Length: 1.5,
115 | note2Length: 4,
116 | patternLength: 8,
117 | patternAmount: 18,
118 | },
119 | {
120 | ...DEFAULT_COMPOSITION,
121 | name: "Sand Prince",
122 | description: "A brooding piece based around E phrygian dominant in 7/8 time.",
123 | tempo: 140,
124 | timeSigBeatAmount: 7,
125 | timeSigBeatUnit: 8,
126 | scale: "F,G#,E,A,B,D,C",
127 | note1Length: 3,
128 | note2Length: 4,
129 | patternLength: 14,
130 | patternAmount: 32,
131 | },
132 | {
133 | ...DEFAULT_COMPOSITION,
134 | name: "Diminished scale in 5/8",
135 | description: "The input used by Drosophelia for the industrial piece \"Vexed\", where each pattern was processed by a separate noise synth.",
136 | timeSigBeatAmount: 5,
137 | timeSigBeatUnit: 8,
138 | tempo: 165,
139 | scale: "E,F,G,G#,A#,B,C#,D",
140 | note1Length: 6,
141 | note2Length: 1,
142 | patternLength: 8,
143 | patternAmount: 16,
144 | },
145 | ];
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Molecular Music Generator (Web)
2 |
3 | The Molecular Music Generator (MMG) is an application that uses a simple algorithm to generate musical patterns which can be fed to hardware or software instruments.
4 |
5 | The properties of the algorithm can easily be defined in a graphical interface, which will then be rendered into a MIDI file, which can in turn be opened in DAW music software or be played back by synthesizers.
6 |
7 | This is the second installment of the application, which previously was available as a downloadable Java application.
8 |
9 | ### I want to tinker with this with the least effort!
10 |
11 | If you just want to make music and aren't interested in modifying the source code, no worries, you can
12 | jump to the hosted version [right here](https://www.igorski.nl/application/molecular-music-generator)
13 |
14 | Read the sections below to learn how the algorithm works and how to tailor a composition to your liking.
15 |
16 | ## About the algorithm
17 |
18 | The algorithm is based on "the Molecular Music Box" by Duncan Lockerby.
19 |
20 | The rules for the algorithm are as follows :
21 |
22 | * two different note lengths need to be defined, e.g. "4" and "3" (in quarter notes)
23 | * a scale needs to be defined, e.g. C major (the white keys on a piano), let's say we start on the E note, the list of
24 | notes will then contain : E, F, G, A, B, C
25 | * a pattern length needs to be defined, e.g. 4 bars
26 |
27 | The algorithm will then function like so (keeping the above definitions in mind) :
28 |
29 | * the first note of the scale (E) is played at the length of the first defined note length (4)
30 | * each time the duration of the played note has ended, the NEXT note in the scale (F) is played
31 | * once the first pattern length has been reached (4 bars), a new pattern will start
32 | * the previously "recorded" pattern will loop its contents indefinitely while the new patterns are created / played
33 | * if a newly played note sounds simultaneously with another note from a PREVIOUS pattern, the note length will
34 | change (in above example from 4 to 3).
35 | * this will be the new note length to use for ALL SUBSEQUENT added notes, until another simultaneously played
36 | note is found, leading it to switch back to the previous note length (in above example, back to 4).
37 | * as the pattern is now played over an existing one, it is likely that notes will be played in unison,
38 | leading to the switching of note length
39 | * as more patterns are accumulated, a perfectly mathematical pattern of notes are weaving in and out of
40 | the notes of the other patterns
41 |
42 | Experimenting with different note lengths, scales or even time signatures can lead to interesting results!
43 |
44 | See https://www.youtube.com/watch?v=3Z8CuAC_-bg for the original video by Duncan Lockerby.
45 |
46 | ## How to setup the pattern properties in the application
47 |
48 | "First / second note lengths" define a list of two notes that describes the alternate note length/duration as a quarter note.
49 |
50 | "Pattern length" describes the length of a single pattern (in measures). Once the algorithm has generated notes
51 | for the given amount of measures, a new pattern will be created. This process will repeat itself until the configured "amount of patterns" has been reached. For instance: a configuration with a pattern length of 4 and a pattern amount of 8 will result in 32 measures of music.
52 |
53 | You can alter the time signature to any exotic meter of your liking, the first number is the upper numeral in
54 | a time signature, e.g. the "3" in 3/4, while the second number is the lower numeral, e.g. the "4" in 3/4.
55 |
56 | "Min octave" determines the octave at which the composition will start, this cannot be lower than 0. "Max octave" determines the octave at which the composition will end, this cannot be higher than 8.
57 |
58 | The value for "scale" can be changed to any sequence (or length!) of comma separated notes you like, meaning you can use exotic scales, or even determine the movement by creating a sequence in thirds, or by re-introducing a previously defined note, etc. Remember that the scale will be repeated over the determined octave range. You can create sharps and flats too, e.g.: "_Eb_", "_F#_", etc. are all valid input.
59 |
60 | "track per pattern" can be either '_true_' or '_false_'. When true, the resulting .MIDI file will have a unique MIDI track for each new pattern, when false, all the patterns are part of the same MIDI track. If the amount of patterns is high enough for the algorithm to go back down the scale, it is best to have this set to true to avoid conflicts in MIDI notes.
61 |
62 | ## Build scripts
63 |
64 | In the project directory, you can run:
65 |
66 | ```
67 | npm start
68 | ```
69 |
70 | Runs the app in the development mode.\
71 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
72 |
73 | The page will reload when you make changes.\
74 | You may also see any lint errors in the console.
75 |
76 | ```
77 | npm test
78 | ```
79 |
80 | Launches the test runner in the interactive watch mode.\
81 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
82 |
83 | ```
84 | npm run build
85 | ```
86 |
87 | Builds the app for production to the `build` folder.\
88 | It correctly bundles React in production mode and optimizes the build for the best performance.
89 |
90 | The build is minified and the filenames include the hashes.\
91 | Your app is ready to be deployed!
92 |
93 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
94 |
--------------------------------------------------------------------------------
/src/components/Player/Player.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2022 Igor Zinken
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | import { Component } from "react";
25 | import Composition from "../../model/Composition";
26 | import { setupCompositionPlayback, play, pause, goToMeasure } from "../../services/AudioService";
27 | import "./Player.scss";
28 |
29 | type PlayerProps = {
30 | composition: Composition
31 | };
32 |
33 | interface PlayerState {
34 | measure : number,
35 | beat : number,
36 | sixteenth : number,
37 | total : number,
38 | time : number,
39 | disabled : boolean,
40 | playing : boolean,
41 | };
42 |
43 | export default class Player extends Component {
44 | _keyHandler: ( e:any ) => void;
45 |
46 | constructor( props: PlayerProps ) {
47 | super( props );
48 |
49 | this.state = {
50 | measure : 0,
51 | beat : 0,
52 | sixteenth : 0,
53 | total : 1,
54 | time : 0,
55 | disabled : !props.composition,
56 | playing : false,
57 | };
58 | }
59 |
60 | componentDidMount(): void {
61 | this._keyHandler = ( e: React.KeyboardEvent ) => {
62 | if ( e.keyCode === 32 && this.props.composition ) {
63 | e.preventDefault();
64 | this.togglePlayBack( true );
65 | }
66 | };
67 | document.body.addEventListener( "keydown", this._keyHandler );
68 | }
69 |
70 | componentWillUnmount(): void {
71 | document.body.removeEventListener( "keydown", this._keyHandler );
72 | }
73 |
74 | componentDidUpdate( prevProps: PlayerProps ): void {
75 | if ( this.props.composition === prevProps.composition ) {
76 | return;
77 | }
78 | this.setState({ disabled: false, measure: 0, beat: 0, sixteenth: 0 });
79 | setupCompositionPlayback( this.props.composition, ( measure: number, beat: number, sixteenth: number, total: number, time: number ) => {
80 | this.setState({ measure, beat, sixteenth, total, time });
81 | });
82 | if ( this.state.playing ) {
83 | play();
84 | }
85 | }
86 |
87 | togglePlayBack( resetPosition: boolean = false ): void {
88 | if ( !this.state.playing ) {
89 | play();
90 | } else {
91 | pause();
92 | }
93 | if ( resetPosition ) {
94 | this.setState({ ...this.state, ...goToMeasure( 0 ) });
95 | }
96 | this.setState({ playing: !this.state.playing });
97 | }
98 |
99 | goToPreviousMeasure(): void {
100 | this.setState({ ...this.state, ...goToMeasure( this.state.measure - 1 ) });
101 | }
102 |
103 | goToNextMeasure(): void {
104 | this.setState({ ...this.state, ...goToMeasure( this.state.measure + 1 ) });
105 | }
106 |
107 | render() {
108 | return (
109 |