Запись телефонных переговоров

altКак-то в один период нашей жизни моя семья столкнулась с некоторыми сложностями в плане детского садика и двух моих сыновей. Моя жена практически постоянно висела на телефоне в разговоре то с заведующей садика, то с воспитателями, то с людьми из Министерства образования. Как я жалел, что в тот момент у меня нет устройства, с помощью которого можно записывать разговоры. Это длилось несколько недель, и каждый раз я думал, что вот-вот сделаю блок прослушки телефона. Но руки не доходили, все что-то отвлекало. В общем, проблемы ушли. А блок я сделал уже потом, и вот теперь наконец-то написал программу. Естественно, это — просто образец, урезка, чтобы просто наглядно показать как можно сделать запись разговоров по телефону самому. Теперь, кстати, моя жена даже имеет сотовый с автоматической записью переговоров.

Аппаратная часть
Итак, с чего начать. Для начала нужно сделать электронную примочку для телефонной линии. Она не сложная и даже человек, не особо дружащий с паяльником, думаю, справится с этим заданием. Схема на рисунке 1.


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

1) Вход – вход телефонной линии. Т.е. эти два контакта подключаются параллельно телефонной линии. Полярность подключения значения не имеет.

2) C1 – конденсатор неполярный емкостью 0,1 микрофарад. Рассчитан на напряжение не менее 250 вольт. Для улучшения качества звука его можно увеличить до 2,2 мкФ. Служит для того, чтобы не замыкать линию. Включение трансформатора к телефонной линии без разделяющего конденсатора приводит к занятию линии, т.е. равносильно поднятию трубки телефонного аппарата. Хотя в интернете есть схемы, которые не используют этого конденсатора.

3) T1 – трансформатор. Он наматывается на пластинах Ш14 с толщиной набора 17 мм, обе обмотки содержат 1000 витков провода ПЭВ 0,12. Обмотки должны быть хорошо изолированы друг от друга. Возможно использование меньшего трансформатора, т.е. любой звуковой малогабаритный, например, от карманных радиоприемников, абонентских громкоговорителей, компьютерных модемов (кстати, они лучше всего подойдут).

4) D1 и D2 – импульсные диоды 1n4148 (отечественный аналог КД522). Их можно заменить на стабилитроны, но тогда они должны быть включены навстречу друг другу не параллельно, а последовательно. Диоды, подключенные встречно, служат для предотвращения выхода из строя звуковой карты. Происходит ограничение до уровня линейного входа аудиокарты, т.е. до прямого падения на диодах ~0.7В (один для одной полуволны, второй диод для второй).

5) R1 – переменный резистор. Можно поставить сопротивление в 10 КОм. Служит для подстройки усиления сигнала из линии.

6) Выход – выход мини-джек 3,5мм. Подключается к линейному входу звуковой карты.

Примечание: вход и выход — полярность подключения значения не имеет.

Чтобы не утруждать себя вытравливанием печатной платы, можно купить готовую. Называется «Макетная печатная плата». Они бывают разные. Я выбрал такую, как на рисунке 2. Отпиливаем небольшой кусок. Остальное использовал под другие схемы. Далее просто вставляем наши детали и припаиваем их. Дорожки делаем с помощью обычных одножильных проводов, очищенных от изоляции. На входе схемы используется гнездо RJ-12 (Рис. 3).


Рисунок 2. Макетная печатная плата

Рисунок 3. Гнездо RJ-12
Штекер мини-джек имеет три контакта, поэтому надо запаять его в режиме МОНО. Внешне блок у меня выглядит так, как показано на рисунках ниже (Рис. 4, 5).


Рисунок 4. Готовое устройство (общий вид)

Рисунок 5. Готовое устройство (крупный план)
Для улучшения качества звука необходимо использовать экранированный провод. В моем случае это — двухжильный провод, где каждая жила находится в экране. Теперь можно этот блок убрать в корпус. Можно сделать корпус из картона. Схема не влияет на телефонную линию. В интернете говорят что схема даже не влияет на модем, хотя модемом по-моему уже никто не пользуется. Янки не в счет…

