Яндекс.Метрика

    Звуки музыки

    Звуки музыки

    Создание задач Redmine голосом


    Как-то недавно с коллегами обсуждали возможность быстрого добавления задач в redmine. Среди немногих, а точнее единственным предложенным вариантом стало определение текста задачи по голосу. А мне стало интересно – смогу я склеить эту ‘балалайку’ за выходные?

    Далее опишу основные моменты и что в итоге получилось.

    Записываем во Flac


    И так, мне потребовалось: Dot NET, Google Speech-Api, кодек Flac, Redmine API .NET.
    Использование speech-api освещается в интернетах в т.ч. и на хабре, для этого необходимо просто отправить POST запрос со звуковым файлом, в ответ получите JSON объект:

    Строка запроса:
    http://www.google.com/speech-api/v1/recognize?lang=ru&client=chromium

    Ответ:
    {status:int, id:string, hypotheses : [{utterance : string, confidence : double}]}


    Сложность заключается в записи файла такого формата, а именно – Flac, 16kHz, 16bit, mono. Получением pcm данных c wave-in интерфейса занимается (WaveLib господина Ianier Munoz) — полученные данные с буферов очереди отправляются в указанный callback, где записываются в кольцевой буфер (ring buffer), перед созданием обработчика записи создаётся тред для извлечения данных из кольцевого буфера и их отправки в кодек. Кодек я обернул в C++/CLI библиотеке, в чём помог пример, прилагающийся к libflac:

    // Обратите внимание что сэмплы для flac имеют размер в 32bit
    
    // Создаётся Енкодер
    static 
    bool InitialiseEncoder(char* filepath, FILE** _file, FLAC__StreamEncoder** _encoder, FLAC__StreamMetadata** _metadata1, FLAC__StreamMetadata** _metadata2)
    {
    	FLAC__bool ok = true;
    	FLAC__StreamEncoder *encoder = 0;
    	FLAC__StreamEncoderInitStatus init_status;
    	FLAC__StreamMetadata *metadata[2];
    
    	unsigned sample_rate = 16000;
    	unsigned channels = 1;
    	unsigned bps = 16;
    
    	/* allocate the encoder */
    	if((encoder = FLAC__stream_encoder_new()) == NULL) {
    		return false;
    	}
    
    	ok &= FLAC__stream_encoder_set_verify(encoder, true);
    	ok &= FLAC__stream_encoder_set_compression_level(encoder, 5);
    	ok &= FLAC__stream_encoder_set_channels(encoder, channels);
    	ok &= FLAC__stream_encoder_set_bits_per_sample(encoder, bps);
    	ok &= FLAC__stream_encoder_set_sample_rate(encoder, sample_rate);
    	ok &= FLAC__stream_encoder_set_total_samples_estimate(encoder, 0);
    
    	// В этот файл будет записан выход Енкодера
    	FILE* flacfile = _wfopen((wchar_t*)filepath, L"wb");
    	// Создаём Енкодер
    	if(ok) {
    		init_status = FLAC__stream_encoder_init_FILE(encoder, flacfile, progress_callback, /*client_data=*/NULL);
    		if(init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) {
    			fprintf(stderr, "ERROR: initializing encoder: %s\n", FLAC__StreamEncoderInitStatusString[init_status]);
    			ok = false;
    		}
    	}
    	
    	*_encoder = encoder;
    	*_file = flacfile;
    
    	return ok;
    }
    // Отправка pcm данных кодеку
    static 
    bool ProcessEncoder(FLAC__StreamEncoder *encoder, FLAC__byte* _pcm, size_t need)
    {
    	// С wave-in данные приходят в 16bit на семпл
    	FLAC__bool ok = true;
    	// На вход екодеру послупают 32bit сэмплы, здесь они просто копируются соблюдая последовательности
    	for(unsigned int i = 0; i < need*1; i++) {
    		pcm[i] = (FLAC__int32)(((FLAC__int16)(FLAC__int8)_pcm[2*i+1] << 8) | (FLAC__int16)_pcm[2*i]);
    	}
    	ok = FLAC__stream_encoder_process_interleaved(encoder, pcm, need);
    	return ok;
    }
    


    Далее был создан класс Recorder. Помимо создания экземпляра WaveInRecorder(менеджер wave-in устройства) создаётся тред для отправки pcm данных в Flac кодек из колцевого буфера:

    	private unsafe void DataArrived(IntPtr data, int size)
            {
    	    // Записываем pcm в кольцевой буфер
                cb.Upload(data.ToPointer(), size);
            }
    
            WaveLib.WaveInRecorder m_Recorder;
            VorbisEnc.FlacEncoder ve;
            VorbisEnc.CircleBuffer cb;
            IntPtr filepath;
    
            public unsafe Recorder(string tempfilepath)
            {
                cb = new VorbisEnc.CircleBuffer();
                ve = new VorbisEnc.FlacEncoder();
                ve.Initialise((sbyte*)System.Runtime.InteropServices.Marshal.StringToHGlobalUni(tempfilepath).ToPointer());
                
                // Тред для кодировщика
                System.Threading.Thread th = new System.Threading.Thread(EncodeData);
    
                WaveLib.WaveFormat fmt = new WaveLib.WaveFormat(16000, 16, 1);
                m_Recorder = new WaveLib.WaveInRecorder(-1, fmt, 4096, 4, new WaveLib.BufferDoneEventHandler(DataArrived));
    
                th.Start();
            }
    
            bool StopThread;
            public bool AllDone = false;
            public void Stop()
            {
                m_Recorder.Dispose();
                StopThread = true;
                cb.Dispose();
            }
            unsafe void EncodeData()
            {
                IntPtr datax = System.Runtime.InteropServices.Marshal.AllocHGlobal(4096);
                sbyte* data = (sbyte*)datax.ToPointer();
                while (!StopThread)
                {
                    System.Threading.Thread.Sleep(10);
    		// Извлекаем из кольца данные, если курсор записи приблизился к курсору
    		// чтения на 4 итерации
                    while (cb.getNeedForUpdate() < 4096 * 4)
                    {
                        cb.Download(data, 4096);
                        ve.Encode(data, 4096);
                    }
                }
                System.Runtime.InteropServices.Marshal.FreeHGlobal(datax);
                ve.Close();
    	    // Данные кодированы и записаны, файл закрыт
                AllDone = true;
            }
    


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

    Аудиофайл записывается одной функцией:
                string file = Path.GetTempFileName();
                Recorder rec = new Recorder(file);
                // Просто ждём пока записывается аудиофайл
                System.Threading.Thread.Sleep(seconds * 1000);
    	    // Останавливаем запись, закрываем файл
                rec.Stop();
    

    Далее передаётся на отправку в google:
                string result = WebUpload.UploadFileEx(flacpath, 									"http://www.google.com/speech-api/v1/recognize?lang=ru&client=chromium",
                     "file", "audio/x-flac; rate=16000", parameters, null);
    

    Функция отправки файла:
                Uri uri = new Uri(url);
    
                FileStream fileStream = new FileStream(uploadfile,
                                            FileMode.Open, FileAccess.Read);
      
                HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(uri);
                if (cookies != null)
                    webrequest.CookieContainer = cookies;
    	    // Лишние заголовки необходимо убрать
                webrequest.Headers.Clear();
                webrequest.ContentLength = fileStream.Length;
                webrequest.ContentType = contenttype;
                webrequest.Method = "POST";
    
                Stream requestStream = webrequest.GetRequestStream();
                byte[] buffer = new Byte[checked((uint)Math.Min(4096,
                                         (int)fileStream.Length))];
    	    // Записываем файл в поток Http запроса
                int bytesRead = 0;
                while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
                    requestStream.Write(buffer, 0, bytesRead);
    
                fileStream.Close();
    
                WebResponse response = webrequest.GetResponse();
                Stream s = response.GetResponseStream();
                StreamReader sr = new StreamReader(s);
    
                string resps = sr.ReadToEnd();
                response.Close();
    
    	    return resps;
    


    Redmine API

    В redmine есть Rest api через который без сложностей, используя логин – пароль добавляется задача

                    RedmineManager manager = new RedmineManager(Configuration.RedmineHost,
                        Configuration.RedmineUser, Configuration.RedminePassword);
    
                    // Задача
                    var newIssue = new Issue
                    {
                        Subject = Title,
                        Description = Description,
                        Project = new IdentifiableName() { Id = ProjectId },
                        Tracker = new IdentifiableName() { Id = TrackerId }
                    };
    		// Находим id текущего пользователя
                    User thisuser = (from u in manager.GetObjectList<User>(new System.Collections.Specialized.NameValueCollection())
                                     where u.Login == Configuration.RedmineUser
                                     select u).FirstOrDefault();
                    if (thisuser != null)
                        newIssue.AssignedTo = new IdentifiableName() { Id = thisuser.Id };
    
                    manager.CreateObject(newIssue);
    


    Получение списка проектов и трекеров:
            public static Dictionary<string, int> GetProjects()
            {
                RedmineManager manager = new RedmineManager(Configuration.RedmineHost,
                    Configuration.RedmineUser, Configuration.RedminePassword);
                Dictionary<string, int> Projects = new Dictionary<string, int>();
    
                foreach (Project proj in manager.GetObjectList<Project>(new NameValueCollection()))
                {
                    Projects.Add(proj.Name, proj.Id);
                }
                return Projects;
             }
    
            public static Dictionary<string, int> GetTrackers()
            {
                RedmineManager manager = new RedmineManager(Configuration.RedmineHost,
                    Configuration.RedmineUser, Configuration.RedminePassword);
                Dictionary<string, int> Trackers = new Dictionary<string, int>();
    
                foreach (Tracker track in manager.GetObjectList<Tracker>(new NameValueCollection()))
                {
                    Trackers.Add(track.Name, track.Id);
                }
                return Trackers;
            }
    


    Кстати, всё то актуально для версий моложе 1.3 (появился список трекеров в REST API)

    Итог или заключение



    В итоге получилась форма с двумя полями для определения по голосу: название задачи, описание задачи. На время записи и распознавания все поля закрываются панелью. Запись производится с устройства, выставленного в системе для записи по умолчанию в течение 4 секунд.

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

    Источники


    Flac
    Redmine API
    WaveLib

    Ссылки


    Бинарники
    Исходники

    Звуки музыки

    Hi-Fi в цифровой век. Часть 2

    Предыдущая часть: «Hi-Fi в цифровой век. Часть 1», — была посвящена цифровым источникам звука и преобразованию (ЦАП, Цифро-Аналоговое Преобразование) цифрового потока в аналоговый аудиосигнал.

    image

    Далее этот сигнал нужно усилить прежде чем вывести на акустические системы. В этой статье речь пойдет о стереоусилителях.