inweb-bootstrap/foundation-module/Chapter_6/Sound_Durations.nw

336 lines
13 KiB
Text

[SoundFiles::] Sound Durations.
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.
@ \section{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;
}
@ \section{OGG Vorbis files.}
<<*>>=
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;
}
@ \section{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;
LOG("frames_per_second = %d\n", frames_per_second);
LOG("subframes_per_frame = %d\n", subframes_per_frame);
LOG("clocks_per_second = %d\n", clocks_per_second);
/* Number of pulses per quarter note unknown */
pulses = 0;
} else {
/* unknown values */
frames_per_second = 0;
subframes_per_frame = 0;
clocks_per_second = 0;
LOG("pulses per quarter note = %d\n", pulses);
}
/* 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"]] */
LOG("track starts\n");
/* 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;
LOG("%d: %s\n", non_midi_event, text);
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;
}