オーディオデータをwavファイルからUWPに抽出する

本日は、長年のパートナーであるMusic Paradiseという会社の資料をご紹介します。これは、皆さんが思い出すように、共同記事「 ソーシャルサービスを非ゲームアプリケーションに統合する 」で音楽アプリケーションの作成の秘密をすでにHabrと共有しています 。 今回、チームの専門家は、元々 iOSおよびMacデバイス用に作成されたAudio Editorユーティリティの基本機能の新しいプラットフォーム( Windows)での実装の詳細と、適応に使用した方法について話します。











「この記事では、ファイルからオーディオデータを取得する最も手頃な方法の1つを見ていきます。 オーディオデータの抽出は、サウンドを扱う最初の一歩を踏み出すすべての開発者にとっての礎石ですが、驚くことにほとんど注意が払われていません。 インターネットで既製のソリューションやUWPの指示を見つけようとすると、問題が特に顕著に感じられます。ほとんどの場合、まったく答えが得られないか、古いソリューションに満足する必要があります。 一方、サウンドを操作する場合、データ抽出は非常に理にかなっており、開発者はデータを編集することができます。コピー、追加、変更、エフェクトの適用、ユーザーの画面での視覚化。 今日議論されるのは視覚化についてです。 NuGetギャラリーでオーディオデータを操作するための特別なライブラリが存在しますが、オーディオファイルのバイトの独立した処理でアプリケーションロジックを構築します。 したがって、このプロセスでは、wavファイルの構造についてより多くのことを学び、実際にはオーディオデータの操作がそれほど難しくないことを確認します。



そのため、目標を達成するため、つまりオーディオウェーブのグラフィック表示を構築するために、まずデータを抽出する必要があります。 このプロセスはwavファイルの例を使用して説明します。これは、作業に最も便利なオーディオ形式の1つであるためです。



しかし、あなたと私は、デジタルオーディオの世界が広すぎて、すべてを単一のフォーマットで動作するように減らすことができないことを理解しています。 したがって、追加の手順をすぐに規定します。他の形式のファイルを取得した場合は、まずそれをwavに変換します。 心配しないでください、このプロセスは通常、多くの時間はかかりません。



MainPage.xamlに2つのボタンを追加して、最も単純なインターフェイスを実装します。



<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <Button Width="250" Height="50" Content="Choose Audio File" Click="ChooseFile_Click" Margin="0,10,0,10"/> <Button Width="250" Height="50" Background="Green" Content="Build And Save Image File" Click="BuildAndSaveImageFile_Click" Margin="0,10,0,10"/> </StackPanel>
      
      





