From 179ebb7cc3bdf1165c0b298909efa8bd7e9f22b2 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:19:48 +0200 Subject: [PATCH] drafting --- tracker/gioui/oscilloscope.go | 182 +++++++--------------------------- tracker/gioui/plot.go | 168 +++++++++++++++++++++++++++++++ tracker/gioui/specanalyzer.go | 92 ++++++++++++----- tracker/gioui/theme.go | 2 +- tracker/spectrum.go | 8 +- 5 files changed, 279 insertions(+), 173 deletions(-) create mode 100644 tracker/gioui/plot.go diff --git a/tracker/gioui/oscilloscope.go b/tracker/gioui/oscilloscope.go index 31eb195..5ca13d2 100644 --- a/tracker/gioui/oscilloscope.go +++ b/tracker/gioui/oscilloscope.go @@ -1,16 +1,9 @@ package gioui import ( - "image" - "image/color" "math" - "gioui.org/f32" - "gioui.org/io/event" - "gioui.org/io/pointer" "gioui.org/layout" - "gioui.org/op/clip" - "gioui.org/op/paint" "gioui.org/unit" "github.com/vsariola/sointu/tracker" ) @@ -21,30 +14,20 @@ type ( wrapBtn *Clickable lengthInBeatsNumber *NumericUpDownState triggerChannelNumber *NumericUpDownState - xScale int - xOffset float32 - yScale float64 - dragging bool - dragId pointer.ID - dragStartPoint f32.Point - } - - OscilloscopeStyle struct { - CurveColors [2]color.NRGBA `yaml:",flow"` - LimitColor color.NRGBA `yaml:",flow"` - CursorColor color.NRGBA `yaml:",flow"` + plot *Plot } Oscilloscope struct { Theme *Theme Model *tracker.ScopeModel State *OscilloscopeState - Style *OscilloscopeStyle + Style *PlotStyle } ) func NewOscilloscope(model *tracker.Model) *OscilloscopeState { return &OscilloscopeState{ + plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}), onceBtn: new(Clickable), wrapBtn: new(Clickable), lengthInBeatsNumber: NewNumericUpDownState(), @@ -72,7 +55,41 @@ func (s *Oscilloscope) Layout(gtx C) D { wrapBtn := ToggleBtn(s.Model.Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full") return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Flexed(1, s.layoutWave), + layout.Flexed(1, func(gtx C) D { + w := s.Model.Waveform() + cx := float32(w.Cursor) / float32(len(w.Buffer)) + + data := func(chn int, xr plotRange) (yr plotRange) { + x1 := max(int(xr.a*float32(len(w.Buffer))), 0) + x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1) + if x1 > x2 { + return plotRange{1, 0} + } + y1 := float32(math.Inf(-1)) + y2 := float32(math.Inf(+1)) + for i := x1; i <= x2; i++ { + sample := w.Buffer[i][chn] + y1 = max(y1, sample) + y2 = min(y2, sample) + } + return plotRange{-y1, -y2} + } + + xticks := func(r plotRange, yield func(pos float32, label string)) { + l := s.Model.LengthInBeats().Value() + a := max(int(math.Ceil(float64(r.a*float32(l)))), 0) + b := min(int(math.Floor(float64(r.b*float32(l)))), l) + for i := a; i <= b; i++ { + yield(float32(i)/float32(l), "") + } + } + yticks := func(r plotRange, yield func(pos float32, label string)) { + yield(-1, "1") + yield(1, "-1") + } + + return s.State.plot.Layout(gtx, data, xticks, yticks, cx, 2) + }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(leftSpacer), @@ -95,126 +112,3 @@ func (s *Oscilloscope) Layout(gtx C) D { }), ) } - -func (s *Oscilloscope) layoutWave(gtx C) D { - s.State.update(gtx, s.Model.Waveform()) - if gtx.Constraints.Max.X == 0 || gtx.Constraints.Max.Y == 0 { - return D{} - } - defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop() - wave := s.Model.Waveform() - event.Op(gtx.Ops, s.State) - paint.ColorOp{Color: s.Style.CursorColor}.Add(gtx.Ops) - cursorX := int(s.State.sampleToPx(gtx, float32(wave.Cursor), wave)) - fillRect(gtx, clip.Rect{Min: image.Pt(cursorX, 0), Max: image.Pt(cursorX+1, gtx.Constraints.Max.Y)}) - paint.ColorOp{Color: s.Style.LimitColor}.Add(gtx.Ops) - minusOneY := int(s.State.ampToY(gtx, -1)) - fillRect(gtx, clip.Rect{Min: image.Pt(0, minusOneY), Max: image.Pt(gtx.Constraints.Max.X, minusOneY+1)}) - plusOneY := int(s.State.ampToY(gtx, 1)) - fillRect(gtx, clip.Rect{Min: image.Pt(0, plusOneY), Max: image.Pt(gtx.Constraints.Max.X, plusOneY+1)}) - leftX := int(s.State.sampleToPx(gtx, 0, wave)) - fillRect(gtx, clip.Rect{Min: image.Pt(leftX, 0), Max: image.Pt(leftX+1, gtx.Constraints.Max.Y)}) - rightX := int(s.State.sampleToPx(gtx, float32(len(wave.Buffer)-1), wave)) - fillRect(gtx, clip.Rect{Min: image.Pt(rightX, 0), Max: image.Pt(rightX+1, gtx.Constraints.Max.Y)}) - for chn := range 2 { - paint.ColorOp{Color: s.Style.CurveColors[chn]}.Add(gtx.Ops) - for px := range gtx.Constraints.Max.X { - // left and right is the sample range covered by the pixel - left := int(s.State.pxToSample(gtx, float32(px)-0.5, wave)) - right := int(s.State.pxToSample(gtx, float32(px)+0.5, wave)) - if right < 0 || left >= len(wave.Buffer) { - continue - } - right = min(right, len(wave.Buffer)-1) - left = max(left, 0) - // smin and smax are the smallest and largest sample values in the pixel range - smax := float32(math.Inf(-1)) - smin := float32(math.Inf(1)) - for x := left; x <= right; x++ { - smax = max(smax, wave.Buffer[x][chn]) - smin = min(smin, wave.Buffer[x][chn]) - } - // y1 and y2 are the pixel range covered by the sample value - y1 := min(max(int(s.State.ampToY(gtx, smax)+0.5), 0), gtx.Constraints.Max.Y-1) - y2 := min(max(int(s.State.ampToY(gtx, smin)+0.5), 0), gtx.Constraints.Max.Y-1) - fillRect(gtx, clip.Rect{Min: image.Pt(px, y1), Max: image.Pt(px+1, y2+1)}) - } - } - return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)} -} - -func fillRect(gtx C, rect clip.Rect) { - stack := rect.Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - stack.Pop() -} - -func (o *OscilloscopeState) update(gtx C, wave tracker.RingBuffer[[2]float32]) { - for { - ev, ok := gtx.Event(pointer.Filter{ - Target: o, - Kinds: pointer.Scroll | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel, - ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6}, - }) - if !ok { - break - } - if e, ok := ev.(pointer.Event); ok { - switch e.Kind { - case pointer.Scroll: - s1 := o.pxToSample(gtx, e.Position.X, wave) - o.xScale += min(max(-1, int(e.Scroll.Y)), 1) - s2 := o.pxToSample(gtx, e.Position.X, wave) - o.xOffset -= s1 - s2 - case pointer.Press: - if e.Buttons&pointer.ButtonSecondary != 0 { - o.xOffset = 0 - o.xScale = 0 - o.yScale = 0 - } - if e.Buttons&pointer.ButtonPrimary != 0 { - o.dragging = true - o.dragId = e.PointerID - o.dragStartPoint = e.Position - } - case pointer.Drag: - if e.Buttons&pointer.ButtonPrimary != 0 && o.dragging && e.PointerID == o.dragId { - deltaX := o.pxToSample(gtx, e.Position.X, wave) - o.pxToSample(gtx, o.dragStartPoint.X, wave) - o.xOffset += deltaX - num := o.yToAmp(gtx, e.Position.Y) - den := o.yToAmp(gtx, o.dragStartPoint.Y) - if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 { - o.yScale += math.Log(l) - o.yScale = min(max(o.yScale, -1e3), 1e3) - } - o.dragStartPoint = e.Position - - } - case pointer.Release | pointer.Cancel: - o.dragging = false - } - } - } -} - -func (o *OscilloscopeState) scaleFactor() float32 { - return float32(math.Pow(1.1, float64(o.xScale))) -} - -func (s *OscilloscopeState) pxToSample(gtx C, px float32, wave tracker.RingBuffer[[2]float32]) float32 { - return px*s.scaleFactor()*float32(len(wave.Buffer))/float32(gtx.Constraints.Max.X) - s.xOffset -} - -func (s *OscilloscopeState) sampleToPx(gtx C, sample float32, wave tracker.RingBuffer[[2]float32]) float32 { - return (sample + s.xOffset) * float32(gtx.Constraints.Max.X) / float32(len(wave.Buffer)) / s.scaleFactor() -} - -func (s *OscilloscopeState) ampToY(gtx C, amp float32) float32 { - scale := float32(math.Exp(s.yScale)) - return (1 - amp*scale) / 2 * float32(gtx.Constraints.Max.Y-1) -} - -func (s *OscilloscopeState) yToAmp(gtx C, y float32) float32 { - scale := float32(math.Exp(s.yScale)) - return (1 - y/float32(gtx.Constraints.Max.Y-1)*2) / scale -} diff --git a/tracker/gioui/plot.go b/tracker/gioui/plot.go new file mode 100644 index 0000000..372a9bd --- /dev/null +++ b/tracker/gioui/plot.go @@ -0,0 +1,168 @@ +package gioui + +import ( + "image" + "image/color" + "math" + + "gioui.org/f32" + "gioui.org/io/event" + "gioui.org/io/pointer" + "gioui.org/op/clip" + "gioui.org/op/paint" +) + +type ( + Plot struct { + origXlim, origYlim plotRange + + xScale, yScale float32 + xOffset float32 + dragging bool + dragId pointer.ID + dragStartPoint f32.Point + } + + PlotStyle struct { + CurveColors [2]color.NRGBA `yaml:",flow"` + LimitColor color.NRGBA `yaml:",flow"` + CursorColor color.NRGBA `yaml:",flow"` + } + + PlotDataFunc func(chn int, xr plotRange) (yr plotRange) + PlotTickFunc func(r plotRange, yield func(pos float32, label string)) + plotRange struct{ a, b float32 } + plotRel float32 + plotPx int + plotLogScale float32 +) + +func NewPlot(xlim, ylim plotRange) *Plot { + return &Plot{ + origXlim: xlim, + origYlim: ylim, + } +} + +func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cursornx float32, numchns int) D { + p.update(gtx) + t := TrackerFromContext(gtx) + style := t.Theme.Oscilloscope + s := gtx.Constraints.Max + if s.X <= 1 || s.Y <= 1 { + return D{} + } + defer clip.Rect(image.Rectangle{Max: s}).Push(gtx.Ops).Pop() + event.Op(gtx.Ops, p) + + xlim := p.xlim() + ylim := p.ylim() + + // draw tick marks + paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops) + + xticks(xlim, func(x float32, label string) { + sx := plotPx(s.X).toScreen(xlim.toRelative(x)) + fillRect(gtx, clip.Rect{Min: image.Pt(sx, 0), Max: image.Pt(sx+1, s.Y)}) + }) + + yticks(ylim, func(y float32, label string) { + sy := plotPx(s.Y).toScreen(ylim.toRelative(y)) + fillRect(gtx, clip.Rect{Min: image.Pt(0, sy), Max: image.Pt(s.X, sy+1)}) + }) + + // draw cursor + paint.ColorOp{Color: style.CursorColor}.Add(gtx.Ops) + csx := plotPx(s.X).toScreen(xlim.toRelative(cursornx)) + fillRect(gtx, clip.Rect{Min: image.Pt(csx, 0), Max: image.Pt(csx+1, s.Y)}) + + // draw curves + for chn := range numchns { + paint.ColorOp{Color: style.CurveColors[chn]}.Add(gtx.Ops) + right := xlim.fromRelative(plotPx(s.X).fromScreen(0)) + for sx := range s.X { + // left and right is the sample range covered by the pixel + left := right + right = xlim.fromRelative(plotPx(s.X).fromScreen(sx + 1)) + yr := data(chn, plotRange{left, right}) + if yr.b < yr.a { + continue + } + y1 := plotPx(s.Y).toScreen(ylim.toRelative(yr.a)) + y2 := plotPx(s.Y).toScreen(ylim.toRelative(yr.b)) + fillRect(gtx, clip.Rect{Min: image.Pt(sx, y1), Max: image.Pt(sx+1, y2+1)}) + } + } + return D{Size: s} +} + +func (r plotRange) toRelative(f float32) plotRel { return plotRel((f - r.a) / (r.b - r.a)) } +func (r plotRange) fromRelative(pr plotRel) float32 { return float32(pr)*(r.b-r.a) + r.a } +func (r plotRange) offset(o float32) plotRange { return plotRange{r.a + o, r.b + o} } +func (r plotRange) scale(logScale float32) plotRange { + s := float32(math.Exp(float64(logScale))) + return plotRange{r.a * s, r.b * s} +} + +func (s plotPx) toScreen(pr plotRel) int { return int(float32(pr)*float32(s-1) + 0.5) } +func (s plotPx) fromScreen(px int) plotRel { return plotRel(float32(px) / float32(s-1)) } +func (s plotPx) fromScreenF32(px float32) plotRel { return plotRel(px / float32(s-1)) } + +func (o *Plot) xlim() plotRange { return o.origXlim.scale(o.xScale).offset(o.xOffset) } +func (o *Plot) ylim() plotRange { return o.origYlim.scale(o.yScale) } + +func fillRect(gtx C, rect clip.Rect) { + stack := rect.Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Pop() +} + +func (o *Plot) update(gtx C) { + s := gtx.Constraints.Max + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: o, + Kinds: pointer.Scroll | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel, + ScrollY: pointer.ScrollRange{Min: -1e6, Max: 1e6}, + }) + if !ok { + break + } + if e, ok := ev.(pointer.Event); ok { + switch e.Kind { + case pointer.Scroll: + x1 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X)) + o.xScale += float32(min(max(-1, int(e.Scroll.Y)), 1)) * 0.1 + x2 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X)) + o.xOffset += x1 - x2 + case pointer.Press: + if e.Buttons&pointer.ButtonSecondary != 0 { + o.xOffset = 0 + o.xScale = 0 + o.yScale = 0 + } + if e.Buttons&pointer.ButtonPrimary != 0 { + o.dragging = true + o.dragId = e.PointerID + o.dragStartPoint = e.Position + } + case pointer.Drag: + if e.Buttons&pointer.ButtonPrimary != 0 && o.dragging && e.PointerID == o.dragId { + x1 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(o.dragStartPoint.X)) + x2 := o.xlim().fromRelative(plotPx(s.X).fromScreenF32(e.Position.X)) + o.xOffset += x1 - x2 + + num := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(e.Position.Y)) + den := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(o.dragStartPoint.Y)) + if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 { + o.yScale -= float32(math.Log(l)) + o.yScale = min(max(o.yScale, -1e3), 1e3) + } + o.dragStartPoint = e.Position + } + case pointer.Release | pointer.Cancel: + o.dragging = false + } + } + } +} diff --git a/tracker/gioui/specanalyzer.go b/tracker/gioui/specanalyzer.go index 8e00883..f573f0f 100644 --- a/tracker/gioui/specanalyzer.go +++ b/tracker/gioui/specanalyzer.go @@ -1,11 +1,9 @@ package gioui import ( - "image" + "math" "gioui.org/layout" - "gioui.org/op/clip" - "gioui.org/op/paint" "gioui.org/unit" "github.com/vsariola/sointu/tracker" ) @@ -15,11 +13,13 @@ type ( resolutionNumber *NumericUpDownState smoothingBtn *Clickable chnModeBtn *Clickable + plot *Plot } ) func NewSpectrumState() *SpectrumState { return &SpectrumState{ + plot: NewPlot(plotRange{0, 1}, plotRange{-1, 0}), resolutionNumber: NewNumericUpDownState(), smoothingBtn: new(Clickable), chnModeBtn: new(Clickable), @@ -56,8 +56,64 @@ func (s *SpectrumState) Layout(gtx C) D { chnModeBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.chnModeBtn, chnModeTxt, "Channel mode") smoothBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.smoothingBtn, smoothTxt, "Smoothing") + numchns := 0 + speclen := len(t.Model.Spectrum()[0]) + if speclen > 0 { + numchns = 1 + if len(t.Model.Spectrum()[1]) == speclen { + numchns = 2 + } + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Flexed(1, s.drawSpectrum), + layout.Flexed(1, func(gtx C) D { + data := func(chn int, xr plotRange) (yr plotRange) { + xr.a = softplus(xr.a*10) / 10 + xr.b = softplus(xr.b*10) / 10 + xr.a = float32(math.Log(float64(xr.a))) + 1 + xr.b = float32(math.Log(float64(xr.b))) + 1 + w1, f1 := math.Modf(float64(xr.a) * float64(speclen)) + w2, f2 := math.Modf(float64(xr.b) * float64(speclen)) + x1 := max(int(w1), 0) + x2 := min(int(w2), speclen-1) + if x1 > x2 { + return plotRange{1, 0} + } + y1 := float32(math.Inf(-1)) + y2 := float32(math.Inf(+1)) + switch { + case x2 <= x1+1 && x2 < speclen-1: // perform smoothstep interpolation when we are overlapping only a few bins + l := t.Model.Spectrum()[chn][x1] + r := t.Model.Spectrum()[chn][x1+1] + y1 = smoothInterpolate(l, r, float32(f1)) + l = t.Model.Spectrum()[chn][x2] + r = t.Model.Spectrum()[chn][x2+1] + y2 = smoothInterpolate(l, r, float32(f2)) + y1, y2 = max(y1, y2), min(y1, y2) + default: + for i := x1; i <= x2; i++ { + sample := t.Model.Spectrum()[chn][i] + y1 = max(y1, sample) + y2 = min(y2, sample) + } + } + y1 = (y1 / 80) + 1 + y2 = (y2 / 80) + 1 + y1 = softplus(y1*10) / 10 + y2 = softplus(y2*10) / 10 + + return plotRange{-y1, -y2} + } + xticks := func(r plotRange, yield func(pos float32, label string)) { + yield(0, "") + yield(1, "") + } + yticks := func(r plotRange, yield func(pos float32, label string)) { + yield(-1, "") + yield(0, "") + } + return s.plot.Layout(gtx, data, xticks, yticks, 0, numchns) + }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(leftSpacer), @@ -71,6 +127,15 @@ func (s *SpectrumState) Layout(gtx C) D { ) } +func softplus(f float32) float32 { + return float32(math.Log(1 + math.Exp(float64(f)))) +} + +func smoothInterpolate(a, b float32, t float32) float32 { + t = t * t * (3 - 2*t) + return (1-t)*a + t*b +} + func (s *SpectrumState) Update(gtx C) { t := TrackerFromContext(gtx) for s.chnModeBtn.Clicked(gtx) { @@ -81,22 +146,3 @@ func (s *SpectrumState) Update(gtx C) { t.Model.SpecAnSmoothing().SetValue((t.SpecAnSmoothing().Value()+1)%(r.Max-r.Min+1) + r.Min) } } - -func (s *SpectrumState) drawSpectrum(gtx C) D { - t := TrackerFromContext(gtx) - for chn := range 2 { - paint.ColorOp{Color: t.Theme.Oscilloscope.CurveColors[chn]}.Add(gtx.Ops) - p := t.Spectrum()[chn] - if len(p) <= 0 { - continue - } - fillRect(gtx, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Max.X, 1)}) - fillRect(gtx, clip.Rect{Min: image.Pt(0, gtx.Constraints.Min.Y-1), Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}) - for px := range gtx.Constraints.Max.X { - y2 := gtx.Constraints.Max.Y - 1 - y1 := int(-p[px*len(p)/gtx.Constraints.Max.X] / 80 * float32(y2)) - fillRect(gtx, clip.Rect{Min: image.Pt(px, y1), Max: image.Pt(px+1, y2+1)}) - } - } - return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)} -} diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index 04f64d0..e01bcd5 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -32,7 +32,7 @@ type Theme struct { Emphasis IconButtonStyle Error IconButtonStyle } - Oscilloscope OscilloscopeStyle + Oscilloscope PlotStyle NumericUpDown NumericUpDownStyle SongPanel struct { RowHeader LabelStyle diff --git a/tracker/spectrum.go b/tracker/spectrum.go index 241592d..e44324e 100644 --- a/tracker/spectrum.go +++ b/tracker/spectrum.go @@ -57,9 +57,9 @@ const ( ) var spectrumSmoothingMap map[SpecSmoothing]float32 = map[SpecSmoothing]float32{ - SpecSmoothingSlow: 0.05, + SpecSmoothingSlow: 0.1, SpecSmoothingMedium: 0.2, - SpecSmoothingFast: 1.0, + SpecSmoothingFast: 0.4, } func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer { @@ -154,8 +154,6 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum { } // convert to decibels for c := range 2 { - vek32.MaximumNumber_Inplace(ret[c], 1e-8) - vek32.MinimumNumber_Inplace(ret[c], 1e8) vek32.Log10_Inplace(ret[c]) vek32.MulNumber_Inplace(ret[c], 10) } @@ -164,7 +162,7 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum { func (sd *SpecAnalyzer) process(buf sointu.AudioBuffer, channel int) { for i := range buf { // de-interleave - sd.temp.tmp1[i] = buf[i][channel] + sd.temp.tmp1[i] = removeNaNsAndClamp(buf[i][channel]) } vek32.Mul_Inplace(sd.temp.tmp1, sd.temp.window) // apply windowing vek32.Gather_Into(sd.temp.tmp2, sd.temp.tmp1, sd.temp.bitPerm) // bit-reversal permutation