Программная часть
Итак, мы подошли к программной части нашего с вами проекта. Скажу сразу, что проект я писал на Delphi 2010. Если ты пользуешься Delphi ниже 2009 версии, то будь внимателен к типам переменных. В частности Char, AnsiChar. AnsiString я не использую. Начнем с того, что создадим новый проект (File –> New –> VCL Forms Application – Delphi). Присвоим свойству формы Name новое имя – fMain. Кинем две кнопки TButton и назовем их bStart и bStop. Еще добавим на форму 4 компонента TLabel. Назовем их так:

1) lCurrentSoundLevel (поле Caption «Текущий уровень звука: 0»);
2) lActuatingThreshold (поле Caption «Порог срабатывания звука: 0»);
3) lCurrentStat (поле Caption «Текущее состояние: простой»);
4) lRecordWAVFile (поле Caption «Запись в WAV-файл: неактивна»);

Так же кинем на форму еще три компонента: TTimer, TGauge и TTrackBar. Их мы обзывать никак не будем, оставим имена по умолчанию. Почему я выбрал TGauge, а не TProgressBar? Я программирую в Windows Seven. А в этой системе слишком много анимации и прогрессбар слишком долго «заполняется». Если ты будешь программировать в Windows XP, то можешь смело использовать прогрессбар. Поставим свойство компоннета MaxValue в 32767. У Timer1 поставим свойство Interval в 10000 (10 секунд). У TrackBar1 свойство Max поставим на 500.


Итак, перейдем в коду. Программу будем писать с использованием WINAPI. Записывать файл будем в файл WAV. Не поверишь, но нигде нет нормального описания записи в WAV-файл. Проштудировал весь интернет, просмотрел код почти на всех языках программирования. В итоге получилось собрать по крупицам информацию, плюс немного прозрения и запись заработала. Для начала определим константы (Листинг 1).
Листинг 1. Определяем константы

const
// Формат
FwFormatTag = WAVE_FORMAT_PCM;
// Количество каналов
FnChannels = 1;
// Частота дискретизации
FnSamplesPerSec = 22050;
// Количество байт, переданных за секунду воспроизведения
FwBitsPerSample = 16;
// Количество байт для одного сэмпла, включая все каналы
FnBlockAlign = FwBitsPerSample div 8 * FnChannels;
// Количество бит в сэмпле
FnAvgBytesPerSec = FnSamplesPerSec * FnBlockAlign;
// Не могу объяснить, просто скопипастил
FnSamplesPerSecCnt = FnSamplesPerSec div 5;
// Размер буфера
FBufferSize = FnSamplesPerSecCnt * 2;
// Не могу объяснить, просто скопипастил
FDiv = FnSamplesPerSecCnt div 100;
// Количество буферов
FBuffersCount = 2;
В секции Type пропишем как в листинге 2.
Листинг 2. Секция Type

TDataEvent = procedure(AData: PAnsiChar; ASize: Cardinal) of object;
// Массив данных
TArrayOfSample = array [0 .. FnSamplesPerSecCnt — 1] of SmallInt;
PArrayOfSample = ^TArrayOfSample;
// Заголовок WAV-файла
TWaveHeader = record
// Идентификатор RIFF. Содержит символы «RIFF»
FchunkId: array [0 .. 3] of AnsiChar;
// Количество байт в области данных плюс 36 или
// размер файла минус 8. Исключены поля FchunkId и FchunkSize
FchunkSize: Longint;
// Идентификатор WAVE. Содержит символы «WAVE»
Fformat: array [0 .. 3] of AnsiChar;
// Фрагмент формата звуковых данных fmt. Содержит символы «fmt «
// (три символа «fmt» и пробел на конце)
Fsubchunk1Id: array [0 .. 3] of AnsiChar;
// 16 для формата PCM. Это оставшийся размер подцепочки,
// начиная с этой позиции.
Fsubchunk1Size: Longint;
// Аудио формат, для PCM = 1 (то есть, Линейное квантование)
FaudioFormat: Word;
// Количество каналов. Моно = 1, Стерео = 2 и т.д.
FnumChannels: Word;
// Частота дискретизации. 44100 Гц и т.п.
FsampleRate: Longint;
// Количество байт, переданных за секунду воспроизведения.
FbyteRate: Longint;
// Количество байт для одного сэмпла, включая все каналы.
FblockAlign: Word;
// Количество бит в сэмпле. Так называемая «глубина» или точность
// звучания. 8 бит, 16 бит и т.д.
FbitsPerSample: Word;
// Идентификатор data. Содержит символы «data»
Fsubchunk2Id: array [0 .. 3] of AnsiChar;
// Количество байт в области данных или
// размер файла минус 44 (размер заголовка).
Fsubchunk2Size: Longint;
end;
Теперь определим переменные и процедуры (Листинг 3).
Листинг 3. Определяем переменные и процедуры

