1
package org.herac.tuxguitar.io.midi;
3
import java.io.InputStream;
4
import java.util.ArrayList;
5
import java.util.Iterator;
8
import org.herac.tuxguitar.io.base.TGFileFormat;
9
import org.herac.tuxguitar.io.base.TGFileFormatException;
10
import org.herac.tuxguitar.io.base.TGSongImporter;
11
import org.herac.tuxguitar.io.midi.base.MidiEvent;
12
import org.herac.tuxguitar.io.midi.base.MidiMessage;
13
import org.herac.tuxguitar.io.midi.base.MidiSequence;
14
import org.herac.tuxguitar.io.midi.base.MidiTrack;
15
import org.herac.tuxguitar.player.base.MidiControllers;
16
import org.herac.tuxguitar.song.factory.TGFactory;
17
import org.herac.tuxguitar.song.managers.TGSongManager;
18
import org.herac.tuxguitar.song.models.TGBeat;
19
import org.herac.tuxguitar.song.models.TGChannel;
20
import org.herac.tuxguitar.song.models.TGColor;
21
import org.herac.tuxguitar.song.models.TGDuration;
22
import org.herac.tuxguitar.song.models.TGMeasure;
23
import org.herac.tuxguitar.song.models.TGMeasureHeader;
24
import org.herac.tuxguitar.song.models.TGNote;
25
import org.herac.tuxguitar.song.models.TGSong;
26
import org.herac.tuxguitar.song.models.TGString;
27
import org.herac.tuxguitar.song.models.TGTempo;
28
import org.herac.tuxguitar.song.models.TGTimeSignature;
29
import org.herac.tuxguitar.song.models.TGTrack;
31
public class MidiSongImporter implements TGSongImporter{
33
private static final int MIN_DURATION_VALUE = TGDuration.SIXTY_FOURTH;
35
private int resolution;
38
private List tempNotes;
39
private List tempChannels;
40
private List trackTuningHelpers;
41
private MidiSettings settings;
42
protected TGFactory factory;
44
public MidiSongImporter(){
48
public TGFileFormat getFileFormat() {
49
return new TGFileFormat("Midi","*.mid;*.midi");
52
public String getImportName() {
56
public boolean configure(boolean setDefaults){
57
this.settings = (setDefaults ? MidiSettings.getDefaults() : new MidiSettingsDialog().open());
58
return (this.settings != null);
61
public TGSong importSong(TGFactory factory,InputStream stream) throws TGFileFormatException {
63
if(this.settings == null){
66
this.factory = factory;
68
MidiSequence sequence = new MidiFileReader().getSequence(stream);
70
for(int i = 0; i < sequence.countTracks(); i++){
71
MidiTrack track = sequence.getTrack(i);
72
int trackNumber = getNextTrackNumber();
73
int events = track.size();
74
for(int j = 0;j < events;j ++){
75
MidiEvent event = track.get(j);
76
parseMessage(trackNumber,event.getTick(),event.getMessage());
81
TGSong song = this.factory.newSong();
83
Iterator headers = this.headers.iterator();
84
while(headers.hasNext()){
85
song.addMeasureHeader((TGMeasureHeader)headers.next());
87
Iterator tracks = this.tracks.iterator();
88
while(tracks.hasNext()){
89
song.addTrack((TGTrack)tracks.next());
91
return new SongAdjuster(this.factory,song).adjustSong();
92
} catch (Throwable throwable) {
93
throw new TGFileFormatException(throwable);
97
private void initFields(MidiSequence sequence){
98
this.resolution = sequence.getResolution();
99
this.headers = new ArrayList();
100
this.tracks = new ArrayList();
101
this.tempNotes = new ArrayList();
102
this.tempChannels = new ArrayList();
103
this.trackTuningHelpers = new ArrayList();
106
private int getNextTrackNumber(){
107
return (this.tracks.size() + 1);
110
private void parseMessage(int trackNumber,long tick,MidiMessage message){
111
long parsedTick = parseTick(tick + this.resolution);
114
if(message.getType() == MidiMessage.TYPE_SHORT && message.getCommand() == MidiMessage.NOTE_ON){
115
parseNoteOn(trackNumber,parsedTick,message.getData());
118
else if(message.getType() == MidiMessage.TYPE_SHORT && message.getCommand() == MidiMessage.NOTE_OFF){
119
parseNoteOff(trackNumber,parsedTick,message.getData());
122
else if(message.getType() == MidiMessage.TYPE_SHORT && message.getCommand() == MidiMessage.PROGRAM_CHANGE){
123
parseProgramChange(message.getData());
126
else if(message.getType() == MidiMessage.TYPE_SHORT && message.getCommand() == MidiMessage.CONTROL_CHANGE){
127
parseControlChange(message.getData());
130
else if(message.getType() == MidiMessage.TYPE_META && message.getCommand() == MidiMessage.TIME_SIGNATURE_CHANGE){
131
parseTimeSignature(parsedTick,message.getData());
134
else if(message.getType() == MidiMessage.TYPE_META && message.getCommand() == MidiMessage.TEMPO_CHANGE){
135
parseTempo(parsedTick,message.getData());
139
private long parseTick(long tick){
140
return Math.abs(TGDuration.QUARTER_TIME * tick / this.resolution);
143
private void parseNoteOn(int track,long tick,byte[] data){
144
int length = data.length;
145
int channel = (length > 0)?((data[0] & 0xFF) & 0x0F):0;
146
int value = (length > 1)?(data[1] & 0xFF):0;
147
int velocity = (length > 2)?(data[2] & 0xFF):0;
149
parseNoteOff(track,tick,data);
151
makeTempNotesBefore(tick,track);
152
getTempChannel(channel).setTrack(track);
153
getTrackTuningHelper(track).checkValue(value);
154
this.tempNotes.add(new TempNote(track,channel,value,tick));
158
private void parseNoteOff(int track,long tick,byte[] data){
159
int length = data.length;
161
int channel = (length > 0)?((data[0] & 0xFF) & 0x0F):0;
162
int value = (length > 1)?(data[1] & 0xFF):0;
164
makeNote(tick,track,channel,value);
167
private void parseProgramChange(byte[] data){
168
int length = data.length;
169
int channel = (length > 0)?((data[0] & 0xFF) & 0x0F):-1;
170
int instrument = (length > 1)?(data[1] & 0xFF):-1;
171
if(channel != -1 && instrument != -1){
172
getTempChannel(channel).setInstrument(instrument);
176
private void parseControlChange(byte[] data){
177
int length = data.length;
178
int channel = (length > 0)?((data[0] & 0xFF) & 0x0F):-1;
179
int control = (length > 1)?(data[1] & 0xFF):-1;
180
int value = (length > 2)?(data[2] & 0xFF):-1;
181
if(channel != -1 && control != -1 && value != -1){
182
if(control == MidiControllers.VOLUME){
183
getTempChannel(channel).setVolume(value);
185
else if(control == MidiControllers.BALANCE){
186
getTempChannel(channel).setBalance(value);
191
private void parseTimeSignature(long tick,byte[] data){
192
if(data.length >= 2){
193
TGTimeSignature timeSignature = this.factory.newTimeSignature();
194
timeSignature.setNumerator(data[0]);
195
timeSignature.getDenominator().setValue(TGDuration.QUARTER);
197
timeSignature.getDenominator().setValue(TGDuration.WHOLE);
198
} else if (data[1] == 1) {
199
timeSignature.getDenominator().setValue(TGDuration.HALF);
200
} else if (data[1] == 2) {
201
timeSignature.getDenominator().setValue(TGDuration.QUARTER);
202
} else if (data[1] == 3) {
203
timeSignature.getDenominator().setValue(TGDuration.EIGHTH);
204
} else if (data[1] == 4) {
205
timeSignature.getDenominator().setValue(TGDuration.SIXTEENTH);
206
} else if (data[1] == 5) {
207
timeSignature.getDenominator().setValue(TGDuration.THIRTY_SECOND);
209
getHeader(tick).setTimeSignature(timeSignature);
213
private void parseTempo(long tick,byte[] data){
214
if(data.length >= 3){
215
TGTempo tempo = TGTempo.fromUSQ(this.factory,(data[2] & 0xff) | ((data[1] & 0xff) << 8) | ((data[0] & 0xff) << 16));
216
getHeader(tick).setTempo(tempo);
220
private TGTrack getTrack(int number){
221
Iterator it = this.tracks.iterator();
223
TGTrack track = (TGTrack)it.next();
224
if(track.getNumber() == number){
228
TGChannel channel = this.factory.newChannel();
229
channel.setChannel((short)-1);
230
channel.setEffectChannel((short)-1);
231
channel.setInstrument((short)0);
233
TGTrack track = this.factory.newTrack();
234
track.setNumber(number);
235
track.setChannel(channel);
236
TGColor.RED.copy(track.getColor());
238
this.tracks.add(track);
242
private TGMeasureHeader getHeader(long tick){
243
long realTick = (tick >= TGDuration.QUARTER_TIME)?tick:TGDuration.QUARTER_TIME;
245
Iterator it = this.headers.iterator();
247
TGMeasureHeader header = (TGMeasureHeader)it.next();
248
if(realTick >= header.getStart() && realTick < header.getStart() + header.getLength()){
252
TGMeasureHeader last = getLastHeader();
253
TGMeasureHeader header = this.factory.newHeader();
254
header.setNumber((last != null)?last.getNumber() + 1:1);
255
header.setStart((last != null)?(last.getStart() + last.getLength()):TGDuration.QUARTER_TIME);
256
header.getTempo().setValue( (last != null)?last.getTempo().getValue():120 );
258
last.getTimeSignature().copy(header.getTimeSignature());
260
header.getTimeSignature().setNumerator(4);
261
header.getTimeSignature().getDenominator().setValue(TGDuration.QUARTER);
263
this.headers.add(header);
265
if(realTick >= header.getStart() && realTick < header.getStart() + header.getLength()){
268
return getHeader(realTick);
271
private TGMeasureHeader getLastHeader(){
272
if(!this.headers.isEmpty()){
273
return (TGMeasureHeader)this.headers.get(this.headers.size() - 1);
278
private TGMeasure getMeasure(TGTrack track,long tick){
279
long realTick = (tick >= TGDuration.QUARTER_TIME)?tick:TGDuration.QUARTER_TIME;
280
Iterator it = track.getMeasures();
282
TGMeasure measure = (TGMeasure)it.next();
283
if(realTick >= measure.getStart() && realTick < measure.getStart() + measure.getLength()){
288
for(int i = 0;i < this.headers.size();i++){
289
boolean exist = false;
290
TGMeasureHeader header = (TGMeasureHeader)this.headers.get(i);
291
int measureCount = track.countMeasures();
292
for(int j = 0;j < measureCount;j++){
293
TGMeasure measure = track.getMeasure(j);
294
if(measure.getHeader().equals(header)){
299
TGMeasure measure = this.factory.newMeasure(header);
300
track.addMeasure(measure);
303
return getMeasure(track,realTick);
306
private TGBeat getBeat(TGMeasure measure, long start){
307
int beatCount = measure.countBeats();
308
for( int i = 0 ; i < beatCount ; i ++){
309
TGBeat beat = measure.getBeat( i );
310
if( beat.getStart() == start){
315
TGBeat beat = this.factory.newBeat();
316
beat.setStart(start);
317
measure.addBeat(beat);
321
private TempNote getTempNote(int track,int channel,int value,boolean purge){
322
for(int i = 0;i < this.tempNotes.size();i ++){
323
TempNote note = (TempNote)this.tempNotes.get(i);
324
if(note.getTrack() == track && note.getChannel() == channel && note.getValue() == value){
326
this.tempNotes.remove(i);
334
protected TrackTuningHelper getTrackTuningHelper(int track){
335
Iterator it = this.trackTuningHelpers.iterator();
337
TrackTuningHelper helper = (TrackTuningHelper)it.next();
338
if(helper.getTrack() == track){
342
TrackTuningHelper helper = new TrackTuningHelper(track);
343
this.trackTuningHelpers.add(helper);
348
private void makeTempNotesBefore(long tick,int track){
349
long nextTick = tick;
350
boolean check = true;
353
for(int i = 0;i < this.tempNotes.size();i ++){
354
TempNote note = (TempNote)this.tempNotes.get(i);
355
if(note.getTick() < nextTick && note.getTrack() == track){
356
nextTick = note.getTick() + (TGDuration.QUARTER_TIME * 5); //First beat + 4/4 measure;
357
makeNote(nextTick,track,note.getChannel(),note.getValue());
365
private void makeNote(long tick,int track,int channel,int value){
366
TempNote tempNote = getTempNote(track,channel,value,true);
367
if(tempNote != null){
369
int nValue = (tempNote.getValue() + this.settings.getTranspose());
371
long nStart = tempNote.getTick();
372
TGDuration minDuration = newDuration(MIN_DURATION_VALUE);
373
TGDuration nDuration = TGDuration.fromTime(this.factory,tick - tempNote.getTick(),minDuration);
375
TGMeasure measure = getMeasure(getTrack(track),tempNote.getTick());
376
TGBeat beat = getBeat(measure, nStart);
377
nDuration.copy(beat.getDuration());
379
TGNote note = this.factory.newNote();
380
note.setValue(nValue);
381
note.setString(nString);
382
note.setVelocity(nVelocity);
388
public TempChannel getTempChannel(int channel){
389
Iterator it = this.tempChannels.iterator();
391
TempChannel tempChannel = (TempChannel)it.next();
392
if(tempChannel.getChannel() == channel){
396
TempChannel tempChannel = new TempChannel(channel);
397
this.tempChannels.add(tempChannel);
402
private void checkAll()throws Exception{
405
int headerCount = this.headers.size();
406
for(int i = 0;i < this.tracks.size();i ++){
407
TGTrack track = (TGTrack)this.tracks.get(i);
409
while(track.countMeasures() < headerCount){
410
long start = TGDuration.QUARTER_TIME;
411
TGMeasure lastMeasure = ((track.countMeasures() > 0)?track.getMeasure(track.countMeasures() - 1) :null);
412
if(lastMeasure != null){
413
start = (lastMeasure.getStart() + lastMeasure.getLength());
416
track.addMeasure(this.factory.newMeasure(getHeader(start)));
420
if(this.headers.isEmpty() || this.tracks.isEmpty()){
421
throw new Exception("Empty Song");
425
private void checkTracks(){
426
Iterator it = this.tracks.iterator();
428
TGTrack track = (TGTrack)it.next();
429
Iterator tcIt = this.tempChannels.iterator();
430
while(tcIt.hasNext()){
431
TempChannel tempChannel = (TempChannel)tcIt.next();
432
if(tempChannel.getTrack() == track.getNumber()){
433
if(track.getChannel().getChannel() < 0){
434
track.getChannel().setChannel((short)tempChannel.getChannel());
435
track.getChannel().setInstrument((short)tempChannel.getInstrument());
436
track.getChannel().setVolume((short)tempChannel.getVolume());
437
track.getChannel().setBalance((short)tempChannel.getBalance());
438
}else if(track.getChannel().getEffectChannel() < 0){
439
track.getChannel().setEffectChannel((short)tempChannel.getChannel());
443
if(track.getChannel().getChannel() < 0){
444
track.getChannel().setChannel((short)(TGSongManager.MAX_CHANNELS - 1));
445
track.getChannel().setInstrument((short)0);
446
track.getChannel().setVolume((short)127);
447
track.getChannel().setBalance((short)64);
449
if(track.getChannel().getEffectChannel() < 0){
450
track.getChannel().setEffectChannel(track.getChannel().getChannel());
453
if(!track.isPercussionTrack()){
454
track.setStrings(getTrackTuningHelper(track.getNumber()).getStrings());
456
track.setStrings(TGSongManager.createPercusionStrings(this.factory,6));
461
protected TGDuration newDuration(int value){
462
TGDuration duration = this.factory.newDuration();
463
duration.setValue(value);
467
private class TempNote{
473
public TempNote(int track, int channel, int value,long tick) {
475
this.channel = channel;
480
public int getChannel() {
484
public void setChannel(int channel) {
485
this.channel = channel;
488
public long getTick() {
492
public void setTick(long tick) {
496
public int getTrack() {
500
public void setTrack(int track) {
504
public int getValue() {
508
public void setValue(int value) {
514
private class TempChannel{
516
private int instrument;
521
public TempChannel(int channel) {
522
this.channel = channel;
529
public int getBalance() {
533
public void setBalance(int balance) {
534
this.balance = balance;
537
public int getChannel() {
541
public void setChannel(int channel) {
542
this.channel = channel;
545
public int getInstrument() {
546
return this.instrument;
549
public void setInstrument(int instrument) {
550
this.instrument = instrument;
553
public int getTrack() {
557
public void setTrack(int track) {
561
public int getVolume() {
565
public void setVolume(int volume) {
566
this.volume = volume;
571
private class TrackTuningHelper{
573
private int maxValue;
574
private int minValue;
576
public TrackTuningHelper(int track){
582
public void checkValue(int value){
583
if(this.minValue < 0 || value < this.minValue){
584
this.minValue = value;
586
if(this.maxValue < 0 || value > this.maxValue){
587
this.maxValue = value;
591
public List getStrings() {
592
List strings = new ArrayList();
596
if(this.minValue >= 40 && this.maxValue <= 64 + maxFret){
597
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,1, 64));
598
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,2, 59));
599
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,3, 55));
600
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,4, 50));
601
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,5, 45));
602
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,6, 40));
604
else if(this.minValue >= 38 && this.maxValue <= 64 + maxFret){
605
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,1, 64));
606
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,2, 59));
607
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,3, 55));
608
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,4, 50));
609
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,5, 45));
610
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,6, 38));
612
else if(this.minValue >= 35 && this.maxValue <= 64 + maxFret){
613
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,1, 64));
614
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,2, 59));
615
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,3, 55));
616
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,4, 50));
617
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,5, 45));
618
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,6, 40));
619
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,7, 35));
621
else if(this.minValue >= 28 && this.maxValue <= 43 + maxFret){
622
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,1, 43));
623
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,2, 38));
624
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,3, 33));
625
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,4, 28));
627
else if(this.minValue >= 23 && this.maxValue <= 43 + maxFret){
628
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,1, 43));
629
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,2, 38));
630
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,3, 33));
631
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,4, 28));
632
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,5, 23));
635
int stringSpacing = ((this.maxValue - (maxFret - 4) - this.minValue) / stringCount);
636
if(stringSpacing > 5){
638
stringSpacing = ((this.maxValue - (maxFret - 4) - this.minValue) / stringCount);
641
int maxStringValue = (this.minValue + (stringCount * stringSpacing));
642
while(strings.size() < stringCount){
643
maxStringValue -= stringSpacing;
644
strings.add(TGSongManager.newString(MidiSongImporter.this.factory,strings.size() + 1,maxStringValue));
651
public int getMaxValue() {
652
return this.maxValue;
655
public int getMinValue() {
656
return this.minValue;
659
public int getTrack() {
667
private TGFactory factory;
669
private long minDurationTime;
671
public SongAdjuster(TGFactory factory,TGSong song){
672
this.factory = factory;
674
this.minDurationTime = 40;
677
public TGSong adjustSong(){
678
Iterator it = this.song.getTracks();
681
TGTrack track = (TGTrack)it.next();
687
private void adjustTrack(TGTrack track){
688
Iterator it = track.getMeasures();
690
TGMeasure measure = (TGMeasure)it.next();
695
public void process(TGMeasure measure){
698
adjustStrings(measure);
701
public void joinBeats(TGMeasure measure){
702
TGBeat previous = null;
703
boolean finish = true;
705
long measureStart = measure.getStart();
706
long measureEnd = (measureStart + measure.getLength());
707
for(int i = 0;i < measure.countBeats();i++){
708
TGBeat beat = measure.getBeat( i );
709
long beatStart = beat.getStart();
710
long beatLength = beat.getDuration().getTime();
711
if(previous != null){
712
long previousStart = previous.getStart();
713
long previousLength = previous.getDuration().getTime();
715
//if(previousStart == beatStart){
716
if(beatStart >= previousStart && (previousStart + this.minDurationTime) > beatStart ){
717
// add beat notes to previous
718
for(int n = 0;n < beat.countNotes();n++){
719
TGNote note = beat.getNote( n );
720
previous.addNote( note );
723
// add beat chord to previous
724
if(!previous.isChordBeat() && beat.isChordBeat()){
725
previous.setChord( beat.getChord() );
728
// add beat text to previous
729
if(!previous.isTextBeat() && beat.isTextBeat()){
730
previous.setText( beat.getText() );
733
// set the best duration
734
if(beatLength > previousLength && (beatStart + beatLength) <= measureEnd){
735
beat.getDuration().copy(previous.getDuration());
738
measure.removeBeat(beat);
743
else if(previousStart < beatStart && (previousStart + previousLength) > beatStart){
744
if(beat.isRestBeat()){
745
measure.removeBeat(beat);
749
TGDuration duration = TGDuration.fromTime(this.factory, (beatStart - previousStart) );
750
duration.copy( previous.getDuration() );
753
if( (beatStart + beatLength) > measureEnd ){
754
if(beat.isRestBeat()){
755
measure.removeBeat(beat);
759
TGDuration duration = TGDuration.fromTime(this.factory, (measureEnd - beatStart) );
760
duration.copy( beat.getDuration() );
770
public void orderBeats(TGMeasure measure){
771
for(int i = 0;i < measure.countBeats();i++){
772
TGBeat minBeat = null;
773
for(int j = i;j < measure.countBeats();j++){
774
TGBeat beat = measure.getBeat(j);
775
if(minBeat == null || beat.getStart() < minBeat.getStart()){
779
measure.moveBeat(i, minBeat);
783
private void adjustStrings(TGMeasure measure){
784
for(int i = 0;i < measure.countBeats();i++){
785
TGBeat beat = measure.getBeat( i );
790
private void adjustStrings(TGBeat beat){
791
TGTrack track = beat.getMeasure().getTrack();
792
List freeStrings = new ArrayList( track.getStrings() );
793
List notesToRemove = new ArrayList();
796
Iterator it = beat.getNotes().iterator();
798
TGNote note = (TGNote)it.next();
800
int string = getStringForValue(freeStrings,note.getValue());
801
for(int j = 0;j < freeStrings.size();j ++){
802
TGString tempString = (TGString)freeStrings.get(j);
803
if(tempString.getNumber() == string){
804
note.setValue(note.getValue() - tempString.getValue());
805
note.setString(tempString.getNumber());
806
freeStrings.remove(j);
811
//Cannot have more notes on same string
812
if(note.getString() < 1){
813
notesToRemove.add( note );
818
while( notesToRemove.size() > 0 ){
819
beat.removeNote( (TGNote)notesToRemove.get( 0 ) );
820
notesToRemove.remove( 0 );
824
private int getStringForValue(List strings,int value){
826
int stringForValue = 0;
827
for(int i = 0;i < strings.size();i++){
828
TGString string = (TGString)strings.get(i);
829
int fret = value - string.getValue();
830
if(minFret < 0 || (fret >= 0 && fret < minFret)){
831
stringForValue = string.getNumber();
835
return stringForValue;
b'\\ No newline at end of file'