Лидеры
Популярный контент
Показан контент с высокой репутацией 14.05.2017 в Записи блога
-
3 баллаПродолжаю рассказывать про Computronics и, в частности, про офигенную звуковую карточку из этого мода. На очереди модуляция: частотная и амплитудная. Помимо этого восполняю долг по основам. Юзать будем мою прогу synth, которую я недавно зарелизил. Она здесь невероятно поможет. Звуковая волна Вы же знаете, как выглядит звуковая волна? Вот, например, синусоида. Как видно, здесь есть некоторый фрагмент, который повторяется несколько раз. Частота показывает, сколько раз в секунду этот фрагмент повторяется, и измеряется в герцах (Гц или Hz). Чем выше частота, тем больше волна "сжата", скажем так, с боков. Вот как выглядят три синусоиды с разными частотами: 110 Гц, 220 Гц, 440 Гц - на одинаковом масштабе. Кроме частоты, звуковая волна характеризуется таким параметром, как амплитудой. Это, скажем так, расстояние от нуля до самого большого по модулю значения волны. Чем больше амплитуда, тем громче звук. Мы примем за единицу амплитуду сигнала на звуковой карте при максимальной громкости. При нулевой амплитуде на графике будет прямая линия вдоль горизонтальной оси. При максимальной громкости у простой синусоиды пиковые значения будут в точках +1 и -1. Они плавно сменяются. Прикольно, да. Синусоиды - это офигенные штуки, но на них клин светом не сошёлся. Из основных типов волн, помимо синусоид, есть меандр, треугольная волна и пилообразная. Они в таком же порядке изображены на рисунках ниже. (по поводу первой и третьей пикчи: вообразите, что волны не разомкнуты, а просто резко переходят с одного конца на другой. Небольшая хрень, над которой нужно побиться.) На самом деле, их можно представить в виде набора бесконечного числа синусоид, о чём когда-то поведал Фурье, но мы с этим заморачиваться не будем: раскладывать на синусоиды в звуковой карте не принято. Поведали об этом для красного словца - и то хорошо. Теперь, когда рассказал про основы, можно переходить к более весёлым и сложным вещам. То бишь к модуляции. Модуляция Модуляция - это изменение одного сигнала (несущего) другим (модулятором). Изменять можно по-разному - мы будем говорить об амплитудной модуляции и частотной модуляции. С модуляцией у нас появляется уникальнейший шанс получить офигенные и красивые звуки, поэтому не будем ждать и сразу перепрыгнем к мясу. Амплитудная модуляция Как я сказал, для модуляции нужны два сигнала: несущий и сам модулятор. Поэтому здесь и далее я привожу на рисунках сразу три графика: несущий сигнал, модулирующий сигнал и результат модуляции. Например, выставим две синусоиды с частотой 440 Гц. Итак, амплитудная модуляция - это умножение одного сигнала на другой. A(t) = C(t) × M(t), где t - время, C - функция, возвращающая значение несущей волны на моменте времени t, M - то же, но для модулятора. Однако не всё так просто. Перед умножением к значениям с модулятора прибавляется единица. Получается, что самая верхняя точка будет на +2, а самая нижняя - на 0. Иными словами, волна перенесена вверх. На низких частотах - до 20-30 Гц, откуда начинается граница слышимого человеком звука, - графики будут выглядеть как-то так, медленно увеличивая амплитуду от 0 до 4 и обратно. И звучать оно будет как увеличение и уменьшение громкости (количество таких увеличений и уменьшений равно частоте модулятора). Однако когда частота модулятора становится больше, наблюдаем вот такую картину (частота модулятора равна здесь 330 Гц). Если прислушаться, то станет заметно, что одновременно будто бы проигрываются три звуковых волны. Одна волна имеет такую же амплитуду и частоту, как и несущая. Две другие частоты, задающие боковую полосу частот (диапазон частот, сконцентрированных рядом с какой-либо), находятся так: f1 = |c - m|, f2 = c + m, где c - частота несущей волны, а m - частота модулятора. Возникающие звуковые волны около этих частот вдвое тише, чем несущая. В звуковой карте можно поставить амплитудный модулятор следующим образом: sound.setAM(carrierChan: number, modulatorChan: number). Амплитудная модуляция - забавная вещь, и в то же время тут очень трудно подобрать что-то интересное и красивое. Поэтому переходим к частотной модуляции - там веселья дофига. Частотная модуляция В частотной модуляции модулятор изменяет частоту несущей волны. Как ни странно. Почему частотная модуляция уделывает амплитудную? Здесь может быть гораздо больше боковых частот. И потому звучать оно может гораздо круче. При когда значение на модуляторе увеличивается, повышается и частота на несущей волне. Звуковая карта поддерживает индекс модуляции. Он задаёт максимальное изменение частоты несущей волны. При индексе, равном 100, частота несущей волны может меняться на 100 Гц вверх и на 100 Гц вниз. Если индекс равен 1000, то частота меняться может на 1000 Гц вверх и на 1000 Гц вниз. Ну и так далее. Иными словами, индекс задаёт силу модуляции. Если частота модулятора будет очень низкой (например, 4 Гц), то получим что-то вроде сирены. c = 440 Гц; m = 4 Гц; i = 200 С повышением частоты получим вибрирующий звук. И потом услышим дополнительные частоты. c = 220 Гц; m = 880 Гц; i = 660 Как видно, получившаяся волна получилась довольно сложной. Установить модулятор в звуковой карте можно с помощью функции sound.setFM(carrierChan: number, modulatorChan: number, index: number). И не забывайте про ADSR-огибающую: из однообразного тона можно получить довольно интересный звук. Как работает эта штука, я рассказывал. На этом всё. Из всех фич остался неразобранным лишь шум LFSR, но там штука очень и очень странная и непонятная. Наверняка всё равно остались некоторые вопросы по поводу модуляции. Поэтому за дополнительной информацией я предлагаю обратиться к другим сайтам. Вот несколько очень полезных ссылочек, где есть примеры звука и детальное описание: FM Synthesis - The Synthesizer Academy Frequency modulation synthesis - Wikipedia FM Synthesis - Music and Computers Modulation synthesis - Wikibooks (здесь рассказывается про амплитудную модуляцию в том числе). Ну и используйте прогу synth, чтобы удобно было изучать звуковую карту.
-
1 баллНемногие знают, как работают палитры в OpenComputers. Расскажу здесь, как избавиться от необходимости прописывать гектары цветов в палитре, покажу, как упаковываются цвета в OpenComputers и дам пару алгоритмов для работы с индексами. Сразу условимся, что индексы палитр у нас будут начинаться с нуля. На каждой из трёх уровней видеокарт и мониторов своя поддерживаемая палитра цветов. Будем двигаться снизу вверх. Первый уровень Палитра состоит из двух цветов: чёрного и заданного в конфиге (по умолчанию белого). Конвертация цвета в индекс палитры тривиальна: цвет нулевой — и индекс нулевой (чёрный цвет); цвет ненулевой — индекс единичный. Цвет в индекс (deflate) и обратно (inflate) превращать — одно удовольствие: local palette = { 0x000000, CONFIG.monochromeColor } local function t1deflate(index) if index == 0 then return 0 else return 1 end end local function t1inflate(index) return palette[index + 1] end Как и говорил. Второй уровень В палитре второго уровня имеется 16 закреплённых цветов: local palette = {0xFFFFFF, 0xFFCC33, 0xCC66CC, 0x6699FF, 0xFFFF33, 0x33CC33, 0xFF6699, 0x333333, 0xCCCCCC, 0x336699, 0x9933CC, 0x333399, 0x663300, 0x336600, 0xFF3333, 0x000000} При конвертации цвета в индекс палитры вернётся ближайший к данному цвет из палитры. Насколько цвета друг к другу близки, рассчитывается по специальной формуле, которая учитывает, что человеческий глаз лучше воспринимает зелёный, нежели красный и синий. В коде этим занимается функция delta. Вот как она выглядит (вместе с функций extract, выделяющей из числа вида 0xABCDEF числа 0xAB, 0xCD, 0xEF): local function extract(color) color = color % 0x1000000 local r = math.floor(color / 0x10000) local g = math.floor((color - r * 0x10000) / 0x100) local b = color - r * 0x10000 - g * 0x100 return r, g, b end local function delta(color1, color2) local r1, g1, b1 = extract(color1) local r2, g2, b2 = extract(color2) local dr = r1 - r2 local dg = g1 - g2 local db = b1 - b2 return (0.2126 * dr^2 + 0.7152 * dg^2 + 0.0722 * db^2) end Теперь можно конвертировать цвет в индекс палитры. Суть такова: выбираем из двух цветов ближайший и возвращаем его. local function t2deflate(color) -- Сначала проверяем, совпадает ли данный цвет -- с каким-либо из палитры for idx, v in pairs(palette) do if v == color then return idx end end -- Составляем таблицу разниц между цветами local deltas = {} for idx, v in pairs(palette) do table.append(deltas, {idx, delta(v, color)}) end -- Сортируем по увеличению разницы table.sort(deltas, function(a, b) return a[2] < b[2] end) -- Первый элемент будет с наименьшей разницей, -- то есть искомый. Возвращаем индекс. return deltas[1][1] - 1 end Обратная же процедура — превращение индекса палитры в цвет — неизменна. local t2inflate = t1inflate Третий уровень Палитра третьего уровня содержит уже 256 цветов: первые 16 цветов изменяются, а остальные соответствуют цветам палитры RGB-6-8-5. Это означает, что можно смешивать 6 оттенков красного, 8 оттенков зелёного и 5 оттенков синего. В общем-то, довольно очевидна причина такого выбора: человеческий глаз лучше всего различает оттенки зелёного и хуже всего — оттенки синего. В любом случае, здесь алгоритмец будет посложнее. Сначала нужно сгенерировать палитру. Начнём с первых 16 цветов. Они не включаются в палитру RGB-6-8-5, поэтому их заполнять нужно отдельно. В OpenComputers по умолчанию они содержат оттенки серого. Так как чёрный и белый уже включены в основную, зафиксированную палитру, то заново их дублировать смысла нет. local palette = {} -- grayscale for i = 1, 16, 1 do palette[i] = 0xFF * i / (16 + 1) * 0x10101 end Таким образом в таблице получаются следующие оттенки серого: 0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78, 0x87, 0x96, 0xA5, 0xB4, 0xC3, 0xD2, 0xE1, 0xF0 Эти цвета мы записываем в индексы от 0 до 15. Теперь нужно сгенерировать остальные цвета — они не изменяются. Здесь будет посложнее. Посмотрим на картинку с палитрой: В OpenComputers левая верхняя ячейка палитры (0x000000) имеет индекс 16, а правая нижняя (0xFFFFFF) имеет индекс 255. Индексы распределяются слева направо, сверху вниз. То есть правая верхняя ячейка (0x00FFFF) имеет индекс 55, а вторая сверху и левая (0x330000) — это номер 56. Отсюда вытекает следующий алгоритм нахождения цвета: сначала найти индексы отдельно по R, G, B, затем для каждого из этих трёх индексов найти соответствующий ему оттенок цвета, а затем всё сложить. for idx = 16, 255, 1 do local i = idx - 16 local iB = i % 5 local iG = (i / 5) % 8 local iR = (i / 5 / 8) % 6 local r = math.floor(iR * 0xFF / (6 - 1) + 0.5) local g = math.floor(iG * 0xFF / (8 - 1) + 0.5) local b = math.floor(iB * 0xFF / (5 - 1) + 0.5) palette[idx + 1] = r * 0x10000 + g * 0x100 + b end Идея следующая. Каждый из трёх каналов принимает значение от 0 до 255 (0xFF). Разбиваем их на определённое число ступеней (по-умному — квантуем): 6 для красного, 8 для зелёного и 5 для синего. Например, синий канал мы разобьём так: 0/4 · 255 = 0 1/4 · 255 = 63.75 2/4 · 255 = 127.5 3/4 · 255 = 191.25 4/4 · 255 = 255 В знаменателе тут 4, а не 5, потому что считаем с нуля. Затем округляем до ближайшего целого конструкцией math.floor(x + 0.5). Перебрав все комбинации, мы получим все 6 × 5 × 8 = 240 цветов неизменяемой части палитры. Всё. Палитра есть, теперь можно, наконец-то, конвертировать индексы между цветами. Из индексов получить цвет довольно просто. Достаточно использовать ту же функцию, что и для предыдущих уровней: t3inflate = t2inflate С обратной же конвертацией всё несколько сложнее. Функция, используемая в OC, подбирает ближайший цвет хитрым алгоритмом, который я привожу ниже. local function t3deflate(color) local paletteIndex = t2deflate(color) -- Если цвет из палитры, то используем значение выше for k, v in pairs(palette) do if v == color then return paletteIndex end end -- Иначе используем хитромудрый код local r, g, b = extract(color) local idxR = math.floor(r * (6 - 1) / 0xFF + 0.5) local idxG = math.floor(g * (8 - 1) / 0xFF + 0.5) local idxB = math.floor(b * (5 - 1) / 0xFF + 0.5) local deflated = 16 + idxR * 8 * 5 + idxG * 5 + idxB if (delta(t3inflate(deflated % 0x100), color) < delta(t3inflate(paletteIndex & 0x100), color)) then return deflated else return paletteIndex end end Хитромудрость здесь не сильно сложная, на самом деле. Мы сначала находим индекс самого близкого цвета из изменяемой части палитры (paletteIndex). Дальше высчитываем индекс цвета из неизменяемой части (deflated), для чего в каждом канале отдельно ищем номер ближайшей ступени, на которые ранее квантовали. Последний if сравнивает 2 варианта и возвращает самый похожий (с точки зрения человека) на исходный. В общем-то, это всё. Показал портированный со Scala на Lua код, который используется в OpenComputers. С помощью этого можно оптимизировать операции с экраном, выбирая поддерживаемые монитором цвета. И заодно избавиться от таблиц цветов, которые некоторые буквально берут и копипастят в файл, даже не задумываясь об изменяемых цветах палитры. Особенно это важно, когда берётся значение цвета через gpu.get, потому что следующий код всегда вернёт false: local gpu = require("component").gpu gpu.setForeground(0x20AFFF) gpu.setBackground(0x20AFFF) gpu.set(1, 1, "HI") return select(2, gpu.get(1, 1)) == 0x20AFFF И всё потому, что gpu.get возвращает уже приведённый к индексу из палитры цвет. А 0x20AFFF в палитре, если не менять первые 16 цветов, не имеется. Enjoy :P
Эта таблица лидеров рассчитана в Москва/GMT+03:00
