unit UScreenOptionsRecord;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses
UThemes,
UMusic,
URecord,
UMenu;
type
TDrawState = record
ChannelIndex: integer;
R, G, B: real; // mapped player color (normal)
RD, GD, BD: real; // mapped player color (dark)
end;
TPeakInfo = record
Volume: single;
Time: cardinal;
end;
TScreenOptionsRecord = class(TMenu)
private
// max. count of input-channels determined for all devices
MaxChannelCount: integer;
// current input device
CurrentDeviceIndex: integer;
PreviewDeviceIndex: integer;
// string arrays for select-slide options
InputSourceNames: array of string;
InputDeviceNames: array of string;
// dynamic generated themes for channel select-sliders
SelectSlideChannelTheme: array of TThemeSelectSlide;
// indices for widget-updates
SelectInputSourceID: integer;
SelectSlideChannelID: array of integer;
TextPitchID: array of integer;
// interaction IDs
ExitButtonIID: integer;
// dummy data for non-available channels
ChannelToPlayerMapDummy: integer;
// preview channel-buffers
PreviewChannel: array of TCaptureBuffer;
ChannelPeak: array of TPeakInfo;
// Device source volume
SourceVolume: single;
NextVolumePollTime: cardinal;
procedure StartPreview;
procedure StopPreview;
procedure UpdateInputDevice;
procedure ChangeVolume(VolumeChange: integer);
procedure DrawVolume(x, y, Width, Height: single);
procedure DrawVUMeter(const State: TDrawState; x, y, Width, Height: single);
procedure DrawPitch(const State: TDrawState; x, y, Width, Height: single);
public
constructor Create; override;
function Draw: boolean; override;
function ParseInput(PressedKey: Cardinal; CharCode: WideChar; PressedDown: Boolean): Boolean; override;
procedure onShow; override;
procedure onHide; override;
end;
const
PeakDecay = 0.2; // strength of peak-decay (reduction after one sec)
const
BarHeight = 11; // height of each bar (volume/vu-meter/pitch)
BarUpperSpacing = 1; // spacing between a bar-area and the previous widget
BarLowerSpacing = 3; // spacing between a bar-area and the next widget
SourceBarsTotalHeight = BarHeight + BarUpperSpacing + BarLowerSpacing;
ChannelBarsTotalHeight = 2*BarHeight + BarUpperSpacing + BarLowerSpacing;
implementation
uses
SysUtils,
Math,
SDL,
gl,
UGraphic,
UDraw,
UMain,
UMenuSelectSlide,
UMenuText,
UFiles,
UDisplay,
UIni,
ULog;
function TScreenOptionsRecord.ParseInput(PressedKey: Cardinal; CharCode: WideChar; PressedDown: Boolean): Boolean;
begin
Result := true;
If (PressedDown) Then
begin // Key Down
// check normal keys
case WideCharUpperCase(CharCode)[1] of
'Q':
begin
Result := false;
Exit;
end;
'+':
begin
// FIXME: add a nice volume-slider instead
// or at least provide visualization and acceleration if the user holds the key pressed.
ChangeVolume(2);
end;
'-':
begin
// FIXME: add a nice volume-slider instead
// or at least provide visualization and acceleration if the user holds the key pressed.
ChangeVolume(-2);
end;
'T':
begin
if ((SDL_GetModState() and KMOD_SHIFT) <> 0) then
Ini.ThresholdIndex := (Ini.ThresholdIndex + Length(IThresholdVals) - 1) mod Length(IThresholdVals)
else
Ini.ThresholdIndex := (Ini.ThresholdIndex + 1) mod Length(IThresholdVals);
end;
end;
// check special keys
case PressedKey of
SDLK_ESCAPE,
SDLK_BACKSPACE:
begin
Ini.Save;
AudioPlayback.PlaySound(SoundLib.Back);
FadeTo(@ScreenOptions);
end;
SDLK_RETURN:
begin
if (SelInteraction = ExitButtonIID) then
begin
Ini.Save;
AudioPlayback.PlaySound(SoundLib.Back);
FadeTo(@ScreenOptions);
end;
end;
SDLK_DOWN:
InteractNext;
SDLK_UP :
InteractPrev;
SDLK_RIGHT:
begin
if (SelInteraction >= 0) and (SelInteraction < ExitButtonIID) then
begin
AudioPlayback.PlaySound(SoundLib.Option);
InteractInc;
end;
UpdateInputDevice;
end;
SDLK_LEFT:
begin
if (SelInteraction >= 0) and (SelInteraction < ExitButtonIID) then
begin
AudioPlayback.PlaySound(SoundLib.Option);
InteractDec;
end;
UpdateInputDevice;
end;
end;
end;
end;
constructor TScreenOptionsRecord.Create;
var
DeviceIndex: integer;
SourceIndex: integer;
ChannelIndex: integer;
InputDevice: TAudioInputDevice;
InputDeviceCfg: PInputDeviceConfig;
ChannelTheme: ^TThemeSelectSlide;
ButtonTheme: TThemeButton;
WidgetYPos: integer;
begin
inherited Create;
LoadFromTheme(Theme.OptionsRecord);
// set CurrentDeviceIndex to a valid device
if (Length(AudioInputProcessor.DeviceList) > 0) then
CurrentDeviceIndex := 0
else
CurrentDeviceIndex := -1;
PreviewDeviceIndex := -1;
// init sliders if at least one device was detected
if (Length(AudioInputProcessor.DeviceList) > 0) then
begin
InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex];
InputDeviceCfg := @Ini.InputDeviceConfig[InputDevice.CfgIndex];
// init device-selection slider
SetLength(InputDeviceNames, Length(AudioInputProcessor.DeviceList));
for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do
begin
InputDeviceNames[DeviceIndex] := AudioInputProcessor.DeviceList[DeviceIndex].Name;
end;
// add device-selection slider (InteractionID: 0)
AddSelectSlide(Theme.OptionsRecord.SelectSlideCard, CurrentDeviceIndex, InputDeviceNames);
// init source-selection slider
SetLength(InputSourceNames, Length(InputDevice.Source));
for SourceIndex := 0 to High(InputDevice.Source) do
begin
InputSourceNames[SourceIndex] := InputDevice.Source[SourceIndex].Name;
end;
// add source-selection slider (InteractionID: 1)
SelectInputSourceID := AddSelectSlide(Theme.OptionsRecord.SelectSlideInput,
InputDeviceCfg.Input, InputSourceNames);
// add space for source volume bar
WidgetYPos := Theme.OptionsRecord.SelectSlideInput.Y +
Theme.OptionsRecord.SelectSlideInput.H +
SourceBarsTotalHeight;
// find max. channel count of all devices
MaxChannelCount := 0;
for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do
begin
if (AudioInputProcessor.DeviceList[DeviceIndex].AudioFormat.Channels > MaxChannelCount) then
MaxChannelCount := AudioInputProcessor.DeviceList[DeviceIndex].AudioFormat.Channels;
end;
// init channel-to-player mapping sliders
SetLength(SelectSlideChannelID, MaxChannelCount);
SetLength(SelectSlideChannelTheme, MaxChannelCount);
SetLength(TextPitchID, MaxChannelCount);
for ChannelIndex := 0 to MaxChannelCount-1 do
begin
// copy reference slide
SelectSlideChannelTheme[ChannelIndex] :=
Theme.OptionsRecord.SelectSlideChannel;
// set current channel-theme
ChannelTheme := @SelectSlideChannelTheme[ChannelIndex];
// adjust vertical position
ChannelTheme.Y := WidgetYPos;
// calc size of next slide (add space for bars)
WidgetYPos := WidgetYPos + ChannelTheme.H + ChannelBarsTotalHeight;
// append channel index to name
ChannelTheme.Text := ChannelTheme.Text + IntToStr(ChannelIndex+1);
// add tone-pitch label
TextPitchID[ChannelIndex] := AddText(
ChannelTheme.X + ChannelTheme.W,
ChannelTheme.Y + ChannelTheme.H/2,
'-');
// show/hide widgets depending on whether the channel exists
if (ChannelIndex < Length(InputDeviceCfg.ChannelToPlayerMap)) then
begin
// current device has this channel
// add slider
SelectSlideChannelID[ChannelIndex] := AddSelectSlide(ChannelTheme^,
InputDeviceCfg.ChannelToPlayerMap[ChannelIndex], IChannelPlayer);
end
else
begin
// current device does not have that many channels
// add slider but hide it and assign a dummy variable to it
SelectSlideChannelID[ChannelIndex] := AddSelectSlide(ChannelTheme^,
ChannelToPlayerMapDummy, IChannelPlayer);
SelectsS[SelectSlideChannelID[ChannelIndex]].Visible := false;
// hide pitch label
Text[TextPitchID[ChannelIndex]].Visible := false;
end;
end;
end;
// add Exit-button
ButtonTheme := Theme.OptionsRecord.ButtonExit;
ButtonTheme.Y := WidgetYPos;
AddButton(ButtonTheme);
if (Length(Button[0].Text) = 0) then
AddButtonText(14, 20, Theme.Options.Description[7]);
// store InteractionID
if (Length(AudioInputProcessor.DeviceList) > 0) then
ExitButtonIID := MaxChannelCount + 2
else
ExitButtonIID := 0;
// set focus
Interaction := 0;
end;
procedure TScreenOptionsRecord.UpdateInputDevice;
var
SourceIndex: integer;
InputDevice: TAudioInputDevice;
InputDeviceCfg: PInputDeviceConfig;
ChannelIndex: integer;
begin
//Log.LogStatus('Update input-device', 'TScreenOptionsRecord.UpdateCard') ;
StopPreview();
// set CurrentDeviceIndex to a valid device
if (CurrentDeviceIndex > High(AudioInputProcessor.DeviceList)) then
CurrentDeviceIndex := 0;
// update sliders if at least one device was detected
if (Length(AudioInputProcessor.DeviceList) > 0) then
begin
InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex];
InputDeviceCfg := @Ini.InputDeviceConfig[InputDevice.CfgIndex];
// update source-selection slider
SetLength(InputSourceNames, Length(InputDevice.Source));
for SourceIndex := 0 to High(InputDevice.Source) do
begin
InputSourceNames[SourceIndex] := InputDevice.Source[SourceIndex].Name;
end;
UpdateSelectSlideOptions(Theme.OptionsRecord.SelectSlideInput, SelectInputSourceID,
InputSourceNames, InputDeviceCfg.Input);
// update channel-to-player mapping sliders
for ChannelIndex := 0 to MaxChannelCount-1 do
begin
// show/hide widgets depending on whether the channel exists
if (ChannelIndex < Length(InputDeviceCfg.ChannelToPlayerMap)) then
begin
// current device has this channel
// show slider
UpdateSelectSlideOptions(SelectSlideChannelTheme[ChannelIndex],
SelectSlideChannelID[ChannelIndex], IChannelPlayer,
InputDeviceCfg.ChannelToPlayerMap[ChannelIndex]);
SelectsS[SelectSlideChannelID[ChannelIndex]].Visible := true;
// show pitch label
Text[TextPitchID[ChannelIndex]].Visible := true;
end
else
begin
// current device does not have that many channels
// hide slider and assign a dummy variable to it
UpdateSelectSlideOptions(SelectSlideChannelTheme[ChannelIndex],
SelectSlideChannelID[ChannelIndex], IChannelPlayer,
ChannelToPlayerMapDummy);
SelectsS[SelectSlideChannelID[ChannelIndex]].Visible := false;
// hide pitch label
Text[TextPitchID[ChannelIndex]].Visible := false;
end;
end;
end;
StartPreview();
end;
procedure TScreenOptionsRecord.ChangeVolume(VolumeChange: integer);
var
InputDevice: TAudioInputDevice;
Volume: integer;
begin
// validate CurrentDeviceIndex
if ((CurrentDeviceIndex < 0) or
(CurrentDeviceIndex > High(AudioInputProcessor.DeviceList))) then
begin
Exit;
end;
InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex];
if not assigned(InputDevice) then
Exit;
// set new volume
Volume := InputDevice.GetVolume() + VolumeChange;
InputDevice.SetVolume(Volume);
//DebugWriteln('Volume: ' + inttostr(InputDevice.GetVolume));
// volume must be polled again
NextVolumePollTime := 0;
end;
procedure TScreenOptionsRecord.onShow;
var
ChannelIndex: integer;
begin
inherited;
Interaction := 0;
// create preview sound-buffers
SetLength(PreviewChannel, MaxChannelCount);
for ChannelIndex := 0 to High(PreviewChannel) do
PreviewChannel[ChannelIndex] := TCaptureBuffer.Create();
SetLength(ChannelPeak, MaxChannelCount);
StartPreview();
end;
procedure TScreenOptionsRecord.onHide;
var
ChannelIndex: integer;
begin
StopPreview();
// free preview buffers
for ChannelIndex := 0 to High(PreviewChannel) do
PreviewChannel[ChannelIndex].Free;
SetLength(PreviewChannel, 0);
SetLength(ChannelPeak, 0);
end;
procedure TScreenOptionsRecord.StartPreview;
var
ChannelIndex: integer;
Device: TAudioInputDevice;
begin
if ((CurrentDeviceIndex >= 0) and
(CurrentDeviceIndex <= High(AudioInputProcessor.DeviceList))) then
begin
Device := AudioInputProcessor.DeviceList[CurrentDeviceIndex];
// set preview channel as active capture channel
for ChannelIndex := 0 to High(Device.CaptureChannel) do
begin
PreviewChannel[ChannelIndex].Clear();
Device.LinkCaptureBuffer(ChannelIndex, PreviewChannel[ChannelIndex]);
FillChar(ChannelPeak[ChannelIndex], SizeOf(TPeakInfo), 0);
end;
Device.Start();
PreviewDeviceIndex := CurrentDeviceIndex;
// volume must be polled again
NextVolumePollTime := 0;
end;
end;
procedure TScreenOptionsRecord.StopPreview;
var
ChannelIndex: integer;
Device: TAudioInputDevice;
begin
if ((PreviewDeviceIndex >= 0) and
(PreviewDeviceIndex <= High(AudioInputProcessor.DeviceList))) then
begin
Device := AudioInputProcessor.DeviceList[PreviewDeviceIndex];
Device.Stop;
for ChannelIndex := 0 to High(Device.CaptureChannel) do
Device.CaptureChannel[ChannelIndex] := nil;
end;
PreviewDeviceIndex := -1;
end;
procedure TScreenOptionsRecord.DrawVolume(x, y, Width, Height: single);
var
x1, y1, x2, y2: single;
VolBarInnerWidth: integer;
const
VolBarInnerHSpacing = 2;
VolBarInnerVSpacing = 1;
begin
// coordinates for black rect
x1 := x;
y1 := y;
x2 := x1 + Width;
y2 := y1 + Height;
// draw black background-rect
glColor4f(0, 0, 0, 0.8);
glBegin(GL_QUADS);
glVertex2f(x1, y1);
glVertex2f(x2, y1);
glVertex2f(x2, y2);
glVertex2f(x1, y2);
glEnd();
VolBarInnerWidth := Trunc(Width - 2*VolBarInnerHSpacing);
// coordinates for first half of the volume bar
x1 := x + VolBarInnerHSpacing;
x2 := x1 + VolBarInnerWidth * SourceVolume;
y1 := y1 + VolBarInnerVSpacing;
y2 := y2 - VolBarInnerVSpacing;
// draw volume-bar
glBegin(GL_QUADS);
// draw volume bar
glColor3f(0.4, 0.3, 0.3);
glVertex2f(x1, y1);
glVertex2f(x1, y2);
glColor3f(1, 0.1, 0.1);
glVertex2f(x2, y2);
glVertex2f(x2, y1);
glEnd();
{ not needed anymore
// coordinates for separator
x1 := x + VolBarInnerHSpacing;
x2 := x1 + VolBarInnerWidth;
// draw separator
glBegin(GL_LINE_STRIP);
glColor4f(0.1, 0.1, 0.1, 0.2);
glVertex2f(x1, y2);
glColor4f(0.4, 0.4, 0.4, 0.2);
glVertex2f((x1+x2)/2, y2);
glColor4f(0.1, 0.1, 0.1, 0.2);
glVertex2f(x2, y2);
glEnd();
}
end;
procedure TScreenOptionsRecord.DrawVUMeter(const State: TDrawState; x, y, Width, Height: single);
var
x1, y1, x2, y2: single;
Volume, PeakVolume: single;
Delta: single;
VolBarInnerWidth: integer;
const
VolBarInnerHSpacing = 2;
VolBarInnerVSpacing = 1;
begin
// coordinates for black rect
x1 := x;
y1 := y;
x2 := x1 + Width;
y2 := y1 + Height;
// draw black background-rect
glColor4f(0, 0, 0, 0.8);
glBegin(GL_QUADS);
glVertex2f(x1, y1);
glVertex2f(x2, y1);
glVertex2f(x2, y2);
glVertex2f(x1, y2);
glEnd();
VolBarInnerWidth := Trunc(Width - 2*VolBarInnerHSpacing);
// vertical positions
y1 := y1 + VolBarInnerVSpacing;
y2 := y2 - VolBarInnerVSpacing;
// coordinates for bevel
x1 := x + VolBarInnerHSpacing;
x2 := x1 + VolBarInnerWidth;
glBegin(GL_QUADS);
Volume := PreviewChannel[State.ChannelIndex].MaxSampleVolume();
// coordinates for volume bar
x1 := x + VolBarInnerHSpacing;
x2 := x1 + VolBarInnerWidth * Volume;
// draw volume bar
glColor3f(State.RD, State.GD, State.BD);
glVertex2f(x1, y1);
glVertex2f(x1, y2);
glColor3f(State.R, State.G, State.B);
glVertex2f(x2, y2);
glVertex2f(x2, y1);
Delta := (SDL_GetTicks() - ChannelPeak[State.ChannelIndex].Time)/1000;
PeakVolume := ChannelPeak[State.ChannelIndex].Volume - Delta*Delta*PeakDecay;
// determine new peak-volume
if (Volume > PeakVolume) then
begin
PeakVolume := Volume;
ChannelPeak[State.ChannelIndex].Volume := Volume;
ChannelPeak[State.ChannelIndex].Time := SDL_GetTicks();
end;
x1 := x + VolBarInnerHSpacing + VolBarInnerWidth * PeakVolume;
x2 := x1 + 2;
// draw peak
glColor3f(0.8, 0.8, 0.8);
glVertex2f(x1, y1);
glVertex2f(x1, y2);
glVertex2f(x2, y2);
glVertex2f(x2, y1);
// draw threshold
x1 := x + VolBarInnerHSpacing;
x2 := x1 + VolBarInnerWidth * IThresholdVals[Ini.ThresholdIndex];
glColor4f(0.3, 0.3, 0.3, 0.6);
glVertex2f(x1, y1);
glVertex2f(x1, y2);
glVertex2f(x2, y2);
glVertex2f(x2, y1);
glEnd();
end;
procedure TScreenOptionsRecord.DrawPitch(const State: TDrawState; x, y, Width, Height: single);
var
x1, y1, x2, y2: single;
i: integer;
ToneBoxWidth: real;
const
PitchBarInnerHSpacing = 2;
PitchBarInnerVSpacing = 1;
begin
// calc tone pitch
PreviewChannel[State.ChannelIndex].AnalyzeBuffer();
// coordinates for black rect
x1 := x;
y1 := y;
x2 := x + Width;
y2 := y + Height;
// draw black background-rect
glColor4f(0, 0, 0, 0.8);
glBegin(GL_QUADS);
glVertex2f(x1, y1);
glVertex2f(x2, y1);
glVertex2f(x2, y2);
glVertex2f(x1, y2);
glEnd();
// coordinates for tone boxes
ToneBoxWidth := Width / NumHalftones;
y1 := y1 + PitchBarInnerVSpacing;
y2 := y2 - PitchBarInnerVSpacing;
glBegin(GL_QUADS);
// draw tone boxes
for i := 0 to NumHalftones-1 do
begin
x1 := x + i * ToneBoxWidth + PitchBarInnerHSpacing;
x2 := x1 + ToneBoxWidth - 2*PitchBarInnerHSpacing;
if ((PreviewChannel[State.ChannelIndex].ToneValid) and
(PreviewChannel[State.ChannelIndex].ToneAbs = i)) then
begin
// highlight current tone-pitch
glColor3f(1, i / (NumHalftones-1), 0)
end
else
begin
// grey other tone-pitches
glColor3f(0.3, i / (NumHalftones-1) * 0.3, 0);
end;
glVertex2f(x1, y1);
glVertex2f(x2, y1);
glVertex2f(x2, y2);
glVertex2f(x1, y2);
end;
glEnd();
// update tone-pitch label
Text[TextPitchID[State.ChannelIndex]].Text :=
PreviewChannel[State.ChannelIndex].ToneString;
end;
function TScreenOptionsRecord.Draw: boolean;
var
i: integer;
Device: TAudioInputDevice;
DeviceCfg: PInputDeviceConfig;
SelectSlide: TSelectSlide;
BarXOffset, BarYOffset, BarWidth: real;
ChannelIndex: integer;
State: TDrawState;
begin
DrawBG;
DrawFG;
if ((PreviewDeviceIndex >= 0) and
(PreviewDeviceIndex <= High(AudioInputProcessor.DeviceList))) then
begin
Device := AudioInputProcessor.DeviceList[PreviewDeviceIndex];
DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex];
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// update source volume
if (SDL_GetTicks() >= NextVolumePollTime) then
begin
NextVolumePollTime := SDL_GetTicks() + 500; // next poll in 500ms
SourceVolume := Device.GetVolume()/100;
end;
// get source select slide
SelectSlide := SelectsS[SelectInputSourceID];
BarXOffset := SelectSlide.TextureSBG.X;
BarYOffset := SelectSlide.TextureSBG.Y + SelectSlide.TextureSBG.H + BarUpperSpacing;
BarWidth := SelectSlide.TextureSBG.W;
DrawVolume(SelectSlide.TextureSBG.X, BarYOffset, BarWidth, BarHeight);
for ChannelIndex := 0 to High(Device.CaptureChannel) do
begin
// load player color mapped to current input channel
if (DeviceCfg.ChannelToPlayerMap[ChannelIndex] > 0) then
begin
// set mapped channel to corresponding player-color
LoadColor(State.R, State.G, State.B, 'P'+ IntToStr(DeviceCfg.ChannelToPlayerMap[ChannelIndex]) + 'Dark');
end
else
begin
// set non-mapped channel to white
State.R := 1; State.G := 1; State.B := 1;
end;
// dark player colors
State.RD := 0.2 * State.R;
State.GD := 0.2 * State.G;
State.BD := 0.2 * State.B;
// channel select slide
SelectSlide := SelectsS[SelectSlideChannelID[ChannelIndex]];
BarXOffset := SelectSlide.TextureSBG.X;
BarYOffset := SelectSlide.TextureSBG.Y + SelectSlide.TextureSBG.H + BarUpperSpacing;
BarWidth := SelectSlide.TextureSBG.W;
State.ChannelIndex := ChannelIndex;
DrawVUMeter(State, BarXOffset, BarYOffset, BarWidth, BarHeight);
DrawPitch(State, BarXOffset, BarYOffset+BarHeight, BarWidth, BarHeight);
end;
glDisable(GL_BLEND);
end;
Result := True;
end;
end.