[オーディオファイルの選択]ボタンをクリックして、実行されるアルゴリズムを説明します。 これを行うには、MainPage.xaml.csに次の行を追加します。



  private StorageFile currentFile; private PlottingGraphImg imgFile; private async void ChooseFile_Click(object sender, RoutedEventArgs e) { var picker = new Windows.Storage.Pickers.FileOpenPicker(); picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.MusicLibrary; picker.FileTypeFilter.Add(".mp4"); picker.FileTypeFilter.Add(".mp3"); picker.FileTypeFilter.Add(".wav"); StorageFile file = await picker.PickSingleFileAsync(); await ConvertToWaveFile(file); } public async Task ConvertToWaveFile(StorageFile sourceFile) { MediaTranscoder transcoder = new MediaTranscoder(); MediaEncodingProfile profile = MediaEncodingProfile.CreateWav(AudioEncodingQuality.Medium); CancellationTokenSource cts = new CancellationTokenSource(); //Create temporary file in temporary folder string fileName = String.Format("TempFile_{0}.wav", Guid.NewGuid()); StorageFile temporaryFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync(fileName); currentFile = temporaryFile; if (sourceFile == null || temporaryFile == null) { return; } try { var preparedTranscodeResult = await transcoder.PrepareFileTranscodeAsync(sourceFile, temporaryFile, profile); if (preparedTranscodeResult.CanTranscode) { var progress = new Progress<double>((percent) => { Debug.WriteLine("Converting file: " + percent + "%"); }); await preparedTranscodeResult.TranscodeAsync().AsTask(cts.Token, progress); } else { Debug.WriteLine("Error: Convert fail"); } } catch { Debug.WriteLine("Error: Exception in ConvertToWaveFile"); } }
      
      





:同様の方法をビデオ形式に使用することもできますが、これも非常に便利です。



カスタムwavファイルが使用可能になったので、そこからオーディオデータを取得する必要があります。 これを行うには、ファイル構造を詳しく調べる必要があります。 理論を深く掘り下げてみると、理由はわかりません。インターネット上のそのような情報は十分すぎるほどで、興味のある情報をいつでも入手できます。 さらなる実装のロジックを明確にするために、ファイルの一般的な構造を概説するように制限しています。



したがって、パルス符号変調(PCM)形式の非圧縮オーディオファイルの構造は次のとおりです。









それでは、オーディオデータを取得するアルゴリズムに移りましょう。 作業では、 次のリソースに依存します



シンボル名「WavFile」でクラスを説明しましょう:



  public class WavFile { public string PathAudioFile { get; } private const int ticksInSecond = 10000000; private TimeSpan duration; public TimeSpan Duration { get { return duration; } } #region AudioData private List<float> floatAudioBuffer = new List<float>(); #endregion public WavFile(string _path) { PathAudioFile = _path; ReadWavFile(_path); } public float[] GetFloatBuffer() { return floatAudioBuffer.ToArray(); } private void ReadWavFile(string filename) { try { using (FileStream fileStream = File.Open(filename, FileMode.Open)) { BinaryReader reader = new BinaryReader(fileStream); // RIFF int chunkID = reader.ReadInt32(); int fileSize = reader.ReadInt32(); int riffType = reader.ReadInt32(); // Format int fmtID; long _position = reader.BaseStream.Position; while (_position != reader.BaseStream.Length - 1) { reader.BaseStream.Position = _position; int _fmtId = reader.ReadInt32(); if (_fmtId == 544501094) { fmtID = _fmtId; break; } _position++; } int fmtSize = reader.ReadInt32(); int fmtCode = reader.ReadInt16(); int channels = reader.ReadInt16(); int sampleRate = reader.ReadInt32(); int byteRate = reader.ReadInt32(); int fmtBlockAlign = reader.ReadInt16(); int bitDepth = reader.ReadInt16(); if (fmtSize == 18) { int fmtExtraSize = reader.ReadInt16(); reader.ReadBytes(fmtExtraSize); } int dataID = reader.ReadInt32(); int dataSize = reader.ReadInt32(); byte[] byteArray = reader.ReadBytes(dataSize); int bytesInSample = bitDepth / 8; int sampleAmount = dataSize / bytesInSample; float[] tempArray = null; switch (bitDepth) { case 16: Int16[] int16Array = new Int16[sampleAmount]; System.Buffer.BlockCopy(byteArray, 0, int16Array, 0, dataSize); IEnumerable<float> tempInt16 = from i in int16Array select i / (float)Int16.MaxValue; tempArray = tempInt16.ToArray(); break; default: return; } floatAudioBuffer.AddRange(tempArray); duration = DeterminateDurationTrack(channels, sampleRate); } } catch { Debug.WriteLine("File error"); return; } } private TimeSpan DeterminateDurationTrack(int channels, int sampleRate) { long _duration = (long)(((double)floatAudioBuffer.Count / sampleRate / channels) * ticksInSecond); return TimeSpan.FromTicks(_duration); } }
      
      





:この方法により、オーディオデータを視覚化するだけでなく、編集することもできます。 ところで、データを保存する逆のプロセスを整理することは難しくありません。このため、FileStreamクラスを再度使用して、受信したデータをファイルに順次書き込むだけで十分です。



:アルゴリズムを説明するときに、上記のwavファイルの構造から逸脱していることに気づいたと思います。 これは、変換によって取得されたファイルの構造がわずかに異なるためです。 特に、重要ではないセクションが追加されます。ID、サイズ、および形式の情報を格納し、その後にゼロバイトのシーケンスが続きます。 ループ内のバイトを並べ替えて値544501094と比較することにより、不要なセクションはスキップされます(これは、Subchunk1Idフィールドの必要な値であり、そこからFormatセクションが始まります)。 上記の構造は例示的であるが必須ではなく、時にはそれから逸脱するという理由で、同様の検索が必要です。



最後に、データを受信した後、最終ステップであるスケジュールの作成に進むことができます。 いくつかの方法がありますが、ここでは最も一般的な2つを示します。



  1. 幾何学的図形を使用した画像の構築。
  2. ビットマップを作成します。


それらについて詳しく見ていきましょう。 リソースコストにもかかわらず、画像を構築する最初の方法は、オーディオトラックでの作業の特定の例を示すときにWebで人気があります。 小さなグラフを作成するのは悪くないかもしれませんが、画面上にオーディオトラック全体の波の画像を表示することになると不合理になります。 したがって、このオプションは適していません。



2番目の方法は、リソースをあまり消費せず、安全でないコードの使用を伴います。 作業の結果に基づいて、アプリケーションで使用できるイメージを取得します。たとえば、将来使用するためにハードディスクに保存します。



UWPでポイントイメージを操作する方法については、 こちらをご覧ください 。 オーディオファイルのイメージを構築するためのロジックを説明するために、主にこの資料に基づきます。



上記の構造とクラスをプロジェクトに追加します。



 public struct GraphicalWavePlot { private float minValue; private float maxValue; private float peakValue; public GraphicalWavePlot( float minValue, float maxValue, float peakValue ) { this.minValue = minValue; this.maxValue = maxValue; this.peakValue = peakValue; } public bool CheckArea(int pos, int heightImg) { double Oh = heightImg / 2; double y0 = Oh - Math.Abs(minValue) * Oh / peakValue; double y1 = Oh + maxValue * Oh / peakValue; return (pos > y0 && pos < y1); } } public class PlottingGraphImg { private List<GraphicalWavePlot> waveSamples = new List<GraphicalWavePlot>(); private SoftwareBitmap softwareBitmap; private WavFile wavFile; private Color backgroundColor = Color.FromArgb(0, 0, 0, 0); public Color BackgroundColor { get { return backgroundColor; } set { backgroundColor = value; } } private Color foregroundColor = Color.FromArgb(255, 255, 255, 255); public Color ForegroundColor { get { return foregroundColor; } set { foregroundColor = value; } } private int image_width; public int ImageWidth { get { return image_width; } set { image_width = value; } } private int image_height; public int ImageHeight { get { return image_height; } set { image_height = value; } } public PlottingGraphImg(WavFile _wavFile, int _image_width, int _image_height) { this.wavFile = _wavFile; this.image_width = _image_width; this.image_height = _image_height; BuildImage(); CreateGraphicFile(); } private void BuildImage() { int xPos = 2; int interval = 1; var yScale = ImageHeight; float[] readBuffer = wavFile.GetFloatBuffer(); int samplesPerPixel = readBuffer.Length / ImageWidth; float negativeLimit = readBuffer.Take(readBuffer.Length).Min(); float positiveLimit = readBuffer.Take(readBuffer.Length).Max(); float peakValue = (positiveLimit > negativeLimit) ? (positiveLimit) : (negativeLimit); peakValue *= 1.2f; for (int i = 0; i < readBuffer.Length; i += samplesPerPixel, xPos += interval) { float[] partBuffer = new float[samplesPerPixel]; int lengthPartBuffer = ((i + samplesPerPixel) > readBuffer.Length) ? (readBuffer.Length - i) : (samplesPerPixel); Array.Copy(readBuffer, i, partBuffer, 0, lengthPartBuffer); var min = partBuffer.Take(samplesPerPixel).Min(); var max = partBuffer.Take(samplesPerPixel).Max(); waveSamples.Add(new GraphicalWavePlot(minValue: min, maxValue: max, peakValue: peakValue)); } } [ComImport] [Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] unsafe interface IMemoryBufferByteAccess { void GetBuffer(out byte* buffer, out uint capacity); } public unsafe void CreateGraphicFile() { softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, ImageWidth, ImageHeight); using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write)) { using (var reference = buffer.CreateReference()) { byte* dataInBytes; uint capacity; ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity); // Fill-in the BGRA plane BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0); for (int i = 0; i < bufferLayout.Width; i++) { for (int j = 0; j < bufferLayout.Height; j++) { Color tempColor = waveSamples[i].CheckArea(j, ImageHeight) ? ForegroundColor : BackgroundColor; //Blue dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * j + 4 * i + 0] = (byte)tempColor.B; //Green dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * j + 4 * i + 1] = (byte)tempColor.G; //Red dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * j + 4 * i + 2] = (byte)tempColor.R; //Alpha dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * j + 4 * i + 3] = (byte)tempColor.A; } } } } } public async Task SaveGraphicFile(StorageFile outputFile) { using (IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite)) { BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream); encoder.SetSoftwareBitmap(softwareBitmap); encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant; encoder.IsThumbnailGenerated = true; try { await encoder.FlushAsync(); } catch (Exception err) { switch (err.HResult) { case unchecked((int)0x88982F81): //WINCODEC_ERR_UNSUPPORTEDOPERATION // If the encoder does not support writing a thumbnail, then try again // but disable thumbnail generation. encoder.IsThumbnailGenerated = false; break; default: throw err; } } if (encoder.IsThumbnailGenerated == false) { await encoder.FlushAsync(); } } } }
      
      





:プロジェクトのプロパティで安全でないコードを有効にすることを忘れないでください(プロジェクトを右クリックし、表示されるコンソールメニューで[プロパティ]を選択し、開いたウィンドウで[ビルド]タブをアクティブにし、[安全でないコードを許可]フィールドをチェックします。以下を参照)。















そこで、オーディオデータの受信とプロットのスキームを検討しました。 あとは、MainPageクラスから呼び出すだけです。 これを行うには、「画像ファイルのビルドと保存」ボタンをクリックする必要があります。このボタンをクリックすると、指定された操作アルゴリズムが起動されます。 それを実装するには、前にMainPage.xaml.csに入力した行に上記のメソッドを追加します。



  private async void BuildAndSaveImageFile_Click(object sender, RoutedEventArgs e) { WavFile wavFile = new WavFile(currentFile.Path.ToString()); imgFile = new PlottingGraphImg(wavFile, 1000, 100); FileSavePicker fileSavePicker = new FileSavePicker(); fileSavePicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary; fileSavePicker.FileTypeChoices.Add("JPEG files", new List<string>() { ".jpg" }); fileSavePicker.SuggestedFileName = "image"; var outputFile = await fileSavePicker.PickSaveFileAsync(); if (outputFile == null) { // The user cancelled the picking operation return; } await imgFile.SaveGraphicFile(outputFile); }
      
      





エラーと追加されていないライブラリについてプロジェクトをチェックし、実行します。 作業の結果、選択したオーディオファイルに対してこのような画像が得られます。










All Articles