Предлагаем вниманию читателей продолжение статьи от наших партнеров, Music Paradise. В прошлый раз команда представила туториал по извлечению аудиоданных из wav-файлов; сегодня речь пойдет о том, как использовать этот функционал в более широком контексте — при разработке полноценного аудиоредактора со стандартным набором функций.


«В предыдущей статье мы рассмотрели процесс извлечения аудиоданных и даже смогли построить график на их основе. Однако никаких изменений в аудиоданные мы не вносили и, соответственно, необходимости сохранять аудиофайл не возникало. Мы лишь отметили, что процесс сохранения обратен чтению. Поэтому, чтобы не быть голословными, мы решили подкрепить слова делом и рассмотреть полный цикл работы с аудиофайлом. Целесообразность этого начинания подтверждается многочисленными вопросами на эту тему на просторах сети Интернет, причем большинство из них так и остается открытыми.

Задавшись целью до конца пролить свет на эту проблему и дать готовое решение для работы с аудиоданными, мы опишем процесс создания нехитрого приложения в арсенале которого будут все основные операции по редактированию аудиофайла: копировать, вырезать, вставить, удалить и, конечно, сохранить полученный результат на устройстве. Ради последовательности изложения материала и простоты восприятия реализацию части функционала мы возьмем из предыдущей статьи, предварительно подвергнув код некоторым изменениям.

Не теряя времени, сразу переносим в наш новый проект уже знакомые нам структуру GraphicalWavePlot и класс PlottingGraphImg, но в последний добавляем метод:

public async Task<SoftwareBitmapSource> GetImage() { 
            if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 || 
softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight) 
            { 
                softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8,
 BitmapAlphaMode.Premultiplied); 
            } 
            var source = new SoftwareBitmapSource(); 
            await source.SetBitmapAsync(softwareBitmap); 
            return source; 
        }

Он нам потребуется для контроля визуального состояния аудиодорожки.

Помимо добавленного метода не забудьте поменять цвет отрисовки волны. В предыдущей статье мы использовали белый, но здесь это не лучшая идея — на белом фоне построенный график будет неразличим.

Еще одному известному нам классу WavFile предстоит претерпеть множественные изменения. Мы должны привести его к следующему виду:

Развернуть
public class WavFile 
    { 
        public string PathAudioFile { get; } 
        public string FileName { get; } 
        private TimeSpan duration; 
        public TimeSpan Duration { get { return duration; } } 
        private const int ticksInSecond = 10000000; 
        #region HeadData 
        int headSize; 

        int chunkID; 
        int fileSize; 
        int riffType; 

        int fmtID; 
        int fmtSize; 
        int fmtCode; 

        int channels; 
        int sampleRate; 
        int byteRate; 
        int fmtBlockAlign; 
        int bitDepth; 
        int fmtExtraSize; 

        int dataID; 
        int dataSize; 

        public int Channels { get { return channels; } } 
        public int SampleRate { get { return sampleRate; } } 
        #endregion 
        #region AudioData 
        private List<float> floatAudioBuffer = new List<float>(); 
        #endregion 
        public WavFile(string _path) 
        { 
            PathAudioFile = _path; 
            FileName = Path.GetFileName(PathAudioFile); 
            ReadWavFile(_path); 
        } 
        public float[] GetFloatBuffer() 
        { 
            return floatAudioBuffer.ToArray(); 
        } 
        public void SetFloatBuffer(float[] _buffer) 
        { 
            floatAudioBuffer.Clear(); 
            floatAudioBuffer.AddRange(_buffer); 
            CalculateDurationTrack(); 
            CalculateDataSize(); 
            CalculateFileSize(); 
        } 
        public void CalculateDurationTrack() => duration = 
TimeSpan.FromTicks((long)(((double)floatAudioBuffer.Count / SampleRate / Channels) * 
ticksInSecond)); 
        public void CalculateDataSize() => dataSize = floatAudioBuffer.Count * sizeof(Int16); 
        public void CalculateFileSize() => fileSize = headSize + dataSize; 

        void ReadWavFile(string filename) 
        { 
            try 
            { 
                using (FileStream fileStream = File.Open(filename, FileMode.Open)) 
                { 
                    BinaryReader reader = new BinaryReader(fileStream); 
                    chunkID = reader.ReadInt32(); 
                    fileSize = reader.ReadInt32(); 
                    riffType = reader.ReadInt32(); 

                    long _position = reader.BaseStream.Position; 
                    int zeroChunkSize = 0; 
                    while (_position != reader.BaseStream.Length - 1) 
                    { 
                        reader.BaseStream.Position = _position; 
                        int _fmtId = reader.ReadInt32(); 
                        if (_fmtId == 544501094) 
                        { 
                            fmtID = _fmtId; 
                            break; 
                        } 
                        else 
                        { 
                            _position++; 
                            zeroChunkSize++; 
                        } 
                    } 

                    fmtSize = reader.ReadInt32(); 
                    fmtCode = reader.ReadInt16(); 
                    channels = reader.ReadInt16(); 
                    sampleRate = reader.ReadInt32(); 
                    byteRate = reader.ReadInt32(); 
                    fmtBlockAlign = reader.ReadInt16(); 
                    bitDepth = reader.ReadInt16(); 
                    if (fmtSize == 18) 
                    { 
                        fmtExtraSize = reader.ReadInt16(); 
                        reader.ReadBytes(fmtExtraSize); 
                    } 
                    dataID = reader.ReadInt32(); 
                    dataSize = reader.ReadInt32(); 
                    
                    headSize = fileSize - dataSize - zeroChunkSize; 
                    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); 
                    CalculateDurationTrack(); 
                } 
            } 
            catch 
            { 
                Debug.WriteLine("File error"); 
            } 
        } 
        public void WriteWavFile() 
        { 
            WriteWavFile(PathAudioFile); 
        } 
        public void WriteWavFile(string filename) 
        { 
            using (FileStream fs = File.Create(filename)) 
            { 
                fs.Write(BitConverter.GetBytes(chunkID), 0, sizeof(Int32)); 
                fs.Write(BitConverter.GetBytes(fileSize), 0, sizeof(Int32)); 
                fs.Write(BitConverter.GetBytes(riffType), 0, sizeof(Int32)); 

                fs.Write(BitConverter.GetBytes(fmtID), 0, sizeof(Int32)); 
                fs.Write(BitConverter.GetBytes(fmtSize), 0, sizeof(Int32)); 
                fs.Write(BitConverter.GetBytes(fmtCode), 0, sizeof(Int16)); 

                fs.Write(BitConverter.GetBytes(channels), 0, sizeof(Int16)); 
                fs.Write(BitConverter.GetBytes(sampleRate), 0, sizeof(Int32)); 
                fs.Write(BitConverter.GetBytes(byteRate), 0, sizeof(Int32)); 
                fs.Write(BitConverter.GetBytes(fmtBlockAlign), 0, sizeof(Int16)); 
                fs.Write(BitConverter.GetBytes(bitDepth), 0, sizeof(Int16)); 
                if (fmtSize == 18) 
                    fs.Write(BitConverter.GetBytes(fmtExtraSize), 0, sizeof(Int16)); 

                fs.Write(BitConverter.GetBytes(dataID), 0, sizeof(Int32)); 
                
                float[] audioBuffer; 
                audioBuffer = floatAudioBuffer.ToArray(); 

                fs.Write(BitConverter.GetBytes(dataSize), 0, sizeof(Int32)); 

                // Add Audio Data to wav file 
                byte[] byteBuffer = new byte[dataSize]; 

                Int16[] asInt16 = new Int16[audioBuffer.Length]; 

                IEnumerable<Int16> temp = 
                                from g in audioBuffer 
                                select (Int16)(g * (float)Int16.MaxValue); 
                asInt16 = temp.ToArray(); 
                Buffer.BlockCopy(asInt16, 0, byteBuffer, 0, dataSize); 
                fs.Write(byteBuffer, 0, dataSize); 
            } 
        } 
    }