private
// Путь к программе
FDir: String;
// Устройство захвата звука
FHWI: HWaveIn;
// Массив буферов
FHeadersIn: Array Of TWaveHdr;
// Формат записи
FWFormat: TWaveFormatEx;
// Загловок WAV-файла
FWaveHeader: TWaveHeader;
// Активно ли устройство записи
FActive: Boolean;
// Текущий уровень звука
FCurrentSoundLevel: Word;
// Порог срабатывания записи
FActuatingThreshold: Word;
// Ждущий режим
FWaitingIsLaunched: Boolean;
// Файловый поток
FFileStream: TFileStream;
FOnData: TDataEvent;
procedure StartRecord;
procedure StopRecord;
procedure WaveInCallback(var msg: TMessage); message MM_WIM_DATA;
procedure WaveInOnData(AData: PAnsiChar; ASize: Cardinal);
procedure SaveWAVFile;
public
На OnCreate формы напишем как в листинге 4.
Листинг 4. OnCreate формы

// Путь к программе
FDir := ExtractFilePath(Application.EXEName);
// Заполняем формат записи
with FWFormat do
begin
wFormatTag := FwFormatTag;
nChannels := FnChannels;
nSamplesPerSec := FnSamplesPerSec;
wBitsPerSample := FwBitsPerSample;
nBlockAlign := FnBlockAlign;
nAvgBytesPerSec := FnAvgBytesPerSec;
cbSize := 0;
end;
// Заполняем заголовок WAV-файла
with FWaveHeader do
begin
FchunkId := ‘RIFF’;
FchunkSize := 0;
Fformat := ‘WAVE’;
Fsubchunk1Id := ‘fmt ‘;
Fsubchunk1Size := 16;
FaudioFormat := FwFormatTag;
FnumChannels := FnChannels;
FsampleRate := FnSamplesPerSec;
FbyteRate := FnAvgBytesPerSec;
FblockAlign := FnBlockAlign;
FbitsPerSample := FwBitsPerSample;
Fsubchunk2Id:= ‘data’;
Fsubchunk2Size := 0;
end;
//
FOnData := WaveInOnData;
// Текущий уровень звука = 0
FCurrentSoundLevel := 0;
// Порог срабатывания записи = 0
FActuatingThreshold := 0;
// Ждущий режим = отключен
FWaitingIsLaunched := false;
В переменной FDir будет сохранен путь к программе. Рядом с EXE будем складывать WAV-файлы. В заголовке FWaveHeader полям FchunkSize и Fsubchunk2Size присваиваем ноль, т.к. мы по завершении записи определим размер файла. На кнопке bStart прописываем процедуру:
procedure TfMain.bStartClick(Sender: TObject);
begin
StartRecord;
end;
Собственно, для начала записи нам нужно открыть устройство захвата с помощью функции WaveInOpen. Описание всегда можно найти в интернете. Затем при успешном открытии инициализируем буферы и подготавливаем их для передачи устройству захвата. Код будет выглядеть так, как в листинге 5.
Листинг 5. Активируем запись

