Книга: Написание скриптов для Blender 2.49

Манипуляция звуковыми файлами

Манипуляция звуковыми файлами

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

Как упомянуто в предыдущем разделе, API Блендера не обеспечивает большого количества инструментов для работы со звуковыми файлами, в основном модуль Sound обеспечивает нас способом загрузки и воспроизведения звуковых файлов, но на этом и всё. Нет способа получить доступ к индивидуальным точкам  волны, закодированным в файле.

К счастью, в стандартный дистрибутив Питона включен модуль wave, который обеспечивает нас средствами для чтения файлов в обыкновенном формате .wav. Хотя он поддерживает только несжатый формат, этого будет достаточно, так как этот формат является очень распространённым, и большинство инструментов работы со звуком, как например, Audacity, могут преобразовывать в этот формат. С этим модулем мы можем открыть .wav-файл, определить частоту сэмплов и длительность звукового клипа, и получить индивидуальные сэмплы. Как мы увидим в объяснении следующего кода, мы все еще должны преобразовывать эти сэмплы в величины, которые мы можем использовать как значения ключей для наших ключей формы, но тяжелую работу уже сделали для нас.

Схема кода: Sound.py

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

1. Определить, имеет ли активный объект пригодные заданные формы, и предложить выбрать их них.

2. Позволить пользователю выбрать .wav-файл.

3. Определить количество звуковых сэмплов в секунду в файле (частота дискретизации).

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

5. Затем, для каждого кадра анимации:

? Усреднить звуковые сэмплы, проходящие в этом кадре

? Установить величину смешивания выбранной кривой IPO этому среднему (нормализованному) числу

Полный код доступен как Sound.py в файле sound000.blend и объясняется следующим образом:

import Blender
from Blender import Scene,Window,Draw
from Blender.Scene import Render
import struct
import wave

Мы начинаем, импортируя необходимые модули, включая модуль Питона wave, чтобы иметь доступ к нашему .wav-файлу и модуль struct, который предоставляет функции для манипулирования двоичными данными, которые мы получим из .wav-файла.

Затем, мы определяем вспомогательную функцию, показывающую всплывающее меню в середине нашего экрана. Она ведёт себя просто подобно стандартной функции PupMenu() из модуля Draw, но устанавливает курсор в позицию середины экрана с помощью функций GetScreenSize() и SetMouseCoords() из модуля Блендера Window:

def popup(msg):
   (w,h)=Window.GetScreenSize()
   Window.SetMouseCoords(w/2,h/2)
   return Draw.PupMenu(msg)

Основная часть работы будет осуществляться функцией sound2active(). Она принимает два аргумента - имя .wav-файла, для использования и имя ключа формы для анимации, основанной на информации из .wav-файла. Сначала, мы пытаемся создавать объект WaveReader, вызывая функцию open() модуля wave (выделено). Если это не удаётся, мы показываем ошибку во всплывающем окне и выходим:

def sound2active(filename,shapekey='Pop out'):
   try:
      wr = wave.open(filename,'rb')
   except wave.Error,e:
      return popup(str(e)+'%t|Ok')

