/// <summary> /// Adjusts the volume level of a WAV file, saving the adjusted file as a separate file. /// </summary> /// <param name="pSrcFilename">The name of the WAV file to adjust</param> /// <param name="pDestFilename">The name to use for the volume-adjusted WAV file</param> /// <param name="pMultiplier">The value by which to multiply the audio samples</param> public static void AdjustVolume_Copy(String pSrcFilename, String pDestFilename, double pMultiplier) { // If an empty source or destination file were passed in, then throw an exception. if (pSrcFilename == "") throw new WAVFileReadException("Blank filename specified.", "WAVFile.AdjustVolume_Copy()"); if (pDestFilename == "") throw new WAVFileWriteException("Blank filename specified.", "WAVFile.AdjustVolume_Copy()"); // Open the srouce file WAVFile srcFile = new WAVFile(); String retval = srcFile.Open(pSrcFilename, WAVFileMode.READ); if (retval == "") { // Check to make sure the input file has a supported number of bits/sample and sample rate. If // not, then throw an exception. if (!SupportedBitsPerSample(srcFile.BitsPerSample)) { WAVFileBitsPerSampleException exc = new WAVFileBitsPerSampleException(pSrcFilename + " has unsupported bits/sample (" + srcFile.BitsPerSample.ToString() + ")", "WAVFile.AdjustVolume_Copy()", srcFile.BitsPerSample); srcFile.Close(); throw exc; } // Open the destination file and start copying the adjusted audio data to it. WAVFile destFile = new WAVFile(); destFile.Create(pDestFilename, srcFile.IsStereo, srcFile.SampleRateHz, srcFile.BitsPerSample); if (srcFile.BitsPerSample == 8) { byte sample = 0; for (int i = 0; i < srcFile.NumSamples; ++i) { // Note: Only multiply the sample if pMultiplier is not 1.0 (if the multiplier is // 1.0, then it would be good to avoid any binary roundoff error). sample = srcFile.GetNextSample_8bit(); if (pMultiplier != 1.0) sample = (byte)((double)sample * pMultiplier); destFile.AddSample_8bit(sample); } } else if (srcFile.BitsPerSample == 16) { short sample = 0; for (int i = 0; i < srcFile.NumSamples; ++i) { // Note: Only multiply the sample if pMultiplier is not 1.0 (if the multiplier is // 1.0, then it would be good to avoid any binary roundoff error). sample = srcFile.GetNextSample_16bit(); if (pMultiplier != 1.0) sample = (short)((double)sample * pMultiplier); destFile.AddSample_16bit(sample); } } srcFile.Close(); destFile.Close(); } else throw new WAVFileReadException(retval, "WAVFile.AdjustVolume_Copy()"); }
/// <summary> /// Converts a WAV file's bits/sample and number of channels to a separate WAV file. /// </summary> /// <param name="pSrcFilename">The name of the file to convert</param> /// <param name="pDestFilename">The destination file name</param> /// <param name="pBitsPerSample">The destination's number of bits/sample</param> /// <param name="pStereo">Whether or not the destination should be stereo</param> /// <param name="pVolumeMultiplier">A multiplier that can be used to adjust the volume of the output audio file</param> public static void CopyAndConvert(String pSrcFilename, String pDestFilename, short pBitsPerSample, bool pStereo, double pVolumeMultiplier) { WAVFile srcFile = new WAVFile(); String retval = srcFile.Open(pSrcFilename, WAVFileMode.READ); if (retval != "") throw new WAVFileException(retval, "WAVFile.Convert_Copy()"); WAVFile destFile = new WAVFile(); destFile.Create(pDestFilename, pStereo, srcFile.SampleRateHz, pBitsPerSample); if ((srcFile.BitsPerSample == 8) && (pBitsPerSample == 8)) { byte sample = 0; if (srcFile.IsStereo && !pStereo) { // 8-bit to 8-bit, stereo to mono: Average each 2 samples while (srcFile.NumSamplesRemaining > 0) { sample = (byte)((short)((short)srcFile.GetNextSample_8bit() + (short)srcFile.GetNextSample_8bit()) / 2); if (pVolumeMultiplier != 1.0) sample = (byte)((double)sample * pVolumeMultiplier); destFile.AddSample_8bit(sample); } } else if ((srcFile.IsStereo && pStereo) || (!srcFile.IsStereo && !pStereo)) { // 8-bit to 8-bit, stereo to stereo or mono to mono while (srcFile.NumSamplesRemaining > 0) { sample = srcFile.GetNextSample_8bit(); if (pVolumeMultiplier != 1.0) sample = (byte)((double)sample * pVolumeMultiplier); destFile.AddSample_8bit(sample); } } else if (!srcFile.IsStereo && pStereo) { // 8-bit to 8-bit, mono to stereo: Write each sample twice while (srcFile.NumSamplesRemaining > 0) { sample = srcFile.GetNextSample_8bit(); if (pVolumeMultiplier != 1.0) sample = (byte)((double)sample * pVolumeMultiplier); destFile.AddSample_8bit(sample); destFile.AddSample_8bit(sample); } } } else if ((srcFile.BitsPerSample == 8) && (pBitsPerSample == 16)) { short sample = 0; if (srcFile.IsStereo && !pStereo) { // 8-bit to 16 bit, stereo to mono: Average each 2 samples while (srcFile.NumSamplesRemaining > 0) { sample = (short)((int)((int)srcFile.GetNextSampleAs16Bit() + (int)srcFile.GetNextSampleAs16Bit()) / 2); if (pVolumeMultiplier != 1.0) sample = (short)((double)sample * pVolumeMultiplier); destFile.AddSample_16bit(sample); } } else if ((srcFile.IsStereo && pStereo) || (!srcFile.IsStereo && !pStereo)) { // 8-bit to 16 bit, stereo to stereo or mono to mono while (srcFile.NumSamplesRemaining > 0) { sample = srcFile.GetNextSampleAs16Bit(); if (pVolumeMultiplier != 1.0) sample = (short)((double)sample * pVolumeMultiplier); destFile.AddSample_16bit(sample); } } else if (!srcFile.IsStereo && pStereo) { // 8-bit to 16 bit, mono to stereo: Write each sample twice while (srcFile.NumSamplesRemaining > 0) { sample = srcFile.GetNextSampleAs16Bit(); if (pVolumeMultiplier != 1.0) sample = (short)((double)sample * pVolumeMultiplier); destFile.AddSample_16bit(sample); destFile.AddSample_16bit(sample); } } } else if ((srcFile.BitsPerSample == 16) && (pBitsPerSample == 8)) { byte sample = 0; if (srcFile.IsStereo && !pStereo) { // 16-bit to 8-bit, stereo to mono: Average each 2 samples short sample_16bit = 0; while (srcFile.NumSamplesRemaining > 0) { sample_16bit = (short)((int)srcFile.GetNextSample_16bit() + (int)srcFile.GetNextSample_16bit() / 2); if (pVolumeMultiplier != 1.0) sample_16bit = (short)((double)sample_16bit * pVolumeMultiplier); sample = ScaleShortToByte(sample_16bit); destFile.AddSample_8bit(sample); } } else if ((srcFile.IsStereo && pStereo) || (!srcFile.IsStereo && !pStereo)) { // 16-bit to 8-bit, stereo to stereo or mono to mono while (srcFile.NumSamplesRemaining > 0) { sample = ScaleShortToByte(srcFile.GetNextSample_16bit()); if (pVolumeMultiplier != 1.0) sample = (byte)((double)sample * pVolumeMultiplier); destFile.AddSample_8bit(sample); } } else if (!srcFile.IsStereo && pStereo) { // 16-bit to 8-bit, mono to stereo: Write each sample twice while (srcFile.NumSamplesRemaining > 0) { sample = ScaleShortToByte(srcFile.GetNextSample_16bit()); if (pVolumeMultiplier != 1.0) sample = (byte)((double)sample * pVolumeMultiplier); destFile.AddSample_8bit(sample); destFile.AddSample_8bit(sample); } } } else if ((srcFile.BitsPerSample == 16) && (pBitsPerSample == 16)) { short sample = 0; if (srcFile.IsStereo && !pStereo) { // 16-bit to 16-bit, stereo to mono: Average each 2 samples while (srcFile.NumSamplesRemaining > 0) { sample = (short)((int)((int)srcFile.GetNextSample_16bit() + (int)srcFile.GetNextSample_16bit()) / 2); if (pVolumeMultiplier != 1.0) sample = (short)((double)sample * pVolumeMultiplier); destFile.AddSample_16bit(sample); } } else if ((srcFile.IsStereo && pStereo) || (!srcFile.IsStereo && !pStereo)) { // 16-bit to 16-bit, stereo to stereo or mono to mono while (srcFile.NumSamplesRemaining > 0) { sample = srcFile.GetNextSample_16bit(); if (pVolumeMultiplier != 1.0) sample = (short)((double)sample * pVolumeMultiplier); destFile.AddSample_16bit(sample); } } else if (!srcFile.IsStereo && pStereo) { // 16-bit to 16-bit, mono to stereo: Write each sample twice while (srcFile.NumSamplesRemaining > 0) { sample = srcFile.GetNextSample_16bit(); if (pVolumeMultiplier != 1.0) sample = (short)((double)sample * pVolumeMultiplier); destFile.AddSample_16bit(sample); destFile.AddSample_16bit(sample); } } } destFile.Close(); srcFile.Close(); }
/// <summary> /// Merges a set of WAV file sinto a single WAV file in such a way that the audio will be overlayed. /// This method will throw a WAVFileException upon error. /// </summary> /// <param name="pFileList">An array containing the audio filenames</param> /// <param name="pOutputFilename">The name of the file to contain the resulting audio</param> /// <param name="pTempDir">The full path to the temporary directory to use for the work. If this directory does not exist, it will be created and then deleted when this method no longer needs it.</param> public static void MergeAudioFiles(String[] pFileList, String pOutputFilename, String pTempDir) { // If pFileList is null or empty, then just return. if (pFileList == null) return; if (pFileList.GetLength(0) == 0) return; // Make sure all the audio files have the sample rate (we can merge 8-bit and 16-bit audio and // convert mono to stereo). If the sample rates don't match, then throw an exception. if (!SampleRatesEqual(pFileList)) throw new WAVFileAudioMergeException("The sample rates of the audio files differ.", "WAVFile.MergeAudioFiles()"); // Check the audio format. If the number of bits/sample or sample rate is not // supported, then throw an exception. WAVFormat firstFileAudioFormat = GetAudioFormat(pFileList[0]); if (!SupportedBitsPerSample(firstFileAudioFormat.BitsPerSample)) throw new WAVFileBitsPerSampleException("Unsupported number of bits per sample: " + firstFileAudioFormat.BitsPerSample.ToString(), "WAVFile.MergeAudioFiles()", firstFileAudioFormat.BitsPerSample); if (!SupportedSampleRate(firstFileAudioFormat.SampleRateHz)) throw new WAVFileSampleRateException("Unsupported sample rate: " + firstFileAudioFormat.SampleRateHz.ToString(), "WAVFile.MergeAudioFiles()", firstFileAudioFormat.SampleRateHz); // 2. Create the temporary directory if it doesn't exist already. This checks for the // existence of the temp directory and stores the result in tempDirExisted so that // later, if the temp directory did not exist, we can delete it. bool tempDirExisted = Directory.Exists(pTempDir); if (!tempDirExisted) { try { Directory.CreateDirectory(pTempDir); if (!Directory.Exists(pTempDir)) throw new WAVFileAudioMergeException("Unable to create temporary work directory (" + pTempDir + ")", "WAVFile.MergeAudioFiles()"); } catch (System.Exception exc) { throw new WAVFileAudioMergeException("Unable to create temporary work directory (" + pTempDir + "): " + exc.Message, "WAVFile.MergeAudioFiles()"); } } // 4. Find the highest sample value of all files, and calculate the sound // multiplier based on this (all files will be scaled down by this amount). int numTracks = pFileList.GetLength(0); double multiplier = 0.0; // The multiplier for scaling down the audio files short highestSampleValue = 0; // Will store the highest sample value (8-bit will be cast to short) // Determine the highest # of bits per sample in all the audio files. short highestBitsPerSample = HighestBitsPerSample(pFileList); bool outputStereo = (HighestNumChannels(pFileList) > 1); if (highestBitsPerSample == 8) { // Get the highest sample value of all of the WAV files byte highestSample = HighestSampleValue_8bit(pFileList); highestSampleValue = (short)highestSample; byte difference = (byte)(highestSample - (byte.MaxValue / (byte)numTracks)); multiplier = 1.0 - ((double)difference / (double)highestSample); } else if (highestBitsPerSample == 16) { // Get the highest sample value of all of the WAV files highestSampleValue = HighestSampleValueAs16Bit(pFileList); short difference = (short)(highestSampleValue - (short.MaxValue / (short)numTracks)); multiplier = 1.0 - ((double)difference / (double)highestSampleValue); } if (double.IsInfinity(multiplier) || (multiplier == 0.0)) { // If the temp dir did not exist, then remove it. if (!tempDirExisted) DeleteDir(pTempDir); // Throw the exception throw new WAVFileAudioMergeException("Could not calculate first volume multiplier.", "WAVFile.MergeAudioFiles()"); } if (multiplier < 0.0) multiplier = -multiplier; // 5. Scale down the audio levels of the source files, and save the output // in the temp directory. Also change the path to the audio files in // inputFilenames so that they point to the temp directory (we'll be // combining the scaled audio files). // This array (scaledAudioFiles) will contain WAVFile objects for the scaled audio files. WAVFile[] scaledAudioFiles = new WAVFile[pFileList.GetLength(0)]; String filename = ""; // For the scaled-down WAV filename WAVFile inputFile = new WAVFile(); WAVFile outputFile = new WAVFile(); for (int i = 0; i < pFileList.GetLength(0); ++i) { // pFileList[i] contains the fully-pathed filename. Using just the // filename, construct the fully-pathed filename for the scaled-down // version of the file in the temporary directory. filename = pTempDir + "\\" + Path.GetFileName(pFileList[i]); // Copy the file to the temp directory, adjusting its bits/sample and number of // channels if necessary. And also ajust its volume using multiplier. CopyAndConvert(pFileList[i], filename, highestBitsPerSample, outputStereo, multiplier); // Create the WAVFile object in the scaledAudioFiles array, and open the scaled // audio file with it. scaledAudioFiles[i] = new WAVFile(); scaledAudioFiles[i].Open(filename, WAVFileMode.READ); } // 7. Now, create the final audio mix file. outputFile.Create(pOutputFilename, outputStereo, firstFileAudioFormat.SampleRateHz, highestBitsPerSample); // 8. Do the merging.. // The basic algorithm for doing the merging is as follows: // while there is at least 1 sample remaining in any of the source files // sample = 0 // for each source file // if the source file has any samples remaining // sample = sample + next available sample from the source file // sample = sample / # of source files // write the sample to the output file if (highestBitsPerSample == 8) { byte sample = 0; while (SamplesRemain(scaledAudioFiles)) { sample = 0; for (int i = 0; i < scaledAudioFiles.GetLength(0); ++i) { if (scaledAudioFiles[i].NumSamplesRemaining > 0) sample += scaledAudioFiles[i].GetNextSample_8bit(); } sample /= (byte)(scaledAudioFiles.GetLength(0)); outputFile.AddSample_8bit(sample); } } else if (highestBitsPerSample == 16) { short sample = 0; while (SamplesRemain(scaledAudioFiles)) { sample = 0; for (int i = 0; i < scaledAudioFiles.GetLength(0); ++i) { if (scaledAudioFiles[i].NumSamplesRemaining > 0) sample += scaledAudioFiles[i].GetNextSampleAs16Bit(); } sample /= (short)(scaledAudioFiles.GetLength(0)); outputFile.AddSample_16bit(sample); } } outputFile.Close(); // 9. Remove the input files(to free up disk space. foreach (WAVFile audioFile in scaledAudioFiles) { filename = audioFile.Filename; audioFile.Close(); File.Delete(filename); } // 10. Now, increase the volume level of the output file. (will first have // to see if the inputs are 8-bit or 16-bit.) // Adjust the volume level of the file so that its volume is // the same as the volume of the input files. This is done by // first finding the highest sample value of all files, then // the highest sample value of the combined audio file, and // scaling the audio of the output file to match the volume // of the input files (such that the highest level of the output // file is the same as the highest level of all files). // For 16-bit audio, this works okay, but for 8-bit audio, the // output seems to sound better if we adjust the volume so that // the highest sample value is 3/4 of the maximum sample value // for the # of bits/sample. if (highestBitsPerSample == 8) { byte highestSampleVal = WAVFile.HighestSampleValue_8bit(pOutputFilename); byte maxValue = byte.MaxValue / 4 * 3; multiplier = (double)maxValue / (double)highestSampleVal; } else if (highestBitsPerSample == 16) { short finalMixFileHighestSample = WAVFile.HighestSampleValueAs16Bit(pOutputFilename); // Calculate the multiplier for adjusting the audio of the final mix file. //short difference = (short)(finalMixFileHighestSample - highestSampleValue); //multiplier = 1.0 - ((double)difference / (double)finalMixFileHighestSample); // This calculates the multiplier based on the highest sample value in the audio // file and the highest possible 16-bit sample value. multiplier = (double)short.MaxValue / (double)finalMixFileHighestSample; } if (multiplier < 0.0) multiplier = -multiplier; // Adjust the volume of the final mix file. AdjustVolumeInPlace(pOutputFilename, multiplier); // If the temporary directory did not exist prior to this method being called, then // delete it. if (!tempDirExisted) { String retval = DeleteDir(pTempDir); if (retval != "") throw new WAVFileAudioMergeException("Unable to remove temp directory (" + pTempDir + "): " + retval, "WAVFile.MergeAudioFiles()"); } }
/// <summary> /// For 8-bit WAV files: Adjusts the volume level and converts it to a 16-bit audio file. /// The converted data is saved to a separate file. /// </summary> /// <param name="pSrcFilename">The name of the WAV file to convert</param> /// <param name="pDestFilename">The name to use for the converted WAV file</param> /// <param name="pMultiplier">The volume multiplier</param> public static void AdjustVolume_Copy_8BitTo16Bit(String pSrcFilename, String pDestFilename, double pMultiplier) { // If an empty source or destination file were passed in, then throw an exception. if (pSrcFilename == "") throw new WAVFileReadException("Blank filename specified.", "WAVFile.AdjustVolume_Copy_8BitTo16Bit()"); if (pDestFilename == "") throw new WAVFileWriteException("Blank filename specified.", "WAVFile.AdjustVolume_Copy_8BitTo16Bit()"); // Open the srouce file WAVFile srcFile = new WAVFile(); String retval = srcFile.Open(pSrcFilename, WAVFileMode.READ); if (retval == "") { // Check to make sure the input file has 8 bits per sample. If not, then throw an exception. if (srcFile.BitsPerSample != 8) { WAVFileBitsPerSampleException exc = new WAVFileBitsPerSampleException(pSrcFilename + ": 8 bits per sample required, and the file has " + srcFile.BitsPerSample.ToString() + " bits per sample.", "WAVFile.AdjustVolume_Copy_8BitTo16Bit()", srcFile.BitsPerSample); srcFile.Close(); throw exc; } // Open the destination file WAVFile destFile = new WAVFile(); destFile.Create(pDestFilename, srcFile.IsStereo, srcFile.SampleRateHz, 16, true); // Copy the data short sample_16bit = 0; while (srcFile.NumSamplesRemaining > 0) { // Scale the sample from 8-bit to 16 bits sample_16bit = ScaleByteToShort(srcFile.GetNextSample_8bit()); // Now, apply pMultiplier if it is not 1.0 if (pMultiplier != 1.0) sample_16bit = (short)((double)sample_16bit * pMultiplier); // Save the sample to the destination file destFile.AddSample_16bit(sample_16bit); } srcFile.Close(); destFile.Close(); } else throw new WAVFileReadException(retval, "WAVFile.AdjustVolume_Copy_8BitTo16Bit()"); }