procedure TfMain.StartRecord;
var
i: Integer;
begin
FActive := false;
// Открыть аудиоустройство для записи
if WaveInOpen(@FHWI, 0, @FWFormat, Handle, 0, CALLBACK_WINDOW or WAVE_MAPPED)
0 then
Exit;
// Обнуляемся
FHeadersIn := nil;
// Устанавливаем количество буферов
SetLength(FHeadersIn, FBuffersCount);
// Проходимся по всем буферам
for i := 0 to FBuffersCount — 1 do
begin
with FHeadersIn do
begin
// Получаем память под наш буфер
lpData := PAnsiChar(GlobalAlloc(GMEM_FIXED, FBufferSize));
// Длина буфера
dwBufferLength := FBufferSize;
dwFlags := 0;
dwUser := 0;
end;
// Подготовить буфер для записи
WaveInPrepareHeader(FHWI, @FHeadersIn[i], SizeOf(TWaveHdr));
// Отдать буфер аудиоустройству
WaveInAddBuffer(FHWI, @FHeadersIn[i], SizeOf(TWaveHdr));
end;
// Начать запись
WaveInStart(FHWI);
// Активируем запись
FActive := true;
end;
При заполнении очередного буфера данными, будет вызвано событие MM_WIM_DATA и передано процедуре WaveInCallback. Листинг 6.
[i]Листинг 6.

procedure TfMain.WaveInCallback(var msg: TMessage);
var
FPHeaderIn: PWaveHdr;
i, p: Integer;
FBuf: PArrayOfSample;
begin
// Если устройство записи активно, то идем дальше…
if FActive then
begin
// Получаем данные
FPHeaderIn := PWaveHdr(msg.LParam);
// Получаем массив данных
FBuf := PArrayOfSample(FPHeaderIn^.lpData);
// Получение текущего уровня звука
p := 0;
for i := 0 to FnSamplesPerSecCnt div FDiv do
p := p + Abs(FBuf[i * FDiv]);
FCurrentSoundLevel := p div (FnSamplesPerSecCnt div FDiv);
// ———————————————————
// Передаем данные и размер процедуре WaveInOnData
if Assigned(FOnData) then
FOnData(FPHeaderIn.lpData, FPHeaderIn.dwBytesRecorded);
// Освободить записанный буфер
WaveInUnPrepareHeader(FHWI, FPHeaderIn, SizeOf(TWaveHdr));
// Подготовить буфер для записи
WaveInPrepareHeader(FHWI, FPHeaderIn, SizeOf(TWaveHdr));
// Отдать буфер аудиоустройству
WaveInAddBuffer(FHWI, FPHeaderIn, SizeOf(TWaveHdr));
end;
end;
Тут мы получаем данные нашего буфера и передаем их в процедуру WaveInOnData, тут же определяем текущий уровень звука, так сказать индикатор громкости. Вся суть находится в процедуре WaveInOnData. Листинг 7.
Листинг 7.

procedure TfMain.WaveInOnData(AData: PAnsiChar; ASize: Cardinal);
begin
// Отображаем текущий уровень звука
lCurrentSoundLevel.Caption := ‘Текущий уровень звука: ‘ + IntToStr
(FCurrentSoundLevel);
Gauge1.Progress := FCurrentSoundLevel;
// Если уровень меньше установленного, то…
if FCurrentSoundLevel < FActuatingThreshold then
begin
// …если активирован ждущий режим и таймер не активен, то…
if FWaitingIsLaunched and (not Timer1.Enabled) then
begin
// …запускаем таймер
Timer1.Enabled := true;
// и отключаем ждущий режим
FWaitingIsLaunched := false;
end;
end
// …иначе
else
begin
// отключаем тацмер
Timer1.Enabled := false;
// и включаем ждущий режим
FWaitingIsLaunched := true;
end;
// Если таймер активен
if Timer1.Enabled then
// значит есть голос
lCurrentStat.Caption := ‘Текущее состояние: простой’
// …иначе
else
begin
// …если активирован ждущий режим, то…
if FWaitingIsLaunched then
begin
lCurrentStat.Caption := ‘Текущее состояние: активен’;
// …если файловый поток еще не создан, то…
if FFileStream = nil then
begin
// …создаем поток и новый файл с именем текущих даты и временем
FFileStream := TFileStream.Create(FDir + ‘WAV-‘ + FormatDateTime
(‘yyyy-mm-dd-hh-mm-ss’, Now) + ‘.wav’, fmCreate);
// Пишем в начало файла наш заголовок структуры TWaveHeader
FFileStream.Write(FWaveHeader, SizeOf(FWaveHeader));
lRecordWAVFile.Caption := ‘Запись в WAV-файл: активна’;
end;
end
// …иначе
else
// …сохраняем наш WAV-файл
SaveWAVFile;
end;
// Если файловый поток создан, то…
if FFileStream nil then
begin
// …пишем наши аудиоданные в наш файл
FFileStream.Write(AData^, ASize);
end;
end;
Тут мы уже сравниваем текущий уровень звука с установленным нашим TrackBar1. Листинг 8.

