MP3 to MIDI Note List: Columns, Note Names, and Seconds vs Ticks

The Copy button hands you six columns of plain text. I copied it out of the live converter eight times and took the text apart byte by byte.

Short answer: Copy note list produces plain text - a header line and then one line per note, six columns separated by exactly two spaces. The times are the detector's own seconds to three decimals, not the ticks stored in the .mid file; to get a tick you need round(seconds x BPM / 60 x 480). Note names use sharps only, and MIDI 60 is written C4, so a black key is C#4 and never Db4. There is no trailing newline, and nothing is padded into columns, so the text is not aligned when you look at it.

Everything below was measured on this page's own converter running in a real browser at easyaudiotomidi.com. Real files were pushed into the file input, the real Convert and Copy buttons were clicked, and the text the page itself built was captured and saved to disk for byte-level analysis. Where a number comes from somewhere else, it says so.

What the button hands you

This is the complete output for an 8-note scale, with every control left at its default. Nothing is trimmed or reordered:

#  Start(s)  End(s)  Dur(s)  Vel  Note
1  0.000  0.432  0.432  100  C4
2  0.480  0.944  0.464  100  D4
3  0.976  1.440  0.464  100  E4
4  1.488  1.936  0.448  100  F4
5  1.984  2.448  0.464  100  G4
6  2.480  2.944  0.464  100  A4
7  2.976  3.440  0.464  100  B4
8  3.488  3.936  0.448  100  C5

That is 294 bytes across 9 lines, and it is the whole clipboard payload. The page's own status line confirms the size of what it copied: Copied 8 note(s) as text.

The six columns

The whole function is short enough to read in full. This is it, unchanged:

function noteListText(notes, transpose) {
  var lines = ['#  Start(s)  End(s)  Dur(s)  Vel  Note'];
  for (var i = 0; i < notes.length; i++) {
    var n = notes[i];
    lines.push(
      (i + 1) + '  ' +
      n.start.toFixed(3) + '  ' +
      n.end.toFixed(3) + '  ' +
      (n.end - n.start).toFixed(3) + '  ' +
      n.vel + '  ' +
      noteName(n.note + transpose)
    );
  }
  return lines.join('\n');
}
ColumnWhat it isSource
#Row number, starting at 1the loop counter
Start(s)Note start, in seconds, 3 decimalsthe detector's own measurement
End(s)Note end, in seconds, 3 decimalsthe detector's own measurement
Dur(s)End minus Start, computed in the page, 3 decimalsarithmetic on the two columns to its left
VelThe velocity the page decided, a whole numberderived from the note's RMS
NoteThe note name after Transposea name table plus the Transpose box

Three consequences follow straight from that code, and all three are visible in the output above:

The exact format, byte for byte

Because the text is meant to be pasted into something else, the details matter more than they look. I saved the captured text to disk as raw bytes and measured it, and every one of the eight runs agreed:

PropertyMeasured
Column separatorTwo spaces, everywhere. No tabs, no commas, no semicolons. Scanning the whole text for whitespace runs found exactly one kind: ' '.
Line endingA single LF. Zero carriage returns in any file.
Trailing newlineNone. The byte count equals the sum of the line lengths plus one LF per gap - nothing is left over at the end.
Column paddingNone. Values are not right-aligned or padded, so the columns do not line up.
Non-ASCII charactersZero. The text is pure ASCII, including the # in sharp note names.

The padding point is easy to see if you look at where each column starts. In the same file, the header puts its second column at character 11, a row with a one-digit number puts it at 8, and a row with a two-digit number puts it at 9. Nothing is being aligned - the separator is just two spaces and the field widths float. The same is true of the Vel column: rows with a two-digit velocity and rows with a three-digit velocity shift the Note column by one character.

How big the text gets

Measured on four files, from a 3-note phrase to a 100-note passage:

Notes detectedBytesLinesBytes per note line
3134431.0
8294931.0
124291331.6
1003,48010133.4

The header line is 38 bytes. Each note line then costs about 31 bytes, which holds while the row number is one or two digits; at 100 rows the row number itself takes three characters and the average rises to 33.4. So a few hundred notes is a few tens of kilobytes - small enough to paste anywhere.

How the note names are built

The naming is done by one small function, and it makes two decisions worth knowing about:

var NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
function noteName(m) {
  if (m < 0 || m > 127) { return '?'; }
  return NOTE_NAMES[m % 12] + (Math.floor(m / 12) - 1);
}

First, only sharps exist in this table. There is no flat name anywhere in it, so a black key above C is always C# and never Db. If your DAW shows you flats, the note is the same note - it is only spelled differently.

Second, the octave numbering puts MIDI 60 at C4. That is the convention where middle C is C4. Plenty of software calls the same note C3, and some call it C5, so a note name copied out of this page can look like it is an octave off when you compare it with a DAW. The number is unambiguous even when the name is not. Here is the mapping at the boundaries, generated by running this page's own function over the whole MIDI range:

MIDI numberName this page writes
0C-1
12C0
24C1
36C2
48C3
60C4
69A4
72C5
127G9
-1 or 128?

The last row is the interesting one. If a number falls outside 0 to 127 the function returns a single question mark instead of a name, and that is what you see in the list when Transpose pushes a note off the end of the range. In the same situation the .mid file still gets a number - it clamps to 0 or 127 - so the list and the file describe that note differently on purpose. That behaviour, and how Transpose interacts with the rest of the controls, is measured in what each control actually changes.

To check that the names really are just a rendering of the same numbers, I compared the Note column against the note numbers inside the .mid the page wrote in the same run, using this page's own naming function as the translator. Every row matched - 35 rows across four files, from C4 at 60 up to C#5 at 73.

The seconds are not the ticks

This is the part that trips people up when they try to reconcile the text with the file. The list shows seconds. The .mid file stores ticks. The conversion the page uses when it writes the file is:

ticksPerSec = (bpm / 60) * 480
tick        = round(seconds * ticksPerSec)

So the same seconds map to different ticks depending on the Tempo box. I verified it against the actual files rather than trusting the formula: for every note start in four runs I read the tick out of the .mid with two independent parsers and compared it with the arithmetic. All 47 starts matched.

Start (s) from the listBPM 60, tick in fileBPM 120, tick in fileBPM 300, tick in file
0.000000
0.9924769522,381
7.9843,8327,66519,162
10.9925,27610,55226,381

One clean consequence: the Tempo box never changes the copied text. I converted the same 12-note file at BPM 60, 120 and 300 and the three captured note lists were not merely equal in content - they had the same MD5 hash and the same 429 bytes. Tempo only ever reached the .mid file. What the file itself contains, byte by byte, is broken down in what MIDI file you actually get.

The list and the .mid can disagree

There is one case where the text and the file genuinely describe different music, and it is worth knowing before you rely on the list as a record of what you downloaded.

The list is rebuilt from scratch every time you click Copy, and it reads the Transpose box at that moment. The .mid file is only built during conversion. So if you convert, then change Transpose, then copy and download without converting again, you get two different answers:

StepCopied note listDownloaded .mid
Converted with Transpose 0C4 E4 G460 bytes, notes 60 / 64 / 67
Transpose changed to +12, not converted againC5 E5 G560 bytes, byte-identical, still notes 60 / 64 / 67

The two files had the same MD5 hash. The copied text named notes that the file does not contain. So if the list is your record of a conversion, copy it and download the file at the same moment, and do not touch a control in between - or press Convert again first. The same "nothing re-runs until you press Convert" behaviour is behind several other surprises, and the file-versus-text split is the same reason the piano roll does not move when you change Transpose either.

That split runs through the whole results panel, not just the list. The readouts and the piano roll are frozen at the conversion too, and Download .mid never reads a control at all, while the status line gets rewritten by all three buttons - so the page can end on a green "saved" message for settings it is no longer using. Measured control by control in which parts of the page read the settings, and when.

Getting it into a spreadsheet

The two-space separator is friendly to your eyes and slightly awkward for a parser. If you split each line on a single space character, every row breaks into 11 fields, five of them empty:

'1  0.000  0.976  0.976  49  C#4'.split(' ')
  -> ['1', '', '0.000', '', '0.976', '', '0.976', '', '49', '', 'C#4']

Split on runs of whitespace instead - a regular expression like \s+, or the "treat consecutive delimiters as one" option in a text-to-columns dialog - and you get exactly 6 fields, which is what the columns are:

'1  0.000  0.976  0.976  49  C#4'.split(/\s+/)
  -> ['1', '0.000', '0.976', '0.976', '49', 'C#4']

Two more things to watch for. The first column of the header is a literal #, so a tool that treats # as a comment marker will drop the header - or worse, a sharp note name. And the time columns carry a unit in their header (Start(s)) but the values themselves are plain numbers, so a spreadsheet will read them as numbers without any cleanup.

When Copy does nothing

There is one case where the button is completely silent: if the conversion found no notes at all, the handler returns before it builds any text, so nothing is written and no message appears. An empty result is not the same as a failure - the .mid download still writes a 33-byte file with no notes in it. The full set of failure states is measured message by message in what the page leaves behind when a conversion does not work.

The other case is the clipboard itself. If the browser refuses the write, the page says so in plain words:

Copy failed - your browser blocked clipboard access.

I saw both outcomes while measuring. Clicking the button with a real mouse event resolved normally and the status line read Copied 12 note(s) as text. Triggering the same button from a script instead - which is not a user gesture - was refused by the browser and produced the blocked message above. So the failure is about how the click reached the page, not about your audio.

What the list is good for

Honest boundaries