File size: 6,270 Bytes
b0bfea8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import { BasicMIDI } from "./basic_midi.js";
import { messageTypes, MIDIMessage } from "./midi_message.js";
import { IndexedByteArray } from "../utils/indexed_array.js";
import { SpessaSynthWarn } from "../utils/loggin.js";

/**
 * A class that helps to build a MIDI file from scratch.
 */
export class MIDIBuilder extends BasicMIDI
{
    /**
     * @param name {string} The MIDI's name
     * @param timeDivision {number} the file's time division
     * @param initialTempo {number} the file's initial tempo
     */
    constructor(name, timeDivision = 480, initialTempo = 120)
    {
        super();
        this.timeDivision = timeDivision;
        this.midiName = name;
        this.encoder = new TextEncoder();
        this.rawMidiName = this.encoder.encode(name);
        
        // create the first track with the file name
        this.addNewTrack(name);
        this.addSetTempo(0, initialTempo);
    }
    
    /**
     * Adds a new Set Tempo event
     * @param ticks {number} the tick number of the event
     * @param tempo {number} the tempo in beats per minute (BPM)
     */
    addSetTempo(ticks, tempo)
    {
        const array = new IndexedByteArray(3);
        
        tempo = 60000000 / tempo;
        
        // Extract each byte in big-endian order
        array[0] = (tempo >> 16) & 0xFF;
        array[1] = (tempo >> 8) & 0xFF;
        array[2] = tempo & 0xFF;
        
        this.addEvent(ticks, 0, messageTypes.setTempo, array);
    }
    
    /**
     * Adds a new MIDI track
     * @param name {string} the new track's name
     * @param port {number} the new track's port
     */
    addNewTrack(name, port = 0)
    {
        this.tracksAmount++;
        if (this.tracksAmount > 1)
        {
            this.format = 1;
        }
        this.tracks.push([]);
        this.tracks[this.tracksAmount - 1].push(
            new MIDIMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0))
        );
        this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
        this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
    }
    
    /**
     * Adds a new MIDI Event
     * @param ticks {number} the tick time of the event
     * @param track {number} the track number to use
     * @param event {number} the MIDI event number
     * @param eventData {Uint8Array|Iterable<number>} the raw event data
     */
    addEvent(ticks, track, event, eventData)
    {
        if (!this.tracks[track])
        {
            throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
        }
        if (event === messageTypes.endOfTrack)
        {
            SpessaSynthWarn(
                "The EndOfTrack is added automatically and does not influence the duration. Consider adding a voice event instead.");
            return;
        }
        // remove the end of track
        this.tracks[track].pop();
        this.tracks[track].push(new MIDIMessage(
            ticks,
            event,
            new IndexedByteArray(eventData)
        ));
        // add the end of track
        this.tracks[track].push(new MIDIMessage(
            ticks,
            messageTypes.endOfTrack,
            new IndexedByteArray(0)
        ));
    }
    
    /**
     * Adds a new Note On event
     * @param ticks {number} the tick time of the event
     * @param track {number} the track number to use
     * @param channel {number} the channel to use
     * @param midiNote {number} the midi note of the keypress
     * @param velocity {number} the velocity of the keypress
     */
    addNoteOn(ticks, track, channel, midiNote, velocity)
    {
        channel %= 16;
        midiNote %= 128;
        velocity %= 128;
        this.addEvent(
            ticks,
            track,
            messageTypes.noteOn | channel,
            [midiNote, velocity]
        );
    }
    
    /**
     * Adds a new Note Off event
     * @param ticks {number} the tick time of the event
     * @param track {number} the track number to use
     * @param channel {number} the channel to use
     * @param midiNote {number} the midi note of the key release
     */
    addNoteOff(ticks, track, channel, midiNote)
    {
        channel %= 16;
        midiNote %= 128;
        this.addEvent(
            ticks,
            track,
            messageTypes.noteOff | channel,
            [midiNote, 64]
        );
    }
    
    /**
     * Adds a new Program Change event
     * @param ticks {number} the tick time of the event
     * @param track {number} the track number to use
     * @param channel {number} the channel to use
     * @param programNumber {number} the MIDI program to use
     */
    addProgramChange(ticks, track, channel, programNumber)
    {
        channel %= 16;
        programNumber %= 128;
        this.addEvent(
            ticks,
            track,
            messageTypes.programChange | channel,
            [programNumber]
        );
    }
    
    /**
     * Adds a new Controller Change event
     * @param ticks {number} the tick time of the event
     * @param track {number} the track number to use
     * @param channel {number} the channel to use
     * @param controllerNumber {number} the MIDI CC to use
     * @param controllerValue {number} the new CC value
     */
    addControllerChange(ticks, track, channel, controllerNumber, controllerValue)
    {
        channel %= 16;
        controllerNumber %= 128;
        controllerValue %= 128;
        this.addEvent(
            ticks,
            track,
            messageTypes.controllerChange | channel,
            [controllerNumber, controllerValue]
        );
    }
    
    /**
     * Adds a new Pitch Wheel event
     * @param ticks {number} the tick time of the event
     * @param track {number} the track to use
     * @param channel {number} the channel to use
     * @param MSB {number} SECOND byte of the MIDI pitchWheel message
     * @param LSB {number} FIRST byte of the MIDI pitchWheel message
     */
    addPitchWheel(ticks, track, channel, MSB, LSB)
    {
        channel %= 16;
        MSB %= 128;
        LSB %= 128;
        this.addEvent(
            ticks,
            track,
            messageTypes.pitchBend | channel,
            [LSB, MSB]
        );
    }
}