Невозможно не заметить перегружаемый метод WriteWavFile. Благодаря ему наш класс умеет теперь не только получать аудиоданные, но и сохранять их в доступный для работы файл. Теперь вы можете сами убедиться: мы не лукавили, когда говорили о том, что процесс записи обратен чтению. Здесь используется все тот же FileStream, только на этот раз для записи. Стало также понятно, почему поля, необходимые для хранения данных структуры файла, стали общедоступными внутри текущего класса. Обращаем ваше внимание на то, что над некоторыми из них — duration, dataSize, fileSize — необходимо осуществлять контроль. Для нахождения fileSize, мы не стали усложнять алгоритм и использовали самый простой вариант вычисления.

Это все фрагменты, которые мы можем перенести из предыдущей статьи, остальное будем писать с нуля. Пойдем по порядку.

Добавим в наш проект класс AudioDataEditor. Заметьте, что он является производным от WavFile:

Развернуть
 class AudioDataEditor:WavFile 
    { 
        private List<float> audioBufferInMemory = new List<float>(); 
        public List<float> AudioBufferInMemory 
        { 
            get { return audioBufferInMemory; } 
            set { audioBufferInMemory = value; } 
        } 
        public AudioDataEditor(string path) : base(path){ } 
        private void SetExactPosition(ref double startPosition, ref double endPosition,
 int length) { 
            SetExactPosition(ref startPosition, length); 
            SetExactPosition(ref endPosition, length); 
        } 
        private void SetExactPosition(ref double position, int length) 
        { 
            position = (int)(length * position); 

            if (Channels == 2) 
            { 
                if (position % 2 == 0) 
                { 
                    if (position + 1 >= length) 
                        position--; 
                    else 
                        position++; 
                } 
            } 
        } 
        public void Copy(double relativeStartPos, double relativeEndPos) 
        { 
            float[] audioData = GetFloatBuffer(); 
            AudioBufferInMemory.Clear(); 
            double startPosition = relativeStartPos; 
            double endPosition = relativeEndPos; 
            SetExactPosition(ref startPosition, ref endPosition, audioData.Length); 
            float[] temp = new float[(int)(endPosition - startPosition)]; 
            Array.Copy(audioData.ToArray(), (int)startPosition, temp, 0, temp.Length); 
            AudioBufferInMemory.AddRange(temp); 
        } 
        public void Cut(double relativeStartPos, double relativeEndPos) 
        { 
            Copy(relativeStartPos, relativeEndPos); 
            Delete(relativeStartPos, relativeEndPos); 
        } 
        public void Paste(double relativeStartPos) { 
            if (AudioBufferInMemory.Count > 0) 
            { 
                List<float> temp = new List<float>(); 
                temp.AddRange(GetFloatBuffer()); 
                double startPosition = relativeStartPos; 
                SetExactPosition(ref startPosition, temp.Count); 
                temp.InsertRange((int)startPosition, AudioBufferInMemory); 
                SetFloatBuffer(temp.ToArray()); 
                WriteWavFile(); 
            } 
        } 
        public void Delete(double relativeStartPos, double relativeEndPos) 
        { 
            List<float> _temp = new List<float>(); 
            _temp.AddRange(GetFloatBuffer()); 
            double startPosition = relativeStartPos; 
            double endPosition = relativeEndPos; 
            SetExactPosition(ref startPosition, ref endPosition, _temp.Count); 
            _temp.RemoveRange((int)startPosition, (int)(endPosition - startPosition)); 
            SetFloatBuffer(_temp.ToArray()); 
            WriteWavFile(); 
        } 
    }


В классе AudioDataEditor реализованы те операции, о которых мы говорили в начале; их реализация не покажется вам сложной при детальном изучении.

И каждый процесс изменения аудиоданных, сопровождается их сохранением в текущем файле. Отметим лишь то, что при работе с двухканальными аудиоданными стоит быть внимательнее с чередующимися семплами.

Итак, отчасти мы реализовали основной механизм работы нашего приложения. Но для полноценного функционирования этого мало, необходим интерфейс, способный корректно управлять приложением. Тем более, для редактирования и совершения основных операций с аудиодорожкой требуется совершенный механизм, по примеру современных редакторов, а не просто ввод начала и конца копируемого или удаляемого промежутка. Здесь нам и пригодится отображение аудио волны, полученное ранее. Посредством выделения ее фрагмента мы будем отмечать нужные отрезки.