Листинг 8.

procedure TfMain.TrackBar1Change(Sender: TObject);
begin
// Установка порога срабатывания записи
FActuatingThreshold := TrackBar1.Max — TrackBar1.Position;
lActuatingThreshold.Caption := ‘Порог срабатывания звука: ‘ + IntToStr
(FActuatingThreshold);
end;
Для чего это нужно? Наша программа висит постоянно в трее и отслеживает громкость звука в линии. Когда трубка положена – один уровень, там не будет 0, т.к. есть естественные шумы линейного входа. Как только вы снимаете трубку, то в линию идет ответ станции, что в простонародье называется «Гудок». При этом уровень сигнала повышается в несколько раз и превышает установленный нами порог. Тогда создается файловый поток и при этом создается WAV-файл. И начинается запись. Как только разговор прекратился (мало ли собеседник отвлекся), то включается таймер, который будет ждать 10 секунд( помнишь, мы в начале договорились, что он будет равен 10с).
procedure TfMain.Timer1Timer(Sender: TObject);
begin
// Отключаем таймер
Timer1.Enabled := false;
end;
Если разговор будет возобновлен, то таймер остановится, если нет, то по истечении 10 секунд будет вызвана процедура SaveWAVFile. Листинг 9.
Листинг 9. Сохраняем wav

procedure TfMain.SaveWAVFile;
var
FFileSize: Integer;
begin
// Если файловый поток создан, то…
if FFileStream nil then
begin
// …отключаем таймер, чтобы не мазолил глаза
Timer1.Enabled := false;
// Смотрим размер нашего файла
FFileSize := FFileStream.Size;
// Меняем два параметра у структуры
with FWaveHeader do
begin
FchunkSize := FFileSize — 8;
Fsubchunk2Size := FFileSize — 44;
end;
// Смещаемся в начало нашего файла
FFileStream.Seek(0, soFromBeginning);
// И пишем измененный заголовок WAV в наш файл
FFileStream.Write(FWaveHeader, SizeOf(FWaveHeader));
// Высвобождаем файловый поток и обнуляем
FreeAndNil(FFileStream);
lRecordWAVFile.Caption := ‘Запись в WAV-файл: неактивна’;
end;
lCurrentStat.Caption := ‘Текущее состояние: простой’;
end;
Помнишь, мы в начале создания формы двум полям заголовка WAV присвоили нули. Так вот теперь настал момент перезаписать заголовок. Подсчитываем размер файла с помощью свойста Size, присваиваем заголовку новые значения, сдвигаемся в начало файла и перезаписываем заголовок. Все!

Теперь осталось еще одно – как закрыть устройство и остановить захват звука. Смотри листинг 10.

Листинг 10. Закрываем устройство, останавливаем захват звука.

procedure TfMain.StopRecord;
var
i: Integer;
begin
// Запрещаем запись
FActive := false;
// Сохраняем наш WAV-файл
SaveWAVFile;
// Проходимся по всем созданным нами буферам
for i := 0 to FBuffersCount — 1 do
// Оосвобождаем записанный буфер
WaveInUnPrepareHeader(FHWI, @FHeadersIn[i], SizeOf(TWaveHdr));
// Остановить запись и установить длительность записанного фрагмента равной нулю
WaveInReset(FHWI);
// Закрыть аудиоустройство
WaveInClose(FHWI);
end;
Процедуру StopRecord можно поставить на кнопку bStop, а также на событие формы OnCloseQuery. Полный код программы можно посмотреть в аттаче к журналу. Я дал тебе основу. Ты можешь сам сделать красивый интерфейс, сворачивание в трей, автозагрузку, конвертер WAV в MP3. Так же можно сделать визуальный отсчет времени, когда включен таймер ожидания. Ну, в общем, фантазируй. Кстати, в коде нет проверок на ошибки. Их тебе придется добавить самому.



Понравилась статья? Поделиться с друзьями: