(ded4a3e0a) v0.9.0.7
This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates and provides operations, such as format conversions, on the
|
||||
/// source audio. This type is produced by the audio importers and used by audio
|
||||
/// processors to produce compiled audio assets.
|
||||
/// </summary>
|
||||
/// <remarks>Note that AudioContent can load and process audio files that are not supported by the importers.</remarks>
|
||||
public class AudioContent : ContentItem, IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
private readonly string _fileName;
|
||||
private readonly AudioFileType _fileType;
|
||||
private ReadOnlyCollection<byte> _data;
|
||||
private TimeSpan _duration;
|
||||
private AudioFormat _format;
|
||||
private int _loopStart;
|
||||
private int _loopLength;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the original source audio file.
|
||||
/// </summary>
|
||||
[ContentSerializer(AllowNull = false)]
|
||||
public string FileName { get { return _fileName; } }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the original source audio file.
|
||||
/// </summary>
|
||||
public AudioFileType FileType { get { return _fileType; } }
|
||||
|
||||
/// <summary>
|
||||
/// The current raw audio data without header information.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This changes from the source data to the output data after conversion.
|
||||
/// For MP3 and WMA files this throws an exception to match XNA behavior.
|
||||
/// </remarks>
|
||||
public ReadOnlyCollection<byte> Data
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_disposed || _data == null)
|
||||
throw new InvalidContentException("Could not read the audio data from file \"" + Path.GetFileName(_fileName) + "\".");
|
||||
return _data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The duration of the audio data.
|
||||
/// </summary>
|
||||
public TimeSpan Duration
|
||||
{
|
||||
get
|
||||
{
|
||||
return _duration;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current format of the audio data.
|
||||
/// </summary>
|
||||
/// <remarks>This changes from the source format to the output format after conversion.</remarks>
|
||||
public AudioFormat Format
|
||||
{
|
||||
get
|
||||
{
|
||||
return _format;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current loop length in samples.
|
||||
/// </summary>
|
||||
/// <remarks>This changes from the source loop length to the output loop length after conversion.</remarks>
|
||||
public int LoopLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return _loopLength;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current loop start location in samples.
|
||||
/// </summary>
|
||||
/// <remarks>This changes from the source loop start to the output loop start after conversion.</remarks>
|
||||
public int LoopStart
|
||||
{
|
||||
get
|
||||
{
|
||||
return _loopStart;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of AudioContent.
|
||||
/// </summary>
|
||||
/// <param name="audioFileName">Name of the audio source file to be processed.</param>
|
||||
/// <param name="audioFileType">Type of the processed audio: WAV, MP3 or WMA.</param>
|
||||
/// <remarks>Constructs the object from the specified source file, in the format specified.</remarks>
|
||||
public AudioContent(string audioFileName, AudioFileType audioFileType)
|
||||
{
|
||||
_fileName = audioFileName;
|
||||
|
||||
try
|
||||
{
|
||||
// Get the full path to the file.
|
||||
audioFileName = Path.GetFullPath(audioFileName);
|
||||
|
||||
// Use probe to get the details of the file.
|
||||
DefaultAudioProfile.ProbeFormat(audioFileName, out _fileType, out _format, out _duration, out _loopStart, out _loopLength);
|
||||
|
||||
// Looks like XNA only cares about type mismatch when
|
||||
// the type is WAV... else it is ok.
|
||||
if ( (audioFileType == AudioFileType.Wav || _fileType == AudioFileType.Wav) &&
|
||||
audioFileType != _fileType)
|
||||
throw new ArgumentException("Incorrect file type!", "audioFileType");
|
||||
|
||||
// Only provide the data for WAV files.
|
||||
if (audioFileType == AudioFileType.Wav)
|
||||
{
|
||||
byte[] rawData;
|
||||
|
||||
// Must be opened in read mode otherwise it fails to open
|
||||
// read-only files (found in some source control systems)
|
||||
using (var fs = new FileStream(audioFileName, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
rawData = new byte[fs.Length];
|
||||
fs.Read(rawData, 0, rawData.Length);
|
||||
}
|
||||
|
||||
AudioFormat riffAudioFormat;
|
||||
var stripped = DefaultAudioProfile.StripRiffWaveHeader(rawData, out riffAudioFormat);
|
||||
|
||||
if (riffAudioFormat != null)
|
||||
{
|
||||
if ((_format.Format != 2 && _format.Format != 17) && _format.BlockAlign != riffAudioFormat.BlockAlign)
|
||||
throw new InvalidOperationException("Calculated block align does not match RIFF " + _format.BlockAlign + " : " + riffAudioFormat.BlockAlign);
|
||||
if (_format.ChannelCount != riffAudioFormat.ChannelCount)
|
||||
throw new InvalidOperationException("Probed channel count does not match RIFF: " + _format.ChannelCount + ", " + riffAudioFormat.ChannelCount);
|
||||
if (_format.Format != riffAudioFormat.Format)
|
||||
throw new InvalidOperationException("Probed audio format does not match RIFF: " + _format.Format + ", " + riffAudioFormat.Format);
|
||||
if (_format.SampleRate != riffAudioFormat.SampleRate)
|
||||
throw new InvalidOperationException("Probed sample rate does not match RIFF: " + _format.SampleRate + ", " + riffAudioFormat.SampleRate);
|
||||
}
|
||||
|
||||
_data = Array.AsReadOnly(stripped);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = string.Format("Failed to open file {0}. Ensure the file is a valid audio file and is not DRM protected.", Path.GetFileNameWithoutExtension(audioFileName));
|
||||
throw new InvalidContentException(message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transcodes the source audio to the target format and quality.
|
||||
/// </summary>
|
||||
/// <param name="formatType">Format to convert this audio to.</param>
|
||||
/// <param name="quality">Quality of the processed output audio. For streaming formats, it can be one of the following: Low (96 kbps), Medium (128 kbps), Best (192 kbps). For WAV formats, it can be one of the following: Low (11kHz ADPCM), Medium (22kHz ADPCM), Best (44kHz PCM)</param>
|
||||
/// <param name="saveToFile">
|
||||
/// The name of the file that the converted audio should be saved into. This is used for SongContent, where
|
||||
/// the audio is stored external to the XNB file. If this is null, then the converted audio is stored in
|
||||
/// the Data property.
|
||||
/// </param>
|
||||
[Obsolete("You should prefer to use AudioProfile.")]
|
||||
public void ConvertFormat(ConversionFormat formatType, ConversionQuality quality, string saveToFile)
|
||||
{
|
||||
// Call the legacy conversion code.
|
||||
DefaultAudioProfile.ConvertToFormat(this, formatType, quality, saveToFile);
|
||||
}
|
||||
|
||||
public void SetData(byte[] data, AudioFormat format, TimeSpan duration, int loopStart, int loopLength)
|
||||
{
|
||||
if (data == null)
|
||||
throw new ArgumentNullException("data");
|
||||
if (format == null)
|
||||
throw new ArgumentNullException("format");
|
||||
|
||||
_data = Array.AsReadOnly(data);
|
||||
_format = format;
|
||||
_duration = duration;
|
||||
_loopStart = loopStart;
|
||||
_loopLength = loopLength;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_data = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the audio file.
|
||||
/// </summary>
|
||||
public enum AudioFileType
|
||||
{
|
||||
/// <summary>
|
||||
/// The MP3 format
|
||||
/// </summary>
|
||||
Mp3,
|
||||
|
||||
/// <summary>
|
||||
/// The WAV format
|
||||
/// </summary>
|
||||
Wav,
|
||||
|
||||
/// <summary>
|
||||
/// The WMA format
|
||||
/// </summary>
|
||||
Wma,
|
||||
|
||||
/// <summary>
|
||||
/// The Ogg format
|
||||
/// </summary>
|
||||
Ogg,
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates the native audio format (WAVEFORMATEX) information of the audio content.
|
||||
/// </summary>
|
||||
public sealed class AudioFormat
|
||||
{
|
||||
int averageBytesPerSecond;
|
||||
int bitsPerSample;
|
||||
int blockAlign;
|
||||
int channelCount;
|
||||
int format;
|
||||
List<byte> nativeWaveFormat;
|
||||
int sampleRate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the average bytes processed per second.
|
||||
/// </summary>
|
||||
/// <value>Average bytes processed per second.</value>
|
||||
public int AverageBytesPerSecond { get { return averageBytesPerSecond; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit depth of the audio content.
|
||||
/// </summary>
|
||||
/// <value>If the audio has not been processed, the source bit depth; otherwise, the bit depth of the new format.</value>
|
||||
public int BitsPerSample { get { return bitsPerSample; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of bytes per sample block, taking channels into consideration. For example, for 16-bit stereo audio (PCM format), the size of each sample block is 4 bytes.
|
||||
/// </summary>
|
||||
/// <value>Number of bytes, per sample block.</value>
|
||||
public int BlockAlign { get { return blockAlign; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of channels.
|
||||
/// </summary>
|
||||
/// <value>If the audio has not been processed, the source channel count; otherwise, the new channel count.</value>
|
||||
public int ChannelCount { get { return channelCount; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the format of the audio content.
|
||||
/// </summary>
|
||||
/// <value>If the audio has not been processed, the format tag of the source content; otherwise, the new format tag.</value>
|
||||
public int Format { get { return format; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw byte buffer for the format. For non-PCM formats, this buffer contains important format-specific information beyond the basic format information exposed in other properties of the AudioFormat type.
|
||||
/// </summary>
|
||||
/// <value>The raw byte buffer represented in a collection.</value>
|
||||
public ReadOnlyCollection<byte> NativeWaveFormat { get { return nativeWaveFormat.AsReadOnly(); } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sample rate of the audio content.
|
||||
/// </summary>
|
||||
/// <value>If the audio has not been processed, the source sample rate; otherwise, the new sample rate.</value>
|
||||
public int SampleRate { get { return sampleRate; } }
|
||||
|
||||
internal AudioFormat(
|
||||
int averageBytesPerSecond,
|
||||
int bitsPerSample,
|
||||
int blockAlign,
|
||||
int channelCount,
|
||||
int format,
|
||||
int sampleRate)
|
||||
{
|
||||
this.averageBytesPerSecond = averageBytesPerSecond;
|
||||
this.bitsPerSample = bitsPerSample;
|
||||
this.blockAlign = blockAlign;
|
||||
this.channelCount = channelCount;
|
||||
this.format = format;
|
||||
this.sampleRate = sampleRate;
|
||||
|
||||
this.nativeWaveFormat = this.ConstructNativeWaveFormat();
|
||||
}
|
||||
|
||||
private List<byte> ConstructNativeWaveFormat()
|
||||
{
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
using (var writer = new BinaryWriter(memory))
|
||||
{
|
||||
writer.Write((short)this.format);
|
||||
writer.Write((short)this.channelCount);
|
||||
writer.Write((int)this.sampleRate);
|
||||
writer.Write((int)this.averageBytesPerSecond);
|
||||
writer.Write((short)this.blockAlign);
|
||||
writer.Write((short)this.bitsPerSample);
|
||||
writer.Write((short)0);
|
||||
|
||||
var bytes = new byte[memory.Position];
|
||||
memory.Seek(0, SeekOrigin.Begin);
|
||||
memory.Read(bytes, 0, bytes.Length);
|
||||
return bytes.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper methods for audio importing, conversion and processing.
|
||||
/// </summary>
|
||||
class AudioHelper
|
||||
{
|
||||
// This array must remain in sync with the ConversionFormat enum.
|
||||
static string[] conversionFormatExtensions = new[] { "wav", "wav", "wma", "xma", "wav", "m4a", "ogg" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file extension for an audio format.
|
||||
/// </summary>
|
||||
/// <param name="format">The conversion format</param>
|
||||
/// <returns>The file extension for the given conversion format.</returns>
|
||||
static public string GetExtension(ConversionFormat format)
|
||||
{
|
||||
return conversionFormatExtensions[(int)format];
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
public abstract class AudioProfile
|
||||
{
|
||||
private static readonly LoadedTypeCollection<AudioProfile> _profiles = new LoadedTypeCollection<AudioProfile>();
|
||||
|
||||
/// <summary>
|
||||
/// Find the profile for this target platform.
|
||||
/// </summary>
|
||||
/// <param name="platform">The platform target for audio.</param>
|
||||
/// <returns></returns>
|
||||
public static AudioProfile ForPlatform(TargetPlatform platform)
|
||||
{
|
||||
var profile = _profiles.FirstOrDefault(h => h.Supports(platform));
|
||||
if (profile != null)
|
||||
return profile;
|
||||
|
||||
throw new PipelineException("There is no supported audio profile for the '" + platform + "' platform!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this profile supports audio processing for this platform.
|
||||
/// </summary>
|
||||
public abstract bool Supports(TargetPlatform platform);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the audio content to work on targeted platform.
|
||||
/// </summary>
|
||||
/// <param name="platform">The platform to build the audio content for.</param>
|
||||
/// <param name="quality">The suggested audio quality level.</param>
|
||||
/// <param name="content">The audio content to convert.</param>
|
||||
/// <returns>The quality used for conversion which could be different from the suggested quality.</returns>
|
||||
public abstract ConversionQuality ConvertAudio(TargetPlatform platform, ConversionQuality quality, AudioContent content);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the audio content to a streaming format that works on targeted platform.
|
||||
/// </summary>
|
||||
/// <param name="platform">The platform to build the audio content for.</param>
|
||||
/// <param name="quality">The suggested audio quality level.</param>
|
||||
/// <param name="content">he audio content to convert.</param>
|
||||
/// <param name="outputFileName"></param>
|
||||
/// <returns>The quality used for conversion which could be different from the suggested quality.</returns>
|
||||
public abstract ConversionQuality ConvertStreamingAudio(TargetPlatform platform, ConversionQuality quality, AudioContent content, ref string outputFileName);
|
||||
|
||||
|
||||
protected static int QualityToSampleRate(ConversionQuality quality, int sourceSampleRate)
|
||||
{
|
||||
switch (quality)
|
||||
{
|
||||
case ConversionQuality.Low:
|
||||
return Math.Max(8000, (int)Math.Floor(sourceSampleRate / 2.0));
|
||||
case ConversionQuality.Medium:
|
||||
return Math.Max(8000, (int)Math.Floor((sourceSampleRate / 4.0) * 3));
|
||||
}
|
||||
|
||||
return Math.Max(8000, sourceSampleRate);
|
||||
}
|
||||
|
||||
protected static int QualityToBitRate(ConversionQuality quality)
|
||||
{
|
||||
switch (quality)
|
||||
{
|
||||
case ConversionQuality.Low:
|
||||
return 96000;
|
||||
case ConversionQuality.Medium:
|
||||
return 128000;
|
||||
}
|
||||
|
||||
return 192000;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Target formats supported for audio source conversions.
|
||||
/// </summary>
|
||||
public enum ConversionFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Microsoft ADPCM encoding technique using 4 bits
|
||||
/// </summary>
|
||||
Adpcm,
|
||||
|
||||
/// <summary>
|
||||
/// 8/16-bit mono/stereo PCM audio 8KHz-48KHz
|
||||
/// </summary>
|
||||
Pcm,
|
||||
|
||||
/// <summary>
|
||||
/// Windows Media CBR formats (64 kbps, 128 kbps, 192 kbps)
|
||||
/// </summary>
|
||||
WindowsMedia,
|
||||
|
||||
/// <summary>
|
||||
/// The Xbox compression format
|
||||
/// </summary>
|
||||
Xma,
|
||||
|
||||
/// <summary>
|
||||
/// QuickTime ADPCM format
|
||||
/// </summary>
|
||||
ImaAdpcm,
|
||||
|
||||
/// <summary>
|
||||
/// Advanced Audio Coding
|
||||
/// </summary>
|
||||
Aac,
|
||||
|
||||
/// <summary>
|
||||
/// Vorbis open, patent-free audio encoding
|
||||
/// </summary>
|
||||
Vorbis,
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Compression quality of the audio content.
|
||||
/// </summary>
|
||||
public enum ConversionQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// High compression yielding lower file size, but could compromise audio quality
|
||||
/// </summary>
|
||||
Low,
|
||||
|
||||
/// <summary>
|
||||
/// Moderate compression resulting in a compromise between audio quality and file size
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// Lowest compression, but the best audio quality
|
||||
/// </summary>
|
||||
Best,
|
||||
}
|
||||
}
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
// MonoGame - Copyright (C) The MonoGame Team
|
||||
// This file is subject to the terms and conditions defined in
|
||||
// file 'LICENSE.txt', which is part of this source code package.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace Microsoft.Xna.Framework.Content.Pipeline.Audio
|
||||
{
|
||||
internal class DefaultAudioProfile : AudioProfile
|
||||
{
|
||||
public override bool Supports(TargetPlatform platform)
|
||||
{
|
||||
return platform == TargetPlatform.Android ||
|
||||
platform == TargetPlatform.DesktopGL ||
|
||||
platform == TargetPlatform.MacOSX ||
|
||||
platform == TargetPlatform.NativeClient ||
|
||||
platform == TargetPlatform.RaspberryPi ||
|
||||
platform == TargetPlatform.Windows ||
|
||||
platform == TargetPlatform.WindowsPhone8 ||
|
||||
platform == TargetPlatform.WindowsStoreApp ||
|
||||
platform == TargetPlatform.iOS;
|
||||
}
|
||||
|
||||
public override ConversionQuality ConvertAudio(TargetPlatform platform, ConversionQuality quality, AudioContent content)
|
||||
{
|
||||
// Default to PCM data, or ADPCM if the source is ADPCM.
|
||||
var targetFormat = ConversionFormat.Pcm;
|
||||
if (quality != ConversionQuality.Best || content.Format.Format == 2 || content.Format.Format == 17)
|
||||
{
|
||||
if (platform == TargetPlatform.iOS || platform == TargetPlatform.MacOSX || platform == TargetPlatform.DesktopGL)
|
||||
targetFormat = ConversionFormat.ImaAdpcm;
|
||||
else
|
||||
targetFormat = ConversionFormat.Adpcm;
|
||||
}
|
||||
|
||||
return ConvertToFormat(content, targetFormat, quality, null);
|
||||
}
|
||||
|
||||
public override ConversionQuality ConvertStreamingAudio(TargetPlatform platform, ConversionQuality quality, AudioContent content, ref string outputFileName)
|
||||
{
|
||||
// Most platforms will use AAC ("mp4") by default
|
||||
var targetFormat = ConversionFormat.Aac;
|
||||
|
||||
if ( platform == TargetPlatform.Windows ||
|
||||
platform == TargetPlatform.WindowsPhone8 ||
|
||||
platform == TargetPlatform.WindowsStoreApp)
|
||||
targetFormat = ConversionFormat.WindowsMedia;
|
||||
|
||||
else if (platform == TargetPlatform.DesktopGL)
|
||||
targetFormat = ConversionFormat.Vorbis;
|
||||
|
||||
// Get the song output path with the target format extension.
|
||||
outputFileName = Path.ChangeExtension(outputFileName, AudioHelper.GetExtension(targetFormat));
|
||||
|
||||
// Make sure the output folder for the file exists.
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputFileName));
|
||||
|
||||
return ConvertToFormat(content, targetFormat, quality, outputFileName);
|
||||
}
|
||||
|
||||
public static void ProbeFormat(string sourceFile, out AudioFileType audioFileType, out AudioFormat audioFormat, out TimeSpan duration, out int loopStart, out int loopLength)
|
||||
{
|
||||
string ffprobeStdout, ffprobeStderr;
|
||||
var ffprobeExitCode = ExternalTool.Run(
|
||||
"ffprobe",
|
||||
string.Format("-i \"{0}\" -show_format -show_entries streams -v quiet -of flat", sourceFile),
|
||||
out ffprobeStdout,
|
||||
out ffprobeStderr);
|
||||
if (ffprobeExitCode != 0)
|
||||
throw new InvalidOperationException("ffprobe exited with non-zero exit code.");
|
||||
|
||||
// Set default values if information is not available.
|
||||
int averageBytesPerSecond = 0;
|
||||
int bitsPerSample = 0;
|
||||
int blockAlign = 0;
|
||||
int channelCount = 0;
|
||||
int sampleRate = 0;
|
||||
int format = 0;
|
||||
string sampleFormat = null;
|
||||
double durationInSeconds = 0;
|
||||
var formatName = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var numberFormat = CultureInfo.InvariantCulture.NumberFormat;
|
||||
foreach (var line in ffprobeStdout.Split(new[] {'\r', '\n', '\0'}, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var kv = line.Split(new[] {'='}, 2);
|
||||
|
||||
switch (kv[0])
|
||||
{
|
||||
case "streams.stream.0.sample_rate":
|
||||
sampleRate = int.Parse(kv[1].Trim('"'), numberFormat);
|
||||
break;
|
||||
case "streams.stream.0.bits_per_sample":
|
||||
bitsPerSample = int.Parse(kv[1].Trim('"'), numberFormat);
|
||||
break;
|
||||
case "streams.stream.0.start_time":
|
||||
{
|
||||
double seconds;
|
||||
if (double.TryParse(kv[1].Trim('"'), NumberStyles.Any, numberFormat, out seconds))
|
||||
durationInSeconds += seconds;
|
||||
break;
|
||||
}
|
||||
case "streams.stream.0.duration":
|
||||
durationInSeconds += double.Parse(kv[1].Trim('"'), numberFormat);
|
||||
break;
|
||||
case "streams.stream.0.channels":
|
||||
channelCount = int.Parse(kv[1].Trim('"'), numberFormat);
|
||||
break;
|
||||
case "streams.stream.0.sample_fmt":
|
||||
sampleFormat = kv[1].Trim('"').ToLowerInvariant();
|
||||
break;
|
||||
case "streams.stream.0.bit_rate":
|
||||
averageBytesPerSecond = (int.Parse(kv[1].Trim('"'), numberFormat)/8);
|
||||
break;
|
||||
case "format.format_name":
|
||||
formatName = kv[1].Trim('"').ToLowerInvariant();
|
||||
break;
|
||||
case "streams.stream.0.codec_tag":
|
||||
{
|
||||
var hex = kv[1].Substring(3, kv[1].Length - 4);
|
||||
format = int.Parse(hex, NumberStyles.HexNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse ffprobe output.", ex);
|
||||
}
|
||||
|
||||
// XNA seems to use the sample format for the bits per sample
|
||||
// in the case of non-PCM formats like MP3 and WMA.
|
||||
if (bitsPerSample == 0 && sampleFormat != null)
|
||||
{
|
||||
switch (sampleFormat)
|
||||
{
|
||||
case "u8":
|
||||
case "u8p":
|
||||
bitsPerSample = 8;
|
||||
break;
|
||||
case "s16":
|
||||
case "s16p":
|
||||
bitsPerSample = 16;
|
||||
break;
|
||||
case "s32":
|
||||
case "s32p":
|
||||
case "flt":
|
||||
case "fltp":
|
||||
bitsPerSample = 32;
|
||||
break;
|
||||
case "dbl":
|
||||
case "dblp":
|
||||
bitsPerSample = 64;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out the file type.
|
||||
var durationMs = (int)Math.Floor(durationInSeconds * 1000.0);
|
||||
if (formatName == "wav")
|
||||
{
|
||||
audioFileType = AudioFileType.Wav;
|
||||
}
|
||||
else if (formatName == "mp3")
|
||||
{
|
||||
audioFileType = AudioFileType.Mp3;
|
||||
format = 1;
|
||||
durationMs = (int)Math.Ceiling(durationInSeconds * 1000.0);
|
||||
bitsPerSample = Math.Min(bitsPerSample, 16);
|
||||
}
|
||||
else if (formatName == "wma" || formatName == "asf")
|
||||
{
|
||||
audioFileType = AudioFileType.Wma;
|
||||
format = 1;
|
||||
durationMs = (int)Math.Ceiling(durationInSeconds * 1000.0);
|
||||
bitsPerSample = Math.Min(bitsPerSample, 16);
|
||||
}
|
||||
else if (formatName == "ogg")
|
||||
{
|
||||
audioFileType = AudioFileType.Ogg;
|
||||
format = 1;
|
||||
durationMs = (int)Math.Ceiling(durationInSeconds * 1000.0);
|
||||
bitsPerSample = Math.Min(bitsPerSample, 16);
|
||||
}
|
||||
else
|
||||
audioFileType = (AudioFileType) (-1);
|
||||
|
||||
// XNA seems to calculate the block alignment directly from
|
||||
// the bits per sample and channel count regardless of the
|
||||
// format of the audio data.
|
||||
// ffprobe doesn't report blockAlign for ADPCM and we cannot calculate it like this
|
||||
if (bitsPerSample > 0 && (format != 2 && format != 17))
|
||||
blockAlign = (bitsPerSample * channelCount) / 8;
|
||||
|
||||
// XNA seems to only be accurate to the millisecond.
|
||||
duration = TimeSpan.FromMilliseconds(durationMs);
|
||||
|
||||
// Looks like XNA calculates the average bps from
|
||||
// the sample rate and block alignment.
|
||||
if (blockAlign > 0)
|
||||
averageBytesPerSecond = sampleRate * blockAlign;
|
||||
|
||||
audioFormat = new AudioFormat(
|
||||
averageBytesPerSecond,
|
||||
bitsPerSample,
|
||||
blockAlign,
|
||||
channelCount,
|
||||
format,
|
||||
sampleRate);
|
||||
|
||||
// Loop start and length in number of samples. For some
|
||||
// reason XNA doesn't report loop length for non-WAV sources.
|
||||
loopStart = 0;
|
||||
if (audioFileType != AudioFileType.Wav)
|
||||
loopLength = 0;
|
||||
else
|
||||
loopLength = (int)Math.Floor(sampleRate * durationInSeconds);
|
||||
}
|
||||
|
||||
internal static byte[] StripRiffWaveHeader(byte[] data, out AudioFormat audioFormat)
|
||||
{
|
||||
audioFormat = null;
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(data)))
|
||||
{
|
||||
var signature = new string(reader.ReadChars(4));
|
||||
if (signature != "RIFF")
|
||||
return data;
|
||||
|
||||
reader.ReadInt32(); // riff_chunck_size
|
||||
|
||||
var wformat = new string(reader.ReadChars(4));
|
||||
if (wformat != "WAVE")
|
||||
return data;
|
||||
|
||||
// Look for the data chunk.
|
||||
while (true)
|
||||
{
|
||||
var chunkSignature = new string(reader.ReadChars(4));
|
||||
if (chunkSignature.ToLowerInvariant() == "data")
|
||||
break;
|
||||
if (chunkSignature.ToLowerInvariant() == "fmt ")
|
||||
{
|
||||
int fmtLength = reader.ReadInt32();
|
||||
short formatTag = reader.ReadInt16();
|
||||
short channels = reader.ReadInt16();
|
||||
int sampleRate = reader.ReadInt32();
|
||||
int avgBytesPerSec = reader.ReadInt32();
|
||||
short blockAlign = reader.ReadInt16();
|
||||
short bitsPerSample = reader.ReadInt16();
|
||||
audioFormat = new AudioFormat(avgBytesPerSec, bitsPerSample, blockAlign, channels, formatTag, sampleRate);
|
||||
|
||||
fmtLength -= 2 + 2 + 4 + 4 + 2 + 2;
|
||||
if (fmtLength < 0)
|
||||
throw new InvalidOperationException("riff wave header has unexpected format");
|
||||
reader.BaseStream.Seek(fmtLength, SeekOrigin.Current);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.BaseStream.Seek(reader.ReadInt32(), SeekOrigin.Current);
|
||||
}
|
||||
}
|
||||
|
||||
var dataSize = reader.ReadInt32();
|
||||
data = reader.ReadBytes(dataSize);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void WritePcmFile(AudioContent content, string saveToFile, int bitRate = 192000, int? sampeRate = null)
|
||||
{
|
||||
string ffmpegStdout, ffmpegStderr;
|
||||
var ffmpegExitCode = ExternalTool.Run(
|
||||
"ffmpeg",
|
||||
string.Format(
|
||||
"-y -i \"{0}\" -vn -c:a pcm_s16le -b:a {2} {3} -f:a wav -strict experimental \"{1}\"",
|
||||
content.FileName,
|
||||
saveToFile,
|
||||
bitRate,
|
||||
sampeRate != null ? "-ar " + sampeRate.Value : ""
|
||||
),
|
||||
out ffmpegStdout,
|
||||
out ffmpegStderr);
|
||||
if (ffmpegExitCode != 0)
|
||||
throw new InvalidOperationException("ffmpeg exited with non-zero exit code: \n" + ffmpegStdout + "\n" + ffmpegStderr);
|
||||
}
|
||||
|
||||
public static ConversionQuality ConvertToFormat(AudioContent content, ConversionFormat formatType, ConversionQuality quality, string saveToFile)
|
||||
{
|
||||
var temporaryOutput = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
string ffmpegCodecName, ffmpegMuxerName;
|
||||
//int format;
|
||||
switch (formatType)
|
||||
{
|
||||
case ConversionFormat.Adpcm:
|
||||
// ADPCM Microsoft
|
||||
ffmpegCodecName = "adpcm_ms";
|
||||
ffmpegMuxerName = "wav";
|
||||
//format = 0x0002; /* WAVE_FORMAT_ADPCM */
|
||||
break;
|
||||
case ConversionFormat.Pcm:
|
||||
// XNA seems to preserve the bit size of the input
|
||||
// format when converting to PCM.
|
||||
if (content.Format.BitsPerSample == 8)
|
||||
ffmpegCodecName = "pcm_u8";
|
||||
else if (content.Format.BitsPerSample == 32 && content.Format.Format == 3)
|
||||
ffmpegCodecName = "pcm_f32le";
|
||||
else
|
||||
ffmpegCodecName = "pcm_s16le";
|
||||
ffmpegMuxerName = "wav";
|
||||
//format = 0x0001; /* WAVE_FORMAT_PCM */
|
||||
break;
|
||||
case ConversionFormat.WindowsMedia:
|
||||
// Windows Media Audio 2
|
||||
ffmpegCodecName = "wmav2";
|
||||
ffmpegMuxerName = "asf";
|
||||
//format = 0x0161; /* WAVE_FORMAT_WMAUDIO2 */
|
||||
break;
|
||||
case ConversionFormat.Xma:
|
||||
throw new NotSupportedException(
|
||||
"XMA is not a supported encoding format. It is specific to the Xbox 360.");
|
||||
case ConversionFormat.ImaAdpcm:
|
||||
// ADPCM IMA WAV
|
||||
ffmpegCodecName = "adpcm_ima_wav";
|
||||
ffmpegMuxerName = "wav";
|
||||
//format = 0x0011; /* WAVE_FORMAT_IMA_ADPCM */
|
||||
break;
|
||||
case ConversionFormat.Aac:
|
||||
// AAC (Advanced Audio Coding)
|
||||
// Requires -strict experimental
|
||||
ffmpegCodecName = "aac";
|
||||
ffmpegMuxerName = "ipod";
|
||||
//format = 0x0000; /* WAVE_FORMAT_UNKNOWN */
|
||||
break;
|
||||
case ConversionFormat.Vorbis:
|
||||
// Vorbis
|
||||
ffmpegCodecName = "libvorbis";
|
||||
ffmpegMuxerName = "ogg";
|
||||
//format = 0x0000; /* WAVE_FORMAT_UNKNOWN */
|
||||
break;
|
||||
default:
|
||||
// Unknown format
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
string ffmpegStdout, ffmpegStderr;
|
||||
int ffmpegExitCode;
|
||||
do
|
||||
{
|
||||
ffmpegExitCode = ExternalTool.Run(
|
||||
"ffmpeg",
|
||||
string.Format(
|
||||
"-y -i \"{0}\" -vn -c:a {1} -b:a {2} -ar {3} -f:a {4} -strict experimental \"{5}\"",
|
||||
content.FileName,
|
||||
ffmpegCodecName,
|
||||
QualityToBitRate(quality),
|
||||
QualityToSampleRate(quality, content.Format.SampleRate),
|
||||
ffmpegMuxerName,
|
||||
temporaryOutput),
|
||||
out ffmpegStdout,
|
||||
out ffmpegStderr);
|
||||
if (ffmpegExitCode != 0)
|
||||
quality--;
|
||||
} while (quality >= 0 && ffmpegExitCode != 0);
|
||||
|
||||
if (ffmpegExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException("ffmpeg exited with non-zero exit code: \n" + ffmpegStdout + "\n" + ffmpegStderr);
|
||||
}
|
||||
|
||||
byte[] rawData;
|
||||
using (var fs = new FileStream(temporaryOutput, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
rawData = new byte[fs.Length];
|
||||
fs.Read(rawData, 0, rawData.Length);
|
||||
}
|
||||
|
||||
if (saveToFile != null)
|
||||
{
|
||||
using (var fs = new FileStream(saveToFile, FileMode.Create, FileAccess.Write))
|
||||
fs.Write(rawData, 0, rawData.Length);
|
||||
}
|
||||
|
||||
// Use probe to get the final format and information on the converted file.
|
||||
AudioFileType audioFileType;
|
||||
AudioFormat audioFormat;
|
||||
TimeSpan duration;
|
||||
int loopStart, loopLength;
|
||||
ProbeFormat(temporaryOutput, out audioFileType, out audioFormat, out duration, out loopStart, out loopLength);
|
||||
|
||||
AudioFormat riffAudioFormat;
|
||||
byte[] data = StripRiffWaveHeader(rawData, out riffAudioFormat);
|
||||
|
||||
// deal with adpcm
|
||||
if (audioFormat.Format == 2 || audioFormat.Format == 17)
|
||||
{
|
||||
// riff contains correct blockAlign
|
||||
audioFormat = riffAudioFormat;
|
||||
|
||||
// fix loopLength -> has to be multiple of sample per block
|
||||
// see https://msdn.microsoft.com/de-de/library/windows/desktop/ee415711(v=vs.85).aspx
|
||||
int samplesPerBlock = SampleAlignment(audioFormat);
|
||||
loopLength = (int)(audioFormat.SampleRate * duration.TotalSeconds);
|
||||
int remainder = loopLength % samplesPerBlock;
|
||||
loopLength += samplesPerBlock - remainder;
|
||||
}
|
||||
|
||||
content.SetData(data, audioFormat, duration, loopStart, loopLength);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ExternalTool.DeleteFile(temporaryOutput);
|
||||
}
|
||||
|
||||
return quality;
|
||||
}
|
||||
|
||||
// Converts block alignment in bytes to sample alignment, primarily for compressed formats
|
||||
// Calculation of sample alignment from http://kcat.strangesoft.net/openal-extensions/SOFT_block_alignment.txt
|
||||
static int SampleAlignment(AudioFormat format)
|
||||
{
|
||||
switch (format.Format)
|
||||
{
|
||||
case 2: // MS-ADPCM
|
||||
return (format.BlockAlign / format.ChannelCount - 7) * 2 + 2;
|
||||
case 17: // IMA/ADPCM
|
||||
return (format.BlockAlign / format.ChannelCount - 4) / 4 * 8 + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user