/* * This program is free software. It comes without any warranty, to * the extent permitted by applicable law. You can redistribute it * and/or modify it under the terms of the Do What The Fuck You Want * To Public License, Version 2, as published by Sam Hocevar. See * http://sam.zoy.org/wtfpl/COPYING for more details. */ /* ChangeLog: * 1 - Initial program * 2 - Changed getAVAudioData to not always grab another packet before decoding * to prevent buffering more compressed data than needed */ #include #include #include #include #include #include #include /* Opaque handles to files and streams. The main app doesn't need to concern * itself with the internals */ typedef struct MyFile *FilePtr; typedef struct MyStream *StreamPtr; /**** Helper function ****/ /* If in C++, you must manually extern "C" {} around these */ #include #include struct MyStream { AVCodecContext *CodecCtx; int StreamIdx; char *Data; size_t DataSize; size_t DataSizeMax; char *DecodedData; size_t DecodedDataSize; FilePtr parent; }; struct MyFile { AVFormatContext *FmtCtx; StreamPtr *Streams; size_t StreamsSize; }; /* This opens a file with ffmpeg and sets up the streams' information */ FilePtr openAVFile(const char *fname) { static int done = 0; FilePtr file; /* We need to make sure ffmpeg is initialized. Optionally silence warning * output from the lib */ if(!done) {av_register_all(); av_log_set_level(AV_LOG_ERROR);} done = 1; file = calloc(1, sizeof(*file)); if(file && av_open_input_file(&file->FmtCtx, fname, NULL, 0, NULL) == 0) { /* After opening, we must search for the stream information because not * all formats will have it in stream headers (eg. system MPEG streams) */ if(av_find_stream_info(file->FmtCtx) >= 0) return file; av_close_input_file(file->FmtCtx); } free(file); return NULL; } /* This closes/frees an opened file and any of its streams. Pretty self- * explanitory... */ void closeAVFile(FilePtr file) { size_t i; if(!file) return; for(i = 0;i < file->StreamsSize;i++) { avcodec_close(file->Streams[i]->CodecCtx); free(file->Streams[i]->Data); free(file->Streams[i]->DecodedData); free(file->Streams[i]); } free(file->Streams); av_close_input_file(file->FmtCtx); free(file); } /* This retrieves a handle for the given audio stream number (generally 0, but * some files can have multiple audio streams in one file) */ StreamPtr getAVAudioStream(FilePtr file, int streamnum) { unsigned int i; if(!file) return NULL; for(i = 0;i < file->FmtCtx->nb_streams;i++) { if(file->FmtCtx->streams[i]->codec->codec_type != CODEC_TYPE_AUDIO) continue; if(streamnum == 0) { StreamPtr stream; AVCodec *codec; void *temp; size_t j; /* Found the requested stream. Check if a handle to this stream * already exists and return it if it does */ for(j = 0;j < file->StreamsSize;j++) { if(file->Streams[j]->StreamIdx == (int)i) return file->Streams[j]; } /* Doesn't yet exist. Now allocate a new stream object and fill in * its info */ stream = calloc(1, sizeof(*stream)); if(!stream) return NULL; stream->parent = file; stream->CodecCtx = file->FmtCtx->streams[i]->codec; stream->StreamIdx = i; /* Try to find the codec for the given codec ID, and open it */ codec = avcodec_find_decoder(stream->CodecCtx->codec_id); if(!codec || avcodec_open(stream->CodecCtx, codec) < 0) { free(stream); return NULL; } /* Allocate space for the decoded data to be stored in before it * gets passed to the app */ stream->DecodedData = malloc(AVCODEC_MAX_AUDIO_FRAME_SIZE); if(!stream->DecodedData) { avcodec_close(stream->CodecCtx); free(stream); return NULL; } /* Append the new stream object to the stream list. The original * pointer will remain valid if realloc fails, so we need to use * another pointer to watch for errors and not leak memory */ temp = realloc(file->Streams, (file->StreamsSize+1) * sizeof(*file->Streams)); if(!temp) { avcodec_close(stream->CodecCtx); free(stream->DecodedData); free(stream); return NULL; } file->Streams = temp; file->Streams[file->StreamsSize++] = stream; return stream; } streamnum--; } return NULL; } /* Returns information about the given audio stream. Currently, ffmpeg always * decodes audio (even 8-bit PCM) to 16-bit PCM. Returns 0 on success. */ int getAVAudioInfo(StreamPtr stream, int *rate, int *channels, int *bits) { if(!stream || stream->CodecCtx->codec_type != CODEC_TYPE_AUDIO) return 1; if(rate) *rate = stream->CodecCtx->sample_rate; if(channels) *channels = stream->CodecCtx->channels; if(bits) *bits = 16; return 0; } /* Used by getAV*Data to search for more compressed data, and buffer it in the * correct stream. It won't buffer data for streams that the app doesn't have a * handle for. */ static void getNextPacket(FilePtr file, int streamidx) { AVPacket packet; while(av_read_frame(file->FmtCtx, &packet) >= 0) { StreamPtr *iter = file->Streams; size_t i; /* Check each stream the user has a handle for, looking for the one * this packet belongs to */ for(i = 0;i < file->StreamsSize;i++,iter++) { if((*iter)->StreamIdx == packet.stream_index) { size_t idx = (*iter)->DataSize; /* Found the stream. Grow the input data buffer as needed to * hold the new packet's data. Additionally, some ffmpeg codecs * need some padding so they don't overread the allocated * buffer */ if(idx+packet.size > (*iter)->DataSizeMax) { void *temp = realloc((*iter)->Data, idx+packet.size + FF_INPUT_BUFFER_PADDING_SIZE); if(!temp) break; (*iter)->Data = temp; (*iter)->DataSizeMax = idx+packet.size; } /* Copy the packet and free it */ memcpy(&(*iter)->Data[idx], packet.data, packet.size); (*iter)->DataSize += packet.size; /* Return if this stream is what we needed a packet for */ if(streamidx == (*iter)->StreamIdx) { av_free_packet(&packet); return; } break; } } /* Free the packet and look for another */ av_free_packet(&packet); } } /* The "meat" function. Decodes audio and writes, at most, length bytes into * the provided data buffer. Will only return less for end-of-stream or error * conditions. Returns the number of bytes written. */ int getAVAudioData(StreamPtr stream, void *data, int length) { int dec = 0; if(!stream || stream->CodecCtx->codec_type != CODEC_TYPE_AUDIO) return 0; while(dec < length) { /* If there's any pending decoded data, deal with it first */ if(stream->DecodedDataSize > 0) { /* Get the amount of bytes remaining to be written, and clamp to * the amount of decoded data we have */ size_t rem = length-dec; if(rem > stream->DecodedDataSize) rem = stream->DecodedDataSize; /* Copy the data to the app's buffer and increment */ memcpy(data, stream->DecodedData, rem); data = (char*)data + rem; dec += rem; /* If there's any decoded data left, move it to the front of the * buffer for next time */ if(rem < stream->DecodedDataSize) memmove(stream->DecodedData, &stream->DecodedData[rem], stream->DecodedDataSize - rem); stream->DecodedDataSize -= rem; } /* Check if we need to get more decoded data */ if(stream->DecodedDataSize == 0) { size_t insize; int size; int len; insize = stream->DataSize; if(insize == 0) { getNextPacket(stream->parent, stream->StreamIdx); /* If there's no more input data, break and return what we have */ if(insize == stream->DataSize) break; insize = stream->DataSize; memset(&stream->Data[insize], 0, FF_INPUT_BUFFER_PADDING_SIZE); } /* Clear the input padding bits */ /* Decode some data, and check for errors */ size = AVCODEC_MAX_AUDIO_FRAME_SIZE; while((len=avcodec_decode_audio2(stream->CodecCtx, (int16_t*)stream->DecodedData, &size, (uint8_t*)stream->Data, insize)) == 0) { if(size > 0) break; getNextPacket(stream->parent, stream->StreamIdx); if(insize == stream->DataSize) break; insize = stream->DataSize; memset(&stream->Data[insize], 0, FF_INPUT_BUFFER_PADDING_SIZE); } if(len < 0) break; if(len > 0) { /* If any input data is left, move it to the start of the * buffer, and decrease the buffer size */ size_t rem = insize-len; if(rem) memmove(stream->Data, &stream->Data[len], rem); stream->DataSize = rem; } /* Set the output buffer size */ stream->DecodedDataSize = size; } } /* Return the number of bytes we were able to get */ return dec; } /**** The main app ****/ /* Create a simple signal handler for SIGINT so ctrl-c cleanly exits. */ static volatile int quitnow = 0; static void handle_sigint(int signum) { (void)signum; quitnow = 1; } /* Define the number of buffers and bytes-per-buffer to use. 3 buffers is a * good amount (one playing, one ready to play, another being filled). The * buffer size must be a multiple of the frame size (of which we can have 1, 2, * 4, 6, 8, and 12-byte frame sizes). 16KB to 32KB is a good size per buffer. * Adding another buffer or two to increase the overall length wouldn't be a * bad idea of you wanted more skip protection */ #define NUM_BUFFERS 3 #define BUFFER_SIZE 19200 int main(int argc, char **argv) { /* Here are the buffers and source to play out through OpenAL with */ ALuint buffers[NUM_BUFFERS]; ALuint source; ALint state; /* This will hold the state of the source */ ALbyte *data; /* A temp data buffer for getAVAudioData to write to and pass * to OpenAL with */ int count; /* The number of bytes read from getAVAudioData */ int i; /* An iterator for looping over the filenames */ /* Print out usage if no file was specified */ if(argc < 2) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } /* Set up our signal handler to run on SIGINT (ctrl-c) */ if(signal(SIGINT, handle_sigint) == SIG_ERR) { fprintf(stderr, "Unable to set handler for SIGINT!\n"); return 1; } data = malloc(BUFFER_SIZE); if(!data) { fprintf(stderr, "Out of memory allocating temp buffer!\n"); return 1; } /* Initialize ALUT with default settings */ if(alutInit(NULL, NULL) == AL_FALSE) { free(data); fprintf(stderr, "Could not initialize ALUT (%s)!\n", alutGetErrorString(alutGetError())); return 1; } /* Generate the buffers and source */ alGenBuffers(NUM_BUFFERS, buffers); if(alGetError() != AL_NO_ERROR) { alutExit(); free(data); fprintf(stderr, "Could not create buffers...\n"); return 1; } alGenSources(1, &source); if(alGetError() != AL_NO_ERROR) { alDeleteBuffers(NUM_BUFFERS, buffers); alutExit(); free(data); fprintf(stderr, "Could not create source...\n"); return 1; } /* Set parameters so mono sources won't distance attenuate */ alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); alSourcei(source, AL_ROLLOFF_FACTOR, 0); if(alGetError() != AL_NO_ERROR) { alDeleteSources(1, &source); alDeleteBuffers(NUM_BUFFERS, buffers); alutExit(); free(data); fprintf(stderr, "Could not set source parameters...\n"); return 1; } /* Play each file listed on the command line */ for(i = 1;i < argc && !quitnow;i++) { static ALenum old_format; static int old_rate; /* Handles for the audio stream */ FilePtr file; StreamPtr stream; /* The format of the output stream */ ALenum format = 0; int channels; int bits; int rate; /* The base time to use when determining the playback time from the * source. */ int basetime = 0; /* Open the file and get the first stream from it */ file = openAVFile(argv[i]); stream = getAVAudioStream(file, 0); if(!stream) { closeAVFile(file); fprintf(stderr, "Could not open audio in %s\n", argv[i]); continue; } /* Get the stream format, and figure out the OpenAL format. We use the * AL_EXT_MCFORMATS extension to provide output of 4 and 5.1 audio * streams */ if(getAVAudioInfo(stream, &rate, &channels, &bits) != 0) { closeAVFile(file); fprintf(stderr, "Error getting audio info for %s\n", argv[i]); continue; } if(bits == 8) { if(channels == 1) format = AL_FORMAT_MONO8; if(channels == 2) format = AL_FORMAT_STEREO8; if(alIsExtensionPresent("AL_EXT_MCFORMATS")) { if(channels == 4) format = alGetEnumValue("AL_FORMAT_QUAD8"); if(channels == 6) format = alGetEnumValue("AL_FORMAT_51CHN8"); } } if(bits == 16) { if(channels == 1) format = AL_FORMAT_MONO16; if(channels == 2) format = AL_FORMAT_STEREO16; if(alIsExtensionPresent("AL_EXT_MCFORMATS")) { if(channels == 4) format = alGetEnumValue("AL_FORMAT_QUAD16"); if(channels == 6) format = alGetEnumValue("AL_FORMAT_51CHN16"); } } if(format == 0) { closeAVFile(file); fprintf(stderr, "Unhandled format (%d channels, %d bits) for %s", channels, bits, argv[i]); continue; } /* If the format of the last file matches the current one, we can skip * the initial load and let the processing loop take over (gap-less * playback!) */ count = 1; if(format != old_format || rate != old_rate) { int j; old_format = format; old_rate = rate; /* Wait for the last song to finish playing */ do { alutSleep(0.01); alGetSourcei(source, AL_SOURCE_STATE, &state); } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING); /* Rewind the source position and clear the buffer queue */ alSourceRewind(source); alSourcei(source, AL_BUFFER, 0); /* Fill and queue the buffers */ for(j = 0;j < NUM_BUFFERS;j++) { /* Make sure we get some data to give to the buffer */ count = getAVAudioData(stream, data, BUFFER_SIZE); if(count <= 0) break; /* Buffer the data with OpenAL and queue the buffer onto the * source */ alBufferData(buffers[j], format, data, count, rate); alSourceQueueBuffers(source, 1, &buffers[j]); } if(alGetError() != AL_NO_ERROR) { closeAVFile(file); fprintf(stderr, "Error buffering initial data...\n"); continue; } /* Now start playback! */ alSourcePlay(source); if(alGetError() != AL_NO_ERROR) { closeAVFile(file); fprintf(stderr, "Error starting playback...\n"); continue; } } else { /* When skipping the initial load of a file (because the previous * one is using the same exact format), set the base time to the * negative of the queued buffers. This is so the timing will be * from the beginning of this file, which won't start playing until * the next buffer to get queued does */ basetime = -NUM_BUFFERS; } fprintf(stderr, "\rPlaying %s (%d-bit, %d channels, %dhz)\n", argv[i], bits, channels, rate); while(count > 0 && !quitnow) { /* Check if any buffers on the source are finished playing */ ALint processed = 0; alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); if(processed == 0) { /* All buffers are full. Check if the source is still playing. * If not, restart it, otherwise, print the time and rest */ alGetSourcei(source, AL_SOURCE_STATE, &state); if(alGetError() != AL_NO_ERROR) { fprintf(stderr, "Error checking source state...\n"); break; } if(state != AL_PLAYING) { alSourcePlay(source); if(alGetError() != AL_NO_ERROR) { closeAVFile(file); fprintf(stderr, "Error restarting playback...\n"); break; } } else { ALint offset; alGetSourcei(source, AL_SAMPLE_OFFSET, &offset); /* Add the base time to the offset. Each count of basetime * represents one buffer, which is BUFFER_SIZE in bytes */ offset += basetime * (BUFFER_SIZE/channels*8/bits); fprintf(stderr, "\rTime: %d:%05.02f", offset/rate/60, (offset%(rate*60))/(float)rate); alutSleep(0.01); } continue; } /* Read the next chunk of data and refill the oldest buffer */ count = getAVAudioData(stream, data, BUFFER_SIZE); if(count > 0) { ALuint buf = 0; alSourceUnqueueBuffers(source, 1, &buf); if(buf != 0) { alBufferData(buf, format, data, count, rate); alSourceQueueBuffers(source, 1, &buf); /* For each successfully unqueued buffer, increment the * base time. The retrieved sample offset for timing is * relative to the start of the buffer queue, so for every * buffer that gets unqueued we need to increment the base * time to keep the reported time accurate and not fall * backwards */ basetime++; } if(alGetError() != AL_NO_ERROR) { fprintf(stderr, "Error buffering data...\n"); break; } } } /* All done with this file. Close it and go to the next */ closeAVFile(file); } fprintf(stderr, "\nDone.\n"); /* All data has been streamed in. Wait until the source stops playing it */ do { alutSleep(0.01); alGetSourcei(source, AL_SOURCE_STATE, &state); } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING); /* All files done. Delete the source and buffers, and close OpenAL */ alDeleteSources(1, &source); alDeleteBuffers(NUM_BUFFERS, buffers); alutExit(); free(data); return 0; }