Механизм выбора фрагмента аудио дорожки мы реализуем через создание собственного элемента интерфейса. Осуществляем следующие шаги: через контекстное меню выберем Add > NewItem…, в появившемся окне выбираем UserControl и даем ему название GraphicsEditor, после чего нажмем кнопку Add.



Созданный GraphicsEditor.xaml в результате изменений принял следующий вид:

<UserControl 
    x:Class="AudioEditorTest.GraphicsEditor" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="using:AudioEditorTest" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" 
    Height="Auto" 
    Width="Auto"> 
    <Grid> 
        <Image Name="BackgroundImage" Stretch="Fill"/> 
        <Canvas Name="MainContainer" Background="#0001FFFF"> 
            <Rectangle Name="SelectedChunk" Visibility="Collapsed" Height="150" Width="50" Fill="#19000000"/> 
        </Canvas> 
    </Grid> 
</UserControl>


GraphicsEditor.xaml.cs:

Развернуть

public sealed partial class GraphicsEditor : UserControl 
    { 
        public GraphicsEditor() 
        { 
            this.InitializeComponent(); 
            DataContext = this; 
            MainContainer.PointerPressed += new PointerEventHandler(PointerPressed); 
            MainContainer.PointerMoved += new PointerEventHandler(PointerMoved); 
            MainContainer.PointerReleased += new PointerEventHandler(PointerReleased); 
            MainContainer.PointerExited += new PointerEventHandler(PointerReleased); 
        } 

        private double leftPos; 
        private double rightPos; 
        private double minSelectionWidth = 10; 

        private bool isPressed = false; 
        private double startSelectionPosition = 0; 
        private double currentSelectionWidth = 0; 
        public event EventHandler SelectionChanged = delegate { }; 
        private bool enableSelection = false; 
        public bool EnableSelection { 
            get { return enableSelection; } 
            set 
            { 
                enableSelection = value; 
            } 
        } 
        private SoftwareBitmapSource imageSource; 
        public SoftwareBitmapSource ImageSource { 
            get { return imageSource; } 
            set { 
                imageSource = value; 
                BackgroundImage.Source = value; 
            } 
        } 
        public double LeftPos 
        { 
            get 
            { 
                return leftPos; 
            } 
            set 
            { 
                leftPos = value; 
                SetRightSelectionMargin(); 
            } 
        } 
        public double RelativeLeftPos 
        { 
            get { return leftPos / ActualWidth; } 
        } 

        public double RightPos 
        { 
            get 
            { 
                return rightPos; 
            } 
            set 
            { 
                rightPos = value; 
                SetRightSelectionMargin(); 
            } 
        } 
        public double RelativeRightPos { 
            get { return rightPos / ActualWidth; } 
        } 
        private void SetRightSelectionMargin() { 
            currentSelectionWidth = Math.Abs(rightPos - leftPos); 
            ManageGraphics(); 
        } 
        private void ManageGraphics() { 
            SelectedChunk.SetValue(Canvas.LeftProperty, LeftPos); 
            SelectedChunk.Width = currentSelectionWidth; 
        } 
        private void PointerReleased(object sender, PointerRoutedEventArgs e) 
        { 
            isPressed = false; 
            if (Math.Abs(startSelectionPosition - GetPointerPositionX(e)) < minSelectionWidth) { 
                EnableSelection = false; 
            } 
        } 
        private void PointerMoved(object sender, PointerRoutedEventArgs e) 
        { 
            if (isPressed) 
            { 
                EnableSelection = true; 
                double xPosition = GetPointerPositionX(e); 
                currentSelectionWidth = Math.Abs(startSelectionPosition - xPosition); 
                
                if (currentSelectionWidth > minSelectionWidth) 
                { 

                    if (xPosition < startSelectionPosition) 
                    { 
                        if (xPosition < 0) 
                        { 
                            LeftPos = 0; 
                        } 
                        else if (xPosition < RightPos - minSelectionWidth) 
                        { 
                            LeftPos = xPosition; 
                        } 
                        RightPos = startSelectionPosition; 
                    } 
                    else if (xPosition > startSelectionPosition) 
                    { 
                        if (xPosition > this.ActualWidth) 
                        { 
                            RightPos = this.ActualWidth; 
                        } 
                        else if (xPosition > LeftPos + minSelectionWidth) 
                        { 
                            RightPos = xPosition; 
                        } 
                        LeftPos = startSelectionPosition; 
                    } 
                } 
            } 
        } 
        private void PointerPressed(object sender, PointerRoutedEventArgs e) 
        { 
            SelectedChunk.Visibility = Visibility.Visible; 

            isPressed = true; 
            startSelectionPosition = GetPointerPositionX(e); 

            LeftPos = startSelectionPosition; 
            RightPos = LeftPos + 2; 
        } 
        private double GetPointerPositionX(PointerRoutedEventArgs e) { 
            PointerPoint pt = e.GetCurrentPoint(MainContainer); 
            Point position = pt.Position; 
            return position.X; 
        } 
    }


Коротко о работе этого элемента: GraphicsEditor позволяет отслеживать положение курсора мыши на экране и реагировать на клик, в качестве результата изменяя параметры Rectangle, который, в свою очередь, при наложении на отрисованную волну позволяет визуально выделить произвольный фрагмент аудиодорожки.

Все готово, чтобы обьединить наши наработки в файлах главной страницы приложения. В MainPage.xaml.cs объявим переменные ссылочного типа:

  private StorageFile currentFile, sourceFile; 
        private PlottingGraphImg imgFile; 
        private AudioDataEditor editor;

Повторим уже реализованные в предыдущей статье методы:

private async Task OpenFileDialog() { 
            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"); 

            sourceFile  = await picker.PickSingleFileAsync(); 
            if (sourceFile == null) await OpenFileDialog(); 
        } 

public async Task ConvertToWaveFile() 
        { 
            OpenLoadWindow(true); 
            MediaTranscoder transcoder = new MediaTranscoder(); 
            MediaEncodingProfile profile = 
MediaEncodingProfile.CreateWav(AudioEncodingQuality.Medium); 
            CancellationTokenSource cts = new CancellationTokenSource(); 

            string fileName = String.Format("{0}_{1}.wav", sourceFile.DisplayName, 
Guid.NewGuid()); 
            currentFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync(fileName); 
            Debug.WriteLine(currentFile.Path.ToString()); 
            try 
            { 
                var preparedTranscodeResult = await transcoder.PrepareFileTranscodeAsync(sourceFile, currentFile, 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"); 
            } 
            OpenLoadWindow(false); 
        }

Скрывать пользовательский интерфейс во время конвертирования аудиофайла будем строчкой:

private void OpenLoadWindow(bool enable) => LoadDummy.Visibility = enable ? Visibility.Visible : Visibility.Collapsed;

Для воспроизведения аудио трека, используем логику:

private async Task SetAudioClip(StorageFile file) { 
            var stream = await file.OpenReadAsync(); 
            Player.SetSource(stream, ""); 
        } 
private void Playback_Click(object sender, RoutedEventArgs e) 
        { 
            if (Player.CurrentState == MediaElementState.Playing) 
            { 
                Player.Stop(); 
            } 
            else 
            { 
                Player.Play(); 
            } 
        } 
private void Player_CurrentStateChanged(object sender, RoutedEventArgs e) => PlayBtn.IsChecked = Player.CurrentState == MediaElementState.Playing;

Обновляем контент при помощи методов:

private async Task BuildImageFile() 
        { 
            editor = new AudioDataEditor(currentFile.Path.ToString()); 
            await Update(); 
        } 
        private async Task Update() { 
            imgFile = new PlottingGraphImg(editor, 
(int)AudioEditorControl.ActualWidth, (int)AudioEditorControl.ActualHeight); 
            AudioEditorControl.ImageSource = await imgFile.GetImage(); 
            await SetAudioClip(currentFile); 
        }

При старте приложения или при попытке подгрузить новый трек будет срабатывать метод:

private async void LoadAudioFile(object sender, RoutedEventArgs e) 
        { 
            await OpenFileDialog(); 
            await ConvertToWaveFile(); 
            await BuildImageFile(); 
        }

Теперь остаются вызовы операций: копирование, вырезать, вставить, удалить.

 private void Copy_Click(object sender, RoutedEventArgs e) 
        { 
          if (AudioEditorControl.EnableSelection)  
editor.Copy(AudioEditorControl.RelativeLeftPos, AudioEditorControl.RelativeRightPos); 
        } 
private void Cut_Click(object sender, RoutedEventArgs e) 
        { 
            if (AudioEditorControl.EnableSelection) 
editor.Cut(AudioEditorControl.RelativeLeftPos, AudioEditorControl.RelativeRightPos); 
            Update(); 
        } 
private void Paste_Click(object sender, RoutedEventArgs e) 
        { 
            editor.Paste(AudioEditorControl.RelativeLeftPos); 
            Update(); 
        } 
private void Delete_Click(object sender, RoutedEventArgs e) 
        { 
            if (AudioEditorControl.EnableSelection) 
editor.Delete(AudioEditorControl.RelativeLeftPos, AudioEditorControl.RelativeRightPos); 
            Update(); 
        }

Переходим к файлу MainPage.xaml и реализуем следующий интерфейс:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 
        <Grid> 
            <Grid.RowDefinitions> 
                <RowDefinition Height="Auto"/> 
                <RowDefinition Height="*"/> 
                <RowDefinition Height="Auto"/> 
            </Grid.RowDefinitions> 
            <StackPanel HorizontalAlignment="Left" VerticalAlignment="Center" 
Orientation="Horizontal"> 
                <Button x:Name="OpenFileBtn" FontFamily="Segoe MDL2 Assets" FontSize="36" 
Content="&# xE8E5;" Background="White" Margin="5" Click="LoadAudioFile"/> 
                <Rectangle Height="50" Width="2" Fill="Black"/> 
                <ToggleButton x:Name="PlayBtn" FontFamily="Segoe MDL2 Assets" FontSize="36" 
Content="&# xE768;" Background="White" Margin="5" Click="Playback_Click"/> 
            </StackPanel> 
            <StackPanel Grid.Row="1" Orientation="Vertical" VerticalAlignment="Center"> 
                    <local:GraphicsEditor x:Name="AudioEditorControl" Height="150"/> 
            </StackPanel> 
            <StackPanel Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center" 
Orientation="Horizontal"> 
                <Button x:Name="CopyBtn" FontFamily="Segoe MDL2 Assets" FontSize="36" 
Content="&# xE8C8;" Background="White" Margin="5" Click="Copy_Click"/> 
                <Button x:Name="CutBtn" FontFamily="Segoe MDL2 Assets" FontSize="36" 
Content="&# xE8C6;" Background="White" Margin="5" Click="Cut_Click"/> 
                <Button x:Name="PasteBtn" FontFamily="Segoe MDL2 Assets" FontSize="36" 
Content="&# xE77F;" Background="White" Margin="5" Click="Paste_Click"/> 
                <Button x:Name="DeleteBtn" FontFamily="Segoe MDL2 Assets" FontSize="36" 
Content="&# xE74D;" Background="White" Margin="5" Click="Delete_Click"/> 
                <MediaElement Name="Player" AutoPlay="False" 
CurrentStateChanged="Player_CurrentStateChanged" /> 
            </StackPanel> 
        </Grid> 
        <Grid x:Name="LoadDummy" Visibility="Collapsed" Background="#7F000000"> 
            <ProgressRing IsActive="True" Width="100" Height="100" Foreground="White"/> 
        </Grid> 
    </Grid>


Отлично! Наше приложение готово, осталось добавить отсутствующие библиотеки и проверить код на наличие ошибок. Мы с вами создали простейший аудио редактор, но даже в нем возможны эксперименты с личными аудиофайлами. Схожая логика реализована в различных наших проектах. Скажем, Audio Editor несет схожий функционал, но его преимуществом является работа с каждым каналом аудиофайла по отдельности. Другое приложение, Audio Genesis, имеет отличие в числе обрабатываемых аудио дорожек одновременно и возможности свести их в один трек. Каждое из разработанных приложений имеет свою область применения, возможно, и вам придет в голову идея уникального инструмента».
Поделиться с друзьями
-->

Комментарии (1)


  1. Smolski
    14.06.2017 09:05

    Интересно. Но я обычно юзаю cubase, за статью спасибо.