These utility routines look at the headers of AIFF, OGG Vorbis or MIDI files to find the durations, and verify that they are what they purport to be.
§1. AIFF files. The code in this section was once again originated by Toby Nelson. To explicate the following, see the specifications for AIFF and OGG headers. Durations are measured in centiseconds.
int SoundFiles::get_AIFF_duration(FILE *pFile, unsigned int *pDuration, unsigned int *pBitsPerSecond, unsigned int *pChannels, unsigned int *pSampleRate) { unsigned int sig; unsigned int chunkID; unsigned int chunkLength; unsigned int numSampleFrames; unsigned int sampleSize; if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; if (sig != 0x464F524D) return FALSE; "FORM" indicating an IFF file if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; if (sig != 0x41494646) return FALSE; "AIFF" indicating an AIFF file Read chunks, skipping over those we are not interested in while (TRUE) { if (!BinaryFiles::read_int32(pFile, &chunkID)) return FALSE; if (!BinaryFiles::read_int32(pFile, &chunkLength)) return FALSE; if (chunkID == 0x434F4D4D) { "COMM" indicates common AIFF data if (chunkLength < 18) return FALSE; Check we have enough data to read if (!BinaryFiles::read_int16(pFile, pChannels)) return FALSE; if (!BinaryFiles::read_int32(pFile, &numSampleFrames)) return FALSE; if (!BinaryFiles::read_int16(pFile, &sampleSize)) return FALSE; if (!BinaryFiles::read_float80(pFile, pSampleRate)) return FALSE; if (*pSampleRate == 0) return FALSE; Sanity check to avoid a divide by zero Result is in centiseconds *pDuration = (unsigned int) (((unsigned long long) numSampleFrames * 100) / *pSampleRate); *pBitsPerSecond = *pSampleRate * *pChannels * sampleSize; break; } else { Skip unwanted chunk if (fseek(pFile, (long) chunkLength, SEEK_CUR) != 0) return FALSE; } } return TRUE; }
int SoundFiles::get_OggVorbis_duration(FILE *pFile, unsigned int *pDuration, unsigned int *pBitsPerSecond, unsigned int *pChannels, unsigned int *pSampleRate) { unsigned int sig; unsigned int version; unsigned int numSegments; unsigned int packetType; unsigned int vorbisSig1; unsigned int vorbisSig2; unsigned int seekPos; unsigned int fileLength, bytesToRead, lastSig, index; unsigned long long granulePosition; unsigned char buffer[256]; if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; if (sig != 0x4F676753) return FALSE; "OggS" indicating an OGG file Check OGG version is zero if (!BinaryFiles::read_int8(pFile, &version)) return FALSE; if (version != 0) return FALSE; Skip header type, granule position, serial number, page sequence and CRC if (fseek(pFile, 21, SEEK_CUR) != 0) return FALSE; Read number of page segments if (!BinaryFiles::read_int8(pFile, &numSegments)) return FALSE; Skip segment table if (fseek(pFile, (long) numSegments, SEEK_CUR) != 0) return FALSE; Vorbis Identification header if (!BinaryFiles::read_int8(pFile, &packetType)) return FALSE; if (packetType != 1) return FALSE; if (!BinaryFiles::read_int32(pFile, &vorbisSig1)) return FALSE; if (vorbisSig1 != 0x766F7262) return FALSE; "VORB" if (!BinaryFiles::read_int16(pFile, &vorbisSig2)) return FALSE; if (vorbisSig2 != 0x6973) return FALSE; "IS" Check Vorbis version is zero if (!BinaryFiles::read_int32(pFile, &version)) return FALSE; if (version != 0) return FALSE; Read number of channels if (!BinaryFiles::read_int8(pFile, pChannels)) return FALSE; Read sample rate if (!BinaryFiles::read_int32(pFile, pSampleRate)) return FALSE; BinaryFiles::swap_bytes32(pSampleRate); Ogg Vorbis uses LSB first Skip bitrate maximum if (fseek(pFile, 4, SEEK_CUR) != 0) return FALSE; Read Nominal Bitrate if (!BinaryFiles::read_int32(pFile, pBitsPerSecond)) return FALSE; BinaryFiles::swap_bytes32(pBitsPerSecond); Ogg Vorbis uses LSB first Encoders can be unhelpful and give no bitrate in the header if (pBitsPerSecond == 0) return FALSE; Search for the final Ogg page (near the end of the file) to read duration, i.e., read the last 4K of the file and look for the final "OggS" sig if (fseek(pFile, 0, SEEK_END) != 0) return FALSE; fileLength = (unsigned int) ftell(pFile); if (fileLength < 4096) seekPos = 0; else seekPos = fileLength - 4096; lastSig = 0xFFFFFFFF; while (seekPos < fileLength) { if (fseek(pFile, (long) seekPos, SEEK_SET) != 0) return FALSE; bytesToRead = fileLength - seekPos; if (bytesToRead > 256) bytesToRead = 256; if (fread(buffer, 1, bytesToRead, pFile) != bytesToRead) return FALSE; for(index = 0; index < bytesToRead; index++) { if ((buffer[index] == 0x4F) && (buffer[index + 1] == 0x67) && (buffer[index + 2] == 0x67) && (buffer[index + 3] == 0x53)) { lastSig = seekPos + index; } } Next place to read from is 256 bytes further on, but to catch sigs that span between these blocks, read the last four bytes again seekPos += 256 - 4; } if (lastSig == 0xFFFFFFFF) return FALSE; if (fseek(pFile, (long) lastSig, SEEK_SET) != 0) return FALSE; if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; if (sig != 0x4F676753) return FALSE; "OggS" indicating an OGG file Check OGG version is zero if (!BinaryFiles::read_int8(pFile, &version)) return FALSE; if (version != 0) return FALSE; Skip header Type if (fseek(pFile, 1, SEEK_CUR) != 0) return FALSE; if (!BinaryFiles::read_int64(pFile, &granulePosition)) return FALSE; BinaryFiles::swap_bytes64(&granulePosition); *pDuration = (unsigned int) ((granulePosition * 100) / (unsigned long long) *pSampleRate); return TRUE; }
§3. MIDI files. At one time it was proposed that Inform 7 should allow a third sound file format: MIDI. This provoked considerable debate in July 2007 and enough doubts were raised that the implementation below was never in fact officially used. It is preserved here in case we ever revive the issue.
Inform is not really able to decide this for itself, in any case, since it can only usefully provide sound files which the virtual machines it compiles for will allow. At present, the Glulx virtual machine does not officially support MIDI, which makes the question moot.
int SoundFiles::get_MIDI_information(FILE *pFile, unsigned int *pType, unsigned int *pNumTracks) { unsigned int sig; unsigned int length; unsigned int pulses; unsigned int frames_per_second; unsigned int subframes_per_frame; unsigned int clocks_per_second; unsigned int start_of_chunk_data; unsigned int status; unsigned int clocks; unsigned int sysex_length; unsigned int non_midi_event_length; unsigned int start_of_non_midi_data; unsigned int non_midi_event; if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; "RIFF" indicating a RIFF file if (sig == 0x52494646) { Skip the filesize and typeID if (fseek(pFile, 8, SEEK_CUR) != 0) return FALSE; now read the real MIDI sig if (!BinaryFiles::read_int32(pFile, &sig)) return FALSE; } "MThd" indicating a MIDI file if (sig != 0x4D546864) return FALSE; Read length of chunk if (!BinaryFiles::read_int32(pFile, &length)) return FALSE; Make sure we have enough data to read if (length < 6) return FALSE; Read the MIDI type: 0,1 or 2 0 means one track containing up to 16 channels to make a single tune 1 means one or more tracks, commonly each with a single channel, making up a single tune 2 means one or more tracks, where each is a separate tune in it's own right if (!BinaryFiles::read_int16(pFile, pType)) return FALSE; Read the number of tracks if (!BinaryFiles::read_int16(pFile, pNumTracks)) return FALSE; Read "Pulses Per Quarter Note" (PPQN) if (!BinaryFiles::read_int16(pFile, &pulses)) return FALSE; if top bit set, then number of subframes per second can be deduced if (pulses >= 0x8000) { First byte is a negative number for the frames per second Second byte is the number of subframes in each frame frames_per_second = (256 - (pulses & 0xff)); subframes_per_frame = (pulses >> 8); clocks_per_second = frames_per_second * subframes_per_frame; Number of pulses per quarter note unknown pulses = 0; } else { unknown values frames_per_second = 0; subframes_per_frame = 0; clocks_per_second = 0; } Skip any remaining bytes in the MThd chunk if (fseek(pFile, (long) (length - 6), SEEK_CUR) != 0) return FALSE; Keep reading chunks, looking for "MTrk" do { Read chunk signature and length if (!BinaryFiles::read_int32(pFile, &sig)) { if (feof(pFile)) return TRUE; return FALSE; } if (!BinaryFiles::read_int32(pFile, &length)) return FALSE; start_of_chunk_data = (unsigned int) ftell(pFile); if (sig == 0x4D54726B) { "MTrk" Read each event, looking for information before the real tune starts, e.g., tempo do { Read the number of clocks since the previous event if (!BinaryFiles::read_variable_length_integer(pFile, &clocks)) return FALSE; We bail out when the track starts if (clocks > 0) break; Read the MIDI Status byte if (!BinaryFiles::read_int8(pFile, &status)) return FALSE; Start or continuation of system exclusive data if ((status == 0xF0) || (status == 0xF7)) { Read length of system exclusive event data if (!BinaryFiles::read_variable_length_integer(pFile, &sysex_length)) return FALSE; Skip sysex event if (fseek(pFile, (long) sysex_length, SEEK_CUR) != 0) return FALSE; } else if (status == 0xFF) { Non-MIDI event Read the Non-MIDI event type and length if (!BinaryFiles::read_int8(pFile, &non_midi_event)) return FALSE; if (!BinaryFiles::read_variable_length_integer(pFile, &non_midi_event_length)) return FALSE; start_of_non_midi_data = (unsigned int) ftell(pFile); switch(non_midi_event) { case 0x01: Comment text case 0x02: Copyright text case 0x03: Track name case 0x04: { Instrument name char text[257]; if (!BinaryFiles::read_string(pFile, text, non_midi_event_length)) return FALSE; break; } case 0x51: Tempo change case 0x58: Time signature case 0x59: Key signature break; } Skip non-midi event if (fseek(pFile, (long) (start_of_non_midi_data + non_midi_event_length), SEEK_SET) != 0) return FALSE; } else { Real MIDI data found: we've read all we can so bail out at this point break; } } while (TRUE); } Seek to start of next chunk if (fseek(pFile, (long) (start_of_chunk_data + length), SEEK_SET) != 0) return FALSE; Reached end of file if (feof(pFile)) return TRUE; Did we try to seek beyond the end of the file? unsigned int position_in_file = (unsigned int) ftell(pFile); if (position_in_file < (start_of_chunk_data + length)) return TRUE; } while (TRUE); return TRUE; }