Затем мы делаем некоторые разумные проверки: мы сначала проверяем, является ли .wav-файл МОНО файлом. Если Вы хотите использовать стерео файл, преобразуйте его сначала в моно, например   с   помощью   свободного   пакета   Audacity (http://audacity.sourceforge.net/). Затем мы проверяем, имеем ли мы дело с несжатым .wav-файлом, поскольку модуль wave не может работать с другими типами. (большинство .wav-файлов являются несжатыми, но если нужно, Audacity также может их преобразовать), и мы проверяем, что сэмплы 16-битовые. Если любая из этих проверок терпит неудачу, мы выводим соответствующее сообщение об ошибке:

   c = wr.getnchannels()
   if c!=1 : return popup('Only mono files are '+
                          'supported%t|Ok')
   t = wr.getcomptype()
   w = wr.getsampwidth()
   if t!='NONE' or w!=2 :
     return popup('Only 16-bit, uncompresses files '+
                  'are supported%t|Ok')

Теперь, когда мы можем работать с файлом, мы получаем его частоту дискретизации (frame rate, количество аудио сэмплов в секунду) и общее число байт (как ни странно, используя неуклюже названную функцию getnframes() из модуля wave). Затем, мы считываем все эти байты и сохраняем их в переменной b.

   fr= wr.getframerate()
   n = wr.getnframes()
   b = wr.readframes(n)

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

   scn         = Scene.GetCurrent()
   context     = scn.getRenderingContext()
   seconds     = float(n)/fr
   sampleratio = fr/float(context.framesPerSec())

Как упомянуто раньше, модуль wave дает нам доступ ко множеству свойств .wav-файла и к сырым (raw) аудио сэмплам, но не предоставляет никаких функций для преобразования этих сырых сэмплов в удобные для использования целые величины. Следовательно, нам нужно сделать это самостоятельно. К счастью, это не так уж трудно, как это может показаться. Поскольку мы знаем, что 16-битовые аудио сэмплы представлены как 2-х байтовое целое в формате "меньший-в-конце" ("little-endian"), мы можем использовать функцию unpack() из модуля Питона struct, чтобы эффективно преобразовывать список байтов в список целых, передавая подходящую спецификацию формата. (Вы можете прочитать больше о .wav-файлах здесь  https://ccrma.stanford.edu/courses/422/ проекты/WaveFormat/,  на русском здесь:  http://www.fpga-cpld.ru/wave.html, работа с модулем struct описана здесь: http://world-python.org/article/tutorialmodules/32-modul-struct.html — прим. пер.)

   samples  = struct.unpack('<%dh'%n,b)

Теперь мы можем начать анимацию ключей формы. Мы получаем стартовый кадр из контекста рендера и вычисляем конечный кадр, умножая время в секундах в .wav-файле на частоту видеокадров. Заметьте, что он может оказаться  дальше или ближе, чем конечный кадр, который мы можем получить из контекста рендера. Последний определяет конечный кадр, который будет отрендерен, когда пользователь нажмёт на кнопку Anim, но мы будем анимировать движение нашего активного объекта независимо от этой величины.

Затем для каждого кадра мы вычисляем от стартового кадра до последнего кадра (исключительно) среднее значение аудио сэмплов, которые попадают на каждый видеокадр суммированием этих аудио сэмплов (находятся в списке samples) и деля на количество этих аудио сэмплов за видеокадр (выделено в следующем куске кода).

Мы задаём выбранный ключ формы в величину в дипазоне [0:1], так что мы должны нормализовать рассчитанные средние числа, определяя минимальную и максимальную величины, и вычислить масштаб:

   staframe = context.startFrame()
   endframe = int(staframe +
                  seconds*context.framesPerSec())
   popout=[]
   for i in range(staframe,endframe):
      popout.append(sum(samples[int(
      (i-1)*sampleratio):int(i*sampleratio)])/sampleratio)
   minvalue = min(popout)
   maxvalue = max(popout)
   scale = 1.0/(maxvalue-minvalue)

Наконец, мы получаем активный объект в текущей сцене и получаем его IPO Формы (выделено). Мы заканчиваем, устанавливая величину ключа формы для каждого кадра в рассматриваемом нами диапазоне в масштабированное среднее аудио сэмплов:

   ob=Blender.Scene.GetCurrent().objects.active
   ipo = ob.getData().getKey().getIpo()
   for i,frame in enumerate(range(staframe,endframe)):
      ipo[shapekey][frame]=(popout[i]-minvalue)*scale

Остальной скрипт теперь довольно прост. Он выбирает активный объект, затем пытается извлечь список имен ключей формы из него (выделено в следующей части). Это действие может потерпеть неудачу (следовательно, применяется  try … except), если, например, активный объект - не меш или он не имеет связанных ключей формы, в этом случае мы предупреждаем пользователя с помощью всплывающего сообщения:

if __name__ == "__main__":
   ob=Blender.Scene.GetCurrent().objects.active
   try:
      shapekeys = ob.getData().getKey(
                  ).getIpo().curveConsts
      key = popup(('Select a shape key%t|'+
                  '|').join(shapekeys))
      if key>0:
          Window.FileSelector (lambda f:sound2active(f,
            shapekeys[key-1]),
           "Select a .wav file",
           Blender.Get('soundsdir'))
   except:
      popup('Not a mesh or no shapekeys defined%t|Ok')

Если мы смогли извлечь список ключей формы, мы предоставляем пользователю всплывающее меню для выбора из этого списка. Если пользователь выбирает один из пунктов, переменная key будет положительной и мы предоставляем пользователю диалог выбора файлов (выделено). В этот диалог передаётся  lambda-функция, которая будет вызвана, если пользователь выберет файл, с передачей имени этого выбранного файла в качестве аргумента. В нашем случае мы создаём эту lambda-функцию, чтобы вызвать функцию  sound2active(), определённую ранее с этим именем файла и выбранным ключом формы.

Начальный каталог, который будет представлен пользователю в выборе файлов, определяется последним аргументом в функции FileSelector(). Мы задали его параметром Блендера soundsdir. Это обычно // (то есть, относительный путь, указывающий на тот же каталог, где находится .blend-файл, с которым пользователь работает), но может быть установлен в окне Пользовательских настроек (секция File Paths) на нечто другое.

Оглавление книги


Генерация: 1.114. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз