Опубликовано 31 октября 2009 18:37 | Coding4Fun
· Автор: Рэндалл Маас (Randall Maas)
· Сложность: средняя
· Необходимое время: 4 часа
· Затраты: бесплатно
· ПО: Visual Basic или Visual C# Express
· Оборудование: нет
· Попробуйте прямо сейчас: запустить приложение
· Исходный код: загрузить

Введение
В этой статье описывается простое приложение Gremlin, позволяющее вытворять на компьютере проделки в духе Хеллоуина. Когда компьютер вашей жертвы простаивает, Gremlin двигает окна по экрану, активирует то одно окно, то другое, перемещает курсор мыши, прокручивает окна и печатает всякую бессмыслицу. Как только появляется фоновый шум (ну, например, кто-то начинает говорить), Gremlin трясет экран, даже если ваша жертва что-то набирает с клавиатуры.
Развертывание
Немедленный запуск. Вы можете скачать и скопировать файлы theGremlin.exe и NAudio.dll на компьютер жертвы (скажем, в каталог c:\), а потом дважды щелкнуть исполняемый файл, чтобы запустить его.
Отложенный запуск. Другой вариант — скопировать исполняемый файл и библиотеку (или ярлык на этот файл) в папку Пуск (Startup) на компьютере жертвы и полюбоваться, какое веселье начнется, когда владелец этого компьютера включит его утром!
Если компьютер работает под управлением Windows XP, путь выглядит так:
1: C:\Documents and Settings\All Users\Start Menu\Programs\Startup
В Vista и Windows 7 путь немного другой:
1: C:\Users\USERNAME\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
Не забудьте заменить USERNAME на имя пользователя компьютера.
Командная строка
Gremlin скомпилирована как оконная программа, поэтому при запуске она не будет открывать окно терминала. Но ее можно запускать из командной строки со следующими флагами:
-aggressive — гремлин станет агрессивнее;
-help — выводит список параметров командной строки;
-name ИМЯ — полезен для тестирования. Gremlin будет использовать окна только с данным заголовком или из указанного приложения. (Расширение «.exe» нужно убрать из имени исполняемого файла приложения.)
Чтобы остановить программу, нажмите CTRL+F2.
А теперь… плутоватые биты
Общая структура программы делится на три вида функциональности: действия, триггеры и кое-какую инфраструктуру (склеивающий слой):
Действия:
• Тряска экрана реализуется специальным окном. Так и хочется проверить, не ослабло ли крепление кабеля на задней панели монитора.
• Случайное событие может раскидать окна по экрану — и либо их движение замедлится от трения, либо...
• Gremlin будет печатать бессмысленные сообщения, которые посылаются с виртуальной клавиатуры, или…
• Двигать курсор мыши или…
• Нажимать кнопки мыши или…
• Случайным образом переключать фокус между окнами.
Триггеры:
• Аудио-модуль ждет появления каких-либо звуков, которые запускают модуль, отвечающий за тряску экрана.
• В ином случае программа вызывает GetLastInputInfo() через механизм P/Invoke и ожидает, когда компьютер не будет использоваться в течение одной-двух минут. Обнаружив, что пользователь ничего не делает, программа случайным образом выбирает и выполняет свои действия.
Другое:
• Скрытое окно принимает события нажатия клавиш. Я часто использую этот модуль для остановки экспериментальных программ, способных устроить бедлам на моем компьютере и помешать работе.
Интереса ради опишу в следующих разделах несколько этих модулей.
Модуль тряски экрана
Это мой любимый модуль. Он получает копию изображения на экране, создает окно, охватывающее весь экран, а затем смещает это изображение туда-сюда на несколько пикселей примерно каждые 30 мс; при этом используется вращение на небольшой угол, выбираемый случайным образом. (На более быстрых компьютерах эффект, конечно, получается лучше.)
У модуля тряски экрана четыре особенности:
• это окно верхнего уровня — другие окна не могут размещаться поверх него;
• оно никогда не становится окном в фокусе (т. е. окном, получающим события от клавиатуры);
• события, связанные с кнопками мыши (например, одинарные и двойные щелчки), передаются через него другим окнам и рабочему столу. Это создает иллюзию того, что трясется «настоящий» рабочий стол: пользователь может щелкнуть кнопку или текст, и этот щелчок передается настоящей кнопке или тексту;
• экран каждого монитора трясется независимо.
Немного базовых сведений: чтобы создать окно, имитирующее трясущийся экран, я использовал два класса. Первый из них — ScrShake (в ScreenShake.cs), который наследует от второго класса, UnfocusableForm. Сначала я опишу ScrShake.
Процесс тряски экрана требует наличия пяти переменных экземпляра:
• animTimer — это System.Windows.Forms.Timer, используемый для принудительного рисования новой рамки на экране;
• bx и by — определяют, на сколько пикселей нужно смещать экран вверх и вниз или влево и вправо;
• angle — определяет, насколько искривляется экран во время тряски;
• screenBitmap — массив битовых карт (растровых изображений), по одной на каждый монитор.
Я создаю окно без границ и других внешних атрибутов, затем превращаю его в окно поверх остальных окон и устанавливаю флаг в переменной ExStyle уровня метода для игнорирования щелчков мыши. Подробнее об этом флаге — чуть позже.
C#
1: public partial class ScrShake : UnfocusableForm
2: {
3: public ScrShake(double ShakeCoef, double AngleCoef) : base(true)
4: {
5: this.SuspendLayout();
6: … Прочий настроечный код…
7: // Это необходимо потому, что наше окно занимает весь экран
8: // и мы не хотим, чтобы другие области были смазанными
9: TransparencyKey = BackColor = ForeColor =
10: System.Drawing.Color.Fuchsia;
11: DoubleBuffered = true;
12: TopMost = true;
13: FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
14:
15: ClientSize = new System.Drawing.Size(300, 300);
16: Name = "topimage";
17: ResumeLayout(false);
18:
19: // Это необходимо для сквозной передачи щелчков мыши
20: // окнам более низких уровней
21: ExStyle |= (int) WS . EX_TRANSPARENT;
22:
23: … Остальной код будет описан позже…
24: }
25: }
Когда таймер выполнит заданное количество анимаций, он вызовет метод Stop(). Это приведет к прекращению анимации, ее очистке и скрытию окна.
C#
1: void Stop()
2: {
3: animTimer . Stop();
4: Hide();
5: screenBitmap = null;
6: }
В основном цикле, запускающем процесс анимации, которая имитирует тряску экрана, вызывается метод Screens(). Он получает изображения экранов, определяет форму каждого монитора и запускает анимацию из десяти кадров.
C#
1: internal void Screens()
2: {
3: if (Visible)
4: {
5: Stop();
6: return ;
7: }
8: // Получает изображения на экранах
9: screenBitmap = Program.GrabScreens();
10: CountDown = 10;
11:
12: animTimer.Start();
13: angle = 0.0f;
14: … Код для вывода окна и получения формы мониторов (см. ниже)…
15: }
Код, отвечающий за захват изображения на экране, находится в ScreenCapture.cs. Метод GrabScreens() создает индивидуальную битовую карту для каждого монитора и возвращает все такие битовые карты в виде массива.
Код, показанный ниже, определяет границы каждого экрана и вычисляет размер всех экранов, сложенных вместе. Тем самым определяется размер окна.
C#
1: Screen[] AllScreens = Screen.AllScreens;
2:
3: // Общая ширина/высота всех мониторов
4: Rectangle fullSize = AllScreens[0].Bounds;
5:
6: // Находим прямоугольник, который будет охватывать
7: // все мониторы в системе (предполагается, что
8: // основной монитор находится в левом верхнем углу!)
9: for (int i = 1; i < AllScreens.Length &&
10: i < screenBitmap.Length; i++)
11: {
12: Rectangle Bounds = AllScreens[i].Bounds;
13:
14: if (Bounds.Left < fullSize.Left)
15: fullSize.X = Bounds.X;
16:
17: if (Bounds.Right > fullSize.Right)
18: fullSize.Width = Bounds.Right - fullSize.X;
19:
20: if (Bounds.Top < fullSize.Top)
21: fullSize.Y = Bounds.Y;
22:
23: if (Bounds.Bottom > fullSize.Bottom)
24: fullSize.Height = Bounds.Bottom - fullSize.Y;
25: }
Код для вывода окна, заполнения всего экрана и перемещения поверх остальных окон выглядит следующим образом.
C#
1: Show();
2: WindowState = FormWindowState.Normal;
3: // Охватываем все мониторы одним гигантским окном
4: Location = new Point(fullSize.Left, fullSize.Top);
5: Size = new Size(fullSize.Width, fullSize.Height);
6:
7: // Выводим его поверх остальных окон
8: BringToFront();
Конструктор также создает таймер, который управляет эффектом тряски экрана. У таймера имеется делегат, который случайным образом выбирает угол поворота и смещение изображения и инициирует перерисовку окна. (Эффект тряски контролируется двумя внешними переменными: AngleCoef и ShakeCoef.)
C#
1: animTimer = new System.Windows.Forms.Timer();
2: animTimer.Tick += delegate(object A, EventArgs E)
3: {
4: if (--CountDown < 1)
5: Stop();
6:
7: angle += (float)((Program.Rnd.NextDouble() - 0.5) * AngleCoef);
8: bx = (float)((Program.Rnd.NextDouble() - 0.5) * ShakeCoef);
9: by = (float)((Program.Rnd.NextDouble() - 0.5) * ShakeCoef);
10: Invalidate();
11: };
12:
13: // Устанавливаем интервал срабатывания таймера на 30 мс
14: animTimer.Interval = 30;
Хотя экран каждого монитора одинаково смещается вверх-вниз и влево-вправо, а также поворачивается на один и тот же угол, рисование осуществляется независимо. Это создает иллюзию, будто изображение на каждом мониторе трясется. Рисование окна выполняется следующим кодом.
C#
1: protected override void OnPaint(PaintEventArgs e)
2: {
3: base.OnPaint(e);
4: Graphics g = e.Graphics;
5: g.CompositingQuality = CompositingQuality.HighQuality;
6:
7: // Эффект создается для каждого монитора отдельно
8: Screen[] AllScreens = Screen.AllScreens;
9: for(int i = 0; i < screenBitmap.Length; i++)
10: {
11: if (null == screenBitmap[i]) continue;
12: g.SmoothingMode = SmoothingMode.HighQuality;
13:
14: // Получаем размер текущего монитора
15: Rectangle region = AllScreens[i].Bounds;
16:
17: double ImWidth = screenBitmap[i].Width *
18: e.Graphics.DpiX / screenBitmap[i].HorizontalResolution;
19: double ImHeight= screenBitmap[i].Height *
20: e.Graphics.DpiY / screenBitmap[i].VerticalResolution;
21: Matrix m = new Matrix();
22: m.Translate( (float)(- ImWidth /2),
23: (float)(- ImHeight /2), MatrixOrder.Append);
24:
25: // Поворачиваем битовую карту вокруг центра
26: m.RotateAt(angle, new Point(0,0), MatrixOrder.Append);
27:
28: // Центрируем изображение независимо от его размера
29: m.Translate( region.Width /2 - bx,
30: region.Height/2 - by, MatrixOrder.Append);
31:
32: // Присваиваем переменной нашу матрицу преобразования
33: g.Transform = m;
34:
35: // Рисуем
36: g.DrawImage(screenBitmap[i], region.Left, region.Top);
37: }
Форма, которая ничего не делает!
Класс ScrShaker использует вспомогательный класс UnfocusableForm (в UnfocusableForm.cs) для создания окна, которое никогда не получает фокус, а значит, никогда не принимает событий, связанных с нажатиями клавиш и кнопок.
У этого окна нет внешней атрибутики — ни границ, ни маркера изменения размеров, ни значка, ни кнопок сворачивания/разворачивания, ни строки заголовка.
C#
1: public partial class UnfocusableForm : Form
2: {
3: public UnfocusableForm(bool X) : base()
4: {
5: if (X)
6: {
7: ControlBox = false;
8: FormBorderStyle =
9: System.Windows.Forms.FormBorderStyle.None;
10: ShowInTaskbar = false;
11: ShowIcon = false;
12: MinimizeBox = false;
13: SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide;
14: StartPosition =
15: System.Windows.Forms.FormStartPosition.Manual;
16: }
17: }
18: }
Далее переопределяется метод ShowWithoutActivation, чтобы при отображении окно не становилось активным.
C#
1: protected override bool ShowWithoutActivation
2: { get { return true; } }
Класс UnfocusableForm также переопределяет метод CreateParams() для задания дополнительных стилей при создании окна. Я не нашел гибкого способа для их задания. Вместо этого UnfocusableForm использует переменную ExStyle уровня метода, которая позволяет производным классам указывать нужные флаги. В данном случае класс ScrShake через эту переменную устанавливает флаг для игнорирования щелчков кнопок мыши.
C#
1: protected override CreateParams CreateParams
2: {
3: get
4: {
5: CreateParams cp=base.CreateParams;
6: cp . ExStyle |= ExStyle;
7: return cp;
8: }
9: }
Наконец, он перехватывает несколько событий, позволяющих сделать окно активным. (Это обычно происходит, когда пользователь щелкает неактивное окно.)
C#
1: internal const int MA_NOACTIVATE = 0x0003;
2: protected override void WndProc(ref Message m)
3: {
4: if (m.Msg == (int) WM.MOUSEACTIVATE)
5: {
6: m.Result = (IntPtr) MA_NOACTIVATE;
7: return;
8: }
9: if (m.Msg == (int) WM.FOCUS)
10: {
11: m.Result = (IntPtr)1;
12: return;
13: }
14: base.WndProc(ref m);
15: }
Отправка событий мыши
Этот процесс прост, но отнюдь не тривиален. В вики на сайте Pinvoke.net можно найти сигнатуры процедур и структур, которые нам понадобится передавать. (Примечание: эти структуры немного различаются в зависимости от факторов, описанных в блоге, который ведет Реймонд Чен (Raymond Chen) (EN).)
C#
1: [DllImport("user32.dll", EntryPoint = "SendInput",
2: SetLastError = true)]
3: static extern uint SendInput(uint nInputs, INPUT[] Inputs,
4: int cbSize);
5: [DllImport("user32.dll", EntryPoint = "GetMessageExtraInfo",
6: SetLastError = true)]
7: static extern IntPtr GetMessageExtraInfo();
8:
9: [StructLayout(LayoutKind.Sequential)]
10: struct INPUT
11: {
12: internal INPUTTYPE type;
13: internal INPUT_UNION i;
14: }
15:
16: // Генерируем анонимное объединение
17: [StructLayout(LayoutKind.Explicit)]
18: struct INPUT_UNION
19: {
20: [FieldOffset(0)]
21: public MOUSEINPUT mi;
22: [FieldOffset(0)]
23: public KEYBDINPUT ki;
24: [FieldOffset(0)]
25: public HARDWAREINPUT hi;
26: };
27:
28: [StructLayout(LayoutKind.Sequential)]
29: struct MOUSEINPUT
30: {
31: public int dx;
32: public int dy;
33: public int mouseData;
34: public MOUSEEVENTF dwFlags;
35: public int time;
36: public IntPtr dwExtraInfo;
37: }
38:
39: [Flags]
40: enum MOUSEEVENTF : int
41: {
42: MOVE = 0x01,
43: LEFTDOWN = 0x02,
44: LEFTUP = 0x04,
45: RIGHTDOWN = 0x08,
46: RIGHTUP = 0x10,
47: MIDDLEDOWN = 0x20,
48: MIDDLEUP = 0x40,
49: ABSOLUTE = 0x8000
50: }
Каждая из этих частей объединяется специфическим образом. Для перемещения курсора мыши нужно создать структуру события мыши. С этой целью в dwFlags указывается тип события мыши (в нашем случае — относительное смещение), а переменным dx и dy присваивается число пикселей, на которое перемещается курсор мыши. Также важно присвоить dwExtraInfo информацию, возвращаемую GetMessageExtraInfo().
C#
1: MOUSEINPUT MEvent = new MOUSEINPUT();
2: MEvent.dwFlags = MOUSEEVENTF.MOVE;
3: MEvent.dx = Rnd.Next(-8, 8) ;
4: MEvent.dy = Rnd.Next(-8, 8);
5: MEvent.dwExtraInfo = GetMessageExtraInfo();
6: Send(MEvent);
Отправка события требует еще нескольких шагов, выполняемых вспомогательным методом Send(). Передавайте эти HID-события (Human Interface Devices) через процедуру SendInput() (была показана ранее). Эта процедура принимает массив событий для разных видов устройств. Поэтому мы должны создать массив, указать тип события, скопировать данные события, а затем выполнить следующий вызов.
C#
1: public virtual bool Send(MOUSEINPUT Event)
2: {
3: INPUT[] Events = new INPUT[1];
4: Events[0].type = INPUTTYPE.MOUSE;
5: Events[0].i.mi = MEvent;
6: return SendInput((uint)Events.Length, Events,
7: Marshal.SizeOf(Events[0])) > 0;
8: }
С этого момента передача щелчка кнопки мыши достаточно прямолинейна. Щелчок мыши — это на самом деле два события: нажатия и отпускания кнопки мыши. Чтобы было интереснее, кнопка мыши выбирается случайным образом. Неприятная часть работы заключается в том, что каждой кнопке мыши присваивается свой бит, поэтому для преобразования номера кнопки в соответствующий бит используется двоичное смещение. А отпускание кнопки — это еще один бит, поэтому второе сообщение сдвигает флаги, чтобы сменить состояние битов с «кнопка нажата» на «кнопка отпущена».
C#
1: MOUSEINPUT MEvent = new MOUSEINPUT();
2: MEvent.dwFlags = (MOUSEEVENTF) (1 << (2*Rnd.Next(0, 3)+1));
3: MEvent.dwExtraInfo = GetMessageExtraInfo ();
4: Send(MEvent);
5:
6: MEvent.dwFlags = (MOUSEEVENTF)((int) MEvent.dwFlags << 1);
7: MEvent.dwExtraInfo = GetMessageExtraInfo ();
8: Send(MEvent);
Генерация бессмысленного текста
Gremlin также может набирать бессмысленные фразы в текстовых редакторах, если пользователь оставит одну из таких программ открытой. Ввод текста осуществляется методом SendWait() класса SendKeys.
C#
1: SendKeys.SendWait(GenerateSentence());
2: SendKeys.SendWait("{ENTER}");
![clip_image001[5] clip_image001[5]](http://blogs.msdn.com/blogfiles/rucoding4fun/WindowsLiveWriter/e79a0a373f10_641/clip_image001%5B5%5D_3ee3b779-f508-4c6b-87ad-be8c3a3d4452.jpg)
Создание фраз — дело нехитрое; я использовал упрощенный генератор Маркова. Этот генератор случайным образом выбирает слово, с которого можно начать предложение. Массив таких слов содержится в переменной Starts. Затем в методе Transition это слово используется для поиска другого слова, которое можно включить в предложение. Для этого применяется словарь NonStart, который, принимая слово как ключ, возвращает список всех слов, которые можно поставить после него. Затем весь процесс повторяется, добавляя слова в конец строки. Процесс прекращается, как только обнаруживается слово, которым можно закончить предложение.
C#
1: static Dictionary<string,string[]> NonStart ;
2: static string[] Starts;
3: static Dictionary<string,string> Terminal ;
4:
5: static string GenerateSentence()
6: {
7: StringBuilder SB = new StringBuilder();
8: string Word = Starts[ Rnd.Next(0, Starts.Length) ];
9:
10: Transition(SB, Word);
11: return SB.ToString();
12: }
13:
14: static void Transition(StringBuilder SB, string Word)
15: {
16: while (true)
17: {
18: SB.Append(Word);
19: SB.Append(' ');
20:
21: // Ищем следующее слово
22: string[] Nexts;
23: if (!NonStart.TryGetValue(Word, out Nexts))
24: break;
25: int Idx = Rnd.Next(Terminal.ContainsKey(Word) ? -1 :
26: 0, Nexts.Length);
27: if (Idx < 0)
28: break;
29: Word = Nexts[Idx];
30: }
31: }
Создание двух словарей и массива — процесс относительно простой. Метод BuildSuffixTree() принимает строку и разбивает ее на фразы. Затем делит каждую фразу на слова (с буквами в нижнем регистре). Первое слово предложения помещается в конец массива Starts. Если слово не первое, оно используется для поиска списка в словаре и дописывается в этот список. Таким образом этот словарь превращается в словарь NonStarts. Последнее слово предложения также помещается в словарь Terminal, чтобы в дальнейшем знать, что им можно заканчивать фразы.
C#
1: static void BuildSuffixTree()
2: {
3: // Сначала создаем список слов, которыми начинаются предложения
4: List<string> Starts1 = new List<string>();
5: Dictionary<string,List<string>> Trans =
6: new Dictionary<string,List<string>>();
7: foreach(string S1 in S.Split(new char[]{'.','?','!'}))
8: {
9: string Prev=null;
10: foreach(string S2 in S1.Split(new char[]{
11: ' ','\n','\r','\t',',',';'}))
12: {
13: if (S2 . Length < 1)
14: continue;
15: if (null == Prev)
16: {
17: Starts1.Add(string.Intern(S2.ToLower()));
18: Prev = string.Intern(S2.ToLower());
19: continue;
20: }
21: List<string> Nextsa;
22: if (! Trans.TryGetValue(Prev, out Nextsa))
23: Trans[Prev] = Nextsa = new List<string>();
24: Prev = string.Intern(S2.ToLower());
25: Nextsa.Add(Prev);
26: }
27: if (null != Prev)
28: Terminal[Prev] = Prev;
29: }
30:
31: // Создаем простой список слов, с которых можно начинать предложение
32: Starts = Starts1.ToArray();
33:
34: // Выравниваем таблицу переходов
35: foreach (string S3 in Trans.Keys)
36: NonStart[S3] = Trans [S3].ToArray();
37: }
Триггеры действий
Аудио-триггер
Аудио-модуль (в файле AudioTrigger.cs) прослушивает все микрофоны. Он использует NAudio в сочетании со структурой делегата и базовой настройкой, заимствованной из статьи Марка Хита (Mark Heath) «.NET Audio Recording». Первое отличие в том, что он регистрирует все аудиоустройства ввода.
C#
1: static WaveIn[] StartMics()
2: {
3: int NumDevices = WaveIn.DeviceCount;
4: WaveIn[] AudIns = new WaveIn[NumDevices];
5: for (int waveInDevice = 0; waveInDevice < NumDevices;
6: waveInDevice++)
7: {
8: AudIns[waveInDevice] = new WaveIn();
9: AudIns[waveInDevice].DeviceNumber = waveInDevice;
10: AudIns[waveInDevice].DataAvailable += waveIn_DataAvailable;
11: AudIns[waveInDevice].WaveFormat = new WaveFormat(8000, 1);
12: AudIns[waveInDevice].StartRecording();
13: }
14: return AudIns;
15: }
Настоящая магия этого триггера кроется в модифицированной версии делегата waveIn_DataAvailable. Вместо записи аудио он делает проверки на наличие любых звуков. Для начала проверяется, не превышают ли амплитуды звуков предопределенного (очень высокого) порога. Далее сэмплы преобразуются в прямоугольные, суммируются, а результат сравнивается с пороговым значением. Если результат превышает пороговое значение, триггер срабатывает. Эти две проверки отлично подходят для реакции на разговоры и звуки, издаваемые при работе за столом или при наборе текста на клавиатуре.
![clip_image001[9] clip_image001[9]](http://blogs.msdn.com/blogfiles/rucoding4fun/WindowsLiveWriter/e79a0a373f10_641/clip_image001%5B9%5D_7b617f02-60b0-4940-97c6-e855550cf956.jpg)
{Текст на иллюстрации:
Will trigger based on RMS threshold — Триггер сработает при пороговом среднеквадратичном значении громкости
Will trigger the amplitude threshold — Триггер сработает при пороговом значении амплитуды
}
C#
1: static double AudioThresh = 0.8;
2: static double AudioThresh2 = 0.09;
3:
4: static void waveIn_DataAvailable(object sender, WaveInEventArgs e)
5: {
6: bool Tr = false;
7: double Sum2 = 0;
8: int Count = e.BytesRecorded / 2;
9: for (int index = 0; index < e.BytesRecorded; index += 2)
10: {
11: double Tmp = (short)((e.Buffer[index + 1] << 8) |
12: e.Buffer[index + 0]);
13: Tmp /= 32768.0;
14: Sum2 += Tmp*Tmp;
15: if (Tmp > AudioThresh)
16: Tr = true;
17: }
18: Sum2 /= Count;
19:
20: // Если среднеквадратичное значение больше порогового,
21: // устанавливаем флаг, указывающий на наличие шума
22: if (Tr || Sum2 > AudioThresh2)
23: Interlocked.Exchange(ref AudioTrigger, 1);
24: }
Этот код устанавливает флаг AudioTrigger, который будет обнаружен (и сброшен) в основном цикле следующим блоком кода.
C#
1: while (0 == Shutdown)
2: {
3: Application.DoEvents();
4: Thread.Sleep (20);
5:
6: //… выполняем какие-то другие операции…
7:
8: // Проверяем реакцию звуковой системы
9: if (0 != AudioTrigger)
10: {
11: Interlocked.Exchange(ref AudioTrigger, 0);
12: …
13: ScreenShaker.Screens();
14: continue;
15: }
16: //… другие проверки на простой компьютера…
17: }
Ожидание простоя
Чтобы проверить, делает ли пользователь что-либо, в основном цикле применяется вспомогательный метод LastInputTime(), возвращающий число миллисекунд, в течение которых пользователь ничего не вводил. Цикл ожидает, когда пользователь ничего не делает в течение достаточно длительного времени, и лишь тогда вызывает метод, который случайным образом выбирает действия программы Gremlin.
C#
1: uint LastTime = Win32.LastInputTime();
2: if ((uint) Environment.TickCount < IdleTimeTrigger + LastTime)
3: continue;
Вспомогательный метод LastInputTime() использует P/Invoke-вызов API-функции GetLastInputInfo(). В вики на сайте Pinvoke.net есть сигнатура этой функции и подходящая структура, которую нужно передать в GetLastInputInfo().
C#
1: [DllImport("User32.dll")]
2: static extern bool GetLastInputInfo(ref LASTINPUTINFO LastInfo);
3:
4: [StructLayout(LayoutKind.Sequential)]
5: struct LASTINPUTINFO
6: {
7: public uint cbSize;
8:
9: /// <summary>
10: /// Число системных тактов
11: /// </summary>
12: public uint dwTime;
13: }
Затем эти две структуры обертываются во вспомогательную процедуру LastInputTime(), которая выполняет всю черновую работу, связанную с созданием и инициализацией структуры.
C#
1: internal static uint LastInputTime()
2: {
3: LASTINPUTINFO lastInput=new LASTINPUTINFO();
4: lastInput.cbSize = (uint)Marshal.SizeOf(lastInput);
5: GetLastInputInfo(ref lastInput);
6: return lastInput.dwTime;
7: }
Заключение
Изначально предполагается, что гремлины не будут слишком шалить. Но с помощью параметра –aggressive в командной строке гремлины станут гораздо активнее.
Дополнительные материалы и ссылки
Эта статья — наглая смесь экспериментального и повторно использованного кода из более ранних экспериментов и других демонстрационных программ. Ниже перечислены некоторые из проектов, откуда я позаимствовал примеры кода.
· Pinvoke.net — сайт в стиле вики, содержащий массу полезных материалов и примеров кода, относящихся к вызову Windows API из .NET.
· .NET Audio Recorder Марка Хита (Mark Heath) — отсюда я украл код, чтобы создать свой аудио-триггер.
· «Possessed PC Pranks for Halloween» Брайена Пика (Brian Peek) — отсюда я позаимствовал базовый код для передачи нажатий клавиш.
· «April Fools’ Day Application» Брайена Пика — отсюда я одолжил код для захвата изображения с экрана и его поворота.
Об авторе
Рэндалл Маас (Randall Maas) пишет микрокод (прошивки) для медицинских устройств и консультирует по вопросам, связанным со встраиваемым ПО. А до этого он много чем занимался…, как и любой другой специалист в индустрии программного обеспечения. Вы можете связаться с ним по адресу randym@acm.org.
Опубликовано 08 октября 2009 в 14:29:00 | Coding4Fun
В этой статье я расскажу, как записывать голос с микрофона в .NET, обеспечивая настройку уровня записи, а также о том, как убрать шумы в начале и конце записи, как с помощью WPF визуализировать звуковые волны и конвертировать запись в формат MP3.
Запись звука в .NET
Непосредственно в .NET framework нет поддержки аудиозаписи, поэтому я воспользовался проектом с открытым исходным кодом NAudio (EN), в котором реализованы оболочки для различных API записи звука, реализованных в Windows.
Примечание: Важно отметить, что .NET – не лучший выбор, если вы хотите записать звук с высокой частотой семплирования и малой задержкой, как это делается с помощью ПО звукозаписывающих студий. Объясняется это тем, что сборщик мусора .NET может в любой момент прервать процесс. Между тем для записи голоса с микрофона .NET framework вполне подойдет. По умолчанию NAudio запрашивает у аудиокарты данные каждые 100 мс, а этого времени вполне достаточно и для сборщика мусора, и для работы нашего собственного кода.
Мы будем использовать оболочки для waveIn API, поскольку они наиболее универсальны и позволяют варьировать частоту семплирования. Мы будем записывать звук моно, 16 бит, с частотой семплирования 8 кГц, что вполне приемлемо для записи речи и не накладно для процессора. Последнее важно, поскольку мы хотим еще и визуализировать звук.
Выбор записывающего устройства
Как правило, запись будет производиться со стандартного устройства, однако если вы хотите предоставить пользователю возможность выбора устройства – NAudio к вашим услугам. С помощью WaveIn.DeviceCount и WaveIn.GetDeviceCapabilities вы можете вычислить количество устройств ввода звука, а также определить их названия и число поддерживаемых ими каналов.
На моем компьютере обнаруживалось единственное waveIn-устройство (Набор микрофонов), пока я не подключил гарнитуру, после чего появилось новое устройство и стало стандартным (устройство 0 всегда является стандартным).
1: for (int waveInDevice = 0; waveInDevice < waveInDevices; waveInDevice++)
2: {
3: WaveInCapabilities deviceInfo = WaveIn.GetCapabilities(waveInDevice);
4: Console.WriteLine("Device {0}: {1}, {2} channels",
5: waveInDevice, deviceInfo.ProductName, deviceInfo.Channels);
6: }
В результате на моем компьютере обнаружилось:
1: Device 0: Microphone / Line In (SigmaTel , 2 channels
2: Device 1: Microphone Array (SigmaTel High, 2 channels
К сожалению, названия устройств обрезались, поскольку структура WAVEINCAPS может хранить лишь 31 символ. Есть способ получения полного названия устройства (EN), но он слишком запутанный.
Обычно вы будете выбирать Device 0 (устройство по умолчанию), а если захотите выбрать другое, просто присвойте свойству DeviceNumber объекта WaveIn нужный номер.
Проверка уровня записи
Как правило, при записи звука сначала пользователю дают возможность определить, работает ли микрофон. Это особенно актуально для пользователей, имеющих более одного входа на звуковой карте. Мы используем такой способ: запустим запись и отобразим уровень улавливаемого звука в виде шкалы. API waveIn ничего не сохраняет на диск, так что звук на самом деле пока еще не записывается, а лишь показывается его уровень.
Для записи мы используем класс WaveIn из NAudio. Прежде чем обратиться к StartRecording и начать запись, мы определим формат в WaveFormat (8 кГц, моно).
1: waveIn = new WaveIn();
2: waveIn.DeviceNumber = selectedDevice;
3: waveIn.DataAvailable += waveIn_DataAvailable;
4: int sampleRate = 8000; // 8 kHz
5: int channels = 1; // mono
6: waveIn.WaveFormat = new WaveFormat(sampleRate, channels);
7: waveIn.StartRecording();
Обработчик события DataAvailable будет уведомлять нас о заполнении буфера данными, поступающими из звуковой карты. Данные поступают в виде массива байтов, представляющего данные семпла импульсно-кодовой модуляции. Для сохранения звука на диске это удобно, но что, если мы хотим что-то делать с самими звуковыми данными? Каждый звуковой семпл – 16-разрядный, т. е. занимает два байта, следовательно нам надо преобразовать их в одно число.
Примечание: В случае записи стерео, мы будем иметь дело с двумя 16-разрядными семплами: первый будет семпл левого канала, второй – правого.
Приведенный ниже фрагмент кода демонстрирует обработку байтов, поступающих в событии DataAvailable, а также считывание отдельных звуковых семплов. Обратите внимание: мы используем поле BytesRecorded, а не свойство Length буфера. Кроме того, я решил преобразовывать семплы в 32-разрядный формат с плавающей точкой и масштабировать их таким образом, что максимальное значение составляет 1.0f. Это значительно упростит наложение эффектов и визуализацию.
1: void waveIn_DataAvailable(object sender, WaveInEventArgs e)
2: {
3: for (int index = 0; index < e.BytesRecorded; index += 2)
4: {
5: short sample = (short)((e.Buffer[index + 1] << 8) |
6: e.Buffer[index + 0]);
7: float sample32 = sample / 32768f;
8: ProcessSample(sample32);
9: }
10: }
Примечание: Одна из трудностей применения API waveIn и waveOut – это выбор механизма обратного вызова. NAudio предлагает три варианта. Первый – обратные вызовы функций. Имеется в виду, что вызовы waveIn API являются заданными (закрепленными) указателями функций, обратный вызов которых и выполнятся. Т. е. обратный вызов DataAvailable будет выполняться в фоновом потоке. В некотором смысле это лучший подход, однако надо опасаться глючных драйверов некоторых звуковых плат, которые могут зависать при обращении к waveOutReset при использовании обратного вызова функций (используемый во многих лэптопах SoundMAX явно страдает этим недугом).
Второй вариант – предоставить описатель окна. Вызовы API waveIn будут передавать окну с этим описателем сообщения, требующие обработки. Это наиболее надежный и распространенный метод. Один момент, которого следует избегать: если остановить запись и немедленно начать новую, сообщение из старого сеанса записи будет обработано в новом, что приведет к исключению.
Третий вариант – позволить NAudio создать собственное новое окно и отправлять сообщения ему. При таком подходе мы застрахованы от ложных сообщений из предыдущих сеансов. В NAudio этот метод применяется по умолчанию, если вы вызываете стандартный конструктор WaveIn. Но в этом случае приложение не должно работать в фоновом потоке или быть консольным, иначе создаваемое NAudio окно не сможет обрабатывать очередь сообщений.
Визуализация уровня записи
Мы разобрались как получать звук с аудио-карты, чтобы можно было проверить уровень записи. Теперь представим его пользователю в визуальном виде. Задействуем WPF. Простейший элемент управления, который позволяет отображать графически числовые значения – это индикатор выполнения ProgressBar. А поскольку мы имеем дело с WPF, мы можем настроить индикатор выполнения так, чтобы он напоминал индикатор уровня звука. Для представления уровня звука я использовал градиент цветов от зеленого до красного. Подробней о создании шаблона ProgressBar написано здесь (EN).
Рис. 1. Индикатор выполнения, отображающий текущий уровень громкости микрофона
В качестве вспомогательного класса для отображения уровня звука я написал класс SampleAggregator. Он передает каждый получаемый нами семпл и отслеживает минимальные и максимальные значения. Затем, через заданное число семплов, он инициирует событие, позволяющее отреагировать компонентам, связанным с графическим интерфейсом. Надо быть осторожным и не генерировать слишком много таких событий, иначе реактивность программы ухудшится. Я генерирую его один раз на 800 семплов, что обеспечивает 10 обновлений индикатора в секунду.
Поскольку я использую привязку данных, при каждом таком обновлении индикатора я должен генерировать событие PropertyChangedEvent для моего объекта DataContext (также известного как “ViewModel” в шаблоне MVVM). Вот фрагмент XAML с привязкой свойства CurrentInputLevel:
1: <ProgressBar Orientation="Horizontal"
2: Value="{Binding CurrentInputLevel, Mode=OneWay}"
3: Height="20" />
А вот код из ViewModel, обеспечивающий обновление внешнего вида приложения при вычислении нового максимального входного уровня:
1: private float lastPeak;
2:
3: void recorder_MaximumCalculated(object sender, MaxSampleEventArgs e)
4: {
5: lastPeak = Math.Max(e.MaxSample, Math.Abs(e.MinSample));
6: RaisePropertyChangedEvent("CurrentInputLevel");
7: }
8:
9: // умножаем на 100, т. к. стандартный максимум индикатора выполнения равен 100
10: public float CurrentInputLevel { get { return lastPeak * 100; } }
Примечание: модель View ViewModel (MVVM) становится все популярней среди разработчиков, работающих с WPF и Silverlight. Основная идея состоит в том, что на уровне представления (View) нет никакого кода – оно описывается xaml-файлом разметки, – а разработчику остается лишь определить взаимодействие с бизнес-логикой приложения посредством привязки данных. Облегчить эту привязку призван адаптер ViewModel. Такой подход позволяет четко разграничить представление и поведение. В большинстве случаев этот шаблон работает прекрасно, однако встречаются ситуации, когда приходится написать несколько строк фонового кода или применять такие хитрости как подключение свойств зависимости или нестандартных триггеров. Есть несколько вспомогательных библиотек с открытым исходным кодом, которые могут помочь в разработке приложений на основе шаблона MVVM. Здесь вы найдете их перечень(EN).
Регулировка уровня записи
Допустим, уровень звука в данный момент слишком высокий или низкий. Нам требуется возможность его изменения. Здесь мы также будем применять привязку данных, а бегунок регулировки уровня описываем на XAML:
1: Slider Orientation="Horizontal"
2: Value="{Binding MicrophoneLevel, Mode=TwoWay}"
3: Maximum="100"
4: Margin="5" />
Нам надо уметь удерживать MixerLine, чтобы обеспечить доступ к управлению входным сигналом устройства waveIn. Нам потребуется API микширования Windows, для которого в Naudio есть оболочка. Удерживание регулятора громкости не такой простой процесс, как может показаться, и требует разных подходов в XP и Vista, однако вот код, который работает на большинстве систем:
1: private void TryGetVolumeControl()
2: {
3: int waveInDeviceNumber = 0;
4: var mixerLine = new MixerLine((IntPtr)waveInDeviceNumber,
5: 0, MixerFlags.WaveIn);
6: foreach (var control in mixerLine.Controls)
7: {
8: if (control.ControlType == MixerControlType.Volume)
9: {
10: volumeControl = control as UnsignedMixerControl;
11: break;
12: }
13: }
14: }
Теперь с помощью свойства Percent компонента UnsignedMixerControl мы можем установить уровень записи в пределах от 0 до 100.
Начинаем запись
Установив нужный уровень, мы готовы начать запись. А поскольку устройство waveIn уже открыто, нам остается лишь начать запись уже полученных данных в файл.
В NAudio есть класс WaveFileWriter, который поможет нам сохранить данные в файле. Для начала сохраним их во временном файле в формате PCM, а позднее преобразуем в сжатый формат – MP3. Вот код создания WAV-файла:
1: writer = new WaveFileWriter(waveFileName, recordingFormat);
2: Теперь можно записывать в файл, получив уведомление от устройства waveIn:
3: void waveIn_DataAvailable(object sender, WaveInEventArgs e)
4: {
5: if (recordingState == RecordingState.Recording)
6: writer.WriteData(e.Buffer, 0, e.BytesRecorded);
7:
8: // ...
9: }
Примечание: Существуют три основных варианта хранения записанного звука. Первый – запись его в MemoryStream. Он позволяет не связываться с временными файлами, но требует осторожности, чтобы не перерасходовать память. Кроме того, при сбое программы в процессе записи, вы потеряете все, что уже было записано. При том качестве, которое мы используем в данном примере, нам надо менее 1 мегабайта для записи одной минуты звука. Однако при записи стерео с частотой 44.1 кГц (стандарт для музыки) потребуется около 10 Мб на минуту.
Второй вариант, который мы как раз применяем в нашем примере – запись во временный WAV-файл с последующим преобразованием в другой формат. Хотя используемый здесь формат не эффективен в плане используемого дискового пространства, он очень прост. Особенно же он полезен, если вы хотите применить к записанному звуку какие-то эффекты или редактировать его после записи.
Третий вариант – передача звука прямо кодировщику (например, WMA или MP3) по мере его записи. Это может быть подходящим выбором в случае длинной записи, которую вы не предполагаете редактировать.
Завершение записи
Естественно, что нам надо уметь останавливать запись, когда пользователь щелкает соответствующую кнопку, но также мы должны предусмотреть завершение записи в случае заполнения диска. В нашем примере мы разрешаем записывать не более одной минуты.
1: long maxFileLength = this.recordingFormat.AverageBytesPerSecond * 60;
2:
3: int toWrite = (int)Math.Min(maxFileLength - writer.Length, bytesRecorded);
4: if (toWrite > 0)
5: writer.WriteData(buffer, 0, bytesRecorded);
6: else
7: Stop();
процедуры обратного вызова, последний фрагмент записываемого звука поступает после того, как вы запросили завершить запись. Так что не закрывайте результирующий файл раньше времени и записывайте весь звук. Момент безопасного закрытия файла WaveFileWriter и освобождения ресурсов вы можете определить с помощью события FinishedRecording объекта WaveIn.
Визуализация звука
Иногда бывает полезно показать пользователю звуковые волны. Запись с визуализацией звукового сигнала иногда называют «уверенной записью», поскольку вы видите, что она не прекращается и уровень в норме.
Для отображения звукового сигнала есть множество способов. Простейший – прорисовка вертикальной линии, отображающей минимальный и максимальный сигнал при каждом срабатывании агрегатора семплов:
Рис. 2. Отображение звука посредством вертикальных отрезков
На первый взгляд, реализовать это в WPF очень просто, однако велик риск потребления излишних ресурсов. Например, просто добавлять новую линию к Canvas при вычислении каждого нового максимума очень неэффективно, лучше иметь фиксированный набор вертикальных отрезков и динамически изменять их высоту.
Другой подход – использовать многоугольник. В этом случае при получении каждого семпла надо добавлять две точки к коллекции Points объекта Polygon. Фокус в том, чтобы добавить эти точки в середину коллекции Points, а не в конец, чтобы в результате получалась единая фигура. Это значит, что наше изображение может иметь разные цвета контура и заливки. Чтобы контуры не были слишком неровными, будем объединять по две точки по оси X.
Рис. 3. Изображение звукового сигнала, визуализированное с помощью Polygon
Как и в случае уровня сигнала микрофона, компонент прорисовки аудио-сигнала должен получать несколько уведомлений в секунду со значениями минимума и максимума от SampleAggregator. При получении значения очередного семпла мы либо вставляем новые точки в многоугольник, либо, – если экран заполнен – переходим к левой границе и продолжаем рисовать оттуда.
Для реализации «уверенной» записи я применял метод многоугольника, который представлен классом PolygonWaveFormControl. Вот код, вычисляющий положение новых или существующих точек при получении нового максимального семпла:
1: public void AddValue(float maxValue, float minValue)
2: {
3: int visiblePixels = (int)(ActualWidth / xScale);
4: if (visiblePixels > 0)
5: {
6: CreatePoint(maxValue, minValue);
7:
8: if (renderPosition > visiblePixels)
9: {
10: renderPosition = 0;
11: }
12: int erasePosition = (renderPosition + blankZone) % visiblePixels;
13: if (erasePosition < Points)
14: {
15: double yPos = SampleToYPosition(0);
16: waveForm.Points[erasePosition] =
17: new Point(erasePosition * xScale, yPos);
18: waveForm.Points[BottomPointIndex(erasePosition)] =
19: new Point(erasePosition * xScale, yPos);
20: }
21: }
22: }
23:
24: private void CreatePoint(float topValue, float bottomValue)
25: {
26: double topYPos = SampleToYPosition(topValue);
27: double bottomYPos = SampleToYPosition(bottomValue);
28: double xPos = renderPosition * xScale;
29: if (renderPosition >= Points)
30: {
31: int insertPos = Points;
32: waveForm.Points.Insert(insertPos, new Point(xPos, topYPos));
33: waveForm.Points.Insert(insertPos + 1, new Point(xPos, bottomYPos));
34: }
35: else
36: {
37: waveForm.Points[renderPosition] = new Point(xPos, topYPos);
38: waveForm.Points[BottomPointIndex(renderPosition)] =
39: new Point(xPos, bottomYPos);
40: }
41: renderPosition++;
42: }
Вычисление erasePosition – это обнуление нескольких значений, чтобы было видно, где будут появляться новые данные при новом цикле:
Рис. 4. «Пустая зона» элемента управления PolygonWaveForm
Примечание: В WPF есть методы более быстрого вывода. Один из них – использование для рисования класса WriteableBitmap. Это подходящий метод при рисовании вертикальных линий. Второй способ заключается в использовании объекта DrawingVisual, экономичного и обеспечивающего большую производительность по сравнению с классами, производными от Shape. Недостатком здесь можно считать отсутствие таких возможностей как DataBinding и описание рисунка в XAML, хотя для изображения звуковых волн это вряд ли препятствия. Я использую метод DrawingVisual в той части приложения, которая отвечает за сохранение звука.
Еще один вопрос заключается в том, как компонент визуализации звука должен получать уведомления: поскольку я применяю MVVM, у меня нет прямого доступа к SampleAggregator. Простой способ решить эту проблему – создать свойство зависимости (Dependency Property) для PolygonWaveFormControl:
1: public static readonly DependencyProperty SampleAggregatorProperty =
2: DependencyProperty.Register(
3: "SampleAggregator",
4: typeof(SampleAggregator),
5: typeof(PolygonWaveFormControl),
6: new PropertyMetadata(null, OnSampleAggregatorChanged));
7:
8: public SampleAggregator SampleAggregator
9: {
10: get { return (SampleAggregator)this.GetValue(SampleAggregatorProperty); }
11: set { this.SetValue(SampleAggregatorProperty, value); }
12: }
13:
14: private static void OnSampleAggregatorChanged(object sender, DependencyPropertyChangedEventArgs e)
15: {
16: PolygonWaveFormControl control = (PolygonWaveFormControl)sender;
17: control.Subscribe();
18: }
Это позволяет нам привязать PolygonWaveFormControl к SampleAggregator, который является открытым в нашем DataContext:
1: my:PolygonWaveFormControl
2: Height="40"
3: SampleAggregator="{Binding SampleAggregator}" />
Подстройка звука
У нас есть временный WAV-файл, и прежде, чем пользователь сохранит его в результирующем файле, мы хотим предоставить ему возможность убрать ненужные куски в начале и конце записи. Для этого я хочу показать весь записанный фрагмент с наложенным поверх прямоугольником, позволяющим выбрать нужную часть записи.
Рис. 5. Интерфейс, позволяющий выбрать часть записи
Для реализации такого интерфейса нам понадобятся три компонента. Первый – это ScrollViewer. Он позволит прокручивать картинку влево/вправо, если она не помещается на экране (что вполне вероятно для записей длительностью более нескольких секунд).
Второй компонент является разновидностью визуализатора звуковых колебаний, отображающим целый файл, в отличие от PolygonWaveFormControl, который перерисовывает картинку при заполнении экрана. Для этого я создал WaveFormVisual, который использует объект DrawingVisual для рисования всей записи. Очевидно, что если нам требуется длительная запись, этот метод требует оптимизации, поскольку создаваемый им многоугольник будет иметь тысячи точек. Между тем, для коротких записей он работает хорошо.
Самый сложный третий элемент – прямоугольник, позволяющий выбирать часть записи с помощью мыши. Он реализован в RangeSelectionControl.
RangeSelectionControl – это голубой прямоугольник со сплошной границей и полупрозрачной заливкой, связанный с холстом Canvas. Самое интересное – в обработчике событий мыши. Нам надо определять, когда пользователь навел указатель на левую или правую границу прямоугольника и устанавливать вид курсора, соответствующий изменению размера по горизонтали. Это делается в обработчике события MouseMove: проверяется координата X и затем устанавливается свойство Cursor:
1: Cursor = Cursors.SizeWE;
Если пользователь щелкает левую кнопку при наведении курсора на границу, мы начинаем перетаскивание. Здесь главное – обращаться к Canvas.CaptureMouse. Если мы не будем этого делать, при попытке увеличить прямоугольник событие перемещения мыши будет потеряно и перейдет к следующему по иерархии компоненту.
1: void RangeSelectionControl_MouseDown(object sender, MouseButtonEventArgs e)
2: {
3: if (e.LeftButton == MouseButtonState.Pressed)
4: {
5: Point position = e.GetPosition(this);
6: Edge edge = EdgeAtPosition(position.X);
7: DragEdge = edge;
8: if (DragEdge != Edge.None)
9: {
10: mainCanvas.CaptureMouse();
11: }
12: }
13: }
Теперь в методе MouseMove мы можем изменить свойства Canvas.Left и Width прямоугольника, чтобы изменились его размеры.
Работать с ScrollViewer довольно просто, но не забудьте установить свойство CanContentScroll в true, а также корректно устанавливать размер элементов в ScrollViewer.
1: <ScrollViewer CanContentScroll="True"
2: HorizontalScrollBarVisibility="Visible"
3: VerticalScrollBarVisibility="Hidden">
4: <Grid>
5: <my:WaveFormVisual Height="100"
6: HorizontalAlignment="Left"
7: x:Name="waveFormRenderer"/>
8: <my:RangeSelectionControl
9: HorizontalAlignment="Left"
10: x:Name="rangeSelection" />
11: </Grid>
12: </ScrollViewer>
Мы присвоили свойству Width объекта WaveFormVisual и RangeSelectionControl значения в соответствии с общим числом точек, выведенных при визуализации звука.
Сохранение записи
Теперь мы готовы сохранять запись. Мы предложим пользователю два варианта формата. Первый – WAV-файл. Если пользователь выбрал сохранение всей записи, нам надо лишь скопировать запись в выбранное пользователем место. Если же пользователь выбрал сохранение некоторого диапазона, нам надо урезать WAV-файл. Проще всего это сделать с помощью функции TrimWavFile, которая копирует исходный WAV-файл в результирующий, опуская указанное число байт в начале и в конце.
1: public static void TrimWavFile(string inPath, string outPath,
2: TimeSpan cutFromStart, TimeSpan cutFromEnd)
3: {
4: using (WaveFileReader reader = new WaveFileReader(inPath))
5: {
6: using (WaveFileWriter writer =
7: new WaveFileWriter(outPath, reader.WaveFormat))
8: {
9: int bytesPerMillisecond =
10: reader.WaveFormat.AverageBytesPerSecond / 1000;
11:
12: int startPos = (int)cutFromStart.TotalMilliseconds *
13: bytesPerMillisecond;
14: startPos = startPos - startPos % reader.WaveFormat.BlockAlign;
15:
16: int endBytes = (int)cutFromEnd.TotalMilliseconds *
17: bytesPerMillisecond;
18: endBytes = endBytes - endBytes % reader.WaveFormat.BlockAlign;
19: int endPos = (int)reader.Length - endBytes;
20:
21: TrimWavFile(reader, writer, startPos, endPos);
22: }
23: }
24: }
25:
26: private static void TrimWavFile(WaveFileReader reader,
27: WaveFileWriter writer, int startPos, int endPos)
28: {
29: reader.Position = startPos;
30: byte[] buffer = new byte[1024];
31: while (reader.Position < endPos)
32: {
33: int bytesRequired = (int)(endPos - reader.Position);
34: if (bytesRequired > 0)
35: {
36: int bytesToRead = Math.Min(bytesRequired, buffer.Length);
37: int bytesRead = reader.Read(buffer, 0, bytesToRead);
38: if (bytesRead > 0)
39: {
40: writer.WriteData(buffer, 0, bytesRead);
41: }
42: }
43: }
44: }
Мы также предложим возможность сохранения в формате MP3. Простейший способ создания MP3-файлов – применение MP3-кодировщика с открытым исходным кодом LAME (если у вас еще нет этого приложения, поищите в веб lame.exe). Наше приложение будет искать его в текущем каталоге и запрашивать пользователя указать местоположение lame.exe, если его там нет, поскольку мы не включили его в загружаемый дистрибутив. Когда нужный путь будет указан, мы сможем преобразовать наш (урезанный) WAV-файл в MP3 простым обращением к lame.exe с нужными параметрами.
1: public static void ConvertToMp3(string lameExePath,
2: string waveFile, string mp3File)
3: {
4: Process converter = Process.Start(lameExePath, "-V2 \"" + waveFile
5: + "\" \"" + mp3File + "\"");
6: converter.WaitForExit();
7: }
Итак, в результате мы получили компактный MP3-файл с выбранной частью микрофонной записи.
Состав решения
Основное демонстрационное WPF-приложение находится в проекте VoiceRecorder. Оно состоит из основного окна с тремя представлениями и связанными с ними ViewModels. VoiceRecorder.Core содержит некоторые вспомогательные WPF-классы и пользовательские компоненты, а VoiceRecorder.Audio включает классы, выполняющие запись, редактирование и преобразование звука.
Об авторе
Марк Хит (Mark Heath) – разработчик ПО, работающий в настоящее время в NICE CTI Systems в Саутгемптоне, Великобритания. Он специализируется на разработке для .NET с уклоном на клиентские технологии и работу со звуком. Его блог о программировании аудио, WPF, Silverlight и передовых методах программной инженерии – http://mark-dot-net.blogspot.com (EN). Он автор нескольких проектов с открытым исходным кодом, опубликованных на CodePlex, таких как низкоуровневый инструментальный набор Naudio (http://www.codeplex.com/naudio (EN)).
Опубликовано 29 октября 2009 14:48:00 | Coding4Fun

Вам приходилось сталкиваться с размытым текстом в WPF? Что ж, в .Net 4.0 и WPF 4.0 этот недостаток устранен. Вы можете использовать исправления при работе с версиями Visual Studio 2010, начиная с beta 2. В этом блоге MSDN описаны способы решения проблемы и причины размытости текста (EN). Многое связано с недостаточной плотностью пикселов на экране и межпикселной визуализацией. Но как решить проблему? Вот пример вывода текста, показанного на рисунке выше:
1: <StackPanel>
2: <TextBlock>
3: Hello World ... Ideal text formatting
4: </TextBlock>
5: <TextBlock TextOptions.TextFormattingMode="Display">
6: Hello World ... Display text formatting
7: </TextBlock>
8: </StackPanel>
Опубликовано 26 октября 2009 18:56:00 | Coding4Fun
В своем блоге, посвященном Visual Basic, Мэтт Гертц (Matt Gertz) (EN) опубликовал интереснейшую статью о различных нюансах, связанных с циклами. Например, он показывает, как обратное прохождение цикла вместо прямого может существенно упростить приложение, описывает применение массивов с циклами, подсчитывает значение счетчика при каждом проходе…
У Мэтта есть и другая интересная статья о скрытых издержках (EN) в программах.
Опубликовано 21 октября 2009 14:56:00 | Coding4Fun

Хотите попробовать новейшую и крутейшую версию Visual Studio Express? Команда разработчиков Visual Studio только что представила VS 2010 Beta 2. Полная финальная версия должна выйти в марте 2010.
Не забывайте, что это бета-версия. При возникновении проблем отправляйте их описание разработчикам, чтобы они могли их устранить.
Опубликовано 19 октября 2009 09:04:00 | Coding4Fun
В этом эпизоде шоу Coding4Fun (EN) Брайан Пик (Brian Peek (EN)) беседует с Джеремая Морриллом (Jeremiah Morrill (EN)), автором WPF MediaKit (EN) – отличной библиотеки, упрощающей создание элементов управления DirectShow и MediaFoundation в WPF. Она содержит очень мощные средства и весьма проста в применении разработчиками – для использования ее возможностей требуются лишь несколько строк XAML. Например, вы можете встроить в свое приложение полноценный DVD-плеер или компонент записи видео, представив каждый из них одним XAML-тегом! Познакомьтесь с этим замечательным проектом: узнайте историю его создания и возможности применения в собственных творениях.
Опубликовано 16 октября 2009 13:11:00 | Coding4Fun
Если вам нужна помощь в освоении XNA, обратитесь к блогу Шоуна Наргрейвза (Shawn Hargreaves) (EN). Он рассматривает такие темы, как анизотропная фильтрация, мипмаппинг и фильтрация текстур.
Не знаете, что такое мипмаппинг? Это предварительная подготовка копии изображения меньшего размера, чем оригинал. Она позволяет управлять визуализацией, а не выполнять ее сразу.
Опубликовано 5 октября 2009 10:00:00 | Coding4Fun
Саймону Хоуду (Simon Hoade) всегда нравились трехмерные стеки фотографий в Windows и он решил реализовать подобный эффект сам (EN).
В своей публикации он демонстрирует применение потоков и обращение к WPF-коду с веб-сервера в приложении, которое не является WPF.
Он упоминает два руководства, которые ему помогли:
· http://www.codeproject.com/KB/WPF/WPFImageEffects.aspx
· http://www.codeproject.com/KB/WPF/WPFImageEffects.aspx
Опубликовано 30 сентября 2009 10:37:00 | Coding4Fun
Рик Барраза ( Rick Barraza) написал три впечатляющих примера применения Silverlight.
- Перезаписываемые растры (EN)
Пример демонстрирует создание интересных эффектов с помощью смещения пикселов при прорисовке линий. - Динамика жидкостей (EN)
Здесь Рик рассказывает о динамических эффектах. - Векторные поля (EN)
В этом примере демонстрируется обесцвечивание изображения, обнаружение границ и использование полученной информации для рисования векторов на картинке.
Если вы хотите получить дополнительные сведения о Рике, почитайте его интервью о специальных возможностях визуализации в Silverlight, которое он дал Брайану Пику (Brian Peek) (EN).
Опубликовано 28 сентября 2009 08:42:00 | Coding4Fun
Коуди Батт (Cody Batt) описывает в статье, опубликованной на сайте DevX, о вызове сценария PowerShell в собственном приложении. Коуди подробно рассказывает о вызове PowerShell и приводит небольшой пример получения работающих процессов (EN).
Для этого требуются некоторые хитрые манипуляции, типа редактирования файла проекта в Блокноте и добавления ссылки на System.Management.Automation.
Подробнее о PowerShell можно почитать на следующих сайтах: http://powershell.com/cs/ps/overview.aspx, http://thepowershellguy.com/blogs/posh/, http://blogs.msdn.com/powershell/.
Опубликовано 23 сентября 2009 11:36:00 | Coding4Fun
Команда ASP.Net анонсировала несколько новых общедоступных сервисов. Если у вас есть веб-сайт, на котором применяются jquery или библиотеки ASP.Net Ajax, вы можете воспользоваться нашими серверами сети доставки содержимого (content delivery network, CDN).
Для этого вам надо на своем веб-сайте заменить текущую ссылку на сценарии jquery следующей ссылкой:
1: <script
2: src="http://ajax.microsoft.com/ajax/jquery/jquery-1.3.2.min.js"
3: type="text/javascript"> 1:
</script>
Полный список библиотек JavaScript и соответствующих URL, которые мы уже выложили в сети CDN, можно посмотреть здесь: www.asp.net/ajax/cdn
[Из ScottGu (EN)]
Опубликовано 18 сентября 2009 09:48:00 | Coding4Fun
Хотите увидеть, как делать всякие интересные вещи с Zune HD?
Инструменты разработчика (устанавливать в указанном порядке):
Дэн Уотерс (Dan Waters) также описывает в своем блоге создание полноценной игры под названием Inertia для ZuneHD (EN). В первой публикации Дэн рассказывает о проектировании и написании игры и привязки ее к Zune HD.
Опубликовано 9 сентября 2009 17:00:00 | Coding4Fun
Дэйв Кук ( Dave Cook (EN)) опубликовал интересный материал о Netflix API. Этот программный интерфейс доступен через веб-службы, так что для его применения необходимо подключение к Интернету.
В настоящий момент есть четыре статьи, в порядке возрастания сложности:
Если хотите создать проект netflix, зарегистрируйтесь как разработчик на домашней странице Netflix Developer Network.
Опубликовано 3 сентября 2009 23:43:00 | Coding4Fun
Введение
Раз за разом я сталкиваюсь с ситуацией, когда надо отправить кому-то файл, но сделать это оказывается не так-то просто. Программы мгновенного обмена сообщениями часто не могут обеспечить обмен из-за наличия брандмауэров, различий в версиях клиентских программ и других несовместимостей. При использовании электронной почты я сталкивался с тем, что почтовый сервер моего корреспондента блокировал файлы определенных типов. Предлагаемая программа позволяет двум пользователям быстро и просто связываться посредством клиентов, написанных на Silverlight 3 и слать друг другу файлы.
Обзор
В данном приложении пользователь сначала выбирает, инициировать ли ему самому сеанс или подключиться к существующему. Если он решает сам управлять сеансом, ему будет предоставлен случайный восьмисимвольный ключ, и программа будет находиться в состоянии ожидания до тех пор, пока не подключится другой пользователь. Когда какой-то пользователь захочет подключиться к данному сеансу, ему потребуется данный ключ для установления связи. Соединившись между собой, пользователи смогут обмениваться файлами и простыми текстовыми сообщениями.
Опрос в дуплексном режиме
Для обмена сообщениями между двумя клиентскими приложениями требуется некая центральная общая точка для маршрутизации сообщений. Поскольку Silverlight-приложение легко можно расположить на странице ASP.NET, мы будем использовать серверные возможности ASP.NET для управления коммуникациями между пользователями. Нам нужна служба, которая будет принимать входящие сообщения от Silverlight-клиента и переправлять их требуемому адресату. Это делается с помощью WCF-канала опроса в дуплексном режиме Polling Duplex (System.ServiceModel.PollingDuplex.dll). Silverlight 3 позволяет добавить ссылку на такую службу и скрывает от нас все сложные подробности ее работы. Я начал с использования файла DuplexService.cs из демонстрационного приложения, опубликованного на MIX09, когда вышла бета-версия Silverlight 3. Там есть пара абстрактных базовых классов и интерфейсов, от которых мы будем наследовать классы собственной службы.
FileSendService
Две главные вещи для создания собственной службы — это определение специальных типов сообщений, которые будут применяться в наших коммуникациях, и переопределение класса DuplexService таким образом, чтобы он правильно обрабатывал эти сообщения. Для создания собственных типов сообщений мы используем в качестве базового класс DuplexMessage, который определен в DuplexService.cs. Наши сообщения должны быть определены с атрибутом [DataContract], а переменные-члены должны быть открытыми и иметь атрибут [DataMember]. Это позволит проекту Silverlight-клиента предоставлять доступ к этим определениям через ссылку службы. Кроме того, класс сообщения Duplex должен иметь атрибут [KnownType] для каждого созданного сообщения-наследника.
C#
1: [KnownType(typeof(HostSessionMessage))]
2: [KnownType(typeof(JoinSessionMessage))]
3: [KnownType(typeof(FileBeginUploadMessage))]
4: [KnownType(typeof(FileTransferBytesMessage))]
5: public class DuplexMessage { }
6: [DataContract]
7: public class HostSessionMessage : DuplexMessage
8: {
9: [DataMember]
10: public string Username;
11: }
12:
13: [DataContract]
14: public class JoinSessionMessage : DuplexMessage
15: {
16: [DataMember]
17: public string Username;
18: [DataMember]
19: public string SessionKey;
20: }
21:
22: [DataContract]
23: public class FileBeginUploadMessage : DuplexMessage
24: {
25: [DataMember]
26: public string FileName;
27: [DataMember]
28: public long TotalBytes;
29: }
30:
31: [DataContract]
32: public class FileTransferBytesMessage : DuplexMessage
33: {
34: [DataMember]
35: public long StartByte;
36: [DataMember]
37: public long PacketSize;
38: [DataMember]
39: public byte[] Bytes;
40: [DataMember]
41: public bool EndFile;
42: }
43:
VB
1: <DataContract(Namespace := "http://samples.microsoft.com/silverlight2/duplex"),
2: KnownType(GetType(HostSessionMessage)), KnownType(GetType(JoinSessionMessage)), KnownType(GetType(FileBeginUploadMessage)), KnownType(GetType(FileTransferBytesMessage))> _
3: Public Class DuplexMessage
4: End Class
5:
6: <DataContract()> _
7: Public Class HostSessionMessage
8: Inherits DuplexMessage
9: <DataMember()> Public Username As String
10: End Class
11:
12: <DataContract()> _
13: Public Class JoinSessionMessage
14: Inherits DuplexMessage
15: <DataMember()> Public Username As String
16: <DataMember()> Public SessionKey As String
17: End Class
18:
19: <DataContract()> _
20: Public Class FileTransferBytesMessage
21: Inherits DuplexMessage
22: <DataMember()> Public StartByte As Long
23: <DataMember()> Public PacketSize As Long
24: <DataMember()> Public Bytes() As Byte
25: <DataMember()> Public EndFile As Boolean
26: End Class
27:
28: <DataContract()> _
29: Public Class FileBeginUploadMessage
30: Inherits DuplexMessage
31: <DataMember()> Public FileName As String
32: <DataMember()> Public TotalBytes As Long
33: End Class
Следующий шаг — создание класса FileSendService, производного от DuplexService, как было сказано выше. Переопределим метод OnMessage чтобы обрабатывать сообщения собственных типов:
C#
1: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
2: public class FileSendService : DuplexService
3: {
4: private List<SessionConnectionInfo> sessionConnections = new List<SessionConnectionInfo>();
5:
6: {...}
7:
8: protected override void OnMessage(string sessionId, DuplexMessage data)
9: {
10: if (data is HostSessionMessage)
11: CreateHostSession(data as HostSessionMessage);
12: else if (data is JoinSessionMessage)
13: JoinSession(data as JoinSessionMessage);
14: else if (data is FileBeginUploadMessage)
15: StartSendFile(data as FileBeginUploadMessage);
16: else
17: SendMessage(data);
18: }
19:
20: }
21:
22: else if (data is JoinSessionMessage)
23: JoinSession(data as JoinSessionMessage);
24: else if (data is FileBeginUploadMessage)
25: StartSendFile(data as FileBeginUploadMessage);
26: else
27: SendMessage(data);
28: }
29:
30: }
31:
32: }
33:
34: }
35:
VB
1: Public Class FileSenderServiceFactory
2: Inherits DuplexServiceFactory(Of FileSendService)
3: End Class
4:
5: <AspNetCompatibilityRequirements(RequirementsMode := AspNetCompatibilityRequirementsMode.Allowed)> _
6: Public Class FileSendService
7: Inherits DuplexService
8: Private sessionConnections As New List(Of SessionConnectionInfo)()
9:
10: ...
11:
12: Protected Overrides Sub OnMessage(ByVal sessionId As String, ByVal data As DuplexMessage)
13: If TypeOf data Is HostSessionMessage Then
14: CreateHostSession(TryCast(data, HostSessionMessage))
15: ElseIf TypeOf data Is JoinSessionMessage Then
16: JoinSession(TryCast(data, JoinSessionMessage))
17: ElseIf TypeOf data Is FileBeginUploadMessage Then
18: StartSendFile(TryCast(data, FileBeginUploadMessage))
19: Else
20: SendMessage(data)
21: End If
22: End Sub
23:
24: End Class
Чтобы отслеживать все хосты и подключенных к ним пользователей, создадим класс SessionConnectionInfo для управления соответствующими данными. Когда пользователь решает создать сеанс и управлять им, наш метод OnMessage получает сообщение HostSessionMessage и просматривает список List<SessionConnectionInfo> на предмет наличия в нем другого хоста с тем же именем пользователя. Если таковой не обнаруживается, создается новый объект SessionConnectionInfo и сеансовый ключ, сгенерированный из случайных значений. Когда пользователь пытается подключиться к сеансу, метод OnMessage получает сообщение JoinSessionMessage, содержащее сеансовый ключ и имя подключающегося к сеансу пользователя. Наша служба ищет объект SessionConnectionInfo с таким сеансовым ключом и, если находит, связывает двух пользователей между собой.
C#
1: public class SessionConnectionInfo
2: {
3: public string HostUserName { get; set; }
4: public string ConnectedUsername { get; private set; }
5: public string SessionKey { get; set; }
6:
7: public string ConnectedUserInternalSession { get; private set; }
8: public string HostInternalSession { get; set; }
9:
10: public bool UserConnected
11: {
12: get { return ConnectedUserInternalSession != string.Empty; }
13: }
14:
15: public SessionConnectionInfo()
16: {
17: ConnectedUserInternalSession = string.Empty;
18: ConnectedUsername = string.Empty;
19: }
20:
21: ....
22: }
VB
1: Public Class SessionConnectionInfo
2: Private privateHostUserName As String
3: Public Property HostUserName() As String
4: Get
5: Return privateHostUserName
6: End Get
7: Set(ByVal value As String)
8: privateHostUserName = value
9: End Set
10: End Property
11: Private privateConnectedUsername As String
12: Public Property ConnectedUsername() As String
13: Get
14: Return privateConnectedUsername
15: End Get
16: Private Set(ByVal value As String)
17: privateConnectedUsername = value
18: End Set
19: End Property
20: Private privateSessionKey As String
21: Public Property SessionKey() As String
22: Get
23: Return privateSessionKey
24: End Get
25: Set(ByVal value As String)
26: privateSessionKey = value
27: End Set
28: End Property
29:
30: Private privateConnectedUserInternalSession As String
31: Public Property ConnectedUserInternalSession() As String
32: Get
33: Return privateConnectedUserInternalSession
34: End Get
35: Private Set(ByVal value As String)
36: privateConnectedUserInternalSession = value
37: End Set
38: End Property
39: Private privateHostInternalSession As String
40: Public Property HostInternalSession() As String
41: Get
42: Return privateHostInternalSession
43: End Get
44: Set(ByVal value As String)
45: privateHostInternalSession = value
46: End Set
47: End Property
48:
49: Public ReadOnly Property UserConnected() As Boolean
50: Get
51: Return ConnectedUserInternalSession <> String.Empty
52: End Get
53: End Property
54:
55: Public Sub New()
56: ConnectedUserInternalSession = String.Empty
57: ConnectedUsername = String.Empty
58: End Sub
59:
60:
61: End Class
62:
Клиентская часть
Основы серверной части готовы и можем добавить ссылку на нашу службу. Наша служба базируется в ASP.NET посредством простого xml-файла (см. FileSendService.svc), который позволяет нашему Silverlight-проекту ее видеть.

Не забудьте установить флажок «Всегда создавать контракты сообщений» (“Always generate message contracts”). Имейте в виду: часть проекта, связанная с веб, должна быть скомпилирована, чтобы можно было обнаружить службу.
Теперь у нас есть доступ к классу DuplexServiceClient, который позволит нам обмениваться сообщениями с сервером. Создадим экземпляр службы, как показано ниже. Предварительно надо вручную добавить ссылку на сборку System.ServiceModel.PollingDuplex.
C#
1: private DuplexServiceClient fileDuplexService;
2:
3: private CustomBinding binding = new CustomBinding(
4: new PollingDuplexBindingElement(),
5: new BinaryMessageEncodingBindingElement(),
6: new HttpTransportBindingElement());
7:
8: public MainPage()
9: {
10: InitializeComponent();
11: fileDuplexService = new DuplexServiceClient(binding, new EndpointAddress("http://localhost:9797/FileSendService.svc"));
12: ...
13: }
VB
1: Private fileDuplexService As DuplexServiceClient
2:
3: Private binding As New CustomBinding(New PollingDuplexBindingElement(), New BinaryMessageEncodingBindingElement(), New HttpTransportBindingElement())
4:
5: Public Sub New()
6: InitializeComponent()
7: fileDuplexService = New DuplexServiceClient(binding, New EndpointAddress("http://localhost:9797/FileSendService.svc"))
8: End Sub
Обратите внимание: на момент написания этой статьи, при добавлении ссылки на службу, файл ServiceReferences.ClientConfig не создавался. По этой причине в приведенном выше коде это делается программно.
Настроим службу таким образом, чтобы она обрабатывала отправляемые и получаемые сообщения. Для отправки сообщения сначала надо создать сообщение DupexMessage (или его производную) и применить метод SendToServiceAsync. Для этого требуется объект SendToService, содержащий данное сообщение, а также необязательный объект userState, который мы можем применять для маркирования запроса. В данном случае мы передаем перечисление, описывающее состояние передачи. В приведенном ниже примере, когда пользователь щелкает кнопку отправки, предварительно выбрав файл, мы открываем наш файл и отправляем сообщение FileBeginUpload, содержащее имя файла и его размер. Заметьте: сервер настроен на отказ от отправки файлов размером более 20 миллионов байт.
C#
1: private void btnSendFile_Click(object sender, RoutedEventArgs e)
2: {
3: OpenFileDialog openFileDialog = new OpenFileDialog();
4: openFileDialog.Multiselect = false;
5: openFileDialog.ShowDialog();
6: if (openFileDialog.File != null)
7: {
8: fileToSend = openFileDialog.File.OpenRead();
9:
10: FileBeginUploadMessage fsm = new FileBeginUploadMessage();
11: fsm.FileName = openFileDialog.File.Name;
12: fsm.TotalBytes = openFileDialog.File.Length;
13: fileDuplexService.SendToServiceAsync(new SendToService(fsm), FileSendState.FileStart);
14: ....
15: }
16: }
VB
1: Private Sub btnSendFile_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
2: Dim openFileDialog As New OpenFileDialog()
3: openFileDialog.Multiselect = False
4: openFileDialog.ShowDialog()
5: If openFileDialog.File IsNot Nothing Then
6: fileToSend = openFileDialog.File.OpenRead()
7: Dim fsm As New FileBeginUploadMessage()
8: fsm.FileName = openFileDialog.File.Name
9: fsm.TotalBytes = openFileDialog.File.Length
10: totalBytesSent = 0
11: fileDuplexService.SendToServiceAsync(New SendToService(fsm), FileSendState.FileStart)
12: ....
13: End If
14: End Sub
Наша служба генерирует два события, требующие обработки: SendToServiceCompleted и SendToClientReceived. Событие SendToServiceCompleted возникает после того как сервер подтверждает получение и завершение обработки отправленного клиентом сообщения. После того, как сервер получил и обработал сообщение FileBeginUploadMessage, приведенный ниже обработчик события получает результаты. В данном случае, если нет ошибки, а userState имеет значение FileSendState.FileStart, метод отправляет сообщение FileTransferBytesMessage, которое передает данные файла.
C#
1: private void FileDuplexServiceSendToServiceCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
2: {
3: if (e.Error == null)
4: {
5: {...}
6: if ((FileSendState)e.UserState == FileSendState.FileEnd)
7: {
8: fileToSend.Close();
9: fileProgress.Value = 100;
10: return;
11: }
12: if ((FileSendState)e.UserState == FileSendState.FileStart || (FileSendState)e.UserState == FileSendState.FileContinue)
13: {
14:
15: ...
16: FileTransferBytesMessage fileMessage = new FileTransferBytesMessage();
17: fileMessage.StartByte = totalBytesSent;
18: fileMessage.EndFile = false;
19: fileMessage.PacketSize = CHUNK;
20:
21: ...
22:
23: byte[] bytes = new byte[numBytesToRead];
24: fileToSend.Read(bytes, 0, numBytesToRead);
25: totalBytesSent += numBytesToRead;
26: fileMessage.Bytes = bytes;
27:
28: if (fileMessage.EndFile)
29: fileDuplexService.SendToServiceAsync(new SendToService(fileMessage), FileSendState.FileEnd);
30: else
31: fileDuplexService.SendToServiceAsync(new SendToService(fileMessage), FileSendState.FileContinue);
32: }
33: }
34: }
VB
1: Private Sub FileDuplexServiceSendToServiceCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)
2: If e.Error Is Nothing Then
3: If e.UserState Is Nothing Then
4: Return
5: End If
6: If CType(e.UserState, FileSendState) = FileSendState.FileEnd Then
7: fileToSend.Close()
8: fileProgress.Value = 100
9: Return
10: End If
11: If CType(e.UserState, FileSendState) = FileSendState.FileStart OrElse CType(e.UserState, FileSendState) = FileSendState.FileContinue Then
12: ...
13: Dim fileMessage As New FileTransferBytesMessage()
14: fileMessage.StartByte = totalBytesSent
15: fileMessage.EndFile = False
16: fileMessage.PacketSize = CHUNK
17: ...
18: Dim bytes(numBytesToRead - 1) As Byte
19: fileToSend.Read(bytes, 0, numBytesToRead)
20: totalBytesSent += numBytesToRead
21: fileMessage.Bytes = bytes
22:
23: If fileMessage.EndFile Then
24: fileDuplexService.SendToServiceAsync(New SendToService(fileMessage), FileSendState.FileEnd)
25: Else
26: fileDuplexService.SendToServiceAsync(New SendToService(fileMessage), FileSendState.FileContinue)
27: End If
28: End If
29: End If
30: End Sub
31:
Событие SendToClientReceived генерируется после отправки сообщения нашей службой. Клиент выясняет тип этого сообщения и соответствующим образом его обрабатывает. В приведенном ниже методе пользователь принимает или отклоняет получение файла при поступлении сообщения FileBeginUpload. Если пользователь не принимает файл, отправляется сообщение FileDenyMessage, указывающее отправителю, что надо прекратить передавать данные. В противном случае данные добавляются в буфер.
C#
1: private void FileDuplexServiceSendToClientReceived(object sender, SendToClientReceivedEventArgs e)
2: {
3: if (e.Error == null)
4: {
5: if (e.request.msg is ClientConnectedMessage)
6: {
7: ClientConnectedMessage msg = (ClientConnectedMessage)e.request.msg;
8: AddMsgToListbox(msg.Username + " has just connected.");
9: connectedTo = msg.Username;
10: UIState = UIState.Chat;
11: }
12:
13: else if (e.request.msg is HostSessionServerMessage)
14: {
15: HostSessionServerMessage hssm = e.request.msg as HostSessionServerMessage;
16: if (hssm.Failed) {...}
17: SessionCreated(hssm);
18:
19: }
20: else if (e.request.msg is JoinSessionServerMessage)
21: {
22: JoinSessionServerMessage jssm = e.request.msg as JoinSessionServerMessage;
23: if (jssm.Failed) {....}
24: SessionJoined(jssm);
25: }
26: else if (e.request.msg is FileBeginUploadMessage )
27: {
28: FileBeginUploadMessage fsm = (FileBeginUploadMessage)e.request.msg;
29:
30: int sizeInKB = (int)fsm.TotalBytes / 1024;
31: totalRevd = 0;
32: if (MessageBox.Show(connectedTo + " would like to send you the file: " + fsm.FileName + ", Size: " + sizeInKB + ". Would you like to receive this file?", "File Upload", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
33: {
34: bytesReceived = new List<byte>((int)fsm.TotalBytes);
35: fileNameReceiving = fsm.FileName;
36: ....
37: }
38: else
39: {
40: fileDuplexService.SendToServiceAsync(new SendToService(new FileDenyMessage()));
41: }
42: }
43: else if (e.request.msg is FileTransferBytesMessage)
44: {
45: if (bytesReceived == null)
46: return;
47: FileTransferBytesMessage fm = (FileTransferBytesMessage)e.request.msg;
48: bytesReceived.AddRange(fm.Bytes);
49: ....
50: }
51: else {....}
52:
53: }
54: }
VB
1: Private Sub FileDuplexServiceSendToClientReceived(ByVal sender As Object, ByVal e As SendToClientReceivedEventArgs)
2: If e.Error Is Nothing Then
3: If TypeOf e.request.msg Is ClientConnectedMessage Then
4: Dim msg As ClientConnectedMessage = CType(e.request.msg, ClientConnectedMessage)
5: AddMsgToListbox(msg.Username & " has just connected.")
6: connectedTo = msg.Username
7: UIState = UIState.Chat
8:
9: ElseIf TypeOf e.request.msg Is HostSessionServerMessage Then
10: Dim hssm As HostSessionServerMessage = TryCast(e.request.msg, HostSessionServerMessage)
11: If hssm.Failed Then ...
12:
13: SessionCreated(hssm)
14:
15: ElseIf TypeOf e.request.msg Is JoinSessionServerMessage Then
16: Dim jssm As JoinSessionServerMessage = TryCast(e.request.msg, JoinSessionServerMessage)
17: If jssm.Failed Then ...
18:
19: SessionJoined(jssm)
20: ElseIf TypeOf e.request.msg Is FileBeginUploadMessage Then
21: Dim fsm As FileBeginUploadMessage = CType(e.request.msg, FileBeginUploadMessage)
22:
23: Dim sizeInKB As Integer = CInt(Fix(fsm.TotalBytes)) / 1024
24: totalRevd = 0
25: If MessageBox.Show(connectedTo & " would like to send you the file: " & fsm.FileName & ", Size: " & sizeInKB & " KB. Would you like to receive this file?", "File Upload", MessageBoxButton.OKCancel) = MessageBoxResult.OK Then
26: bytesReceived = New List(Of Byte)(CInt(Fix(fsm.TotalBytes)))
27: fileNameReceiving = fsm.FileName
28: ...
29: Else
30: fileDuplexService.SendToServiceAsync(New SendToService(New FileDenyMessage()))
31: End If
32: ElseIf TypeOf e.request.msg Is FileTransferBytesMessage Then
33: If bytesReceived Is Nothing Then
34: Return
35: End If
36: Dim fm As FileTransferBytesMessage = CType(e.request.msg, FileTransferBytesMessage)
37: bytesReceived.AddRange(fm.Bytes)
38: ...
39: ElseIf
40: ...
41:
42: End If
43: End Sub
По завершении передачи файла пользователь увидит две кнопки. Одна позволяет сохранить файл, а другая — уничтожить полученные данные. Если пользователь выбирает сохранение, появляется диалоговое окно SaveFileDialog в котором можно задать имя файла, а расширение водить нет необходимости. После задания пользователем имени фала, данные записываются на диск. В завершении серверу отправляется ответное сообщение, которое позволит ему уведомить пользователя, что принимающая сторона сделала что-то с файлом. Во время передачи файла кнопка Send File остается недоступной и разблокируется только после того, как был получен или отклонен или операция была прервана.
C#
1: private void btnSaveFile_Click(object sender, RoutedEventArgs e)
2: {
3: SaveFileDialog sfd = new SaveFileDialog();
4: string extension = GetExtension(fileNameReceiving);
5: sfd.DefaultExt = extension;
6: sfd.Filter = extension + " Files|" + extension;
7:
8: if (sfd.ShowDialog() == true)
9: {
10: using (Stream fsx = sfd.OpenFile())
11: {
12: byte[] fBytes = bytesReceived.ToArray();
13: fsx.Write(fBytes, 0, fBytes.Length);
14: fsx.Close();
15: }
16:
17: fileDuplexService.SendToServiceAsync(new SendToService(new FileReceivedMessage()));
18: UIState = UIState.Chat;
19: btnSendFile.IsEnabled = true;
20: }
21: }
22:
VB
1: Private Sub btnSaveFile_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
2: Dim sfd As New SaveFileDialog()
3: Dim extension As String = GetExtension(fileNameReceiving)
4: sfd.DefaultExt = extension
5: sfd.Filter = extension & " Files|" & extension
6:
7: If sfd.ShowDialog() = True Then
8: Using fsx As Stream = sfd.OpenFile()
9: Dim fBytes() As Byte = bytesReceived.ToArray()
10: fsx.Write(fBytes, 0, fBytes.Length)
11: fsx.Close()
12: End Using
13:
14: fileDuplexService.SendToServiceAsync(New SendToService(New FileReceivedMessage()))
15: UIState = UIState.Chat
16: btnSendFile.IsEnabled = True
17: End If
18: End Sub
Завершение
В целом приложение выполняет мои задумки. Пользователи могут обмениваться файлами, и есть даже элементарный чат. Конечно же, есть много вариантов усовершенствования этой программы и увеличения ее надежности. Сейчас поддерживается список соединений, но не выполняется простое пингование, позволяющее убедиться в том, что клиент еще на месте. Единственный способ узнать, что клиент отключился, это сбой в передаче сообщения или когда пользователь щелкает кнопку отключения. Требуется лучшая поддержка этого списка. Данные файла хранятся в памяти, что заставило меня ввести ограничения по объему, но я уверен, что с использованием таких вещей, как локальное хранилище Silverlight, это ограничение можно облегчить.
Благодарности
Я хочу поблагодарить Брайана Пика (Brian Peek (EN)), который нашел время рецензировать мою статью и проверить код программы.
Дополнительные замечания
В проекте ASP.NET необходимо указать в качестве ссылки файл System.ServiceModel.PollingDuplex.dll. Где-то в промежутке между Silverlight 3 Beta и Silverlight 3 RTW, этот файл исчез из числа доступных в основном списке ссылок .NET. Я его добавлял из %Program Files%\Microsoft SDKs\Silverlight\v3.0\Libraries\Server. Для пользователей 64-разрядной версии это будет папка Program Files (x86).
Для простоты использования я в данном проекте работал со статическим портом 9797. При установке на сервере вам надо переименовать все ссылки http://localhost:9797 в Silverlight-проекте. Это останется в силе, пока поддерживается файл config.
В пользовательском интерфейсе задействована тема TwilightBlue из Silverlight Toolkit. Дополнительные сведения по этому вопросу см. на странице http://www.codeplex.com/Silverlight (EN).
Опубликовано 27 августа 2009 в 11:01:00 | Coding4Fun
Поскольку проект Popfly закрывается 31 августа, игровой движок выпущен с лицензией Ms-Pl. Если вы используете Popfly Game Downloader (EN), который мы выпустили несколько недель назад, там вы можете найти инструкцию по дальнейшей работе с играми и получению файлов данных. Это означает, что вы сможете воссоздать первоначальный внешний вид!
Обращайтесь в Codeplex (EN).
[Из блога Ben Anderson (EN)]